From cec0766e67d483dd20930cd09167f2b379990612 Mon Sep 17 00:00:00 2001 From: lipan Date: Thu, 11 Jun 2026 02:30:04 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8D=87=E7=BA=A7=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E5=8C=85=20delivery=5Fv1.1=20=E2=86=92=20v1.2=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BB=BB=E5=8A=A1=E7=BA=A7=20CAD=20=E4=B8=8E?= =?UTF-8?q?=E6=B5=8B=E8=B7=9D=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 算法包替换为 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++ 依赖 --- .gitignore | 4 +- .../coupler_panel_release/README.md | 61 - .../coupler_panel_release/USAGE.md | 376 -- .../configs/experiments/density_sweep.yaml | 13 - .../configs/experiments/full_sweep.yaml | 34 - .../configs/experiments/lu1_af4_sweep.yaml | 47 - .../experiments/lu1_deflection_sweep.yaml | 74 - .../experiments/lu1_density_sweep.yaml | 73 - .../configs/experiments/lu1_noise_sweep.yaml | 73 - .../experiments/lu1_sampling_sweep.yaml | 78 - .../configs/experiments/noise_sweep.yaml | 16 - .../configs/experiments/sampling_sweep.yaml | 12 - .../configs/experiments/transform_sweep.yaml | 16 - .../coupler_panel_release/requirements.txt | 52 - .../test_data/01_gt.json | 1907 --------- .../test_data/10_metrics.json | 2512 ----------- .../test_data/config.resolved.yaml | 94 - .../test_data/report.json | 2512 ----------- .../test_data/synth/gt.json | 3727 ----------------- .../test_data/synth/manifest.json | 70 - .../underframe_deflection/README.md | 200 - .../underframe_deflection/USAGE.md | 363 -- .../underframe_deflection/cli.py | 83 - alg/delivery_v1.1/underframe_deflection/io.py | 88 - .../test_data/sample_roi.json | 8 - .../coupler_panel_release/README.md | 68 + .../coupler_panel_release/USAGE.md | 256 ++ .../configs/default.yaml | 8 +- .../configs/profiles/high_density.yaml | 0 .../configs/profiles/low_density.yaml | 0 .../configs/profiles/lu1_af4.yaml | 0 .../configs/profiles/train2_coupler.yaml | 94 + .../coupler_panel/__init__.py | 0 .../coupler_panel/_version.py | 0 .../coupler_panel/api.py | 0 .../coupler_panel/cad/__init__.py | 0 .../coupler_panel/cad/gt_extractor.py | 0 .../coupler_panel/cad/mesh.py | 0 .../coupler_panel/cad/stp_parser.py | 0 .../coupler_panel/cli.py | 0 .../coupler_panel/config.py | 0 .../coupler_panel/data_prep.py | 0 .../coupler_panel/denoise/__init__.py | 0 .../coupler_panel/denoise/passthrough.py | 0 .../coupler_panel/denoise/pipeline.py | 0 .../coupler_panel/denoise/radius.py | 0 .../coupler_panel/denoise/smoothing.py | 0 .../coupler_panel/denoise/statistical.py | 0 .../coupler_panel/denoise/voxel.py | 0 .../coupler_panel/detection/__init__.py | 0 .../coupler_panel/detection/autotune.py | 114 + .../coupler_panel/detection/circle_fit.py | 0 .../coupler_panel/detection/detect_holes.py | 0 .../detection/four_hole_selector.py | 0 .../coupler_panel/detection/layer_select.py | 129 + .../coupler_panel/detection/occupancy_grid.py | 0 .../coupler_panel/detection/plane_local.py | 0 .../coupler_panel/exceptions.py | 0 .../coupler_panel/geometry/__init__.py | 0 .../coupler_panel/geometry/plane_fit.py | 0 .../coupler_panel/geometry/reference.py | 0 .../coupler_panel/geometry/roi.py | 0 .../coupler_panel/io_utils/__init__.py | 2 + .../coupler_panel/io_utils/artifacts.py | 0 .../coupler_panel/io_utils/ply_stream.py | 165 + .../coupler_panel/io_utils/pointcloud_io.py | 0 .../coupler_panel/metrics/__init__.py | 0 .../coupler_panel/metrics/compare.py | 0 .../coupler_panel/metrics/deflection.py | 0 .../coupler_panel/pipeline.py | 157 +- .../coupler_panel/py.typed | 0 .../coupler_panel/registration/__init__.py | 0 .../coupler_panel/registration/kabsch.py | 0 .../coupler_panel/reporting/__init__.py | 0 .../coupler_panel/reporting/excel.py | 0 .../coupler_panel/roi_select/__init__.py | 0 .../coupler_panel/roi_select/apply.py | 0 .../coupler_panel/roi_select/schema.py | 0 .../coupler_panel/sampling/__init__.py | 0 .../coupler_panel/sampling/adaptive.py | 0 .../coupler_panel/sampling/grid_spacing.py | 0 .../coupler_panel/sampling/poisson_disk.py | 0 .../coupler_panel/sampling/uniform_area.py | 0 .../coupler_panel/simulation/__init__.py | 0 .../coupler_panel/simulation/noise.py | 0 .../simulation/scan_simulator.py | 0 .../coupler_panel/simulation/transform.py | 0 .../coupler_panel/structures.py | 0 .../coupler_panel/viz/__init__.py | 0 .../coupler_panel/viz/_offscreen.py | 0 .../coupler_panel/viz/cloud_view.py | 0 .../coupler_panel/viz/heatmap.py | 0 .../coupler_panel/viz/holes_view.py | 0 .../coupler_panel/viz/plane_view.py | 0 .../coupler_panel/viz/render_all.py | 0 .../examples/generate_test_clouds.py | 0 .../examples/integrate_minimal.py | 0 .../examples/roi_all.json | 0 .../examples/roi_train2_coupler.json | 9 + .../examples/run_batch_eval.py | 0 .../examples/run_full_validation.py | 0 .../examples/run_integration.py | 113 + .../coupler_panel_release/pyproject.toml | 0 .../coupler_panel_release/requirements.txt | 30 + .../underframe_deflection/README.md | 73 + .../underframe_deflection/USAGE.md | 196 + .../underframe_deflection/__init__.py | 6 + .../underframe_deflection/_version.py | 2 +- .../underframe_deflection/api.py | 0 .../underframe_deflection/artifacts.py | 0 .../underframe_deflection/beams.py | 0 .../underframe_deflection/cad_io.py | 0 .../underframe_deflection/cli.py | 176 + .../underframe_deflection/config.py | 11 + .../underframe_deflection/excel_report.py | 0 .../underframe_deflection/exceptions.py | 0 .../underframe_deflection/fitting.py | 43 + .../underframe_deflection/frame.py | 0 alg/delivery_v1.2/underframe_deflection/io.py | 278 ++ .../underframe_deflection/pipeline.py | 32 + .../underframe_deflection/ply_overlay.py | 0 .../underframe_deflection/preprocess.py | 47 +- .../underframe_deflection/py.typed | 0 .../underframe_deflection/pyproject.toml | 8 +- .../underframe_deflection/reference.py | 0 .../underframe_deflection/registration.py | 47 +- .../underframe_deflection/requirements.txt | 0 .../underframe_deflection/roi.py | 0 .../underframe_deflection/single_beam.py | 340 ++ .../single_beam_pipeline.py | 285 ++ .../underframe_deflection/structures.py | 0 .../test_data/roi_left_beam.json | 8 + .../test_data/roi_right_beam.json | 8 + .../test_data/sample_roi.json | 8 + .../tools/cad_sampling/README.md | 54 + .../tools/cad_sampling/step_to_pointcloud.py | 332 ++ .../underframe_deflection/visualization.py | 0 client/src/components/TaskManager.tsx | 60 +- client/src/pages/Home.tsx | 9 - server/plugins/adapters/coupler_panel_run.py | 9 +- .../adapters/underframe_deflection_run.py | 9 +- server/plugins/manifests/coupler-panel.json | 6 +- .../manifests/underframe-deflection.json | 6 +- 143 files changed, 3087 insertions(+), 12604 deletions(-) delete mode 100644 alg/delivery_v1.1/coupler_panel_release/README.md delete mode 100644 alg/delivery_v1.1/coupler_panel_release/USAGE.md delete mode 100644 alg/delivery_v1.1/coupler_panel_release/configs/experiments/density_sweep.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/configs/experiments/full_sweep.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_af4_sweep.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_deflection_sweep.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_density_sweep.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_noise_sweep.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_sampling_sweep.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/configs/experiments/noise_sweep.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/configs/experiments/sampling_sweep.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/configs/experiments/transform_sweep.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/requirements.txt delete mode 100644 alg/delivery_v1.1/coupler_panel_release/test_data/01_gt.json delete mode 100644 alg/delivery_v1.1/coupler_panel_release/test_data/10_metrics.json delete mode 100644 alg/delivery_v1.1/coupler_panel_release/test_data/config.resolved.yaml delete mode 100644 alg/delivery_v1.1/coupler_panel_release/test_data/report.json delete mode 100644 alg/delivery_v1.1/coupler_panel_release/test_data/synth/gt.json delete mode 100644 alg/delivery_v1.1/coupler_panel_release/test_data/synth/manifest.json delete mode 100644 alg/delivery_v1.1/underframe_deflection/README.md delete mode 100644 alg/delivery_v1.1/underframe_deflection/USAGE.md delete mode 100644 alg/delivery_v1.1/underframe_deflection/cli.py delete mode 100644 alg/delivery_v1.1/underframe_deflection/io.py delete mode 100644 alg/delivery_v1.1/underframe_deflection/test_data/sample_roi.json create mode 100644 alg/delivery_v1.2/coupler_panel_release/README.md create mode 100644 alg/delivery_v1.2/coupler_panel_release/USAGE.md rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/configs/default.yaml (96%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/configs/profiles/high_density.yaml (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/configs/profiles/low_density.yaml (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/configs/profiles/lu1_af4.yaml (100%) create mode 100644 alg/delivery_v1.2/coupler_panel_release/configs/profiles/train2_coupler.yaml rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/_version.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/api.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/cad/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/cad/gt_extractor.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/cad/mesh.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/cad/stp_parser.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/cli.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/config.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/data_prep.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/denoise/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/denoise/passthrough.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/denoise/pipeline.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/denoise/radius.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/denoise/smoothing.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/denoise/statistical.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/denoise/voxel.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/detection/__init__.py (100%) create mode 100644 alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/autotune.py rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/detection/circle_fit.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/detection/detect_holes.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/detection/four_hole_selector.py (100%) create mode 100644 alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/layer_select.py rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/detection/occupancy_grid.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/detection/plane_local.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/exceptions.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/geometry/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/geometry/plane_fit.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/geometry/reference.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/geometry/roi.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/io_utils/__init__.py (88%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/io_utils/artifacts.py (100%) create mode 100644 alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/ply_stream.py rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/io_utils/pointcloud_io.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/metrics/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/metrics/compare.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/metrics/deflection.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/pipeline.py (63%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/py.typed (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/registration/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/registration/kabsch.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/reporting/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/reporting/excel.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/roi_select/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/roi_select/apply.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/roi_select/schema.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/sampling/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/sampling/adaptive.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/sampling/grid_spacing.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/sampling/poisson_disk.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/sampling/uniform_area.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/simulation/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/simulation/noise.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/simulation/scan_simulator.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/simulation/transform.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/structures.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/viz/__init__.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/viz/_offscreen.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/viz/cloud_view.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/viz/heatmap.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/viz/holes_view.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/viz/plane_view.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/coupler_panel/viz/render_all.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/examples/generate_test_clouds.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/examples/integrate_minimal.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/examples/roi_all.json (100%) create mode 100644 alg/delivery_v1.2/coupler_panel_release/examples/roi_train2_coupler.json rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/examples/run_batch_eval.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/examples/run_full_validation.py (100%) create mode 100644 alg/delivery_v1.2/coupler_panel_release/examples/run_integration.py rename alg/{delivery_v1.1 => delivery_v1.2}/coupler_panel_release/pyproject.toml (100%) create mode 100644 alg/delivery_v1.2/coupler_panel_release/requirements.txt create mode 100644 alg/delivery_v1.2/underframe_deflection/README.md create mode 100644 alg/delivery_v1.2/underframe_deflection/USAGE.md rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/__init__.py (88%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/_version.py (96%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/api.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/artifacts.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/beams.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/cad_io.py (100%) create mode 100644 alg/delivery_v1.2/underframe_deflection/cli.py rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/config.py (94%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/excel_report.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/exceptions.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/fitting.py (85%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/frame.py (100%) create mode 100644 alg/delivery_v1.2/underframe_deflection/io.py rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/pipeline.py (88%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/ply_overlay.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/preprocess.py (82%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/py.typed (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/pyproject.toml (87%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/reference.py (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/registration.py (81%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/requirements.txt (100%) rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/roi.py (100%) create mode 100644 alg/delivery_v1.2/underframe_deflection/single_beam.py create mode 100644 alg/delivery_v1.2/underframe_deflection/single_beam_pipeline.py rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/structures.py (100%) create mode 100644 alg/delivery_v1.2/underframe_deflection/test_data/roi_left_beam.json create mode 100644 alg/delivery_v1.2/underframe_deflection/test_data/roi_right_beam.json create mode 100644 alg/delivery_v1.2/underframe_deflection/test_data/sample_roi.json create mode 100644 alg/delivery_v1.2/underframe_deflection/tools/cad_sampling/README.md create mode 100644 alg/delivery_v1.2/underframe_deflection/tools/cad_sampling/step_to_pointcloud.py rename alg/{delivery_v1.1 => delivery_v1.2}/underframe_deflection/visualization.py (100%) diff --git a/.gitignore b/.gitignore index ba15763..0e53501 100644 --- a/.gitignore +++ b/.gitignore @@ -143,5 +143,5 @@ alg/点云测距v1.1/train_measurement_system/results/ **/dist-info/ # 算法运行中间产物 -alg/delivery_v1.1/coupler_panel_release/runs/ -alg/delivery_v1.1/underframe_deflection/runs/ +alg/delivery_v1.2/coupler_panel_release/runs/ +alg/delivery_v1.2/underframe_deflection/runs/ diff --git a/alg/delivery_v1.1/coupler_panel_release/README.md b/alg/delivery_v1.1/coupler_panel_release/README.md deleted file mode 100644 index 45646ec..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# coupler-panel-validation - -车钩面板 CAD-STP 到点云测量验证系统。 - -从 CAD STP 文件出发,构造 GT、合成模拟点云、跑完整测量流水线, -对比 GT,输出实验报告与可视化。 - -## 一句话用法 - -```python -from coupler_panel import compute_coupler_measurement, load_point_cloud - -points = load_point_cloud("scan.ply") # (N, 3) ndarray, 单位 mm -result = compute_coupler_measurement(points) -print(result.theta_right_deg, result.theta_ref_deg) -print(result.dx_y_max_mm, result.dx_z_max_mm) -``` - -完整文档见 [docs/USAGE.md](docs/USAGE.md)。 - -## 安装 - -```bash -# 仅核心计算(numpy/scipy) -pip install -e . - -# 加上点云 IO + 去噪 + 配准 -pip install -e ".[cloud]" - -# 加上 STP 解析 -pip install -e ".[cad]" - -# 加上可视化 -pip install -e ".[viz]" - -# 全装 -pip install -e ".[all]" -``` - -## 命令行 - -```bash -# 1) 已有点云,直接测量 -coupler-measure --input-cloud scan.ply --out-dir runs/demo - -# 2) 从 CAD STP 生成模拟点云并测量 -coupler-measure --stp panel.stp --out-dir runs/from_cad - -# 3) 跑参数 sweep(采样×密度×噪声×变换矩阵) -coupler-measure --sweep configs/experiments/full_sweep.yaml --out-dir runs/sweep -``` - -## 坐标系约定 - -- x = 车体纵向 -- y = 车体宽度 -- z = 车体高度 -- 单位 mm -- 拟合面板法向 `n = (n_x, n_y, n_z)` 强制 `n_x > 0` - -详见 USAGE.md 第 5 节。 diff --git a/alg/delivery_v1.1/coupler_panel_release/USAGE.md b/alg/delivery_v1.1/coupler_panel_release/USAGE.md deleted file mode 100644 index 16c0327..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/USAGE.md +++ /dev/null @@ -1,376 +0,0 @@ -# coupler_panel — 集成开发者使用指南 - -> 高铁车钩面板偏转角与最大偏差测量算法 -> -> 主输入:**CAD STP + 点云 PLY + ROI JSON** -> -> 主输出:两个方向偏转角 `θ_right` / `θ_ref`、最大 x 向偏差 `dx_y_max` / `dx_z_max`,以及一份完整可序列化的 `report.json`。 - ---- - -## 1. 一句话集成 - -```python -from coupler_panel.pipeline import run_coupler_measurement_pipeline - -result = run_coupler_measurement_pipeline( - input_cloud = "scan.ply", # 你的测量点云(已在 CAD 帧) - cad_stp_path = "panel.stp", # 同款面板的 CAD STP - roi_json = "roi.json", # ROI 描述(不裁剪就传 mode=all) - out_dir = "runs/today", # 中间产物 + report.json 落盘目录 -) - -print(result.theta_right_deg, result.theta_ref_deg) # 两个偏转角 -print(result.dx_y_max_mm, result.dx_z_max_mm) # 最大 x 偏差 -``` - -返回 `CouplerMeasurement`,全字段语义见 §4。`out_dir` 下额外得到 8 个文件(含 `report.json`,便于排查 / 后处理)。 - ---- - -## 2. 安装 - -```bash -# 全量安装(推荐)— core + 点云 IO + STP 解析 + 可视化 + Excel -python -m pip install -r requirements.txt -pip install -e ".[all]" - -# 也可以按需取子集(见 pyproject.toml 的 extras): -pip install -e ".[cloud]" # 只要点云 + 去噪 -pip install -e ".[cad]" # 需要解析 STP -pip install -e ".[report]" # 需要 Excel 报表 -``` - -`requirements.txt` 锁定了端到端跑通过的版本(Python 3.10 验证)。 - ---- - -## 3. 三件输入的准备 - -### 3.1 CAD STP - -- 提供面板的 STEP 文件路径。 -- 算法用它做两件事: - 1. 从拓扑直接提 4 个定位孔 + 中心大孔 + 面板法向(写入 `01_gt.json`,作审计用) - 2. 把 CAD 法向作为 `panel_normal_seed` 喂给 hole-detection 的投影几何,加速并稳定 2D 网格化 -- **可以不传**(`cad_stp_path=None`)—— 此时 hole detection 走 PCA 自行估法向;适用于你已经预先把法向算好/扫描坐标系本来就规范的场景。建议生产里**始终传**,便于审计。 - -### 3.2 点云 PLY - -- 支持 `.ply` / `.pcd` / `.xyz` / `.txt` / `.csv`(PCD 与二进制 PLY 需要 open3d)。 -- 数组形状 `(N, 3)`,单位 **mm**。 -- **必须在 CAD 坐标系**: - - x = 车体纵向,y = 车体宽度,z = 车体高度 - - 算法默认面板法向 `n_x > 0` -- **不再做配准**(参见 §6 "为什么不配准")。这意味着上游扫描端外参必须已经标定 —— 把点云对齐到 CAD 帧是扫描软件的职责,不是本算法的职责。 -- 可以带颜色 / 法向等额外列;算法只读前 3 列。 - -### 3.3 ROI JSON - -定义"PLY 里哪些点要纳入后续算法",**轴对齐 bounding box(AABB)**格式: - -```json -{ - "x_min": -12500.0, - "x_max": 12500.0, - "y_min": 150.0, - "y_max": 4100.0, - "z_min": -1680.0, - "z_max": 200.0 -} -``` - -六个 key 全部必填,单位 mm,坐标系 CAD 帧。点 `(x, y, z)` 被保留的条件是: - -``` -x_min <= x <= x_max ∧ y_min <= y <= y_max ∧ z_min <= z <= z_max -``` - -- **想保留全部点云**:把 6 个边界写得比点云自身 bbox 还宽即可(不需要任何 mode 切换)。 -- **想不写文件**:传 `roi_json=None`,算法走"±∞" 默认(pass-through)。 -- **想程序内构造**:传 `roi_spec=ROISpec(x_min=..., ...)`。 - ---- - -## 4. 公共 API 详解 - -### 4.1 主入口 `run_coupler_measurement_pipeline` - -```python -def run_coupler_measurement_pipeline( - input_cloud : str | Path, # 必填:点云路径 - out_dir : str | Path, # 必填:输出目录(自动创建) - *, - config_path : str | Path | None = None, # 可选:自定义 YAML - profile : str = "auto", # 可选:preset profile - cad_stp_path: str | Path | None = None, # 推荐:CAD STP 路径 - roi_spec : ROISpec | None = None, # 二选一:内存 ROI 对象 - roi_json : str | Path | None = None, # 二选一:ROI JSON 路径 - progress : Callable[[str, float], None] | None = None, -) -> CouplerMeasurement -``` - -**算法链路(固定不可绕过)**: -``` -load_point_cloud → apply_roi → [optional CAD parse → panel_normal_seed] - → denoise_cloud → compute_coupler_measurement - ├─ validate - ├─ detect 4 holes - ├─ build reference frame - ├─ extract panel ROI - ├─ fit panel plane (RANSAC + SVD) - └─ compute metrics -``` - -**Profile**:传 `"auto"` 即可;若你的面板型号对应到一个特定 profile,可指定 `profile="lu1_af4"` 等(详见 `configs/profiles/`)。 - -**配置覆盖**: -```python -result = run_coupler_measurement_pipeline( - input_cloud="scan.ply", - out_dir="runs/x", - config_path={ - # 任何在 configs/default.yaml 里的字段都可以这样覆盖 - "denoise": {"steps": [{"type": "voxel", "voxel_mm": 0.3}]}, - "plane_fit": {"ransac_inlier_thresh_mm": 1.0}, - "metrics": {"right_axis_sign": -1}, - }, -) -``` - -### 4.2 返回值 `CouplerMeasurement` - -```python -@dataclass -class CouplerMeasurement: - # 版本号 - schema_version: str # 字段语义稳定性 → SCHEMA_VERSION - algorithm_version: str # 数值结果稳定性 → ALGORITHM_VERSION - - # === 6 个主测量量 === - theta_right_deg: float # 右偏转角 atan2(n_y, n_x),applied right_axis_sign - theta_ref_deg: float # 上偏转角 atan2(n_z, n_x) - D_y_max_mm: float # max |y_i - C_y| - D_z_max_mm: float # max |z_i - C_z| - dx_y_max_mm: float # |A| · D_y_max, A = -n_y/n_x - dx_z_max_mm: float # |B| · D_z_max, B = -n_z/n_x - - # === 几何 === - center_C: tuple[float, float, float] # 面板中心 (4 孔均值) - normal: tuple[float, float, float] # 拟合面板法向,n_x > 0 - - # === 详细子结构 === - holes: HoleSet # 检测到的 4 个定位孔 + 可选中心孔 - plane: PanelPlane # normal + offset + inlier_ratio + RMS - frame: ReferenceFrame # C + 参考面 z=C_z + 纵向中面 y=C_y - - # === 诊断 === - quality_flags: list[str] # 例如 'plane_inlier_ratio_low' - timings: list[StageTiming] # 每阶段秒数 + 峰值内存 - report: dict # 全量可序列化,与 report.json 一致 -``` - -```python -result.to_dict() # 完全 JSON 化的 dict(落 DB / 网络传输用) -``` - -### 4.3 异常 - -``` -CouplerError # 所有可预见异常的基类 -├── InvalidPointCloudError # 输入形状/dtype/NaN 非法 -│ └── UnsupportedFormatError # 文件后缀不支持 -├── InsufficientPointsError # 点数 < 200 或 ROI 后 < 100 -├── CADParseError # STP 解析失败 / 找不到 4 孔 -├── HoleDetectionError # 点云上 4 孔检测失败 -└── PlaneFitError # RANSAC 找不到平面 -``` - -非致命问题通过 `result.quality_flags` 暴露: - -| Flag | 含义 | -|---|---| -| `plane_inlier_ratio_low` | 平面拟合内点率低于 `plane_fit.min_inlier_ratio` | - -### 4.4 进度回调 - -```python -def on_progress(stage: str, fraction: float) -> None: - print(f"[{fraction:5.1%}] {stage}") - -result = run_coupler_measurement_pipeline(..., progress=on_progress) -``` - -阶段顺序:`validate → detect_holes → build_frame → extract_roi → fit_plane → metrics`。 - ---- - -## 5. ROI JSON schema 详细 - -**完整字段**(6 个 key,全部必填,单位 mm,坐标系 CAD 帧): - -```json -{ - "x_min": -12500.0, - "x_max": 12500.0, - "y_min": 150.0, - "y_max": 4100.0, - "z_min": -1680.0, - "z_max": 200.0 -} -``` - -**规则**: - -- 必须满足 `x_min < x_max` 且 `y_min < y_max` 且 `z_min < z_max`,否则 `load_roi_json` 抛 `ValueError` -- 不在 [min, max] 闭区间内的点被丢弃(用 `>=` 与 `<=`,包含边界) -- 多余的 key(例如 `comment`、`source`)会被忽略,方便挂元数据 - -**手动构造 ROI(不读文件)**: - -```python -from coupler_panel.roi_select import ROISpec, default_roi_spec - -# 全保留默认(六个轴都是 ±∞) -spec = default_roi_spec() - -# 自定义 bbox -spec = ROISpec( - x_min=-12000, x_max= 100, - y_min= -300, y_max= 1200, - z_min= -1000, z_max= 800, -) - -result = run_coupler_measurement_pipeline(..., roi_spec=spec) -``` - -**诊断输出**: - -每次运行后 `out_dir/roi.json` 写出实际生效的 spec + 应用诊断: - -```jsonc -{ - "spec": { "x_min": -12000.0, ... }, - "diagnostics": { - "kind": "aabb", - "n_in": 96662, - "n_out": 96662, - "kept_ratio": 1.0, - "bounds": { "x_min": -12000.0, ... } - } -} -``` - ---- - -## 6. 为什么主路径**不做配准** - -车钩面板上的 4 个定位孔与面板一起刚体旋转。如果用"扫到的 4 孔 → CAD 原始 4 孔"做 Kabsch 配准,数学上会**精确解出**"扫描器位姿误差 · 面板偏转"的复合刚体变换并反向作用 —— 把面板偏转随同位姿误差**一起消掉**,下游平面拟合再读不到偏转信号。 - -这是几何性质,不是实现 bug。 - -工程上,主算法假设扫描端外参标定让点云已经在 CAD 帧。如果你的设备做不到位姿标定,要么: -1. 在面板**外**装基准(不动)做 Kabsch -2. 用 ICP 或其他不依赖 4 孔的方法 - -研究模式(`coupler_panel.pipeline.run_from_cad_pipeline`)保留了 4 孔 Kabsch + 注入偏转 oracle 的能力,用于上限对照实验;生产集成请用 `run_coupler_measurement_pipeline`。 - ---- - -## 7. 落盘产物(`out_dir/`) - -| 文件 | 内容 | -|---|---| -| `config.resolved.yaml` | 解析合并后实际生效的完整配置 | -| `roi.json` | ROI spec + 应用诊断(n_in / n_out / kept_ratio) | -| `01_gt.json` | (传了 cad_stp_path 时)从 STP 提的审计用 GT | -| `02_sampled.ply` | ROI 裁剪后的点云 | -| `05_denoised.ply` | 去噪后点云(喂给测量算法的最终点云) | -| `10_metrics.json` | 等价于 `report.json` 的内部 report 部分 | -| `report.json` | **完整结果**,等于 `result.to_dict()`;schema 见 §4.2 | -| `images/` | 占位空目录(仅 `viz` 模块写图时会用) | - ---- - -## 8. 集成模板 - -```python -import logging -from pathlib import Path - -from coupler_panel.pipeline import run_coupler_measurement_pipeline -from coupler_panel.exceptions import CouplerError - -log = logging.getLogger(__name__) - - -def measure_coupler( - cloud_path: Path, - cad_stp: Path, - roi_json: Path, - out_dir: Path, -) -> dict | None: - """主集成入口。失败时返回 None;成功时返回 6 个测量量 + flags。""" - try: - result = run_coupler_measurement_pipeline( - input_cloud = cloud_path, - cad_stp_path = cad_stp, - roi_json = roi_json, - out_dir = out_dir, - progress = lambda s, f: log.debug("%s %.0f%%", s, f * 100), - ) - except CouplerError as exc: - log.error("coupler measurement failed: %s", exc) - return None - - if result.quality_flags: - log.warning("quality flags: %s", result.quality_flags) - - return { - "schema_version": result.schema_version, - "algorithm_version": result.algorithm_version, - "theta_right_deg": result.theta_right_deg, - "theta_ref_deg": result.theta_ref_deg, - "dx_y_max_mm": result.dx_y_max_mm, - "dx_z_max_mm": result.dx_z_max_mm, - "center_C_mm": list(result.center_C), - "normal": list(result.normal), - "flags": result.quality_flags, - } -``` - ---- - -## 9. 命令行(备用,非主集成路径) - -```bash -# 等价于 run_coupler_measurement_pipeline(input_cloud=..., out_dir=...) -coupler-measure --input-cloud scan.ply --out-dir runs/today -``` - -> 当前 CLI 暂未把 `--cad` 与 `--roi` 暴露出来(设计上 CLI 主要给研究流程用);集成方请直接用 Python API。 - ---- - -## 10. 版本兼容性 - -| 标识 | 在哪 | 含义 | -|---|---|---| -| `coupler_panel.__version__` | `coupler_panel._version` | 包版本,遵循语义化版本 | -| `coupler_panel.ALGORITHM_VERSION` | 同上 | 算法版本;升级算法时递增,**数值结果可能变化** | -| `coupler_panel.SCHEMA_VERSION` | 同上 | `CouplerMeasurement` / `report.json` 字段结构;只在破坏性变更时递增;**新增可选字段不递增** | - -集成建议: -1. 落库时把 `algorithm_version` 写进每条记录,便于历史复现 -2. 反序列化 `report.json` 时校验 `schema_version` 主版本号 - ---- - -## 11. 坐标 & 单位速查 - -| 量 | 单位 | 约定 | -|---|---|---| -| 点云坐标 | mm | x=纵向、y=宽度、z=高度 | -| 角度 | 度 (deg) | `θ_right` = atan2(n_y, n_x)、`θ_ref` = atan2(n_z, n_x) | -| 法向 | 单位向量 | 强制 `n_x > 0` | -| `right_axis_sign` | ±1 | 如果车体"右"是 +y 留 1;如果是 -y 则配置 -1 | diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/density_sweep.yaml b/alg/delivery_v1.1/coupler_panel_release/configs/experiments/density_sweep.yaml deleted file mode 100644 index a502e97..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/density_sweep.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Parameter sweep over point cloud density. -matrix: - sampling.spacing_mm: - - 0.25 - - 0.5 - - 1.0 - - 2.0 - - 5.0 -fixed: - sampling.method: poisson_disk - noise.sigma_mm: 0.5 - transform.rotation_deg: 3.0 - transform.translation_mm: 5.0 diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/full_sweep.yaml b/alg/delivery_v1.1/coupler_panel_release/configs/experiments/full_sweep.yaml deleted file mode 100644 index aac5ddf..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/full_sweep.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Full cartesian product. WARNING: 4 * 5 * 8 * 4 * 4 = 2560 runs. -# Use only when you have time and disk. -matrix: - sampling.method: - - uniform_area - - poisson_disk - - adaptive - - grid_spacing - sampling.spacing_mm: - - 0.25 - - 0.5 - - 1.0 - - 2.0 - - 5.0 - noise.sigma_mm: - - 0.0 - - 0.25 - - 0.5 - - 1.0 - - 2.0 - - 4.0 - - 10.0 - - 20.0 - transform.rotation_deg: - - 1.0 - - 3.0 - - 5.0 - - 10.0 - transform.translation_mm: - - 0.5 - - 1.0 - - 5.0 - - 10.0 -fixed: {} diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_af4_sweep.yaml b/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_af4_sweep.yaml deleted file mode 100644 index 63ccbda..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_af4_sweep.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Sweep matrix for the lu1-AF4 coupler panel STP. Total 9 runs. -stp_path: D:/myDocuments/PKU/high_speed_train_proj/lu1-M000004835653_AF4_ASMP072708187_N_AF4.stp - -matrix: - noise.sigma_mm: [0.0, 0.5, 2.0] - transform.rotation_deg: [1.0, 3.0, 10.0] - -fixed: - sampling.method: poisson_disk - sampling.spacing_mm: null - sampling.num_points: 500000 - sampling.seed: 42 - scan_simulator.enabled: true - scan_simulator.view_origin: [10000.0, 0.0, 0.0] - scan_simulator.hpr_radius_mm: 200000.0 - noise.seed: 43 - transform.translation_mm: 5.0 - transform.seed: 44 - cad.locating_hole_radius_mm: [14.0, 18.0] - cad.central_hole_radius_min_mm: 80.0 - detection.grid_pixel_mm: 2.0 - detection.fill_bg_below_mm2: 100.0 - detection.hole_min_area_mm2: 600.0 - detection.hole_max_area_mm2: 5000.0 - detection.locating_hole_radius_mm: [14.0, 22.0] - detection.panel_min_area_ratio: 0.15 - detection.rectangle_aspect_tol: 0.40 - detection.circle_fit.method: algebraic_then_lsq - detection.circle_fit.inlier_residual_mm: 1.5 - roi.hole_dilate_mm: 5.0 - roi.central_hole_dilate_mm: 10.0 - roi.edge_buffer_mm: 8.0 - roi.chamfer_normal_dot_min: 0.80 - roi.outlier_z_score: 3.0 - plane_fit.ransac_iterations: 300 - plane_fit.ransac_inlier_thresh_mm: auto - plane_fit.min_inlier_ratio: 0.5 - plane_fit.enforce_nx_positive: true - metrics.right_axis_sign: 1 - denoise.steps: - - type: passthrough - bbox_padding_mm: 50.0 - - type: voxel - voxel_mm: 1.0 - - type: statistical - nb_neighbors: 20 - std_ratio: 2.5 diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_deflection_sweep.yaml b/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_deflection_sweep.yaml deleted file mode 100644 index d034b5a..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_deflection_sweep.yaml +++ /dev/null @@ -1,74 +0,0 @@ -# Deflection-recovery sweep: vary the INJECTED panel-deflection angle and -# verify the algorithm reads it back. Single-axis at a time so the -# response of theta_right and theta_ref can be analysed independently. -stp_path: D:/myDocuments/PKU/high_speed_train_proj/lu1-M000004835653_AF4_ASMP072708187_N_AF4.stp - -matrix: - panel_deflection.theta_right_deg: [0.0, 0.1, 0.3, 0.5, 1.0, 2.0] - -fixed: - panel_deflection.enabled: true - panel_deflection.theta_ref_deg: 0.0 - - sampling.method: poisson_disk - sampling.spacing_mm: null - sampling.num_points: 500000 - sampling.seed: 42 - - scan_simulator.enabled: true - scan_simulator.view_origin: [10000.0, 0.0, 0.0] - scan_simulator.hpr_radius_mm: 200000.0 - - noise.enabled: true - noise.sigma_mm: 0.05 - noise.mode: isotropic - noise.seed: 43 - - transform.enabled: true - transform.rotation_deg: 0.5 - transform.translation_mm: 2.0 - transform.rotation_axis: random - transform.translation_dir: random - transform.seed: 44 - - cad.locating_hole_radius_mm: [14.0, 18.0] - cad.central_hole_radius_min_mm: 80.0 - cad.linear_deflection_mm: 0.5 - cad.angular_deflection_rad: 0.1 - - detection.panel_normal_seed: pca - detection.grid_pixel_mm: 2.0 - detection.fill_bg_below_mm2: 100.0 - detection.hole_min_area_mm2: 600.0 - detection.hole_max_area_mm2: 5000.0 - detection.locating_hole_radius_mm: [14.0, 22.0] - detection.panel_min_area_ratio: 0.15 - detection.rectangle_aspect_tol: 0.40 - detection.circle_fit.method: algebraic_then_lsq - detection.circle_fit.inlier_residual_mm: 1.5 - - roi.hole_dilate_mm: 5.0 - roi.central_hole_dilate_mm: 10.0 - roi.edge_buffer_mm: 8.0 - roi.chamfer_normal_dot_min: 0.80 - roi.outlier_z_score: 3.0 - - plane_fit.ransac_iterations: 300 - plane_fit.ransac_inlier_thresh_mm: auto - plane_fit.min_inlier_ratio: 0.5 - plane_fit.enforce_nx_positive: true - - metrics.right_axis_sign: 1 - metrics.angle_clamp_deg: 0.0 - - registration.method: kabsch_four_holes - registration.refuse_if_residual_above_mm: 5.0 - - denoise.steps: - - type: passthrough - bbox_padding_mm: 50.0 - - type: voxel - voxel_mm: 0.5 - - type: statistical - nb_neighbors: 20 - std_ratio: 2.5 diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_density_sweep.yaml b/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_density_sweep.yaml deleted file mode 100644 index 87e6f02..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_density_sweep.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Point-cloud density sweep: fix deflection + noise, vary num_points to -# show how many points the algorithm needs. -stp_path: D:/myDocuments/PKU/high_speed_train_proj/lu1-M000004835653_AF4_ASMP072708187_N_AF4.stp - -matrix: - sampling.num_points: [200000, 350000, 500000, 750000, 1000000] - -fixed: - panel_deflection.enabled: true - panel_deflection.theta_right_deg: 0.5 - panel_deflection.theta_ref_deg: 0.3 - - sampling.method: poisson_disk - sampling.spacing_mm: null - sampling.seed: 42 - - scan_simulator.enabled: true - scan_simulator.view_origin: [10000.0, 0.0, 0.0] - scan_simulator.hpr_radius_mm: 200000.0 - - noise.enabled: true - noise.sigma_mm: 0.05 - noise.mode: isotropic - noise.seed: 43 - - transform.enabled: true - transform.rotation_deg: 0.5 - transform.translation_mm: 2.0 - transform.rotation_axis: random - transform.translation_dir: random - transform.seed: 44 - - cad.locating_hole_radius_mm: [14.0, 18.0] - cad.central_hole_radius_min_mm: 80.0 - cad.linear_deflection_mm: 0.5 - cad.angular_deflection_rad: 0.1 - - detection.panel_normal_seed: pca - detection.grid_pixel_mm: 2.0 - detection.fill_bg_below_mm2: 100.0 - detection.hole_min_area_mm2: 600.0 - detection.hole_max_area_mm2: 5000.0 - detection.locating_hole_radius_mm: [14.0, 28.0] # wider window for low-density tests - detection.panel_min_area_ratio: 0.15 - detection.rectangle_aspect_tol: 0.40 - detection.circle_fit.method: algebraic_then_lsq - detection.circle_fit.inlier_residual_mm: 1.5 - - roi.hole_dilate_mm: 5.0 - roi.central_hole_dilate_mm: 10.0 - roi.edge_buffer_mm: 8.0 - roi.chamfer_normal_dot_min: 0.80 - roi.outlier_z_score: 3.0 - - plane_fit.ransac_iterations: 300 - plane_fit.ransac_inlier_thresh_mm: auto - plane_fit.min_inlier_ratio: 0.5 - plane_fit.enforce_nx_positive: true - - metrics.right_axis_sign: 1 - metrics.angle_clamp_deg: 0.0 - - registration.method: kabsch_four_holes - registration.refuse_if_residual_above_mm: 5.0 - - denoise.steps: - - type: passthrough - bbox_padding_mm: 50.0 - - type: voxel - voxel_mm: 0.5 - - type: statistical - nb_neighbors: 20 - std_ratio: 2.5 diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_noise_sweep.yaml b/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_noise_sweep.yaml deleted file mode 100644 index 0a97453..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_noise_sweep.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Noise sweep: fix the panel deflection at (theta_right=0.5°, theta_ref=0.3°) -# and vary Gaussian noise sigma to characterise robustness. -stp_path: D:/myDocuments/PKU/high_speed_train_proj/lu1-M000004835653_AF4_ASMP072708187_N_AF4.stp - -matrix: - noise.sigma_mm: [0.0, 0.025, 0.05, 0.1, 0.2, 0.5] - -fixed: - panel_deflection.enabled: true - panel_deflection.theta_right_deg: 0.5 - panel_deflection.theta_ref_deg: 0.3 - - sampling.method: poisson_disk - sampling.spacing_mm: null - sampling.num_points: 500000 - sampling.seed: 42 - - scan_simulator.enabled: true - scan_simulator.view_origin: [10000.0, 0.0, 0.0] - scan_simulator.hpr_radius_mm: 200000.0 - - noise.enabled: true - noise.mode: isotropic - noise.seed: 43 - - transform.enabled: true - transform.rotation_deg: 0.5 - transform.translation_mm: 2.0 - transform.rotation_axis: random - transform.translation_dir: random - transform.seed: 44 - - cad.locating_hole_radius_mm: [14.0, 18.0] - cad.central_hole_radius_min_mm: 80.0 - cad.linear_deflection_mm: 0.5 - cad.angular_deflection_rad: 0.1 - - detection.panel_normal_seed: pca - detection.grid_pixel_mm: 2.0 - detection.fill_bg_below_mm2: 100.0 - detection.hole_min_area_mm2: 600.0 - detection.hole_max_area_mm2: 5000.0 - detection.locating_hole_radius_mm: [14.0, 22.0] - detection.panel_min_area_ratio: 0.15 - detection.rectangle_aspect_tol: 0.40 - detection.circle_fit.method: algebraic_then_lsq - detection.circle_fit.inlier_residual_mm: 1.5 - - roi.hole_dilate_mm: 5.0 - roi.central_hole_dilate_mm: 10.0 - roi.edge_buffer_mm: 8.0 - roi.chamfer_normal_dot_min: 0.80 - roi.outlier_z_score: 3.0 - - plane_fit.ransac_iterations: 300 - plane_fit.ransac_inlier_thresh_mm: auto - plane_fit.min_inlier_ratio: 0.5 - plane_fit.enforce_nx_positive: true - - metrics.right_axis_sign: 1 - metrics.angle_clamp_deg: 0.0 - - registration.method: kabsch_four_holes - registration.refuse_if_residual_above_mm: 5.0 - - denoise.steps: - - type: passthrough - bbox_padding_mm: 50.0 - - type: voxel - voxel_mm: 0.5 - - type: statistical - nb_neighbors: 20 - std_ratio: 2.5 diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_sampling_sweep.yaml b/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_sampling_sweep.yaml deleted file mode 100644 index e28a5de..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/lu1_sampling_sweep.yaml +++ /dev/null @@ -1,78 +0,0 @@ -# Sampling-strategy sweep: 4 methods × same other settings, so the saved -# intermediates per run//02_sampled.ply, 03_noisy.ply, ... can be -# compared apples-to-apples. -stp_path: D:/myDocuments/PKU/high_speed_train_proj/lu1-M000004835653_AF4_ASMP072708187_N_AF4.stp - -matrix: - sampling.method: - - uniform_area - - poisson_disk - - adaptive - - grid_spacing - -fixed: - panel_deflection.enabled: true - panel_deflection.theta_right_deg: 0.5 - panel_deflection.theta_ref_deg: 0.3 - - sampling.spacing_mm: null - sampling.num_points: 500000 - sampling.seed: 42 - - scan_simulator.enabled: true - scan_simulator.view_origin: [10000.0, 0.0, 0.0] - scan_simulator.hpr_radius_mm: 200000.0 - - noise.enabled: true - noise.sigma_mm: 0.05 - noise.mode: isotropic - noise.seed: 43 - - transform.enabled: true - transform.rotation_deg: 0.5 - transform.translation_mm: 2.0 - transform.rotation_axis: random - transform.translation_dir: random - transform.seed: 44 - - cad.locating_hole_radius_mm: [14.0, 18.0] - cad.central_hole_radius_min_mm: 80.0 - cad.linear_deflection_mm: 0.5 - cad.angular_deflection_rad: 0.1 - - detection.panel_normal_seed: pca - detection.grid_pixel_mm: 2.0 - detection.fill_bg_below_mm2: 100.0 - detection.hole_min_area_mm2: 600.0 - detection.hole_max_area_mm2: 5000.0 - detection.locating_hole_radius_mm: [14.0, 22.0] - detection.panel_min_area_ratio: 0.15 - detection.rectangle_aspect_tol: 0.40 - detection.circle_fit.method: algebraic_then_lsq - detection.circle_fit.inlier_residual_mm: 1.5 - - roi.hole_dilate_mm: 5.0 - roi.central_hole_dilate_mm: 10.0 - roi.edge_buffer_mm: 8.0 - roi.chamfer_normal_dot_min: 0.80 - roi.outlier_z_score: 3.0 - - plane_fit.ransac_iterations: 300 - plane_fit.ransac_inlier_thresh_mm: auto - plane_fit.min_inlier_ratio: 0.5 - plane_fit.enforce_nx_positive: true - - metrics.right_axis_sign: 1 - metrics.angle_clamp_deg: 0.0 - - registration.method: kabsch_four_holes - registration.refuse_if_residual_above_mm: 5.0 - - denoise.steps: - - type: passthrough - bbox_padding_mm: 50.0 - - type: voxel - voxel_mm: 0.5 - - type: statistical - nb_neighbors: 20 - std_ratio: 2.5 diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/noise_sweep.yaml b/alg/delivery_v1.1/coupler_panel_release/configs/experiments/noise_sweep.yaml deleted file mode 100644 index 0eed81c..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/noise_sweep.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Parameter sweep over Gaussian noise sigma. -matrix: - noise.sigma_mm: - - 0.0 - - 0.25 - - 0.5 - - 1.0 - - 2.0 - - 4.0 - - 10.0 - - 20.0 -fixed: - sampling.method: poisson_disk - sampling.spacing_mm: 1.0 - transform.rotation_deg: 3.0 - transform.translation_mm: 5.0 diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/sampling_sweep.yaml b/alg/delivery_v1.1/coupler_panel_release/configs/experiments/sampling_sweep.yaml deleted file mode 100644 index e1b7f40..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/sampling_sweep.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Parameter sweep over sampling methods (other knobs fixed at default). -matrix: - sampling.method: - - uniform_area - - poisson_disk - - adaptive - - grid_spacing -fixed: - sampling.spacing_mm: 1.0 - noise.sigma_mm: 0.5 - transform.rotation_deg: 3.0 - transform.translation_mm: 5.0 diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/transform_sweep.yaml b/alg/delivery_v1.1/coupler_panel_release/configs/experiments/transform_sweep.yaml deleted file mode 100644 index eecdce1..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/configs/experiments/transform_sweep.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Parameter sweep over GT rigid transform magnitude. -matrix: - transform.rotation_deg: - - 1.0 - - 3.0 - - 5.0 - - 10.0 - transform.translation_mm: - - 0.5 - - 1.0 - - 5.0 - - 10.0 -fixed: - sampling.method: poisson_disk - sampling.spacing_mm: 1.0 - noise.sigma_mm: 0.5 diff --git a/alg/delivery_v1.1/coupler_panel_release/requirements.txt b/alg/delivery_v1.1/coupler_panel_release/requirements.txt deleted file mode 100644 index 6da3bcf..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/requirements.txt +++ /dev/null @@ -1,52 +0,0 @@ -# coupler-panel-validation — locked dependency list. -# -# Versions in this file are the ones the package has been *end-to-end -# verified* against on Python 3.10 (Windows 11). For a clean install use: -# -# python -m pip install -r requirements.txt -# pip install -e . -# -# If you only need a subset of the functionality, use the optional -# extras declared in pyproject.toml instead: -# -# pip install -e ".[cloud]" # core + point-cloud IO + denoise -# pip install -e ".[cad]" # core + STP parsing + GT extraction -# pip install -e ".[viz]" # core + matplotlib diagnostics -# pip install -e ".[report]" # core + Excel batch reporting -# pip install -e ".[all]" # everything above -# -# --------------------------------------------------------------------------- -# Core compute — required even for the thinnest integration deployment. -# --------------------------------------------------------------------------- -numpy==1.26.4 -scipy==1.15.3 -pyyaml==6.0.2 -tqdm==4.65.0 - -# --------------------------------------------------------------------------- -# Point-cloud IO + denoise helpers (PLY/PCD read & write, voxel/SOR/ROR, -# normal-via-PCA). Required for the main pipeline; the package will fall -# back to ASCII-only PLY reading without open3d, which is not enough for -# the binary PLYs produced by the data generator. -# --------------------------------------------------------------------------- -open3d==0.19.0 -trimesh==4.12.2 - -# --------------------------------------------------------------------------- -# STP / STEP parsing — required if you pass cad_stp_path. (Skip this group -# if you don't have CAD STP at integration time and instead pre-compute -# the panel-normal seed yourself.) -# --------------------------------------------------------------------------- -cadquery-ocp==7.8.1.1.post1 - -# --------------------------------------------------------------------------- -# Diagnostic PNG renders + offscreen 3D screenshots — only used by -# coupler_panel.viz.render_all (optional). -# --------------------------------------------------------------------------- -matplotlib==3.10.8 - -# --------------------------------------------------------------------------- -# Excel batch-eval reporting — required if you use -# coupler_panel.reporting.write_excel_report or examples/run_batch_eval.py. -# --------------------------------------------------------------------------- -openpyxl==3.1.2 diff --git a/alg/delivery_v1.1/coupler_panel_release/test_data/01_gt.json b/alg/delivery_v1.1/coupler_panel_release/test_data/01_gt.json deleted file mode 100644 index 5f2ef58..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/test_data/01_gt.json +++ /dev/null @@ -1,1907 +0,0 @@ -{ - "holes": { - "locating": [ - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - } - ], - "central": { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "radius_mm": 100.0, - "fit_residual_rms_mm": 0.0, - "role": "central" - } - }, - "plane": { - "normal": [ - 1.0, - -0.0, - -0.0 - ], - "point_on_plane": [ - 0.0, - 0.9999987923150044, - 9.878999946977274e-07 - ], - "d_offset": -0.0, - "inlier_ratio": 1.0, - "inlier_residual_rms_mm": 0.0 - }, - "frame": { - "center_C": [ - 0.0, - 0.9999987923150044, - 9.878999946977274e-07 - ], - "line_direction": [ - 1.0, - 0.0, - 0.0 - ], - "reference_plane_z": 9.878999946977274e-07, - "longitudinal_mid_y": 0.9999987923150044 - }, - "raw_circles": [ - { - "center": [ - 0.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - 0.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 136.0, - -699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - 0.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 227.0000009879 - ], - "axis": [ - 0.0, - -1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -226.9999990121 - ], - "axis": [ - -0.0, - 1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -76.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -76.0, - 136.0, - -699.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 227.0000009879 - ], - "axis": [ - 0.0, - -1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - 190.0000009879 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - 190.0000009879 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - -189.9999990121 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - -189.9999990121 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -226.9999990121 - ], - "axis": [ - -0.0, - 1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 136.0, - -699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 136.0, - -699.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.00000000000001, - 180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -39.99999999999999, - 180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.00000000000001, - -180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 39.99999999999999, - -180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - } - ] -} \ No newline at end of file diff --git a/alg/delivery_v1.1/coupler_panel_release/test_data/10_metrics.json b/alg/delivery_v1.1/coupler_panel_release/test_data/10_metrics.json deleted file mode 100644 index f73d814..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/test_data/10_metrics.json +++ /dev/null @@ -1,2512 +0,0 @@ -{ - "stp": "D:\\myDocuments\\PKU\\high_speed_train_proj\\lu1-M000004835653_AF4_ASMP072708187_N_AF4.stp", - "panel_deflection_injected": { - "theta_right_deg": 0.5, - "theta_ref_deg": 0.3, - "R": [ - [ - 0.9999482158335473, - -0.008726535498373935, - -0.0052357644619607695 - ], - [ - 0.008726415877184502, - 0.9999619230641713, - -4.569182424308496e-05 - ], - [ - 0.00523596383141958, - 0.0, - 0.9999862922474267 - ] - ] - }, - "scan_pose_perturbation": [ - [ - 0.9999979783441972, - -0.0019163717426360569, - 0.0006089555505671704, - 1.4967337082610368 - ], - [ - 0.0019214737281491043, - 0.9999621035526889, - -0.008491136389996271, - 1.0855384993902797 - ], - [ - -0.000592660299474585, - 0.00849228931593314, - 0.9999637642314565, - 0.7624922116957428 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], - "registration": { - "matrix": [ - [ - 0.9999991698935765, - 0.0011435168147077325, - -0.0005937856954023229, - -1.42312474833891 - ], - [ - -0.001138363345940219, - 0.999962302539521, - 0.008607997952637694, - -1.3922014879928315 - ], - [ - 0.0006036067015895135, - -0.008607314863212156, - 0.9999627742020176, - -1.1453314127968168 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], - "rmse_mm": 1.279801346259311, - "max_residual_mm": 1.7348149080058168 - }, - "measured": { - "schema_version": "1.0", - "algorithm_version": "0.1.0", - "theta_right_deg": 0.544354519421902, - "theta_ref_deg": 0.2997466331976193, - "D_y_max_mm": 94.08063004084244, - "D_z_max_mm": 172.21417148158895, - "dx_y_max_mm": 0.893866137444892, - "dx_z_max_mm": 0.9009579671326199, - "center_C": [ - -0.007647024901048821, - 0.9158090934040075, - -0.06590994857895538 - ], - "normal": [ - 0.9999411851861564, - 0.00950050573095007, - 0.005231305703281311 - ], - "holes": { - "locating": [ - { - "center": [ - 1.8237358542499582, - -101.1715281534813, - -179.67591341367824 - ], - "radius_mm": 19.34475104619178, - "fit_residual_rms_mm": 2.2818699650684904, - "role": "locating" - }, - { - "center": [ - 0.05005657933939589, - 102.44798627110123, - -180.30256731680095 - ], - "radius_mm": 19.71317672500548, - "fit_residual_rms_mm": 3.220214937115534, - "role": "locating" - }, - { - "center": [ - -1.820222447435446, - 100.64818807060959, - 179.87714825154933 - ], - "radius_mm": 18.68294341110619, - "fit_residual_rms_mm": 2.0067435575879573, - "role": "locating" - }, - { - "center": [ - -0.08415808575810335, - -98.26140981461349, - 179.83769268461404 - ], - "radius_mm": 20.21695365907737, - "fit_residual_rms_mm": 2.5642764666909246, - "role": "locating" - } - ], - "central": null - }, - "plane": { - "normal": [ - 0.9999411851861564, - 0.00950050573095007, - 0.005231305703281311 - ], - "point_on_plane": [ - 0.07084859134750632, - -0.6075903692234997, - 0.7670896792673196 - ], - "d_offset": -0.06908488922999657, - "inlier_ratio": 1.0, - "inlier_residual_rms_mm": 0.04977002501374635 - }, - "frame": { - "center_C": [ - -0.007647024901048821, - 0.9158090934040075, - -0.06590994857895538 - ], - "line_direction": [ - 1.0, - 0.0, - 0.0 - ], - "reference_plane_z": -0.06590994857895538, - "longitudinal_mid_y": 0.9158090934040075 - }, - "quality_flags": [], - "timings": [ - { - "stage": "detect_holes", - "seconds": 2.090514300041832, - "peak_kib": 32527.3603515625 - }, - { - "stage": "build_frame", - "seconds": 4.1000021155923605e-05, - "peak_kib": 0.0 - }, - { - "stage": "extract_roi", - "seconds": 0.33327039994765073, - "peak_kib": 17444.6552734375 - }, - { - "stage": "fit_plane", - "seconds": 0.07231109996791929, - "peak_kib": 626.2568359375 - }, - { - "stage": "metrics", - "seconds": 4.96999709866941e-05, - "peak_kib": 0.0 - } - ], - "report": { - "schema_version": "1.0", - "algorithm_version": "0.1.0", - "config": { - "schema_version": "1.0", - "cad": { - "linear_deflection_mm": 0.5, - "angular_deflection_rad": 0.1, - "locating_hole_radius_mm": [ - 14.0, - 18.0 - ], - "central_hole_radius_min_mm": 80.0, - "rectangle_aspect_tol": 0.15 - }, - "sampling": { - "method": "grid_spacing", - "spacing_mm": null, - "num_points": 500000, - "seed": 42, - "adaptive": { - "curvature_weight": 1.0, - "min_spacing_factor": 0.25, - "max_spacing_factor": 4.0 - } - }, - "scan_simulator": { - "enabled": true, - "view_origin": [ - 10000.0, - 0.0, - 0.0 - ], - "hpr_radius_mm": 200000.0 - }, - "panel_deflection": { - "enabled": true, - "theta_right_deg": 0.5, - "theta_ref_deg": 0.3 - }, - "noise": { - "enabled": true, - "sigma_mm": 0.05, - "mode": "isotropic", - "seed": 43 - }, - "transform": { - "enabled": true, - "rotation_deg": 0.5, - "rotation_axis": "random", - "translation_mm": 2.0, - "translation_dir": "random", - "seed": 44 - }, - "denoise": { - "steps": [ - { - "type": "passthrough", - "bbox_padding_mm": 50.0 - }, - { - "type": "voxel", - "voxel_mm": 0.5 - }, - { - "type": "statistical", - "nb_neighbors": 20, - "std_ratio": 2.5 - } - ], - "smoothing": { - "enabled": false, - "search_radius_mm": 2.0, - "polynomial_order": 2 - } - }, - "registration": { - "method": "kabsch_four_holes", - "pre_align": "pca_centroid", - "refuse_if_residual_above_mm": 5.0 - }, - "detection": { - "panel_normal_seed": "pca", - "panel_normal_pca_kth": 1, - "grid_pixel_mm": 2.0, - "panel_min_area_ratio": 0.15, - "hole_min_area_mm2": 600.0, - "hole_max_area_mm2": 5000.0, - "circle_fit": { - "method": "algebraic_then_lsq", - "inlier_residual_mm": 1.5 - }, - "select_top_k_by_radius_match": 4, - "rectangle_aspect_tol": 0.4, - "fill_bg_below_mm2": 100.0, - "locating_hole_radius_mm": [ - 14.0, - 22.0 - ] - }, - "roi": { - "hole_dilate_mm": 5.0, - "central_hole_dilate_mm": 10.0, - "edge_buffer_mm": 8.0, - "chamfer_normal_dot_min": 0.8, - "outlier_z_score": 3.0 - }, - "plane_fit": { - "method": "ransac_svd", - "ransac_iterations": 300, - "ransac_inlier_thresh_mm": 0.5, - "min_inlier_ratio": 0.5, - "enforce_nx_positive": true - }, - "metrics": { - "right_axis_sign": 1, - "angle_clamp_deg": 0.0 - }, - "artifacts": { - "save_intermediate": true, - "ply_format": "binary", - "image_dpi": 150, - "heatmap_colormap": "viridis" - } - }, - "n_points_in": 184135, - "detection": { - "n_cavities": 4, - "n_circle_fits": 4, - "selection": { - "radius_score": 0.02869114494902652, - "rectangle_score": 0.025074534730867147, - "score": 0.053765679679893666, - "radii_mean": 19.489456210345203, - "radii_std": 0.5591748131087193, - "selected_indices": [ - 0, - 1, - 3, - 2 - ], - "selected_radii": [ - 19.34475104619178, - 19.71317672500548, - 18.68294341110619, - 20.21695365907737 - ] - } - }, - "roi": { - "n_in": 184135, - "n_out": 9855, - "stages": [ - { - "name": "rectangle", - "n_kept": 10009 - }, - { - "name": "locating_holes", - "n_kept": 9855 - }, - { - "name": "chamfer_normal", - "n_kept": 9855 - }, - { - "name": "z_score", - "n_kept": 9855 - } - ] - }, - "plane": { - "normal": [ - 0.9999411851861564, - 0.00950050573095007, - 0.005231305703281311 - ], - "point_on_plane": [ - 0.07084859134750632, - -0.6075903692234997, - 0.7670896792673196 - ], - "d_offset": -0.06908488922999657, - "inlier_ratio": 1.0, - "inlier_residual_rms_mm": 0.04977002501374635 - }, - "frame": { - "center_C": [ - -0.007647024901048821, - 0.9158090934040075, - -0.06590994857895538 - ], - "reference_plane_z": -0.06590994857895538, - "longitudinal_mid_y": 0.9158090934040075 - }, - "metrics": { - "theta_right_deg": 0.544354519421902, - "theta_ref_deg": 0.2997466331976193, - "D_y_max_mm": 94.08063004084244, - "D_z_max_mm": 172.21417148158895, - "dx_y_max_mm": 0.893866137444892, - "dx_z_max_mm": 0.9009579671326199, - "A": -0.009501064534291972, - "B": -0.0052316133996495135 - }, - "quality_flags": [], - "timings": [ - { - "stage": "detect_holes", - "seconds": 2.090514300041832, - "peak_kib": 32527.3603515625 - }, - { - "stage": "build_frame", - "seconds": 4.1000021155923605e-05, - "peak_kib": 0.0 - }, - { - "stage": "extract_roi", - "seconds": 0.33327039994765073, - "peak_kib": 17444.6552734375 - }, - { - "stage": "fit_plane", - "seconds": 0.07231109996791929, - "peak_kib": 626.2568359375 - }, - { - "stage": "metrics", - "seconds": 4.96999709866941e-05, - "peak_kib": 0.0 - } - ] - } - }, - "gt_cad": { - "holes": { - "locating": [ - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - } - ], - "central": { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "radius_mm": 100.0, - "fit_residual_rms_mm": 0.0, - "role": "central" - } - }, - "plane": { - "normal": [ - 1.0, - -0.0, - -0.0 - ], - "point_on_plane": [ - 0.0, - 0.9999987923150044, - 9.878999946977274e-07 - ], - "d_offset": -0.0, - "inlier_ratio": 1.0, - "inlier_residual_rms_mm": 0.0 - }, - "frame": { - "center_C": [ - 0.0, - 0.9999987923150044, - 9.878999946977274e-07 - ], - "line_direction": [ - 1.0, - 0.0, - 0.0 - ], - "reference_plane_z": 9.878999946977274e-07, - "longitudinal_mid_y": 0.9999987923150044 - }, - "raw_circles": [ - { - "center": [ - 0.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - 0.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 136.0, - -699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - 0.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 227.0000009879 - ], - "axis": [ - 0.0, - -1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -226.9999990121 - ], - "axis": [ - -0.0, - 1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -76.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -76.0, - 136.0, - -699.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 227.0000009879 - ], - "axis": [ - 0.0, - -1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - 190.0000009879 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - 190.0000009879 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - -189.9999990121 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - -189.9999990121 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -226.9999990121 - ], - "axis": [ - -0.0, - 1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 136.0, - -699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 136.0, - -699.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.00000000000001, - 180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -39.99999999999999, - 180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.00000000000001, - -180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 39.99999999999999, - -180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - } - ] - }, - "gt_after_deflection": { - "normal": [ - 0.9999482158335473, - 0.008726415877184502, - 0.00523596383141958 - ], - "locating_centers": [ - [ - 1.8063646228583214, - -98.98800706265835, - -179.99753161665035 - ], - [ - 0.061057523183796314, - 101.00437755014592, - -179.99753161665035 - ], - [ - -1.8238176831220807, - 100.98792849341841, - 179.99753359242328 - ], - [ - -0.07851058344755557, - -99.00445611938586, - 179.99753359242328 - ] - ] - }, - "gt_metrics": { - "theta_right_deg": 0.5, - "theta_ref_deg": 0.30001142330692215, - "D_y_max_mm": 100.00441683476589, - "D_z_max_mm": 179.99753260453681, - "dx_y_max_mm": 0.8727253242089344, - "dx_z_max_mm": 0.9425093775246102, - "A": -0.00872686779075879, - "B": -0.005236234985483653 - }, - "diff": { - "theta_right_deg": 0.04435451942190205, - "theta_ref_deg": -0.0002647901093028593, - "D_y_max_mm": -5.923786793923455, - "D_z_max_mm": -7.783361122947866, - "dx_y_max_mm": 0.021140813235957512, - "dx_z_max_mm": -0.041551410391990284 - }, - "config": { - "schema_version": "1.0", - "cad": { - "linear_deflection_mm": 0.5, - "angular_deflection_rad": 0.1, - "locating_hole_radius_mm": [ - 14.0, - 18.0 - ], - "central_hole_radius_min_mm": 80.0, - "rectangle_aspect_tol": 0.15 - }, - "sampling": { - "method": "grid_spacing", - "spacing_mm": null, - "num_points": 500000, - "seed": 42, - "adaptive": { - "curvature_weight": 1.0, - "min_spacing_factor": 0.25, - "max_spacing_factor": 4.0 - } - }, - "scan_simulator": { - "enabled": true, - "view_origin": [ - 10000.0, - 0.0, - 0.0 - ], - "hpr_radius_mm": 200000.0 - }, - "panel_deflection": { - "enabled": true, - "theta_right_deg": 0.5, - "theta_ref_deg": 0.3 - }, - "noise": { - "enabled": true, - "sigma_mm": 0.05, - "mode": "isotropic", - "seed": 43 - }, - "transform": { - "enabled": true, - "rotation_deg": 0.5, - "rotation_axis": "random", - "translation_mm": 2.0, - "translation_dir": "random", - "seed": 44 - }, - "denoise": { - "steps": [ - { - "type": "passthrough", - "bbox_padding_mm": 50.0 - }, - { - "type": "voxel", - "voxel_mm": 0.5 - }, - { - "type": "statistical", - "nb_neighbors": 20, - "std_ratio": 2.5 - } - ], - "smoothing": { - "enabled": false, - "search_radius_mm": 2.0, - "polynomial_order": 2 - } - }, - "registration": { - "method": "kabsch_four_holes", - "pre_align": "pca_centroid", - "refuse_if_residual_above_mm": 5.0 - }, - "detection": { - "panel_normal_seed": "pca", - "panel_normal_pca_kth": 1, - "grid_pixel_mm": 2.0, - "panel_min_area_ratio": 0.15, - "hole_min_area_mm2": 600.0, - "hole_max_area_mm2": 5000.0, - "circle_fit": { - "method": "algebraic_then_lsq", - "inlier_residual_mm": 1.5 - }, - "select_top_k_by_radius_match": 4, - "rectangle_aspect_tol": 0.4, - "fill_bg_below_mm2": 100.0, - "locating_hole_radius_mm": [ - 14.0, - 22.0 - ] - }, - "roi": { - "hole_dilate_mm": 5.0, - "central_hole_dilate_mm": 10.0, - "edge_buffer_mm": 8.0, - "chamfer_normal_dot_min": 0.8, - "outlier_z_score": 3.0 - }, - "plane_fit": { - "method": "ransac_svd", - "ransac_iterations": 300, - "ransac_inlier_thresh_mm": 0.5, - "min_inlier_ratio": 0.5, - "enforce_nx_positive": true - }, - "metrics": { - "right_axis_sign": 1, - "angle_clamp_deg": 0.0 - }, - "artifacts": { - "save_intermediate": true, - "ply_format": "binary", - "image_dpi": 150, - "heatmap_colormap": "viridis" - } - } -} \ No newline at end of file diff --git a/alg/delivery_v1.1/coupler_panel_release/test_data/config.resolved.yaml b/alg/delivery_v1.1/coupler_panel_release/test_data/config.resolved.yaml deleted file mode 100644 index 346e75c..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/test_data/config.resolved.yaml +++ /dev/null @@ -1,94 +0,0 @@ -schema_version: '1.0' -cad: - linear_deflection_mm: 0.5 - angular_deflection_rad: 0.1 - locating_hole_radius_mm: - - 14.0 - - 18.0 - central_hole_radius_min_mm: 80.0 - rectangle_aspect_tol: 0.15 -sampling: - method: grid_spacing - spacing_mm: null - num_points: 500000 - seed: 42 - adaptive: - curvature_weight: 1.0 - min_spacing_factor: 0.25 - max_spacing_factor: 4.0 -scan_simulator: - enabled: true - view_origin: - - 10000.0 - - 0.0 - - 0.0 - hpr_radius_mm: 200000.0 -panel_deflection: - enabled: true - theta_right_deg: 0.5 - theta_ref_deg: 0.3 -noise: - enabled: true - sigma_mm: 0.05 - mode: isotropic - seed: 43 -transform: - enabled: true - rotation_deg: 0.5 - rotation_axis: random - translation_mm: 2.0 - translation_dir: random - seed: 44 -denoise: - steps: - - type: passthrough - bbox_padding_mm: 50.0 - - type: voxel - voxel_mm: 0.5 - - type: statistical - nb_neighbors: 20 - std_ratio: 2.5 - smoothing: - enabled: false - search_radius_mm: 2.0 - polynomial_order: 2 -registration: - method: kabsch_four_holes - pre_align: pca_centroid - refuse_if_residual_above_mm: 5.0 -detection: - panel_normal_seed: pca - panel_normal_pca_kth: 1 - grid_pixel_mm: 2.0 - panel_min_area_ratio: 0.15 - hole_min_area_mm2: 600.0 - hole_max_area_mm2: 5000.0 - circle_fit: - method: algebraic_then_lsq - inlier_residual_mm: 1.5 - select_top_k_by_radius_match: 4 - rectangle_aspect_tol: 0.4 - fill_bg_below_mm2: 100.0 - locating_hole_radius_mm: - - 14.0 - - 22.0 -roi: - hole_dilate_mm: 5.0 - central_hole_dilate_mm: 10.0 - edge_buffer_mm: 8.0 - chamfer_normal_dot_min: 0.8 - outlier_z_score: 3.0 -plane_fit: - method: ransac_svd - ransac_iterations: 300 - ransac_inlier_thresh_mm: 0.5 - min_inlier_ratio: 0.5 - enforce_nx_positive: true -metrics: - right_axis_sign: 1 - angle_clamp_deg: 0.0 -artifacts: - save_intermediate: true - ply_format: binary - image_dpi: 150 - heatmap_colormap: viridis diff --git a/alg/delivery_v1.1/coupler_panel_release/test_data/report.json b/alg/delivery_v1.1/coupler_panel_release/test_data/report.json deleted file mode 100644 index f73d814..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/test_data/report.json +++ /dev/null @@ -1,2512 +0,0 @@ -{ - "stp": "D:\\myDocuments\\PKU\\high_speed_train_proj\\lu1-M000004835653_AF4_ASMP072708187_N_AF4.stp", - "panel_deflection_injected": { - "theta_right_deg": 0.5, - "theta_ref_deg": 0.3, - "R": [ - [ - 0.9999482158335473, - -0.008726535498373935, - -0.0052357644619607695 - ], - [ - 0.008726415877184502, - 0.9999619230641713, - -4.569182424308496e-05 - ], - [ - 0.00523596383141958, - 0.0, - 0.9999862922474267 - ] - ] - }, - "scan_pose_perturbation": [ - [ - 0.9999979783441972, - -0.0019163717426360569, - 0.0006089555505671704, - 1.4967337082610368 - ], - [ - 0.0019214737281491043, - 0.9999621035526889, - -0.008491136389996271, - 1.0855384993902797 - ], - [ - -0.000592660299474585, - 0.00849228931593314, - 0.9999637642314565, - 0.7624922116957428 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], - "registration": { - "matrix": [ - [ - 0.9999991698935765, - 0.0011435168147077325, - -0.0005937856954023229, - -1.42312474833891 - ], - [ - -0.001138363345940219, - 0.999962302539521, - 0.008607997952637694, - -1.3922014879928315 - ], - [ - 0.0006036067015895135, - -0.008607314863212156, - 0.9999627742020176, - -1.1453314127968168 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], - "rmse_mm": 1.279801346259311, - "max_residual_mm": 1.7348149080058168 - }, - "measured": { - "schema_version": "1.0", - "algorithm_version": "0.1.0", - "theta_right_deg": 0.544354519421902, - "theta_ref_deg": 0.2997466331976193, - "D_y_max_mm": 94.08063004084244, - "D_z_max_mm": 172.21417148158895, - "dx_y_max_mm": 0.893866137444892, - "dx_z_max_mm": 0.9009579671326199, - "center_C": [ - -0.007647024901048821, - 0.9158090934040075, - -0.06590994857895538 - ], - "normal": [ - 0.9999411851861564, - 0.00950050573095007, - 0.005231305703281311 - ], - "holes": { - "locating": [ - { - "center": [ - 1.8237358542499582, - -101.1715281534813, - -179.67591341367824 - ], - "radius_mm": 19.34475104619178, - "fit_residual_rms_mm": 2.2818699650684904, - "role": "locating" - }, - { - "center": [ - 0.05005657933939589, - 102.44798627110123, - -180.30256731680095 - ], - "radius_mm": 19.71317672500548, - "fit_residual_rms_mm": 3.220214937115534, - "role": "locating" - }, - { - "center": [ - -1.820222447435446, - 100.64818807060959, - 179.87714825154933 - ], - "radius_mm": 18.68294341110619, - "fit_residual_rms_mm": 2.0067435575879573, - "role": "locating" - }, - { - "center": [ - -0.08415808575810335, - -98.26140981461349, - 179.83769268461404 - ], - "radius_mm": 20.21695365907737, - "fit_residual_rms_mm": 2.5642764666909246, - "role": "locating" - } - ], - "central": null - }, - "plane": { - "normal": [ - 0.9999411851861564, - 0.00950050573095007, - 0.005231305703281311 - ], - "point_on_plane": [ - 0.07084859134750632, - -0.6075903692234997, - 0.7670896792673196 - ], - "d_offset": -0.06908488922999657, - "inlier_ratio": 1.0, - "inlier_residual_rms_mm": 0.04977002501374635 - }, - "frame": { - "center_C": [ - -0.007647024901048821, - 0.9158090934040075, - -0.06590994857895538 - ], - "line_direction": [ - 1.0, - 0.0, - 0.0 - ], - "reference_plane_z": -0.06590994857895538, - "longitudinal_mid_y": 0.9158090934040075 - }, - "quality_flags": [], - "timings": [ - { - "stage": "detect_holes", - "seconds": 2.090514300041832, - "peak_kib": 32527.3603515625 - }, - { - "stage": "build_frame", - "seconds": 4.1000021155923605e-05, - "peak_kib": 0.0 - }, - { - "stage": "extract_roi", - "seconds": 0.33327039994765073, - "peak_kib": 17444.6552734375 - }, - { - "stage": "fit_plane", - "seconds": 0.07231109996791929, - "peak_kib": 626.2568359375 - }, - { - "stage": "metrics", - "seconds": 4.96999709866941e-05, - "peak_kib": 0.0 - } - ], - "report": { - "schema_version": "1.0", - "algorithm_version": "0.1.0", - "config": { - "schema_version": "1.0", - "cad": { - "linear_deflection_mm": 0.5, - "angular_deflection_rad": 0.1, - "locating_hole_radius_mm": [ - 14.0, - 18.0 - ], - "central_hole_radius_min_mm": 80.0, - "rectangle_aspect_tol": 0.15 - }, - "sampling": { - "method": "grid_spacing", - "spacing_mm": null, - "num_points": 500000, - "seed": 42, - "adaptive": { - "curvature_weight": 1.0, - "min_spacing_factor": 0.25, - "max_spacing_factor": 4.0 - } - }, - "scan_simulator": { - "enabled": true, - "view_origin": [ - 10000.0, - 0.0, - 0.0 - ], - "hpr_radius_mm": 200000.0 - }, - "panel_deflection": { - "enabled": true, - "theta_right_deg": 0.5, - "theta_ref_deg": 0.3 - }, - "noise": { - "enabled": true, - "sigma_mm": 0.05, - "mode": "isotropic", - "seed": 43 - }, - "transform": { - "enabled": true, - "rotation_deg": 0.5, - "rotation_axis": "random", - "translation_mm": 2.0, - "translation_dir": "random", - "seed": 44 - }, - "denoise": { - "steps": [ - { - "type": "passthrough", - "bbox_padding_mm": 50.0 - }, - { - "type": "voxel", - "voxel_mm": 0.5 - }, - { - "type": "statistical", - "nb_neighbors": 20, - "std_ratio": 2.5 - } - ], - "smoothing": { - "enabled": false, - "search_radius_mm": 2.0, - "polynomial_order": 2 - } - }, - "registration": { - "method": "kabsch_four_holes", - "pre_align": "pca_centroid", - "refuse_if_residual_above_mm": 5.0 - }, - "detection": { - "panel_normal_seed": "pca", - "panel_normal_pca_kth": 1, - "grid_pixel_mm": 2.0, - "panel_min_area_ratio": 0.15, - "hole_min_area_mm2": 600.0, - "hole_max_area_mm2": 5000.0, - "circle_fit": { - "method": "algebraic_then_lsq", - "inlier_residual_mm": 1.5 - }, - "select_top_k_by_radius_match": 4, - "rectangle_aspect_tol": 0.4, - "fill_bg_below_mm2": 100.0, - "locating_hole_radius_mm": [ - 14.0, - 22.0 - ] - }, - "roi": { - "hole_dilate_mm": 5.0, - "central_hole_dilate_mm": 10.0, - "edge_buffer_mm": 8.0, - "chamfer_normal_dot_min": 0.8, - "outlier_z_score": 3.0 - }, - "plane_fit": { - "method": "ransac_svd", - "ransac_iterations": 300, - "ransac_inlier_thresh_mm": 0.5, - "min_inlier_ratio": 0.5, - "enforce_nx_positive": true - }, - "metrics": { - "right_axis_sign": 1, - "angle_clamp_deg": 0.0 - }, - "artifacts": { - "save_intermediate": true, - "ply_format": "binary", - "image_dpi": 150, - "heatmap_colormap": "viridis" - } - }, - "n_points_in": 184135, - "detection": { - "n_cavities": 4, - "n_circle_fits": 4, - "selection": { - "radius_score": 0.02869114494902652, - "rectangle_score": 0.025074534730867147, - "score": 0.053765679679893666, - "radii_mean": 19.489456210345203, - "radii_std": 0.5591748131087193, - "selected_indices": [ - 0, - 1, - 3, - 2 - ], - "selected_radii": [ - 19.34475104619178, - 19.71317672500548, - 18.68294341110619, - 20.21695365907737 - ] - } - }, - "roi": { - "n_in": 184135, - "n_out": 9855, - "stages": [ - { - "name": "rectangle", - "n_kept": 10009 - }, - { - "name": "locating_holes", - "n_kept": 9855 - }, - { - "name": "chamfer_normal", - "n_kept": 9855 - }, - { - "name": "z_score", - "n_kept": 9855 - } - ] - }, - "plane": { - "normal": [ - 0.9999411851861564, - 0.00950050573095007, - 0.005231305703281311 - ], - "point_on_plane": [ - 0.07084859134750632, - -0.6075903692234997, - 0.7670896792673196 - ], - "d_offset": -0.06908488922999657, - "inlier_ratio": 1.0, - "inlier_residual_rms_mm": 0.04977002501374635 - }, - "frame": { - "center_C": [ - -0.007647024901048821, - 0.9158090934040075, - -0.06590994857895538 - ], - "reference_plane_z": -0.06590994857895538, - "longitudinal_mid_y": 0.9158090934040075 - }, - "metrics": { - "theta_right_deg": 0.544354519421902, - "theta_ref_deg": 0.2997466331976193, - "D_y_max_mm": 94.08063004084244, - "D_z_max_mm": 172.21417148158895, - "dx_y_max_mm": 0.893866137444892, - "dx_z_max_mm": 0.9009579671326199, - "A": -0.009501064534291972, - "B": -0.0052316133996495135 - }, - "quality_flags": [], - "timings": [ - { - "stage": "detect_holes", - "seconds": 2.090514300041832, - "peak_kib": 32527.3603515625 - }, - { - "stage": "build_frame", - "seconds": 4.1000021155923605e-05, - "peak_kib": 0.0 - }, - { - "stage": "extract_roi", - "seconds": 0.33327039994765073, - "peak_kib": 17444.6552734375 - }, - { - "stage": "fit_plane", - "seconds": 0.07231109996791929, - "peak_kib": 626.2568359375 - }, - { - "stage": "metrics", - "seconds": 4.96999709866941e-05, - "peak_kib": 0.0 - } - ] - } - }, - "gt_cad": { - "holes": { - "locating": [ - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - } - ], - "central": { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "radius_mm": 100.0, - "fit_residual_rms_mm": 0.0, - "role": "central" - } - }, - "plane": { - "normal": [ - 1.0, - -0.0, - -0.0 - ], - "point_on_plane": [ - 0.0, - 0.9999987923150044, - 9.878999946977274e-07 - ], - "d_offset": -0.0, - "inlier_ratio": 1.0, - "inlier_residual_rms_mm": 0.0 - }, - "frame": { - "center_C": [ - 0.0, - 0.9999987923150044, - 9.878999946977274e-07 - ], - "line_direction": [ - 1.0, - 0.0, - 0.0 - ], - "reference_plane_z": 9.878999946977274e-07, - "longitudinal_mid_y": 0.9999987923150044 - }, - "raw_circles": [ - { - "center": [ - 0.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - 0.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 136.0, - -699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - 0.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 227.0000009879 - ], - "axis": [ - 0.0, - -1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -226.9999990121 - ], - "axis": [ - -0.0, - 1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -76.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -76.0, - 136.0, - -699.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 227.0000009879 - ], - "axis": [ - 0.0, - -1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - 190.0000009879 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - 190.0000009879 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - -189.9999990121 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - -189.9999990121 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -226.9999990121 - ], - "axis": [ - -0.0, - 1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 136.0, - -699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 136.0, - -699.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.00000000000001, - 180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -39.99999999999999, - 180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.00000000000001, - -180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 39.99999999999999, - -180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - } - ] - }, - "gt_after_deflection": { - "normal": [ - 0.9999482158335473, - 0.008726415877184502, - 0.00523596383141958 - ], - "locating_centers": [ - [ - 1.8063646228583214, - -98.98800706265835, - -179.99753161665035 - ], - [ - 0.061057523183796314, - 101.00437755014592, - -179.99753161665035 - ], - [ - -1.8238176831220807, - 100.98792849341841, - 179.99753359242328 - ], - [ - -0.07851058344755557, - -99.00445611938586, - 179.99753359242328 - ] - ] - }, - "gt_metrics": { - "theta_right_deg": 0.5, - "theta_ref_deg": 0.30001142330692215, - "D_y_max_mm": 100.00441683476589, - "D_z_max_mm": 179.99753260453681, - "dx_y_max_mm": 0.8727253242089344, - "dx_z_max_mm": 0.9425093775246102, - "A": -0.00872686779075879, - "B": -0.005236234985483653 - }, - "diff": { - "theta_right_deg": 0.04435451942190205, - "theta_ref_deg": -0.0002647901093028593, - "D_y_max_mm": -5.923786793923455, - "D_z_max_mm": -7.783361122947866, - "dx_y_max_mm": 0.021140813235957512, - "dx_z_max_mm": -0.041551410391990284 - }, - "config": { - "schema_version": "1.0", - "cad": { - "linear_deflection_mm": 0.5, - "angular_deflection_rad": 0.1, - "locating_hole_radius_mm": [ - 14.0, - 18.0 - ], - "central_hole_radius_min_mm": 80.0, - "rectangle_aspect_tol": 0.15 - }, - "sampling": { - "method": "grid_spacing", - "spacing_mm": null, - "num_points": 500000, - "seed": 42, - "adaptive": { - "curvature_weight": 1.0, - "min_spacing_factor": 0.25, - "max_spacing_factor": 4.0 - } - }, - "scan_simulator": { - "enabled": true, - "view_origin": [ - 10000.0, - 0.0, - 0.0 - ], - "hpr_radius_mm": 200000.0 - }, - "panel_deflection": { - "enabled": true, - "theta_right_deg": 0.5, - "theta_ref_deg": 0.3 - }, - "noise": { - "enabled": true, - "sigma_mm": 0.05, - "mode": "isotropic", - "seed": 43 - }, - "transform": { - "enabled": true, - "rotation_deg": 0.5, - "rotation_axis": "random", - "translation_mm": 2.0, - "translation_dir": "random", - "seed": 44 - }, - "denoise": { - "steps": [ - { - "type": "passthrough", - "bbox_padding_mm": 50.0 - }, - { - "type": "voxel", - "voxel_mm": 0.5 - }, - { - "type": "statistical", - "nb_neighbors": 20, - "std_ratio": 2.5 - } - ], - "smoothing": { - "enabled": false, - "search_radius_mm": 2.0, - "polynomial_order": 2 - } - }, - "registration": { - "method": "kabsch_four_holes", - "pre_align": "pca_centroid", - "refuse_if_residual_above_mm": 5.0 - }, - "detection": { - "panel_normal_seed": "pca", - "panel_normal_pca_kth": 1, - "grid_pixel_mm": 2.0, - "panel_min_area_ratio": 0.15, - "hole_min_area_mm2": 600.0, - "hole_max_area_mm2": 5000.0, - "circle_fit": { - "method": "algebraic_then_lsq", - "inlier_residual_mm": 1.5 - }, - "select_top_k_by_radius_match": 4, - "rectangle_aspect_tol": 0.4, - "fill_bg_below_mm2": 100.0, - "locating_hole_radius_mm": [ - 14.0, - 22.0 - ] - }, - "roi": { - "hole_dilate_mm": 5.0, - "central_hole_dilate_mm": 10.0, - "edge_buffer_mm": 8.0, - "chamfer_normal_dot_min": 0.8, - "outlier_z_score": 3.0 - }, - "plane_fit": { - "method": "ransac_svd", - "ransac_iterations": 300, - "ransac_inlier_thresh_mm": 0.5, - "min_inlier_ratio": 0.5, - "enforce_nx_positive": true - }, - "metrics": { - "right_axis_sign": 1, - "angle_clamp_deg": 0.0 - }, - "artifacts": { - "save_intermediate": true, - "ply_format": "binary", - "image_dpi": 150, - "heatmap_colormap": "viridis" - } - } -} \ No newline at end of file diff --git a/alg/delivery_v1.1/coupler_panel_release/test_data/synth/gt.json b/alg/delivery_v1.1/coupler_panel_release/test_data/synth/gt.json deleted file mode 100644 index 742e4b2..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/test_data/synth/gt.json +++ /dev/null @@ -1,3727 +0,0 @@ -{ - "holes": { - "locating": [ - { - "center": [ - -11598.00007887, - 835.00000000003, - -180.0 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - -11598.00007887, - 1035.0, - -180.0 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - -11598.00007887, - 1035.0, - 179.99999999996 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - }, - { - "center": [ - -11598.00007887, - 835.00000000003, - 179.99999999996 - ], - "radius_mm": 16.0, - "fit_residual_rms_mm": 0.0, - "role": "locating" - } - ], - "central": { - "center": [ - -11598.00007887, - 935.00000000003, - -9.879379026e-07 - ], - "radius_mm": 100.0, - "fit_residual_rms_mm": 0.0, - "role": "central" - } - }, - "plane": { - "normal": [ - 1.0, - -0.0, - -0.0 - ], - "point_on_plane": [ - -11598.00007887, - 935.000000000015, - -1.999467258428922e-11 - ], - "d_offset": 11598.00007887, - "inlier_ratio": 1.0, - "inlier_residual_rms_mm": 0.0 - }, - "frame": { - "center_C": [ - -11598.00007887, - 935.000000000015, - -1.999467258428922e-11 - ], - "line_direction": [ - 1.0, - 0.0, - 0.0 - ], - "reference_plane_z": -1.999467258428922e-11, - "longitudinal_mid_y": 935.000000000015 - }, - "raw_circles": [ - { - "center": [ - -11598.00007887, - 1040.000001208, - 669.00001447846 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -11598.00007887, - 1070.000001208, - 699.00001447846 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -11598.00007887, - 1103.000001208, - 689.99953498956 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11598.00007887, - 1103.000001208, - -689.9995369654 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11598.00007887, - 1070.000001208, - -699.0000164543 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -11598.00007887, - 1040.000001208, - -669.0000164543 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -11598.00007887, - 935.00000000003, - -9.879379026e-07 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -11598.00007887, - 935.00000000003, - -9.879379026e-07 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -11598.00007887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.00007887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.00007887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.00007887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.00007887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.00007887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.00007887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.00007887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.00007887, - 1035.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 1035.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 1035.0, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 1035.0, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 835.00000000003, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 835.00000000003, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 835.00000000003, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 835.00000000003, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11678.00007887, - 835.00000000003, - -190.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -11678.00007887, - 835.00000000003, - 189.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -11677.00007887, - 835.00000000003, - 226.99999999996 - ], - "axis": [ - 0.0, - -1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11678.00007887, - 835.00000000003, - 189.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -11677.00007887, - 835.00000000003, - 189.99999999996 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -11678.00007887, - 835.00000000003, - -190.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -11677.00007887, - 835.00000000003, - -190.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -11677.00007887, - 835.00000000003, - -227.0 - ], - "axis": [ - -0.0, - 1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - 189.99999999996 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - -190.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -11674.00007887, - 1040.000001208, - -669.0000164543 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -11674.00007887, - 1070.000001208, - -699.0000164543 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -11674.00007887, - 1103.000001208, - -689.9995369654 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11674.00007887, - 1103.000001208, - 689.99953498956 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11674.00007887, - 1070.000001208, - 699.00001447846 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -11674.00007887, - 1040.000001208, - 669.00001447846 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 1035.0, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 1035.0, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 1035.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 1035.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 935.00000000003, - -9.879379026e-07 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -11674.00007887, - 935.00000000003, - -9.879379026e-07 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 835.00000000003, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 835.00000000003, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 835.00000000003, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 835.00000000003, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 1035.0, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 1035.0, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 1035.0, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 1035.0, - 179.99999999996 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 1035.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 1035.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 1035.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11598.00007887, - 1035.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -11674.00007887, - 935.00000000003, - -9.879379026e-07 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -11598.00007887, - 935.00000000003, - -9.879379026e-07 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -11674.00007887, - 935.00000000003, - -9.879379026e-07 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -11598.00007887, - 935.00000000003, - -9.879379026e-07 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - 189.99999999996 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -11677.00007887, - 835.00000000003, - 226.99999999996 - ], - "axis": [ - 0.0, - -1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11677.00007887, - 835.00000000003, - 189.99999999996 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -11677.00007887, - 798.0, - 189.99999999996 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11677.00007887, - 798.0, - 189.99999999996 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11677.00007887, - 798.0, - -190.0 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11674.00007887, - 835.00000000003, - -190.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -11677.00007887, - 798.0, - -190.0 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11677.00007887, - 835.00000000003, - -190.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -11677.00007887, - 835.00000000003, - -227.0 - ], - "axis": [ - -0.0, - 1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11674.00007887, - 1040.000001208, - -669.0000164543 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -11598.00007887, - 1040.000001208, - -669.0000164543 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -11598.00007887, - 1070.000001208, - -699.0000164543 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -11674.00007887, - 1070.000001208, - -699.0000164543 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -11674.00007887, - 1103.000001208, - -689.9995369654 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11598.00007887, - 1103.000001208, - -689.9995369654 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11674.00007887, - 1103.000001208, - 689.99953498956 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11598.00007887, - 1103.000001208, - 689.99953498956 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -11598.00007887, - 1070.000001208, - 699.00001447846 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -11674.00007887, - 1070.000001208, - 699.00001447846 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -11674.00007887, - 1040.000001208, - 669.00001447846 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -11598.00007887, - 1040.000001208, - 669.00001447846 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -11598.00007887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.72507887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.00007887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.72507887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.72507887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11638.00007887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11638.00007887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.72507887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11643.37031650775, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.8884436236987767e-09 - }, - { - "center": [ - -11638.00007887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11643.37031650775, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.8884498409477146e-09 - }, - { - "center": [ - -11638.00007887, - 974.0000012077, - 179.99999901206 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.00007887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.72507887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.00007887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.72507887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.72507887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11638.00007887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11638.00007887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.72507887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11643.37031650775, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.8884436236987767e-09 - }, - { - "center": [ - -11638.00007887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11643.37031650775, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.8884498409477146e-09 - }, - { - "center": [ - -11638.00007887, - 894.0000012077, - 179.99999901206 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.00007887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.72507887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.00007887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.72507887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.72507887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11638.00007887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11638.00007887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.72507887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11643.37031650775, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.8884436236987767e-09 - }, - { - "center": [ - -11638.00007887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11643.37031650775, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.8884498409477146e-09 - }, - { - "center": [ - -11638.00007887, - 894.0000012077, - -180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.00007887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.72507887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.00007887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -11598.72507887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.72507887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11638.00007887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11638.00007887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11598.72507887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11643.37031650775, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.8884436236987767e-09 - }, - { - "center": [ - -11638.00007887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -11643.37031650775, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.8884498409477146e-09 - }, - { - "center": [ - -11638.00007887, - 974.0000012077, - -180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - 0.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 136.0, - -699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - 0.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 227.0000009879 - ], - "axis": [ - 0.0, - -1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -80.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -226.9999990121 - ], - "axis": [ - -0.0, - 1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -76.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -76.0, - 136.0, - -699.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - -99.00000120767, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - 180.0000009879 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - 0.0, - 100.9999987923, - -179.9999990121 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 16.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - 0.0, - 0.999998792332, - 0.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 100.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 227.0000009879 - ], - "axis": [ - 0.0, - -1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - 190.0000009879 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - 190.0000009879 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - 190.0000009879 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - -189.9999990121 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 37.0 - }, - { - "center": [ - -79.0, - -136.0000012077, - -189.9999990121 - ], - "axis": [ - 0.0, - 0.0, - -1.0 - ], - "radius": 3.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -189.9999990121 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 40.0 - }, - { - "center": [ - -79.0, - -99.00000120767, - -226.9999990121 - ], - "axis": [ - -0.0, - 1.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 106.0, - -669.0000154664 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 136.0, - -699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 136.0, - -699.0000154664 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - -689.9995359775 - ], - "axis": [ - 1.0, - 0.0, - -0.0 - ], - "radius": 3.0 - }, - { - "center": [ - -76.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 169.0, - 689.9995359775 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 3.0 - }, - { - "center": [ - 0.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 136.0, - 699.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 10.0 - }, - { - "center": [ - -76.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 106.0, - 669.0000154664 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 20.0 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.00000000000001, - 180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - 40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -39.99999999999999, - 180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - -40.0, - 180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - -40.00000000000001, - -180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - -40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - 0.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 7.125 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -0.725, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 1.7505996652289468e-12 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - 1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - }, - { - "center": [ - -45.37023763953209, - 39.99999999999999, - -180.0 - ], - "axis": [ - 1.0, - -0.0, - 0.0 - ], - "radius": 1.7483792191796965e-12 - }, - { - "center": [ - -40.0, - 40.0, - -180.0 - ], - "axis": [ - -1.0, - 0.0, - 0.0 - ], - "radius": 6.4 - } - ] -} \ No newline at end of file diff --git a/alg/delivery_v1.1/coupler_panel_release/test_data/synth/manifest.json b/alg/delivery_v1.1/coupler_panel_release/test_data/synth/manifest.json deleted file mode 100644 index 0c207d0..0000000 --- a/alg/delivery_v1.1/coupler_panel_release/test_data/synth/manifest.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "stp": "test_data\\lu1.stp", - "profile": "lu1_af4", - "gt": "gt.json", - "samples": [ - { - "sample_id": "rp000_zp000", - "ply": "cloud_rp000_zp000.ply", - "theta_right_deg": 0.0, - "theta_ref_deg": 0.0, - "n_points": 96662 - }, - { - "sample_id": "rp010_zp000", - "ply": "cloud_rp010_zp000.ply", - "theta_right_deg": 0.1, - "theta_ref_deg": 0.0, - "n_points": 96459 - }, - { - "sample_id": "rp030_zp000", - "ply": "cloud_rp030_zp000.ply", - "theta_right_deg": 0.3, - "theta_ref_deg": 0.0, - "n_points": 96605 - }, - { - "sample_id": "rp050_zp000", - "ply": "cloud_rp050_zp000.ply", - "theta_right_deg": 0.5, - "theta_ref_deg": 0.0, - "n_points": 96706 - }, - { - "sample_id": "rp100_zp000", - "ply": "cloud_rp100_zp000.ply", - "theta_right_deg": 1.0, - "theta_ref_deg": 0.0, - "n_points": 96664 - }, - { - "sample_id": "rp200_zp000", - "ply": "cloud_rp200_zp000.ply", - "theta_right_deg": 2.0, - "theta_ref_deg": 0.0, - "n_points": 96713 - }, - { - "sample_id": "rp000_zp050", - "ply": "cloud_rp000_zp050.ply", - "theta_right_deg": 0.0, - "theta_ref_deg": 0.5, - "n_points": 96666 - }, - { - "sample_id": "rp050_zp050", - "ply": "cloud_rp050_zp050.ply", - "theta_right_deg": 0.5, - "theta_ref_deg": 0.5, - "n_points": 96614 - }, - { - "sample_id": "rp100_zp100", - "ply": "cloud_rp100_zp100.ply", - "theta_right_deg": 1.0, - "theta_ref_deg": 1.0, - "n_points": 96601 - } - ] -} \ No newline at end of file diff --git a/alg/delivery_v1.1/underframe_deflection/README.md b/alg/delivery_v1.1/underframe_deflection/README.md deleted file mode 100644 index 2cdfd9b..0000000 --- a/alg/delivery_v1.1/underframe_deflection/README.md +++ /dev/null @@ -1,200 +0,0 @@ -# underframe-deflection - -**从扫描点云测量高铁底架纵梁挠度的 Python 算法包:接受"CAD 基准 + 扫描点云 + ROI JSON"三件套输入,自动配准、去噪、识别梁、出 Excel 误差报表。** - -[![python](https://img.shields.io/badge/python-3.10%2B-blue)]() [![version](https://img.shields.io/badge/version-1.1.0-green)]() [![status](https://img.shields.io/badge/status-beta-yellow)]() - ---- - -## 这是什么 - -输入一份现场扫描点云(`.ply / .xyz`),一份 CAD 基准(`.ply / .stl / .obj`),一份描述测量区域的 ROI JSON,本包会: - -1. **去噪**:passthrough → 统计离群 → 半径离群 → 双边滤波 → 体素降采样 -2. **配准**:用 ICP 把扫描点云刚性对齐到 CAD 坐标系 -3. **识别梁**:沿车长切片、Beam-search 跟踪左右纵梁 -4. **拟合**:鲁棒样条拟合代表线 -5. **找参考**:基于车体孔特征或线端点定参考线 -6. **算挠度**:沿 X 轴逐点计算并选出绝对值最大的测量点 - -输出:完整 JSON 报告、Excel 误差报表(5 sheet)、沿 X 的挠度 CSV、候选切片诊断 CSV、5 张诊断 PNG、4 个着色 PLY 叠加图。 - ---- - -## 安装 - -```bash -# 进入本目录后 -pip install -e .[all] # 推荐:core + matplotlib + openpyxl - -# 或仅安装核心 + 自己选 extra -pip install -e . # 仅 numpy + scipy(只能调用 compute_deflection) -pip install -e .[viz] # +matplotlib(PNG 诊断图) -pip install -e .[excel] # +openpyxl(.xlsx 报表) - -# 或直接用 requirements 锁版本 -pip install -r requirements.txt -``` - -安装后会注册命令行 `underframe-measure`,Python 内可直接 `import underframe_deflection`。 - ---- - -## 上手 - -```bash -underframe-measure \ - --input-cloud scan.ply \ - --cad reference.ply \ - --roi roi.json \ - --out-dir out/ -``` - -或者 Python 调用: - -```python -from underframe_deflection import run_measurement_pipeline - -report = run_measurement_pipeline( - input_cloud="scan.ply", - cad_path="reference.ply", - roi_path="roi.json", - out_dir="out/", -) - -print(report["deflection"]["left"]["selected_measurement"]["signed_deflection_mm"]) -print(report["deflection"]["right"]["selected_measurement"]["signed_deflection_mm"]) -``` - -`test_data/` 自带可直接运行的样例: - -```bash -underframe-measure \ - --input-cloud test_data/m000004802409t02_asm_100000pts.ply \ - --cad test_data/m000004802409t02_asm_100000pts.ply \ - --roi test_data/sample_roi.json \ - --out-dir test_data/runs/demo -``` - ---- - -## 文档导览 - -| 文件 | 用途 | -| --- | --- | -| **`README.md`** (本文件) | 仓库概览,30 秒上手 | -| **`USAGE.md`** | 集成开发者完整指南:三种调用方式、输入格式、读结果、异常、配置覆盖、模板 | -| `requirements.txt` | 锁定版本的依赖清单 | -| `pyproject.toml` | 打包描述(pip install 的入口) | - ---- - -## 仓库结构 - -``` -underframe_deflection/ -├── README.md ← 本文件 -├── USAGE.md ← 集成开发者文档 -├── requirements.txt -├── pyproject.toml ← pip 打包描述(无需 setup.py) -├── _version.py ← __version__ / ALGORITHM_VERSION / SCHEMA_VERSION -├── __init__.py ← 公开 API 出口 -│ -├── api.py ← compute_deflection() 主入口 -├── pipeline.py ← run_measurement_pipeline() 端到端编排 -├── cli.py ← 命令行入口(underframe-measure) -│ -├── config.py ← DEFAULT_CONFIG 与档位 -├── preprocess.py ← SOR / radius / voxel / normals / bilateral -├── cad_io.py ← CAD 读取 (PLY/XYZ/STL/OBJ) -├── registration.py ← PCA 粗对齐 + trimmed ICP -├── roi.py ← ROI JSON 读取 + AABB passthrough -│ -├── frame.py ← 车体坐标系推断 + ROI mask -├── beams.py ← 切片 + 候选条带 + Beam-search 跟踪 -├── fitting.py ← 代表点 + 鲁棒样条拟合 -├── reference.py ← 参考线检测(孔 / 端点) -│ -├── artifacts.py ← JSON / CSV / PNG / PLY writer -├── excel_report.py ← Excel 写入器 (5 sheet) -├── ply_overlay.py ← 着色 PLY 叠加图 -├── visualization.py ← 诊断 PNG 绘图 -│ -├── io.py ← 文件 I/O 工具 -├── exceptions.py ← 公开异常层级 -├── structures.py ← 内部数据类 -│ -└── test_data/ - ├── m000004802409t02_asm.stp ← 原始 STEP(仅参考,不被解析) - ├── m000004802409t02_asm_100000pts.ply ← STEP 采样得到的 CAD 基准点云 - └── sample_roi.json ← 样例 ROI(AABB) -``` - ---- - -## 输入约定 - -所有点云的坐标轴必须: - -- **X**:纵向(车长) -- **Y**:横向(车宽) -- **Z**:高度,**向上为正** -- 单位:**毫米** - -CAD 基准也必须遵循同样的轴向;ROI JSON 中的 bbox 字段在 CAD 坐标系下。 -如果你的扫描设备轴向不同,请在调用前自行做旋转/置换。 - ---- - -## 公开 API - -```python -from underframe_deflection import ( - # 主入口 - compute_deflection, # 纯内存计算 - run_measurement_pipeline, # 文件读 + 算 + 文件写 - - # 单步工具(高级用法) - load_point_cloud, # 读扫描点云 - read_cad_reference, # 读 CAD - load_roi_aabb, # 读 ROI JSON - register_to_cad, # ICP 配准 - bilateral_filter_points, # 双边滤波 - apply_passthrough, # AABB 过滤 - - # 结果数据类 - DeflectionResult, - SideDeflection, - DeflectionCurves, - RegistrationResult, - - # 异常 - DeflectionError, - InvalidPointCloudError, - InsufficientPointsError, - UnsupportedFormatError, - FrameInferenceError, - - # 版本 - __version__, - ALGORITHM_VERSION, - SCHEMA_VERSION, -) -``` - -详见 [`USAGE.md`](./USAGE.md)。 - ---- - -## 状态与版本 - -- **当前版本**:1.1.0 -- **算法版本**:0.1.0(v1.1 没有动算法主流程,仅扩展 I/O / 预处理 / 配准) -- **Schema 版本**:1.0(v1.1 仅新增可选字段,向后兼容) -- **Python**:≥ 3.10(Windows / Linux / macOS 通用) - ---- - -## License - -Proprietary — PKU High-Speed Train Project. diff --git a/alg/delivery_v1.1/underframe_deflection/USAGE.md b/alg/delivery_v1.1/underframe_deflection/USAGE.md deleted file mode 100644 index 80d2b92..0000000 --- a/alg/delivery_v1.1/underframe_deflection/USAGE.md +++ /dev/null @@ -1,363 +0,0 @@ -# underframe_deflection 集成调用指南 (v1.1) - -> 高铁底架纵梁挠度测量算法 —— 基于点云的计算。 -> 本文档面向把本算法集成进上层软件的开发者:怎么准备 **CAD 基准 + 扫描点云 + ROI JSON** 三件套输入、怎么调用、怎么读结果。 - ---- - -## 0. TL;DR - -```python -from underframe_deflection import run_measurement_pipeline - -report = run_measurement_pipeline( - input_cloud="scan.ply", - cad_path="reference.ply", - roi_path="roi.json", - out_dir="out/", -) - -# 关心的两个核心数值 -print(report["deflection"]["left"]["selected_measurement"]["signed_deflection_mm"]) -print(report["deflection"]["right"]["selected_measurement"]["signed_deflection_mm"]) -``` - -落盘后会得到: - -``` -out/ -├── report.json # 完整结果 -├── report.xlsx # 误差报表(Summary + 4 张中间表) -├── deflection_curves.csv # 沿 X 的挠度曲线 -├── beam_tracks.csv # 候选梁切片诊断 -├── images/ # 5 张诊断 PNG -└── ply/ # 4 个着色 PLY 叠加图 -``` - ---- - -## 1. 算法的三个输入 - -| 输入 | 类型 | 必需 | 说明 | -| --- | --- | --- | --- | -| **扫描点云** | `.ply / .xyz / .txt / .csv` | ✅ 必须 | 现场采集得到的 (N, 3) XYZ 点云,单位 mm | -| **CAD 基准** | `.ply / .xyz / .stl / .obj` | 可选但强烈建议 | 预先 tessellate / 采样过的 CAD 几何,作为配准基准坐标系;不直接吃 STEP/IGES | -| **ROI JSON** | `.json` | 可选但强烈建议 | 描述车底测量区域的轴对齐 bbox,坐标基于 CAD 坐标系 | - -> 三者**全给**时走 v1.1 完整流程:去噪 → 配准到 CAD 系 → 按 ROI JSON 截区 → 算挠度 → 出 Excel。 -> 只给扫描点云时走 legacy 流程,与本算法 v1.0 行为完全一致(仅多出 bilateral 这一步可用 `--no-bilateral` 关掉)。 - -### 1.1 ROI JSON 文件格式 - -固定 6 个 float 字段,单位 mm,CAD 坐标系下: - -```json -{ - "x_min": -12500.0, - "x_max": 12500.0, - "y_min": 150.0, - "y_max": 4100.0, - "z_min": -1680.0, - "z_max": 200.0 -} -``` - -约束:每个轴 `min < max`,且都是有限实数;任一字段缺失或非法 → `InvalidPointCloudError`。 - -`test_data/sample_roi.json` 是一个可直接使用的样例。 - -### 1.2 CAD 基准 - -- **不解析 STEP / IGES**。如果你只有 `.stp`,请用 CAD 软件或离线脚本先把它转成 `.stl` / `.ply` / `.obj`,再喂给本算法。 -- 三角网格的话只读顶点位置,face 信息忽略 —— 算法内部把 CAD 当作"参考点集"用 ICP 对齐。 -- `test_data/m000004802409t02_asm_100000pts.ply` 是从原始 STEP 采样得到的 10 万点 PLY,可直接当 CAD 基准。 - ---- - -## 2. 三种调用方式 - -### 2.1 命令行(最简单的集成方式) - -```bash -underframe-measure \ - --input-cloud scan.ply \ - --cad reference.ply \ - --roi roi.json \ - --out-dir out/ -``` - -可选开关: - -| 开关 | 含义 | -| --- | --- | -| `--config CONFIG.json` | 覆盖部分内部超参数(结构同 `DEFAULT_CONFIG`) | -| `--profile {auto,M,TC,TP,custom}` | 调参档位 | -| `--skip-registration` | 给了 `--cad` 但不跑 ICP(适合调试或扫描已经预对齐) | -| `--no-bilateral` | 关闭双边滤波(v1.0 行为) | -| `--skip-excel` | 不写 `.xlsx` | - -未安装本包时,等价命令: - -```bash -python -m underframe_deflection.cli --input-cloud scan.ply ... -``` - -### 2.2 一次性 Python 函数(推荐) - -```python -from underframe_deflection import run_measurement_pipeline - -report = run_measurement_pipeline( - input_cloud="scan.ply", - cad_path="reference.ply", # 可选 - roi_path="roi.json", # 可选 - out_dir="out/", - config_path=None, # 可选 JSON 配置覆盖 - profile="auto", # 可选档位 - skip_registration=False, - enable_bilateral=True, - skip_excel=False, -) -``` - -**返回**:`report` 是一个 `dict`,结构与 `out/report.json` 完全一致。 - -### 2.3 纯内存计算(不读写文件) - -如果你已经从其它来源拿到了点云和 ROI(例如直接从相机驱动 / 数据库取数据),可以避开文件 I/O: - -```python -import numpy as np -from underframe_deflection import compute_deflection, load_point_cloud - -points = load_point_cloud("scan.ply") # 或自己构造 (N, 3) ndarray -roi_aabb = { # 与 ROI JSON 同结构的 dict - "x_min": -12500, "x_max": 12500, - "y_min": 150, "y_max": 4100, - "z_min": -1680, "z_max": 200, -} - -result = compute_deflection(points, roi_aabb=roi_aabb) -print(result.left.selected_signed_mm, result.right.selected_signed_mm) -``` - -> 注意:`compute_deflection` 本身**不做 CAD 配准**。如果你想自己控制配准过程,先调用 `register_to_cad`,把变换应用到点云后再喂给 `compute_deflection`,配合 `config["frame"]["use_input_axes"]=True`(这本来就是默认值)即可: - -```python -from underframe_deflection import ( - read_cad_reference, register_to_cad, compute_deflection, -) -from underframe_deflection.registration import apply_transform - -scan = load_point_cloud("scan.ply") -cad = read_cad_reference("reference.ply") - -# 你需要一份预解算好的 config(spacing 之类)。最简单的办法: -from underframe_deflection.config import load_config, resolve_numeric_parameters, cloud_geometry -from underframe_deflection.preprocess import estimate_spacing -cfg = load_config(None, "auto") -cfg = resolve_numeric_parameters(cfg, cloud_geometry(scan), estimate_spacing(scan)) - -reg = register_to_cad(scan, cad, cfg) -scan_aligned = apply_transform(scan, reg.R, reg.t) - -result = compute_deflection(scan_aligned, roi_aabb=roi_aabb) -``` - ---- - -## 3. 读取输出 - -### 3.1 核心数值(最常用) - -```python -left = report["deflection"]["left"]["selected_measurement"] -right = report["deflection"]["right"]["selected_measurement"] - -if left["available"]: - x_mm = left["x_mm"] - signed = left["signed_deflection_mm"] # 带符号,向下为负 - absolute = left["absolute_deflection_mm"] - type_ = left["selected_type"] # "downward" / "upward" - beam_pt = left["beam_point"] # [x, y, z] - ref_pt = left["reference_point"] # [x, y, z] -``` - -如果你用的是 `compute_deflection`,公开数据类更直接: - -```python -result.left.selected_signed_mm -result.left.max_downward_mm -result.left.max_downward_x_mm -result.left.max_upward_mm -result.left.reference_confidence -result.curves.x_mm # ndarray -result.curves.left_deflection_mm # ndarray, NaN 表示该 X 处无效 -``` - -### 3.2 质量警告 `quality_flags` - -**非致命**问题列在这里,调用方应判断是否要重测: - -| Flag | 含义 | -| --- | --- | -| `left_track_missing` / `right_track_missing` | 单侧梁追踪失败 | -| `left_representative_line_missing` / `right_...` | 单侧代表线拟合失败 | -| `left_reference_unavailable` / `right_...` | 单侧参考线不可用 | -| `left_reference_low_confidence` / `right_...` | 参考线置信度低 | -| `left_track_low_coverage` / `right_...` | 单侧追踪覆盖率 < 45% | -| `left_right_symmetry_warning` | 左右对称性偏差 > 18% | -| **`registration_not_converged`** | ICP 未收敛到容差内 | -| **`registration_poor_quality`** | ICP 最终 RMS > max(10 mm, 2×点间距) | -| **`registration_low_inlier_ratio`** | ICP 内点比例 < 50% | -| **`passthrough_empty`** | ROI passthrough 后点过少,已自动回退 | -| **`bilateral_degraded`** | bilateral k 邻域被裁剪 | - -加粗的几项是 v1.1 新增。 - -### 3.3 配准信息 - -```python -reg = report["registration"] -if reg["performed"]: - reg["rms_mm"] # 最终对齐残差 - reg["initial_rms_mm"] # 粗对齐后的初始残差 - reg["iterations"] - reg["inlier_ratio"] - reg["converged"] # bool - reg["R"] # 3x3 list - reg["t"] # length-3 list -``` - -### 3.4 Excel 误差报表 - -`out/report.xlsx` 5 个 sheet: - -| Sheet | 内容 | -| --- | --- | -| **Summary** | key/value 形式:版本、输入路径、配准统计、预处理各阶段计数、左右挠度核心数值、质量警告 | -| **Deflection_Curves** | 沿 X 的逐点表:x / 左右 beam_y, beam_z / 左右 ref_y, ref_z / 左右 deflection / 左右 confidence | -| **Beam_Tracks** | 每片候选条带:slice_index、y_center、z_low、width、score、selected_rank 等 | -| **Reference_Lines** | 左右参考线 front/back 端点、direction、residual、confidence | -| **Representative_Points** | 左右每个代表点的 x/y/z/width/residual/confidence | - -> 用 `openpyxl`、`pandas.read_excel`、Excel/LibreOffice 都可以直接读。 - -### 3.5 异常 - -| 异常类 | 触发条件 | -| --- | --- | -| `InvalidPointCloudError` | 点云 / CAD / ROI JSON 文件格式非法、字段缺失、AABB 反转 | -| `UnsupportedFormatError` | 文件后缀不在支持列表 | -| `InsufficientPointsError` | 点数过少(点云 < 256)或 ROI 过滤后无重叠 | -| `FrameInferenceError` | 自动坐标系推断失败(仅 legacy 路径) | - -集成方应在外层 `try / except DeflectionError` 兜底,并把 `quality_flags` 落入日志或上层告警。 - ---- - -## 4. 配置覆盖(高级) - -绝大多数场景不需要碰配置。需要微调时,通过 `--config my.json` 或 `config=` 传入嵌套 dict,与 `DEFAULT_CONFIG` 深度合并: - -```python -config = { - "preprocess": { - "bilateral": {"enabled": True, "k": 32, "sigma_n_deg": 10.0}, - # 也可以从外部传 passthrough,但通常用 ROI JSON 即可 - }, - "registration": { - "icp_max_iter": 80, - "trim_ratio": 0.15, - }, - "reference": { - "low_confidence_threshold": 0.5, - }, - "deflection": { - "evaluation_grid_factor": 0.5, # 更密的挠度采样 - }, -} -``` - -完整字段定义见 `underframe_deflection/config.py: DEFAULT_CONFIG`。 - ---- - -## 5. 进度回调 - -`compute_deflection` 支持 `progress` 回调: - -```python -def on_progress(stage: str, fraction: float) -> None: - print(f"[{fraction:5.1%}] {stage}") - -result = compute_deflection(points, roi_aabb=roi_aabb, progress=on_progress) -``` - -阶段顺序:`preprocess → frame → beam_tracking → line_fitting → reference → deflection`。 - -`run_measurement_pipeline` 当前未透传 progress 回调,需要时请直接调 `compute_deflection`。 - ---- - -## 6. 坐标系约定 - -输入点云**必须**满足以下约定,否则结果无意义: - -- **X 轴**:纵向(车长方向) -- **Y 轴**:横向(车宽方向) -- **Z 轴**:高度方向,**向上为正** -- 单位:毫米 - -CAD 基准也必须遵循同样的轴向;ROI JSON 中的 bbox 字段在 CAD 坐标系下。 -扫描点云轴向不同 → 调用前自行做置换 / 旋转。 - ---- - -## 7. 版本兼容性 - -- `__version__` —— 包版本 -- `ALGORITHM_VERSION` —— 算法版本(升级会改变数值结果) -- `SCHEMA_VERSION` —— `report.json` 结构版本(仅在破坏性变更时递增;新字段不递增) - -v1.1 新增了非破坏性的可选字段(`cad_reference / roi_source / roi_aabb / registration / preprocess.passthrough / preprocess.bilateral / roi.source / quality_flags 新值`),`SCHEMA_VERSION` 保持 `"1.0"` 不变。 - ---- - -## 8. 集成模板 - -```python -import logging -from underframe_deflection import ( - run_measurement_pipeline, - DeflectionError, -) - -log = logging.getLogger(__name__) - - -def measure(scan_path: str, cad_path: str, roi_path: str, out_dir: str) -> dict | None: - try: - report = run_measurement_pipeline( - input_cloud=scan_path, - cad_path=cad_path, - roi_path=roi_path, - out_dir=out_dir, - ) - except DeflectionError as exc: - log.error("deflection failed: %s", exc) - return None - - flags = report.get("quality_flags") or [] - if flags: - log.warning("quality flags: %s", flags) - - return { - "schema_version": report["schema_version"], - "algorithm_version": report["algorithm_version"], - "left_signed_mm": report["deflection"]["left"].get("selected_measurement", {}).get("signed_deflection_mm"), - "right_signed_mm": report["deflection"]["right"].get("selected_measurement", {}).get("signed_deflection_mm"), - "registration_rms_mm": report["registration"].get("rms_mm"), - "flags": flags, - } -``` diff --git a/alg/delivery_v1.1/underframe_deflection/cli.py b/alg/delivery_v1.1/underframe_deflection/cli.py deleted file mode 100644 index 546bac8..0000000 --- a/alg/delivery_v1.1/underframe_deflection/cli.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Command-line entry point installed by ``pyproject.toml``. - -This mirrors the project-root ``run_underframe_measurement.py`` script so -that ``pip install`` exposes a console command (``underframe-measure``). -""" - -from __future__ import annotations - -import argparse - -from .pipeline import run_measurement_pipeline - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Measure underframe longitudinal beam deflection from a point cloud.", - ) - parser.add_argument( - "--input-cloud", - required=True, - help="Path to input scan point cloud (.ply or .xyz).", - ) - parser.add_argument( - "--out-dir", - required=True, - help="Directory for report.json/.xlsx, CSV files, PLY overlays, and PNG plots.", - ) - parser.add_argument( - "--cad", - default=None, - help="Optional CAD reference (.ply/.xyz/.stl/.obj) used as the registration baseline.", - ) - parser.add_argument( - "--roi", - default=None, - help="Optional ROI JSON file holding an axis-aligned bbox in the CAD frame.", - ) - parser.add_argument( - "--config", - default=None, - help="Optional JSON config override file.", - ) - parser.add_argument( - "--profile", - default="auto", - choices=["auto", "M", "TC", "TP", "custom"], - help="Optional profile for scale-related tuning.", - ) - parser.add_argument( - "--skip-registration", - action="store_true", - help="Bypass ICP even when --cad is supplied (useful for debugging).", - ) - parser.add_argument( - "--no-bilateral", - action="store_true", - help="Disable the bilateral denoising step.", - ) - parser.add_argument( - "--skip-excel", - action="store_true", - help="Skip writing the .xlsx error report.", - ) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - run_measurement_pipeline( - input_cloud=args.input_cloud, - out_dir=args.out_dir, - config_path=args.config, - profile=args.profile, - cad_path=args.cad, - roi_path=args.roi, - skip_registration=args.skip_registration, - enable_bilateral=not args.no_bilateral, - skip_excel=args.skip_excel, - ) - - -if __name__ == "__main__": - main() diff --git a/alg/delivery_v1.1/underframe_deflection/io.py b/alg/delivery_v1.1/underframe_deflection/io.py deleted file mode 100644 index 60e49e9..0000000 --- a/alg/delivery_v1.1/underframe_deflection/io.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import annotations - -import csv -import json -from pathlib import Path -from typing import Iterable - -import numpy as np - - -def read_point_cloud(path: str | Path) -> np.ndarray: - path = Path(path) - suffix = path.suffix.lower() - if suffix == ".ply": - return _read_ascii_ply(path) - if suffix in {".xyz", ".txt", ".csv"}: - return np.loadtxt(path, dtype=np.float64) - raise ValueError(f"Unsupported point cloud format: {path.suffix}") - - -def _read_ascii_ply(path: Path) -> np.ndarray: - header_lines = 0 - vertex_count = None - with path.open("r", encoding="ascii", errors="strict") as fh: - for line in fh: - header_lines += 1 - stripped = line.strip() - if stripped.startswith("element vertex"): - vertex_count = int(stripped.split()[-1]) - if stripped == "end_header": - break - points = np.loadtxt(path, skiprows=header_lines, dtype=np.float64) - if points.ndim == 1: - points = points[None, :] - if points.shape[1] < 3: - raise ValueError(f"PLY file does not contain XYZ coordinates: {path}") - points = points[:, :3] - if vertex_count is not None and len(points) != vertex_count: - raise ValueError( - f"PLY vertex count mismatch: header={vertex_count}, data={len(points)}" - ) - return points - - -def ensure_output_dir(path: str | Path) -> Path: - out_dir = Path(path) - out_dir.mkdir(parents=True, exist_ok=True) - return out_dir - - -def write_json(path: str | Path, payload: dict) -> None: - Path(path).write_text( - json.dumps(payload, ensure_ascii=False, indent=2), - encoding="utf-8", - ) - - -def write_csv(path: str | Path, fieldnames: list[str], rows: Iterable[dict]) -> None: - with Path(path).open("w", encoding="utf-8", newline="") as fh: - writer = csv.DictWriter(fh, fieldnames=fieldnames) - writer.writeheader() - for row in rows: - writer.writerow(row) - - -def write_colored_ply( - path: str | Path, - points: np.ndarray, - colors: np.ndarray, -) -> None: - path = Path(path) - if len(points) != len(colors): - raise ValueError("Point/color length mismatch for PLY export.") - points = np.asarray(points, dtype=np.float64) - colors = np.asarray(colors, dtype=np.uint8) - with path.open("w", encoding="ascii", newline="\n") as fh: - fh.write("ply\n") - fh.write("format ascii 1.0\n") - fh.write(f"element vertex {len(points)}\n") - fh.write("property float x\n") - fh.write("property float y\n") - fh.write("property float z\n") - fh.write("property uchar red\n") - fh.write("property uchar green\n") - fh.write("property uchar blue\n") - fh.write("end_header\n") - for (x, y, z), (r, g, b) in zip(points, colors, strict=True): - fh.write(f"{x:.6f} {y:.6f} {z:.6f} {int(r)} {int(g)} {int(b)}\n") diff --git a/alg/delivery_v1.1/underframe_deflection/test_data/sample_roi.json b/alg/delivery_v1.1/underframe_deflection/test_data/sample_roi.json deleted file mode 100644 index 595aa12..0000000 --- a/alg/delivery_v1.1/underframe_deflection/test_data/sample_roi.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "x_min": -12500.0, - "x_max": 12500.0, - "y_min": 150.0, - "y_max": 4100.0, - "z_min": -1680.0, - "z_max": 200.0 -} diff --git a/alg/delivery_v1.2/coupler_panel_release/README.md b/alg/delivery_v1.2/coupler_panel_release/README.md new file mode 100644 index 0000000..0d897c3 --- /dev/null +++ b/alg/delivery_v1.2/coupler_panel_release/README.md @@ -0,0 +1,68 @@ +# coupler-panel-validation + +高铁车钩面板**偏转角测量**算法。输入待测点云 + ROI + 理论 CAD,输出面板相对车体纵轴的两个方向偏转角及最大偏差,并生成 Excel 报表。 + +## 这个项目做什么 + +给定一块车钩安装面板的扫描点云,算法测量它相对理论安装姿态的**偏转**: + +- `θ_right` —— 面板法向在一个方向上的偏转角(度) +- `θ_ref` —— 面板法向在另一个方向上的偏转角(度) +- `dx_y_max` / `dx_z_max` —— 由偏转引起的最大 x 向偏差(mm) + +支持两种输入规模: +- **单零件点云**(对零件 STP 采样得到的稠密点云) +- **整车扫描点云**(GB 级 PLY,用 ROI 框出车钩区域后内部流式裁剪) + +## 三件套输入(集成主路径) + +| 输入 | 说明 | +|---|---| +| **ROI JSON** | 6 个数的轴对齐包围盒,框出待测面板区域(即裁剪超参数) | +| **点云 PLY** | 待测点云,可为整车(多 GB),算法内部按 ROI 流式裁剪 | +| **CAD STP** | 理论模型,作辅助 / 审计 | + +输出:`eval_report.xlsx`(含两个偏转角的 GT / 预测 / 误差 + 中间量 + 运行时间)。 + +## 快速开始 + +```bash +# 安装(锁定版本见 requirements.txt) +python -m pip install -r requirements.txt +pip install -e ".[all]" + +# 三件套集成入口 +python examples/run_integration.py \ + --roi examples/roi_train2_coupler.json \ + --cloud /path/to/train_2_1T.ply \ + --stp /path/to/asm.stp \ + --out runs/train2 \ + --profile train2_coupler +``` + +库调用: + +```python +from coupler_panel.pipeline import run_coupler_measurement_pipeline +from coupler_panel.roi_select import load_roi_json + +result = run_coupler_measurement_pipeline( + input_cloud = "train_2_1T.ply", + out_dir = "runs/train2", + profile = "train2_coupler", + roi_spec = load_roi_json("roi_train2_coupler.json"), +) +print(result.theta_right_deg, result.theta_ref_deg) +``` + +## 文档 + +| 文档 | 内容 | +|---|---| +| [USAGE.md](USAGE.md) | **集成方使用指南** —— 三件套输入、API、ROI JSON、profile、输出、坐标系 | +| [CHANGES.md](CHANGES.md) | **修改记录** —— 每轮改了什么、为什么、遇到的问题与定位解决 | + +## 坐标系约定 + +- 算法以面板法向 `n=(n_x,n_y,n_z)` 相对 x 轴的夹角定义偏转,强制 `n_x>0`。 +- **坐标轴含义由数据集决定**:项目标称约定为 x=纵向、y=宽度、z=高度;但某些整车数据集为 x=车长、y=车高、z=车宽(y/z 对调)。偏转角公式对称、理论 0 不受影响,但 `D_y_max`/`D_z_max` 对应的物理轴随之对调。ROI JSON 的六个边界必须与**点云自身坐标系**一致。详见 USAGE.md。 diff --git a/alg/delivery_v1.2/coupler_panel_release/USAGE.md b/alg/delivery_v1.2/coupler_panel_release/USAGE.md new file mode 100644 index 0000000..3c988d0 --- /dev/null +++ b/alg/delivery_v1.2/coupler_panel_release/USAGE.md @@ -0,0 +1,256 @@ +# coupler_panel — 集成开发者使用指南 + +> 高铁车钩面板偏转角测量算法 +> +> **主输入:ROI JSON + 点云 PLY + CAD STP(三件套)** +> **主输出:含两个方向偏转角的 Excel 报表** + +--- + +## 1. 一句话集成 + +```python +from coupler_panel.pipeline import run_coupler_measurement_pipeline +from coupler_panel.roi_select import load_roi_json + +result = run_coupler_measurement_pipeline( + input_cloud = "train_2_1T.ply", # 待测点云(可为整车,内部按 ROI 流式裁剪) + out_dir = "runs/today", # 中间产物 + report.json 落盘 + profile = "train2_coupler", # 该零件 profile + roi_spec = load_roi_json("roi.json"), # ROI = 裁剪超参数 ./examples/roi_train2_coupler.json +) +print(result.theta_right_deg, result.theta_ref_deg) # 两个偏转角 +print(result.dx_y_max_mm, result.dx_z_max_mm) # 最大 x 偏差 +``` + +要直接产出 Excel,用集成入口脚本(§3)。 + +--- + +## 2. 安装 + +```bash +python -m pip install -r requirements.txt # 锁定版本(Python 3.10 验证) +pip install -e ".[all]" # core + cloud + cad + viz + report +``` + +按需子集见 `pyproject.toml` 的 extras:`[cloud]` `[cad]` `[viz]` `[report]`。 + +--- + +## 3. 集成入口(三文件 → Excel) + +`examples/run_integration.py` 是给集成方的标准入口: + +```bash +python examples/run_integration.py \ + --roi examples/roi_train2_coupler.json \ # ROI JSON + --cloud /path/to/train_2_1T.ply \ # 整车点云(可多 GB) + --stp /path/to/asm.stp \ # CAD STP(审计) + --out runs/train2 \ # 输出目录 + --profile train2_coupler +``` + +产出 `runs/train2/eval_report.xlsx`(30 列,见 §7)+ 全部中间产物。 + +可选参数:`--gt-theta-right` / `--gt-theta-ref`(已知理论偏转,用于误差列,默认 0)、`--sample-id`、`--config`。 + +--- + +## 4. 三件输入说明 + +### 4.1 ROI JSON(裁剪超参数) + +6-key 轴对齐包围盒,**坐标系必须与点云一致**,单位 mm: + +```json +{ + "x_min": 11526.0, "x_max": 11733.0, + "y_min": 588.0, "y_max": 1192.0, + "z_min": -950.0, "z_max": 950.0 +} +``` + +- 点 `(x,y,z)` 保留条件:`x_min<=x<=x_max ∧ y_min<=y<=y_max ∧ z_min<=z<=z_max` +- 必须 `min **坐标系坑(必读)**:ROI 的六个边界写在**点云自己的坐标系**里。若点云是 x=车长 / y=车高 / z=车宽(部分整车数据集如此),ROI 也按此填。ROI 裁出 0 点时算法抛 `InsufficientPointsError` 并提示检查坐标系。 + +### 4.2 点云 PLY + +- 支持 `.ply`(二进制/ASCII)/ `.pcd` / `.xyz` / `.txt` / `.csv` +- 单位 **mm**;算法只读 x/y/z 前三列(颜色/法向等额外列容忍) +- **可为整车扫描**(多 GB)——ROI 流式裁剪后才进入算法 +- **算法不做配准**:假设上游扫描端外参已标定、点云已在 CAD/车体帧(原因见 §8) + +### 4.3 CAD STP + +当前 STP 的角色取决于规模: + +- **单零件 STP**:可被 `parse_cad` 解析,用于提供面板法向 seed + 自动整定检测参数(见 §6)。传 `cad_stp_path=...`。 +- **整车装配 STP**(数百 MB、数千实例):项目的 `parse_cad`(单零件设计)无法处理,此时 STP 仅作**审计/可追溯**输入(路径记入报表),不参与计算——检测参数由 profile 提供、法向用 PCA。 + +> 若将来需要从整车 STP 提取目标几何,需另写装配级 XCAF 解析(不能复用单零件 `parse_cad`)。 + +--- + +## 5. 主入口 API + +```python +def run_coupler_measurement_pipeline( + input_cloud : str | Path, # 必填:点云路径(可整车) + out_dir : str | Path, # 必填:输出目录 + *, + config_path : str | Path | None = None, # 可选:自定义 YAML / dict + profile : str = "auto", # 可选:preset profile + cad_stp_path: str | Path | None = None, # 可选:单零件 STP(法向 + autotune) + roi_spec : ROISpec | None = None, # 二选一:内存 ROI 对象 + roi_json : str | Path | None = None, # 二选一:ROI JSON 路径 + progress : Callable[[str, float], None] | None = None, +) -> CouplerMeasurement +``` + +**算法链路**: + +``` +crop_ply_bbox(流式 ROI 裁剪) + → [layer_select] 沿法向隔离前层(profile 开关,去多层重叠) + → [autotune] 从 CAD 孔径 + 点距自整定(仅默认 profile + 单零件 CAD 时) + → denoise_cloud + → compute_coupler_measurement + validate → detect 4 holes → build frame + → extract ROI → fit plane(RANSAC+SVD) → metrics +``` + +**profile 选择**: +- 有匹配 profile(如 `train2_coupler` / `lu1_af4`)→ 直接指定,最稳。 +- 无 profile 但传了单零件 `cad_stp_path` → `profile="auto"`,走自动整定(§6)。 + +**配置覆盖**:`config_path` 可传 dict 覆盖任意 `configs/default.yaml` 字段。 + +### 5.1 返回值 `CouplerMeasurement` + +```python +theta_right_deg, theta_ref_deg # 两个偏转角(度) +D_y_max_mm, D_z_max_mm # 两方向最大距中心偏差 +dx_y_max_mm, dx_z_max_mm # 最大 x 向偏差 +center_C, normal # 面板中心、拟合法向(n_x>0) +holes, plane, frame # 4 孔 / 平面 / 参考系子结构 +quality_flags, timings, report # 诊断;report == out_dir/report.json +result.to_dict() # 完全 JSON 化 +``` + +### 5.2 异常 + +``` +CouplerError +├── InvalidPointCloudError / UnsupportedFormatError 输入非法 / 格式不支持 +├── InsufficientPointsError 点数过少 / ROI 裁出 0 点 +├── CADParseError STP 解析失败 +├── HoleDetectionError 4 孔检测失败 +└── PlaneFitError 平面拟合失败 +``` + +非致命问题在 `result.quality_flags`(如 `plane_inlier_ratio_low`)。 + +--- + +## 6. 检测参数:profile vs 自动整定 + +不同零件的定位孔半径、是否有中心孔、点云密度都不同,检测参数必须匹配,否则报 `HoleDetectionError: only N hole-shaped cavities found`。两种适配方式: + +1. **per-part profile(推荐用于已知零件)**:`configs/profiles/.yaml` 写死该零件的孔径带、面积窗、是否前层隔离等。已有 `train2_coupler`(整车车钩面板)、`lu1_af4`(单零件车钩面板)。 +2. **autotune(单零件 + 默认 profile 时)**:传单零件 `cad_stp_path` 且 `profile=auto`、无 `config`,算法从 CAD 定位孔半径 + 去噪后点距自动推导检测参数(`coupler_panel/detection/autotune.py`)。指定具体 profile 或 config 时**完全跳过**。 + +> 整车场景用 profile(`train2_coupler`),因为整车 STP 不能 parse、autotune 不可用。 + +--- + +## 7. 输出 + +### 7.1 Excel 报表(`out_dir/eval_report.xlsx`,30 列) + +样本标识、输入 PLY、ROI kind、**θ_right / θ_ref 的 GT/预测/误差**、D_y/D_z_max、dx_y/dx_z_max、中心 C(x/y/z)、法向 n(x/y/z)、平面内点率/残差、配准 RMSE/最大残差(主路径恒空)、ROI 前后点数、quality_flags、**算法运行时间 `Algorithm runtime (s)`**(输入→返回整段 wall-clock)、内核阶段累计、算法/schema 版本。 + +多组结果用 `coupler_panel.reporting.write_excel_report(rows, path)` 合并成一张表。 + +### 7.2 落盘中间产物(`out_dir/`) + +| 文件 | 内容 | +|---|---| +| `config.resolved.yaml` | 实际生效的合并配置 | +| `roi.json` | ROI spec + 裁剪诊断 | +| `01_gt.json` | (单零件 CAD parse 时)审计 GT | +| `02_sampled.ply` | ROI 裁剪后的点云 | +| `transformed.ply` | (前层隔离开启时)隔离后的层 | +| `05_denoised.ply` | 去噪后点云(喂给测量算法的最终点云) | +| `report.json` | 完整结果,等于 `result.to_dict()` | + +--- + +## 8. 为什么不做配准 + +车钩面板的 4 个定位孔与面板一起刚体旋转。用「扫到的 4 孔 → CAD 原始 4 孔」做 Kabsch,会精确解出「扫描器位姿 · 面板偏转」的复合刚体变换并反向作用,把偏转随位姿误差**一起消掉**,下游再读不到偏转——这是几何性质,不是 bug。 + +故主算法假设上游扫描端外参标定、点云已在车体帧。研究路径 `run_from_cad_pipeline`(含 Kabsch + 注入 oracle)完整保留作上限对照。 + +--- + +## 9. 集成模板 + +```python +import logging, time, json +from pathlib import Path +from coupler_panel.pipeline import run_coupler_measurement_pipeline +from coupler_panel.roi_select import load_roi_json +from coupler_panel.reporting import build_row_from_summary, write_excel_report +from coupler_panel.exceptions import CouplerError + +log = logging.getLogger(__name__) + +def measure(cloud, roi_json, stp, out_dir, profile="train2_coupler"): + t0 = time.perf_counter() + try: + r = run_coupler_measurement_pipeline( + input_cloud=cloud, out_dir=out_dir, + profile=profile, roi_spec=load_roi_json(roi_json)) + except CouplerError as e: + log.error("measurement failed: %s", e) + return None + dt = time.perf_counter() - t0 + + diag = json.loads((Path(out_dir)/"roi.json").read_text(encoding="utf-8"))["diagnostics"] + row = build_row_from_summary( + sample_id=Path(cloud).stem, ply_path=str(cloud), roi_kind="aabb", + measured=r.to_dict(), gt_angles=(0.0, 0.0), registration=None, + n_points_in=diag.get("n_in", 0), n_points_after_roi=diag.get("n_out", 0), + pipeline_seconds=dt) + write_excel_report([row], Path(out_dir)/"eval_report.xlsx") + return r +``` + +--- + +## 10. 坐标 & 单位速查 + +| 量 | 单位 | 说明 | +|---|---|---| +| 点云坐标 | mm | 轴含义由数据集决定(见 README / §4.1) | +| 偏转角 | 度 | `θ_right=atan2(n_y,n_x)`、`θ_ref=atan2(n_z,n_x)` | +| 法向 | 单位向量 | 强制 `n_x>0` | +| `right_axis_sign` | ±1 | 右方向符号(profile `metrics` 配置) | + +--- + +## 11. 版本兼容 + +| 标识 | 含义 | +|---|---| +| `coupler_panel.__version__` | 包版本(语义化) | +| `coupler_panel.ALGORITHM_VERSION` | 算法版本,升级时**数值可能变化** | +| `coupler_panel.SCHEMA_VERSION` | 结果结构版本,仅破坏性变更递增 | + +建议:落库时记录 `algorithm_version`;反序列化 `report.json` 时校验 `schema_version` 主版本。 diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/default.yaml b/alg/delivery_v1.2/coupler_panel_release/configs/default.yaml similarity index 96% rename from alg/delivery_v1.1/coupler_panel_release/configs/default.yaml rename to alg/delivery_v1.2/coupler_panel_release/configs/default.yaml index 414a185..957c01d 100644 --- a/alg/delivery_v1.1/coupler_panel_release/configs/default.yaml +++ b/alg/delivery_v1.2/coupler_panel_release/configs/default.yaml @@ -101,12 +101,10 @@ detection: panel_normal_seed: from_cad # from_cad | pca | given panel_normal_pca_kth: 1 # use k-th smallest PCA eigenvector as panel normal # 2D occupancy grid. - grid_pixel_mm: 0.3 + grid_pixel_mm: 0.5 panel_min_area_ratio: 0.4 # largest connected component must be ≥ this fraction of bbox - hole_min_area_mm2: 10.0 - hole_max_area_mm2: 1500.0 - closing_iters: 2 # 原来没有,加形态学闭运算填补边界断裂 - fill_bg_below_mm2: 5.0 # 原来没有,填充小碎片 + hole_min_area_mm2: 30.0 + hole_max_area_mm2: 800.0 # Circle fitting on hole boundary. circle_fit: method: algebraic_then_lsq # algebraic_then_lsq | hyper | taubin diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/profiles/high_density.yaml b/alg/delivery_v1.2/coupler_panel_release/configs/profiles/high_density.yaml similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/configs/profiles/high_density.yaml rename to alg/delivery_v1.2/coupler_panel_release/configs/profiles/high_density.yaml diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/profiles/low_density.yaml b/alg/delivery_v1.2/coupler_panel_release/configs/profiles/low_density.yaml similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/configs/profiles/low_density.yaml rename to alg/delivery_v1.2/coupler_panel_release/configs/profiles/low_density.yaml diff --git a/alg/delivery_v1.1/coupler_panel_release/configs/profiles/lu1_af4.yaml b/alg/delivery_v1.2/coupler_panel_release/configs/profiles/lu1_af4.yaml similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/configs/profiles/lu1_af4.yaml rename to alg/delivery_v1.2/coupler_panel_release/configs/profiles/lu1_af4.yaml diff --git a/alg/delivery_v1.2/coupler_panel_release/configs/profiles/train2_coupler.yaml b/alg/delivery_v1.2/coupler_panel_release/configs/profiles/train2_coupler.yaml new file mode 100644 index 0000000..57674aa --- /dev/null +++ b/alg/delivery_v1.2/coupler_panel_release/configs/profiles/train2_coupler.yaml @@ -0,0 +1,94 @@ +# Profile for the train_2 coupler mounting panel measured from a full-vehicle +# scan (train_2_1T.ply, cropped to the coupler ROI). +# +# Part topology (measured from the 1T cloud, dataset frame x=length, +# y=height, z=width): +# - a wide base plate with a RAISED central square mounting plate +# - the central plate has 4 corner bolt holes, r≈15.6 mm, +# rectangle 360 mm (z) × 200 mm (y), at z=±180, y=835/1035 +# - NO central pass-through hole +# - the ROI box also slices a roof truss + the base plate behind the +# raised plate, so a normal-direction layer isolation is required +# +# Use: +# run_coupler_measurement_pipeline(input_cloud=roi.ply, +# profile="train2_coupler", roi_spec=) # no CAD, no Kabsch + +# --- CAD block (not used for STP parse here; kept for radius thresholds) --- +cad: + linear_deflection_mm: 0.5 + angular_deflection_rad: 0.1 + locating_hole_radius_mm: [11.0, 19.0] # r≈15.6 corner bolt holes + central_hole_radius_min_mm: 999.0 # this part has NO central hole + rectangle_aspect_tol: 0.40 + +# --- Normal-direction layer isolation (the key pre-processing step) ------- +# Isolate the frontmost dominant layer = the raised central plate, so the +# back plate + truss don't fill the holes / pollute the plane fit. +layer_select: + enabled: true + normal_seed: pca # pca | from_cad + half_width_mm: 8.0 # slab half-thickness around the chosen layer + pick: frontmost # frontmost (toward scanner) | densest + bins: 240 + +# --- Denoise: drop the density-fragile radius filter (sparse-ish scan) ---- +denoise: + steps: + - type: passthrough + bbox_padding_mm: 50.0 + - type: voxel + voxel_mm: 0.5 + - type: statistical + nb_neighbors: 20 + std_ratio: 2.5 + +# --- 4-hole detection tuned for r≈15.6 mm bolt holes ---------------------- +detection: + panel_normal_seed: pca + grid_pixel_mm: 2.0 + closing_iters: 1 + fill_bg_below_mm2: 60.0 + panel_min_area_ratio: 0.15 + hole_min_area_mm2: 300.0 # r=15.6 -> ~765 mm²; floor well below + hole_max_area_mm2: 1400.0 # ceil above; excludes larger back-layer holes + locating_hole_radius_mm: [11.0, 19.0] + rectangle_aspect_tol: 0.40 + circle_fit: + method: algebraic_then_lsq + inlier_residual_mm: 1.5 + +# --- Panel ROI extraction ------------------------------------------------- +roi: + hole_dilate_mm: 5.0 + central_hole_dilate_mm: 10.0 + edge_buffer_mm: 8.0 + chamfer_normal_dot_min: 0.80 + outlier_z_score: 3.0 + +# --- Plane fit ------------------------------------------------------------ +plane_fit: + ransac_iterations: 400 + ransac_inlier_thresh_mm: 1.0 + min_inlier_ratio: 0.3 + enforce_nx_positive: true + +# --- Metrics -------------------------------------------------------------- +# NOTE: this dataset's frame is x=length, y=height, z=width — i.e. y and z +# are swapped relative to the package's nominal (y=width, z=height). The +# angle formulas are symmetric in form; here theta_right = atan2(n_y,n_x) +# is the tilt toward car-height and theta_ref = atan2(n_z,n_x) is the tilt +# toward car-width. Both are ~0 for an undeflected panel. +metrics: + right_axis_sign: 1 + angle_clamp_deg: 0.0 + +registration: + method: kabsch_four_holes + refuse_if_residual_above_mm: 5.0 + +artifacts: + save_intermediate: true + ply_format: binary + image_dpi: 150 + heatmap_colormap: viridis diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/_version.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/_version.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/_version.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/_version.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/api.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/api.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/api.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/api.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/cad/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/cad/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/cad/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/cad/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/cad/gt_extractor.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/cad/gt_extractor.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/cad/gt_extractor.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/cad/gt_extractor.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/cad/mesh.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/cad/mesh.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/cad/mesh.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/cad/mesh.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/cad/stp_parser.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/cad/stp_parser.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/cad/stp_parser.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/cad/stp_parser.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/cli.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/cli.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/cli.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/cli.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/config.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/config.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/config.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/config.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/data_prep.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/data_prep.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/data_prep.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/data_prep.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/passthrough.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/passthrough.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/passthrough.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/passthrough.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/pipeline.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/pipeline.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/pipeline.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/pipeline.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/radius.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/radius.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/radius.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/radius.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/smoothing.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/smoothing.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/smoothing.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/smoothing.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/statistical.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/statistical.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/statistical.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/statistical.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/voxel.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/voxel.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/denoise/voxel.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/denoise/voxel.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/__init__.py diff --git a/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/autotune.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/autotune.py new file mode 100644 index 0000000..95a90ef --- /dev/null +++ b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/autotune.py @@ -0,0 +1,114 @@ +"""Derive scale/density-dependent config from CAD geometry + cloud spacing. + +This is a **configuration helper**, not part of the measurement algorithm. +The denoise, detection, ROI and plane-fit algorithms are all unchanged; +this module only fills in the knobs whose *correct value depends on the +panel size and the point spacing*, so that the "CAD STP + cloud + ROI" +input contract is self-sufficient and does not need a hand-tuned profile +per panel. + +Why it is needed +---------------- + +The shipped ``default.yaml`` is tuned for a small, densely-sampled panel. +Two groups of knobs break on a large, sparsely-scanned panel (e.g. the +lu1 coupler panel: r≈16 mm locating holes, ~3-4 mm point spacing over a +~1.5 m face): + +1. **Denoise radius filter** ``{type: radius, nb_points: 8, radius_mm: 2}`` + demands 8 neighbours within 2 mm. At 3.7 mm spacing *every* point fails + that test, so the whole cloud is deleted → ``too few points: 0``. + +2. **Detection grid / area band** (``grid_pixel_mm=0.5``, + ``hole_*_area_mm2=[30,800]``, ``locating_hole_radius_mm=[4,12]``) make + the occupancy grid too fine and the cavity-area window too small, so the + locating holes never form detectable cavities → + ``only N hole-shaped cavities found``. + +Two-stage application (around denoise) +-------------------------------------- + +The two groups must be tuned at *different* moments because the point +spacing changes across denoising: + +- :func:`relax_denoise_steps` runs **before** denoising. It drops the + density-fragile radius filter (the statistical filter still removes + outliers), so a sparse cloud survives. +- :func:`detection_overlay` runs **after** denoising, measuring the point + spacing on the *denoised* cloud — the same cloud detection will see — + and sizes the grid / fill / area band from the CAD locating-hole radius + plus that spacing. + +Together they reproduce, to within a few percent, the values previously +hand-tuned in ``configs/profiles/lu1_af4.yaml``. +""" + +from __future__ import annotations + +import copy +from typing import Any + +import numpy as np + + +def median_nn_spacing(points: np.ndarray, *, max_samples: int = 5000) -> float: + """Median nearest-neighbour distance, sub-sampled for speed (deterministic).""" + pts = np.asarray(points, dtype=np.float64) + n = pts.shape[0] + if n < 2: + return 1.0 + try: + from scipy.spatial import cKDTree + except ImportError: + span = float(np.linalg.norm(pts.max(axis=0) - pts.min(axis=0))) + return max(span / max(n ** (1.0 / 3.0), 1.0), 1e-6) + + step = max(1, n // max_samples) + probe = pts[::step] + tree = cKDTree(pts) + d, _ = tree.query(probe, k=2) + return float(np.median(d[:, 1])) + + +def relax_denoise_steps(denoise_cfg: dict[str, Any]) -> dict[str, Any]: + """Return a denoise cfg with the density-fragile ``radius`` filter removed. + + The radius outlier filter deletes everything on a cloud sparser than its + ``radius_mm``; the statistical filter already handles outliers, so for an + auto-tuned run we simply drop the radius step (this matches the proven + lu1_af4 denoise recipe). Other steps are kept as-is. + """ + out = copy.deepcopy(denoise_cfg) if denoise_cfg else {} + steps = out.get("steps", []) + out["steps"] = [s for s in steps if s.get("type") != "radius"] + return out + + +def detection_overlay(locating_hole_radius_mm: float, points: np.ndarray) -> dict[str, Any]: + """Detection sub-config derived from locating-hole radius + DENOISED spacing. + + ``points`` must be the cloud detection will actually run on (i.e. the + denoised cloud), because the fill-gap threshold is sized from its point + spacing. + """ + r = float(locating_hole_radius_mm) + nn = median_nn_spacing(points) + # Grid fine enough to resolve a locating hole (~16 px across its + # diameter), but never finer than 0.5 mm. + pixel = round(max(r / 8.0, 0.5), 2) + # Fill background gaps up to a few point-spacings so sub-sampled panel + # regions read as solid rather than as spurious cavities. + fill_bg = round((2.5 * nn) ** 2, 1) + a_loc = np.pi * r * r + return { + "grid_pixel_mm": pixel, + "fill_bg_below_mm2": fill_bg, + "panel_min_area_ratio": 0.15, + "hole_min_area_mm2": round(0.30 * a_loc, 1), + "hole_max_area_mm2": round(2.2 * a_loc, 1), + "locating_hole_radius_mm": [round(r * 0.80, 2), round(r * 1.45, 2)], + "panel_normal_seed": "pca", + } + + +__all__ = ["median_nn_spacing", "relax_denoise_steps", "detection_overlay"] diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/circle_fit.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/circle_fit.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/circle_fit.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/circle_fit.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/detect_holes.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/detect_holes.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/detect_holes.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/detect_holes.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/four_hole_selector.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/four_hole_selector.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/four_hole_selector.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/four_hole_selector.py diff --git a/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/layer_select.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/layer_select.py new file mode 100644 index 0000000..ef4de0d --- /dev/null +++ b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/layer_select.py @@ -0,0 +1,129 @@ +"""Isolate a single panel layer from a multi-layer ROI cloud. + +A ROI box cut out of a real vehicle scan can slice through several +structures stacked along the panel normal — e.g. the big base plate, a +raised central mounting plate, and a roof truss behind them. The 4-hole +detector and the plane fit both assume a *single* thin plate; feeding +them the multi-layer cloud makes holes fill in (back-layer points project +into the front holes) and pollutes the plane fit. + +This module keeps only the points belonging to one layer along the panel +normal, selected by a 1-D histogram of signed distance to a robust plane +seed (PCA normal by default, or a supplied CAD normal). + +Pure geometry — it does not touch the detection / plane-fit algorithms. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import numpy as np + + +@dataclass +class LayerResult: + points: np.ndarray # kept layer points + normal: np.ndarray # (3,) unit normal used, n_x > 0 + center_signed: float # signed-distance center of the kept layer + half_width_mm: float + diagnostics: dict[str, Any] + + +def _pca_normal(points: np.ndarray) -> np.ndarray: + c = points - points.mean(axis=0) + cov = (c.T @ c) / max(points.shape[0] - 1, 1) + _, V = np.linalg.eigh(cov) + n = V[:, 0] + if n[0] < 0: + n = -n + return n / max(np.linalg.norm(n), 1e-12) + + +def select_panel_layer( + points: np.ndarray, + *, + normal_seed: np.ndarray | str = "pca", + half_width_mm: float = 6.0, + bins: int = 240, + pick: str = "frontmost", +) -> LayerResult: + """Keep one layer of ``points`` along the panel normal. + + Parameters + ---------- + normal_seed : (3,) or "pca" + Plane normal used to compute signed distances. ``"pca"`` estimates + it from the cloud (smallest-variance axis). + half_width_mm : float + Half-thickness of the kept slab around the chosen layer center. + bins : int + Histogram resolution for finding layer peaks. + pick : "frontmost" | "densest" + ``"frontmost"`` keeps the layer with the largest signed distance + (the face toward the scanner, +normal side) among the dominant + peaks — this is the raised plate the user wants. ``"densest"`` + keeps the single most-populated layer. + """ + pts = np.asarray(points, dtype=np.float64) + if pts.shape[0] == 0: + raise ValueError("empty cloud") + + if isinstance(normal_seed, str): + if normal_seed != "pca": + raise ValueError(f"unknown normal_seed '{normal_seed}'") + normal = _pca_normal(pts) + else: + normal = np.asarray(normal_seed, dtype=np.float64).reshape(3) + normal = normal / max(np.linalg.norm(normal), 1e-12) + if normal[0] < 0: + normal = -normal + + sd = pts @ normal + hist, edges = np.histogram(sd, bins=bins) + centers = 0.5 * (edges[:-1] + edges[1:]) + + # Candidate layers = histogram peaks holding a meaningful share of points. + thresh = 0.25 * hist.max() + peak_idx = [i for i in range(len(hist)) if hist[i] >= thresh] + # group contiguous bins into peaks; take each group's argmax center + groups: list[list[int]] = [] + for i in peak_idx: + if groups and i == groups[-1][-1] + 1: + groups[-1].append(i) + else: + groups.append([i]) + peak_centers = [] + for g in groups: + gi = max(g, key=lambda i: hist[i]) + peak_centers.append((centers[gi], int(hist[gi]))) + + if not peak_centers: + peak_centers = [(centers[int(np.argmax(hist))], int(hist.max()))] + + if pick == "densest": + chosen = max(peak_centers, key=lambda c: c[1])[0] + else: # frontmost = max signed distance (toward +normal / scanner) + chosen = max(peak_centers, key=lambda c: c[0])[0] + + mask = np.abs(sd - chosen) <= half_width_mm + kept = pts[mask] + + return LayerResult( + points=kept, + normal=normal, + center_signed=float(chosen), + half_width_mm=float(half_width_mm), + diagnostics={ + "n_in": int(pts.shape[0]), + "n_out": int(kept.shape[0]), + "normal": [float(v) for v in normal], + "layer_center_signed": float(chosen), + "peak_centers": [[float(c), int(n)] for c, n in peak_centers], + "pick": pick, + }, + ) + + +__all__ = ["LayerResult", "select_panel_layer"] diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/occupancy_grid.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/occupancy_grid.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/occupancy_grid.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/occupancy_grid.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/plane_local.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/plane_local.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/detection/plane_local.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/detection/plane_local.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/exceptions.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/exceptions.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/exceptions.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/exceptions.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/geometry/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/geometry/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/geometry/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/geometry/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/geometry/plane_fit.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/geometry/plane_fit.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/geometry/plane_fit.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/geometry/plane_fit.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/geometry/reference.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/geometry/reference.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/geometry/reference.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/geometry/reference.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/geometry/roi.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/geometry/roi.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/geometry/roi.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/geometry/roi.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/io_utils/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/__init__.py similarity index 88% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/io_utils/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/__init__.py index 37796e7..9a8cd8d 100644 --- a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/io_utils/__init__.py +++ b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/__init__.py @@ -5,6 +5,7 @@ from .pointcloud_io import ( write_point_cloud, write_metadata_json, ) +from .ply_stream import crop_ply_bbox from .artifacts import ( ensure_dir, write_json, @@ -17,6 +18,7 @@ __all__ = [ "load_point_cloud", "write_point_cloud", "write_metadata_json", + "crop_ply_bbox", "ensure_dir", "write_json", "write_yaml", diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/io_utils/artifacts.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/artifacts.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/io_utils/artifacts.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/artifacts.py diff --git a/alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/ply_stream.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/ply_stream.py new file mode 100644 index 0000000..6400cb7 --- /dev/null +++ b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/ply_stream.py @@ -0,0 +1,165 @@ +"""Streaming AABB crop for large binary PLY point clouds. + +A full-vehicle scan PLY can be 1-12 GB. Loading it whole to apply a +region-of-interest box would blow up memory, so this module parses the +PLY header, memory-maps the vertex block, and filters it in chunks, +keeping only points inside the ROI bounding box. + +Supports binary little-/big-endian PLY with a single ``element vertex`` +block whose first three float properties are x, y, z (extra properties +such as colours or normals are tolerated — the per-vertex stride is read +from the header). ASCII PLY and other layouts fall back to the normal +loader in :mod:`coupler_panel.io_utils.pointcloud_io`. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import numpy as np + +from ..exceptions import InvalidPointCloudError + + +# PLY property type -> (numpy dtype char, byte size) +_PLY_TYPES = { + "char": ("i1", 1), "int8": ("i1", 1), + "uchar": ("u1", 1), "uint8": ("u1", 1), + "short": ("i2", 2), "int16": ("i2", 2), + "ushort": ("u2", 2), "uint16": ("u2", 2), + "int": ("i4", 4), "int32": ("i4", 4), + "uint": ("u4", 4), "uint32": ("u4", 4), + "float": ("f4", 4), "float32": ("f4", 4), + "double": ("f8", 8), "float64": ("f8", 8), +} + + +@dataclass +class _PlyHeader: + fmt: str # "binary_little_endian" | "binary_big_endian" | "ascii" + n_vertices: int + data_offset: int # byte offset where vertex data starts + props: list[tuple[str, str]] # ordered (name, type) for the vertex element + + +def _parse_header(path: Path) -> _PlyHeader: + fmt = None + n_vertices = 0 + props: list[tuple[str, str]] = [] + in_vertex = False + # Read a generous prefix; PLY headers are tiny (well under 64 KB). + with path.open("rb") as f: + prefix = f.read(65536) + marker = b"end_header" + mpos = prefix.find(marker) + if mpos < 0: + raise InvalidPointCloudError(f"{path}: no end_header in first 64KB") + # data starts right after the newline(s) following 'end_header' + after = prefix[mpos + len(marker):] + if after.startswith(b"\r\n"): + nl_len = 2 + elif after.startswith(b"\n") or after.startswith(b"\r"): + nl_len = 1 + else: + nl_len = 0 + data_offset = mpos + len(marker) + nl_len + + text = prefix[:mpos].decode("ascii", errors="replace") + lines = text.splitlines() + for ln in lines: + parts = ln.split() + if not parts: + continue + if parts[0] == "format": + fmt = parts[1] + elif parts[0] == "element": + in_vertex = parts[1] == "vertex" + if in_vertex: + n_vertices = int(parts[2]) + elif parts[0] == "property" and in_vertex: + # property (lists not supported for vertex) + if parts[1] == "list": + raise InvalidPointCloudError(f"{path}: list property in vertex unsupported") + props.append((parts[-1], parts[1])) + elif parts[0] == "element" and not in_vertex: + in_vertex = False + + if fmt is None or n_vertices <= 0 or not props: + raise InvalidPointCloudError(f"{path}: malformed PLY header") + return _PlyHeader(fmt=fmt, n_vertices=n_vertices, data_offset=data_offset, props=props) + + +def crop_ply_bbox( + path: str | Path, + *, + x_min: float, x_max: float, + y_min: float, y_max: float, + z_min: float, z_max: float, + chunk: int = 10_000_000, +) -> tuple[np.ndarray, dict[str, Any]]: + """Stream-crop a binary PLY to an AABB. Returns ``(points (M,3), diag)``. + + Falls back to a full load (via the standard reader) for ASCII PLY or + headers this fast path can't handle. + """ + path = Path(path) + if not path.exists(): + raise InvalidPointCloudError(f"point cloud not found: {path}") + + try: + hdr = _parse_header(path) + except InvalidPointCloudError: + hdr = None + + if hdr is None or not hdr.fmt.startswith("binary"): + # Fallback: load whole then filter (ASCII / unknown layout). + from .pointcloud_io import load_point_cloud + pts = load_point_cloud(path) + m = ((pts[:, 0] >= x_min) & (pts[:, 0] <= x_max) + & (pts[:, 1] >= y_min) & (pts[:, 1] <= y_max) + & (pts[:, 2] >= z_min) & (pts[:, 2] <= z_max)) + out = pts[m] + return out, {"mode": "fallback_full_load", "n_in": int(pts.shape[0]), + "n_out": int(out.shape[0])} + + # Build a structured dtype matching the vertex layout. + byteorder = "<" if "little" in hdr.fmt else ">" + names = [p[0] for p in hdr.props] + formats = [] + for _, t in hdr.props: + if t not in _PLY_TYPES: + raise InvalidPointCloudError(f"{path}: unsupported property type {t}") + formats.append(byteorder + _PLY_TYPES[t][0]) + dt = np.dtype({"names": names, "formats": formats}) + if not all(c in names for c in ("x", "y", "z")): + raise InvalidPointCloudError(f"{path}: vertex has no x/y/z properties") + + mm = np.memmap(path, dtype=dt, mode="r", offset=hdr.data_offset, shape=(hdr.n_vertices,)) + + kept = [] + n_in = hdr.n_vertices + for start in range(0, n_in, chunk): + end = min(start + chunk, n_in) + blk = mm[start:end] + x = np.asarray(blk["x"], dtype=np.float64) + y = np.asarray(blk["y"], dtype=np.float64) + z = np.asarray(blk["z"], dtype=np.float64) + m = ((x >= x_min) & (x <= x_max) + & (y >= y_min) & (y <= y_max) + & (z >= z_min) & (z <= z_max)) + if m.any(): + kept.append(np.column_stack([x[m], y[m], z[m]])) + out = np.concatenate(kept, axis=0) if kept else np.zeros((0, 3)) + return out, { + "mode": "streaming", + "format": hdr.fmt, + "n_in": int(n_in), + "n_out": int(out.shape[0]), + "bbox": {"x_min": x_min, "x_max": x_max, "y_min": y_min, + "y_max": y_max, "z_min": z_min, "z_max": z_max}, + } + + +__all__ = ["crop_ply_bbox"] diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/io_utils/pointcloud_io.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/pointcloud_io.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/io_utils/pointcloud_io.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/io_utils/pointcloud_io.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/metrics/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/metrics/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/metrics/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/metrics/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/metrics/compare.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/metrics/compare.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/metrics/compare.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/metrics/compare.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/metrics/deflection.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/metrics/deflection.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/metrics/deflection.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/metrics/deflection.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/pipeline.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/pipeline.py similarity index 63% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/pipeline.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/pipeline.py index a1f0e0a..f871f4e 100644 --- a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/pipeline.py +++ b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/pipeline.py @@ -1,23 +1,18 @@ """End-to-end orchestration for *research / validation* runs. -Composition:: +Two entry points: - data_prep.prepare_cloud_from_cad / from_scan (Phase A) - api.compute_coupler_measurement (Phase B) - -Phase A is the validation / data-preparation part: STP → mesh → sample -→ noise → transform → denoise → register. Phase B is the public -measurement algorithm. This module saves every intermediate artifact -under a structured run directory so engineers can debug and so the -sweep runner can compare runs. - -Production callers should *not* use this module — they should call -:func:`coupler_panel.api.compute_coupler_measurement` directly with -their own clean point cloud. +- :func:`run_coupler_measurement_pipeline` — the integration path: + cloud (in CAD frame) + optional CAD STP + ROI → ROI clip → denoise → + measure. **No registration.** +- :func:`run_from_cad_pipeline` — the research path: STP → synthesise → + denoise → 4-hole Kabsch (against oracle deflected GT) → measure → diff + GT. Saves every intermediate artifact under a structured run directory. """ from __future__ import annotations +import math from pathlib import Path from typing import Any @@ -34,20 +29,24 @@ from .data_prep import ( register_cloud, synthesize_cloud, ) +from .exceptions import InsufficientPointsError from .io_utils import ( Artifacts, + crop_ply_bbox, load_point_cloud, write_json, write_point_cloud, write_yaml, ) from .metrics import compute_deflection_metrics +from .detection.autotune import detection_overlay, relax_denoise_steps +from .detection.layer_select import select_panel_layer from .roi_select import ROISpec, apply_roi, default_roi_spec, load_roi_json from .structures import CouplerMeasurement # --------------------------------------------------------------------------- -# Cloud-only research entry point +# Integration entry point # --------------------------------------------------------------------------- @@ -75,12 +74,12 @@ def run_coupler_measurement_pipeline( Inputs: - ``cad_stp_path`` — optional STEP file. Parsed for the analytical - panel-normal seed (speeds up + stabilises hole detection's - in-plane projection); the parsed GT is written as ``01_gt.json`` - to ``out_dir`` for auditing. - - ``roi_spec`` / ``roi_json`` — ROI selector applied to the cloud - *before* denoising. If both omitted, the default ``mode='all'`` - spec (pass-through) is used. ``roi_spec`` wins over ``roi_json``. + panel-normal seed and, when no profile/config is given, to + auto-tune the scale/density-dependent denoise + detection knobs + (see below). The parsed GT is written as ``01_gt.json`` for audit. + - ``roi_spec`` / ``roi_json`` — axis-aligned-bbox ROI applied to the + cloud *before* denoising. If both omitted, the full cloud is kept. + ``roi_spec`` wins over ``roi_json``. Writes every intermediate to ``out_dir``. Returns a :class:`CouplerMeasurement`. @@ -91,24 +90,110 @@ def run_coupler_measurement_pipeline( write_yaml(cfg, art.resolved_config) # ---- Load + ROI clip -------------------------------------------------- - raw_pts = load_point_cloud(input_cloud) + # The ROI JSON is the cropping hyper-parameter. When it is a finite + # bounding box and the input is a (possibly multi-GB) binary PLY, crop + # it by streaming the file so we never hold the whole vehicle cloud in + # memory. Otherwise fall back to load-then-filter. if roi_spec is None: roi_spec = load_roi_json(roi_json) if roi_json is not None else default_roi_spec() - pts, roi_diag = apply_roi(raw_pts, roi_spec) + + finite_bbox = all( + math.isfinite(v) for v in ( + roi_spec.x_min, roi_spec.x_max, + roi_spec.y_min, roi_spec.y_max, + roi_spec.z_min, roi_spec.z_max, + ) + ) + is_ply = str(input_cloud).lower().endswith(".ply") + if finite_bbox and is_ply: + pts, roi_diag = crop_ply_bbox( + input_cloud, + x_min=roi_spec.x_min, x_max=roi_spec.x_max, + y_min=roi_spec.y_min, y_max=roi_spec.y_max, + z_min=roi_spec.z_min, z_max=roi_spec.z_max, + ) + roi_diag["kind"] = roi_spec.kind + else: + raw_pts = load_point_cloud(input_cloud) + pts, roi_diag = apply_roi(raw_pts, roi_spec) + + if pts.shape[0] == 0: + raise InsufficientPointsError( + f"ROI bbox kept 0 points from {input_cloud}; check the ROI JSON " + f"bounds match the cloud's coordinate frame" + ) write_point_cloud(pts, art.path("sampled", ext="ply")) write_json({"spec": roi_spec.to_dict(), "diagnostics": roi_diag}, art.root / "roi.json") + # ---- Optional normal-direction layer isolation ----------------------- + # For a ROI cut from a full-vehicle scan, the box can slice through + # several structures stacked along the panel normal (base plate + + # raised mounting plate + roof truss). Detection and plane fit assume a + # single thin plate, so we keep only one layer when the active config + # turns this on (layer_select.enabled). Default config leaves it off, + # so single-part clouds are unaffected. + ls_cfg = cfg.get("layer_select", {}) + if ls_cfg.get("enabled", False): + layer = select_panel_layer( + pts, + normal_seed=ls_cfg.get("normal_seed", "pca"), + half_width_mm=float(ls_cfg.get("half_width_mm", 6.0)), + bins=int(ls_cfg.get("bins", 240)), + pick=str(ls_cfg.get("pick", "frontmost")), + ) + pts = layer.points + write_point_cloud(pts, art.path("transformed", ext="ply")) + write_json(layer.diagnostics, art.root / "layer.json") + + # Auto-tune the scale/density-dependent knobs from CAD + cloud when the + # caller relies on the generic default config (no explicit config file + # and no hand-tuned profile) but provides a CAD STP. This makes the + # "CAD + cloud + ROI" contract self-sufficient for an adequately-sampled + # scan of a panel of any scale, instead of (a) the default radius filter + # deleting a sparse cloud or (b) 4-hole detection failing with the + # small-panel defaults. A specific profile (e.g. lu1_af4) or an explicit + # config is respected as-is (autotune skipped), so existing setups are + # byte-for-byte unchanged. + autotune = ( + cad_stp_path is not None + and config_path is None + and str(profile) in ("auto", "default", "none", "None") + ) + # ---- Optional CAD parse (panel_normal_seed + audit GT JSON) ---------- panel_seed: np.ndarray | None = None + parsed = None if cad_stp_path is not None: + if autotune: + # The default locating-hole band ([4,12] mm) selects the small + # inner holes on a large panel, from which autotune would derive + # too fine a grid. Widen it so parse_cad picks the large corner + # locating holes (the reliably-scannable rectangle) and excludes + # the big central pass-through hole. + cfg["cad"] = {**cfg["cad"], "locating_hole_radius_mm": [6.0, 30.0]} parsed = parse_cad(cad_stp_path, cfg["cad"]) write_json(parsed.gt.to_dict(), art.path("gt", ext="json")) panel_seed = np.asarray(parsed.gt.plane.normal, dtype=np.float64) # ---- Denoise (no Kabsch, no prepare_cloud_from_scan) ----------------- + # Stage 1 of autotune (before denoise): drop the density-fragile radius + # filter so a sparse cloud is not deleted. + if autotune: + cfg["denoise"] = relax_denoise_steps(cfg.get("denoise", {})) denoised = denoise_cloud(pts, cfg.get("denoise", {})) write_point_cloud(denoised.points, art.path("denoised", ext="ply")) + # Stage 2 of autotune (after denoise): size the detection grid / fill / + # area band from the CAD locating-hole radius + the DENOISED point + # spacing (the cloud detection actually sees). + if autotune and parsed is not None: + loc_r = float(np.median([h.radius_mm for h in parsed.gt.holes.locating])) + cfg["detection"] = { + **cfg.get("detection", {}), + **detection_overlay(loc_r, denoised.points), + } + write_yaml(cfg, art.resolved_config) + # ---- Measurement ------------------------------------------------------ result = compute_coupler_measurement( denoised.points, @@ -170,11 +255,6 @@ def run_from_cad_pipeline( write_point_cloud(denoised.points, art.path("denoised", ext="ply")) # ---- Phase A.4 — register --------------------------------------------- - # Use PCA on the cloud itself for the projection seed (the cloud is - # still in the pre-Kabsch frame here; using the CAD normal would - # cause foreshortening bias). Register against the *deflected* GT - # holes so Kabsch only cancels scanner-pose jitter, not the panel - # deflection signal we want to measure. registered = register_cloud( denoised.points, target_holes=synth.gt_holes_after_deflection, @@ -195,10 +275,6 @@ def run_from_cad_pipeline( ) # ---- GT metrics -------------------------------------------------------- - # GT for the metrics is the *deflected* panel: angles equal the - # injection, and the maximum-distance / maximum-x-deviation values are - # computed over the deflected 4-hole rectangle (the same primitive the - # algorithm later sees through the noise + jitter chain). gt_normal = synth.gt_normal_after_deflection centers = synth.gt_holes_after_deflection.locating_centers() gt_metrics = compute_deflection_metrics( @@ -252,25 +328,6 @@ def run_from_cad_pipeline( # --------------------------------------------------------------------------- -def _try_load_normal_from_gt_json(path: str | Path) -> np.ndarray | None: - """Read ``plane.normal`` from a GT JSON if present. Returns ``None`` otherwise.""" - import json - - try: - with Path(path).open("r", encoding="utf-8") as fh: - d = json.load(fh) - except Exception: - return None - n = d.get("plane", {}).get("normal") - if n is None or len(n) != 3: - return None - arr = np.asarray(n, dtype=np.float64) - norm = float(np.linalg.norm(arr)) - if norm < 1e-9: - return None - return arr / norm - - def _load_holeset_from_json(path: str | Path): """Reconstruct a HoleSet from the JSON written by ``GroundTruth.to_dict``.""" import json diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/py.typed b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/py.typed similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/py.typed rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/py.typed diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/registration/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/registration/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/registration/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/registration/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/registration/kabsch.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/registration/kabsch.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/registration/kabsch.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/registration/kabsch.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/reporting/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/reporting/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/reporting/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/reporting/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/reporting/excel.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/reporting/excel.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/reporting/excel.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/reporting/excel.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/roi_select/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/roi_select/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/roi_select/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/roi_select/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/roi_select/apply.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/roi_select/apply.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/roi_select/apply.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/roi_select/apply.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/roi_select/schema.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/roi_select/schema.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/roi_select/schema.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/roi_select/schema.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/sampling/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/sampling/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/sampling/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/sampling/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/sampling/adaptive.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/sampling/adaptive.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/sampling/adaptive.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/sampling/adaptive.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/sampling/grid_spacing.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/sampling/grid_spacing.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/sampling/grid_spacing.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/sampling/grid_spacing.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/sampling/poisson_disk.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/sampling/poisson_disk.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/sampling/poisson_disk.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/sampling/poisson_disk.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/sampling/uniform_area.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/sampling/uniform_area.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/sampling/uniform_area.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/sampling/uniform_area.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/simulation/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/simulation/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/simulation/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/simulation/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/simulation/noise.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/simulation/noise.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/simulation/noise.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/simulation/noise.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/simulation/scan_simulator.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/simulation/scan_simulator.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/simulation/scan_simulator.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/simulation/scan_simulator.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/simulation/transform.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/simulation/transform.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/simulation/transform.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/simulation/transform.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/structures.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/structures.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/structures.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/structures.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/__init__.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/__init__.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/__init__.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/__init__.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/_offscreen.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/_offscreen.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/_offscreen.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/_offscreen.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/cloud_view.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/cloud_view.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/cloud_view.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/cloud_view.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/heatmap.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/heatmap.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/heatmap.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/heatmap.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/holes_view.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/holes_view.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/holes_view.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/holes_view.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/plane_view.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/plane_view.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/plane_view.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/plane_view.py diff --git a/alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/render_all.py b/alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/render_all.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/coupler_panel/viz/render_all.py rename to alg/delivery_v1.2/coupler_panel_release/coupler_panel/viz/render_all.py diff --git a/alg/delivery_v1.1/coupler_panel_release/examples/generate_test_clouds.py b/alg/delivery_v1.2/coupler_panel_release/examples/generate_test_clouds.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/examples/generate_test_clouds.py rename to alg/delivery_v1.2/coupler_panel_release/examples/generate_test_clouds.py diff --git a/alg/delivery_v1.1/coupler_panel_release/examples/integrate_minimal.py b/alg/delivery_v1.2/coupler_panel_release/examples/integrate_minimal.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/examples/integrate_minimal.py rename to alg/delivery_v1.2/coupler_panel_release/examples/integrate_minimal.py diff --git a/alg/delivery_v1.1/coupler_panel_release/examples/roi_all.json b/alg/delivery_v1.2/coupler_panel_release/examples/roi_all.json similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/examples/roi_all.json rename to alg/delivery_v1.2/coupler_panel_release/examples/roi_all.json diff --git a/alg/delivery_v1.2/coupler_panel_release/examples/roi_train2_coupler.json b/alg/delivery_v1.2/coupler_panel_release/examples/roi_train2_coupler.json new file mode 100644 index 0000000..cfc9c04 --- /dev/null +++ b/alg/delivery_v1.2/coupler_panel_release/examples/roi_train2_coupler.json @@ -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." +} diff --git a/alg/delivery_v1.1/coupler_panel_release/examples/run_batch_eval.py b/alg/delivery_v1.2/coupler_panel_release/examples/run_batch_eval.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/examples/run_batch_eval.py rename to alg/delivery_v1.2/coupler_panel_release/examples/run_batch_eval.py diff --git a/alg/delivery_v1.1/coupler_panel_release/examples/run_full_validation.py b/alg/delivery_v1.2/coupler_panel_release/examples/run_full_validation.py similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/examples/run_full_validation.py rename to alg/delivery_v1.2/coupler_panel_release/examples/run_full_validation.py diff --git a/alg/delivery_v1.2/coupler_panel_release/examples/run_integration.py b/alg/delivery_v1.2/coupler_panel_release/examples/run_integration.py new file mode 100644 index 0000000..5d514f0 --- /dev/null +++ b/alg/delivery_v1.2/coupler_panel_release/examples/run_integration.py @@ -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 /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()) diff --git a/alg/delivery_v1.1/coupler_panel_release/pyproject.toml b/alg/delivery_v1.2/coupler_panel_release/pyproject.toml similarity index 100% rename from alg/delivery_v1.1/coupler_panel_release/pyproject.toml rename to alg/delivery_v1.2/coupler_panel_release/pyproject.toml diff --git a/alg/delivery_v1.2/coupler_panel_release/requirements.txt b/alg/delivery_v1.2/coupler_panel_release/requirements.txt new file mode 100644 index 0000000..d04f69b --- /dev/null +++ b/alg/delivery_v1.2/coupler_panel_release/requirements.txt @@ -0,0 +1,30 @@ +# coupler-panel-validation — locked dependency list (Python 3.10, end-to-end verified). +# +# python -m pip install -r requirements.txt +# pip install -e . +# +# Or install only a subset via the extras in pyproject.toml: +# pip install -e ".[cloud]" # core + point-cloud IO + denoise +# pip install -e ".[cad]" # core + STP parsing + GT extraction +# pip install -e ".[viz]" # core + matplotlib diagnostics +# pip install -e ".[report]" # core + Excel batch reporting +# pip install -e ".[all]" # everything + +# --- Core compute (always required) --- +numpy==1.26.4 +scipy==1.15.3 +pyyaml==6.0.2 +tqdm==4.65.0 + +# --- Point-cloud IO + denoise (PLY/PCD read/write, voxel/SOR/ROR) --- +open3d==0.19.0 +trimesh==4.12.2 + +# --- STP/STEP parsing (only needed when passing cad_stp_path) --- +cadquery-ocp==7.8.1.1.post1 + +# --- Diagnostic PNG renders (coupler_panel.viz, optional) --- +matplotlib==3.10.8 + +# --- Excel batch-eval reporting (coupler_panel.reporting) --- +openpyxl==3.1.2 diff --git a/alg/delivery_v1.2/underframe_deflection/README.md b/alg/delivery_v1.2/underframe_deflection/README.md new file mode 100644 index 0000000..64ffa6f --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/README.md @@ -0,0 +1,73 @@ +# underframe-deflection + +**高铁底架纵梁挠度测量算法(基于点云)。** 输入待测点云 + 理论 CAD + ROI,输出含挠度核心数据的 Excel。 + +[![python](https://img.shields.io/badge/python-3.10%2B-blue)]() [![version](https://img.shields.io/badge/version-1.2.0-green)]() + +--- + +## 这是什么 + +两种测量模式: + +- **单梁模式(默认)** — ROI 已框住单根纵梁(左右各一个 ROI),裁剪后直接测量。 + 支持流式裁剪超大点云(实测 12GB / 10 亿点整车 PLY),左右各出一份 Excel。**这是当前交付的主模式。** +- **双梁模式(`--mode dual`)** — 一片含左右两梁的车底点云 + CAD 配准 + 双梁追踪(v1.1 算法,保留)。 + +原理(单梁):沿车长切片 → 每片取下表面主平面高度 → 鲁棒拟合直线作理论基准 → 挠度 = 下表面 − 基准。 +理论平直的梁,挠度 ≈ 0。 + +--- + +## 快速开始 + +```bash +pip install -e .[all] + +underframe-measure \ + --input-cloud train.ply \ + --left-roi roi_left_beam.json \ + --right-roi roi_right_beam.json \ + --cad theoretical.stp \ + --out-dir out/ +``` + +→ `out/left/report.xlsx` 和 `out/right/report.xlsx`(左右各一份独立结果)。 + +`test_data/roi_left_beam.json` / `roi_right_beam.json` 是可直接用的样例 ROI。 + +--- + +## 文档 + +| 文档 | 面向 | 内容 | +| --- | --- | --- | +| **README.md**(本文件) | 所有人 | 项目总览、快速开始 | +| **[USAGE.md](./USAGE.md)** | 集成开发者 | 输入三件套、调用方式、输出 Excel 字段、配准开关、异常 | +| **[DEV_NOTES.md](./DEV_NOTES.md)** | 维护者 | 本次修改记录:改了什么、为什么、遇到什么问题、怎么解决 | + +--- + +## 坐标系约定 + +`x = 车长(纵向)`,`y = 车高(挠度方向)`,`z = 车宽(横向)`,单位 mm。 +ROI JSON 的 bbox 字段用同一坐标系。详见 USAGE.md。 + +--- + +## 安装 + +```bash +pip install -e .[all] # core + matplotlib(图) + openpyxl(Excel) +pip install -e . # 仅核心 numpy + scipy +``` + +安装后注册命令 `underframe-measure`,Python 内 `import underframe_deflection`。 + +--- + +## 状态与版本 + +- **当前版本**:1.2.0(单梁直接测量模式 + 流式裁剪超大点云) +- **Python**:≥ 3.10(Windows / Linux / macOS) +- License:Proprietary — PKU High-Speed Train Project diff --git a/alg/delivery_v1.2/underframe_deflection/USAGE.md b/alg/delivery_v1.2/underframe_deflection/USAGE.md new file mode 100644 index 0000000..f101f9b --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/USAGE.md @@ -0,0 +1,196 @@ +# underframe_deflection 使用指南 + +> 面向软件集成方。说明如何用「**待测整车点云 + 理论 CAD(stp)+ ROI JSON**」三件套输入, +> 调用算法,得到含挠度核心数据的 Excel 输出。 + +--- + +## 1. 输入与输出 + +### 输入(集成方提供) + +| 文件 | 格式 | 必需 | 说明 | +| --- | --- | --- | --- | +| **待测整车点云** | `.ply`(ASCII 或 binary) | ✅ | 现场扫描的整车点云。支持超大文件(实测 12GB / 10 亿点,流式裁剪不爆内存) | +| **理论 CAD** | `.stp` | ✅(按需用) | 理论模型。**当前实例点云已配准**,CAD 仅记录路径作溯源;需重新配准时见 §4 | +| **ROI JSON** | `.json` | ✅ | 每根梁一个,描述该梁的轴对齐裁剪框(AABB) | + +> 左右梁是**两次独立调用**:左梁一个 ROI → 一份输出;右梁一个 ROI → 一份输出。 + +### 输出(左右各一份) + +``` +out_dir/ +├── left/ +│ ├── report.xlsx ← 挠度核心数据 Excel(Summary + Deflection_Curve) +│ ├── report.json ← 完整结果(机器读) +│ ├── deflection_curve.csv ← 沿车长的逐点挠度 +│ └── deflection_curve.png ← 诊断图 +├── right/ ← 同上结构 +└── summary.json ← 左右汇总 +``` + +--- + +## 2. ROI JSON 格式 + +坐标系:**x = 车长(纵向),y = 车高(挠度方向),z = 车宽(横向)**,单位 mm。 + +```json +{ + "x_min": -100000.0, + "x_max": 100000.0, + "y_min": 919.950, + "y_max": 1399.950, + "z_min": 948.623, + "z_max": 1752.212 +} +``` + +- `x` 给一个覆盖整车长度的大区间即可。 +- `y`(车高)框住梁所在的高度带。 +- `z`(车宽)分左右:左梁取正侧,右梁取负侧。 +- 仓库 `test_data/roi_left_beam.json` / `roi_right_beam.json` 是可直接用的样例。 + +约束:每轴 `min < max`,均为有限实数;非法 → `InvalidPointCloudError`。 + +--- + +## 3. 调用方式 + +### 3.1 命令行 + +```bash +underframe-measure \ + --input-cloud train.ply \ + --left-roi roi_left_beam.json \ + --right-roi roi_right_beam.json \ + --cad theoretical.stp \ + --out-dir out/ +``` + +默认 `--mode single`、默认**不配准**(点云已配准),`--cad` 的 stp 路径记录进报告。 + +可选开关:`--enable-registration`(§4)、`--config FILE.json`(覆盖参数)、 +`--skip-excel`、`--skip-plot`、`--side left|right`(配 `--roi` 单根时用)。 + +### 3.2 Python API(推荐集成) + +```python +from underframe_deflection import run_both_beams + +summary = run_both_beams( + input_cloud="train.ply", + left_roi="roi_left_beam.json", + right_roi="roi_right_beam.json", + out_dir="out/", + cad_path="theoretical.stp", # 记录路径;配准默认关 + enable_registration=False, # 见 §4 +) + +for side in ("left", "right"): + d = summary[side]["deflection"] + print(side, + "最大挠度(带符号) =", d["selected_signed_mm"], "mm", + "@ x =", d["selected_x_mm"], "mm", + "| RMS =", d["rms_mm"], "mm") +``` + +单根梁独立调用: + +```python +from underframe_deflection import run_single_beam_pipeline + +report = run_single_beam_pipeline( + input_cloud="train.ply", + roi_path="roi_left_beam.json", + out_dir="out/left", + side="left", + cad_path="theoretical.stp", +) +``` + +--- + +## 4. 配准开关 + +挠度测量的完整物理流程:**扫描点云 → 用 CAD 配准到标准姿态 → 测梁下表面相对理想基准的偏差**。 +配准消除安装 / 摆放 / 扫描姿态引入的整体倾斜。 + +| `enable_registration` | 行为 | 适用 | +| --- | --- | --- | +| `False`(默认) | 不配准,点云按原样裁剪测量 | **点云已预配准**(本实例) | +| `True` | 先把整车点云刚性对齐到 CAD,再裁剪测量 | 点云未配准 | + +配准开启时对 CAD 的要求: +- 必须是**可读的点云 / 网格**(`.ply / .xyz / .stl / .obj`)。 +- `.stp` **不直接解析**(STEP 解析需重型依赖)。若只有 stp,先用 + `tools/cad_sampling/step_to_pointcloud.py` 离线采样成 `.ply` 再传 `--cad`。 +- 传 stp + 开配准时**不会崩溃**,而是记录提示"STEP not parsed; convert first"并跳过配准。 + +> 本次交付的测试样本点云**已配准**,所以默认关。配准算法(ICP)已完整保留,随时可启用。 + +--- + +## 5. 输出 Excel 字段(report.xlsx) + +### Sheet「Summary」(key/value) + +| 字段 | 含义 | +| --- | --- | +| `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` | 质量警告(空 = 正常) | + +### Sheet「Deflection_Curve」 + +沿车长逐点表:`x_mm, underside_height_mm, reference_height_mm, deflection_mm, kept`。 + +--- + +## 6. 异常与质量标记 + +| 异常 | 触发 | +| --- | --- | +| `InvalidPointCloudError` | 点云 / ROI 格式非法、AABB 反转 | +| `InsufficientPointsError` | ROI 命中点过少(< 2000) | + +非致命质量标记(写进 `quality_flags`,不抛异常): +`many_underside_spikes`(悬挂物/噪点偏多,已鲁棒剔除)、`low_surface_support`(下表面支撑点偏少)、 +`insufficient_valid_slices` / `fit_unstable`(有效切片不足)。 + +集成方建议:外层 `try / except DeflectionError` 兜底,并把 `quality_flags` 落日志。 + +--- + +## 7. 算法原理 + +沿车长(x)分细切片 → 每片取**下表面主平面高度**(下半部直方图主峰,对悬挂物 / 噪点鲁棒)→ +鲁棒拟合一条直线作理论基准 → 挠度 = 下表面高度 − 基准线。理论平直的梁,挠度 ≈ 0。 + +> 设计与验证的完整记录见 [`DEV_NOTES.md`](./DEV_NOTES.md)。 + +--- + +## 附录:双梁模式(legacy) + +输入一片含左右两梁的车底点云 + CAD 基准 + ROI,自动配准 + 双梁追踪 + 样条拟合 + 孔参考线。 + +```bash +underframe-measure --mode dual \ + --input-cloud scan.ply --cad reference.ply --roi roi.json \ + --out-dir out/ --input-axis y_up +``` + +- `--input-axis {y_up,z_up}`:高度轴约定,默认 `y_up`(CAD/扫描约定 y=高度)。 +- 输出 5-sheet Excel + 5 PNG + 4 PLY 叠加图。 +- 该模式针对"双梁车底slab",与单梁模式互不影响。 diff --git a/alg/delivery_v1.1/underframe_deflection/__init__.py b/alg/delivery_v1.2/underframe_deflection/__init__.py similarity index 88% rename from alg/delivery_v1.1/underframe_deflection/__init__.py rename to alg/delivery_v1.2/underframe_deflection/__init__.py index 806717a..3371c39 100644 --- a/alg/delivery_v1.1/underframe_deflection/__init__.py +++ b/alg/delivery_v1.2/underframe_deflection/__init__.py @@ -35,6 +35,8 @@ from .pipeline import run_measurement_pipeline from .preprocess import bilateral_filter_points from .registration import RegistrationResult, register_to_cad from .roi import apply_passthrough, load_roi_aabb +from .single_beam import SingleBeamResult, measure_single_beam +from .single_beam_pipeline import run_both_beams, run_single_beam_pipeline __all__ = [ "__version__", @@ -58,4 +60,8 @@ __all__ = [ "bilateral_filter_points", "register_to_cad", "RegistrationResult", + "measure_single_beam", + "SingleBeamResult", + "run_single_beam_pipeline", + "run_both_beams", ] diff --git a/alg/delivery_v1.1/underframe_deflection/_version.py b/alg/delivery_v1.2/underframe_deflection/_version.py similarity index 96% rename from alg/delivery_v1.1/underframe_deflection/_version.py rename to alg/delivery_v1.2/underframe_deflection/_version.py index 9daa655..6c4cc18 100644 --- a/alg/delivery_v1.1/underframe_deflection/_version.py +++ b/alg/delivery_v1.2/underframe_deflection/_version.py @@ -11,7 +11,7 @@ schema version bump. from __future__ import annotations -__version__ = "1.1.0" +__version__ = "1.2.0" ALGORITHM_VERSION = "0.1.0" SCHEMA_VERSION = "1.0" diff --git a/alg/delivery_v1.1/underframe_deflection/api.py b/alg/delivery_v1.2/underframe_deflection/api.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/api.py rename to alg/delivery_v1.2/underframe_deflection/api.py diff --git a/alg/delivery_v1.1/underframe_deflection/artifacts.py b/alg/delivery_v1.2/underframe_deflection/artifacts.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/artifacts.py rename to alg/delivery_v1.2/underframe_deflection/artifacts.py diff --git a/alg/delivery_v1.1/underframe_deflection/beams.py b/alg/delivery_v1.2/underframe_deflection/beams.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/beams.py rename to alg/delivery_v1.2/underframe_deflection/beams.py diff --git a/alg/delivery_v1.1/underframe_deflection/cad_io.py b/alg/delivery_v1.2/underframe_deflection/cad_io.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/cad_io.py rename to alg/delivery_v1.2/underframe_deflection/cad_io.py diff --git a/alg/delivery_v1.2/underframe_deflection/cli.py b/alg/delivery_v1.2/underframe_deflection/cli.py new file mode 100644 index 0000000..f5b6f8f --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/cli.py @@ -0,0 +1,176 @@ +"""Command-line entry point installed by ``pyproject.toml``. + +The DEFAULT mode is single-beam measurement: the ROI(s) already isolate one +beam each, the cloud is cropped and measured directly (no registration, no +dual-beam tracking). Provide ``--roi`` for one beam, or ``--left-roi`` and +``--right-roi`` to measure both in one invocation. + +The legacy dual-beam pipeline (registration + left/right tracking on a +car-bottom slab) is still available via ``--mode dual``. +""" + +from __future__ import annotations + +import argparse + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Measure underframe longitudinal beam deflection from a point cloud.", + ) + parser.add_argument( + "--mode", + default="single", + choices=["single", "dual"], + help="'single' (default): ROI isolates one beam, measure directly. " + "'dual': legacy car-bottom slab with left+right tracking + registration.", + ) + parser.add_argument( + "--input-cloud", + required=True, + help="Path to input scan point cloud (.ply or .xyz). Large binary PLYs " + "are stream-cropped by the ROI in single mode.", + ) + parser.add_argument( + "--out-dir", + required=True, + help="Output directory for report.json/.xlsx, CSV, and PNG.", + ) + # --- single mode --- + parser.add_argument( + "--roi", + default=None, + help="Single mode: ROI JSON isolating ONE beam (native frame " + "x=length, y=height, z=width).", + ) + parser.add_argument( + "--left-roi", + default=None, + help="Single mode: ROI JSON for the left beam (use with --right-roi).", + ) + parser.add_argument( + "--right-roi", + default=None, + help="Single mode: ROI JSON for the right beam (use with --left-roi).", + ) + parser.add_argument( + "--side", + default=None, + help="Single mode label for --roi (left/right); inferred from the file name if omitted.", + ) + parser.add_argument( + "--enable-registration", + action="store_true", + help="Single mode: rigidly align the cloud to --cad before measuring. " + "Default OFF (cloud assumed already registered). Requires --cad as a " + "sampled .ply/.stl/.obj (.stp must be converted first).", + ) + # --- shared --- + parser.add_argument( + "--config", + default=None, + help="Optional JSON config override file.", + ) + parser.add_argument( + "--skip-excel", + action="store_true", + help="Skip writing the .xlsx report.", + ) + parser.add_argument( + "--skip-plot", + action="store_true", + help="Single mode: skip the PNG plot.", + ) + # --- dual (legacy) mode --- + parser.add_argument( + "--cad", + default=None, + help="Dual mode: CAD reference (.ply/.xyz/.stl/.obj) for registration.", + ) + parser.add_argument( + "--profile", + default="auto", + choices=["auto", "M", "TC", "TP", "custom"], + help="Dual mode: profile for scale-related tuning.", + ) + parser.add_argument( + "--skip-registration", + action="store_true", + help="Dual mode: bypass ICP even when --cad is supplied.", + ) + parser.add_argument( + "--no-bilateral", + action="store_true", + help="Dual mode: disable bilateral denoising.", + ) + parser.add_argument( + "--input-axis", + default="y_up", + choices=["z_up", "y_up"], + help="Dual mode: height axis convention (default y_up).", + ) + return parser.parse_args() + + +def _load_config(path): + if not path: + return None + import json + from pathlib import Path + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def main() -> None: + args = parse_args() + + if args.mode == "single": + from .single_beam_pipeline import run_single_beam_pipeline, run_both_beams + config = _load_config(args.config) + if args.left_roi and args.right_roi: + run_both_beams( + input_cloud=args.input_cloud, + left_roi=args.left_roi, + right_roi=args.right_roi, + out_dir=args.out_dir, + config=config, + cad_path=args.cad, + enable_registration=args.enable_registration, + skip_excel=args.skip_excel, + skip_plot=args.skip_plot, + ) + elif args.roi: + run_single_beam_pipeline( + input_cloud=args.input_cloud, + roi_path=args.roi, + out_dir=args.out_dir, + side=args.side, + config=config, + cad_path=args.cad, + enable_registration=args.enable_registration, + skip_excel=args.skip_excel, + skip_plot=args.skip_plot, + ) + else: + raise SystemExit( + "single mode requires --roi (one beam) or both --left-roi and --right-roi." + ) + return + + # dual (legacy) mode + from .pipeline import run_measurement_pipeline + run_measurement_pipeline( + input_cloud=args.input_cloud, + out_dir=args.out_dir, + config_path=args.config, + profile=args.profile, + cad_path=args.cad, + roi_path=args.roi, + skip_registration=args.skip_registration, + enable_bilateral=not args.no_bilateral, + skip_excel=args.skip_excel, + input_axis=args.input_axis, + ) + + +if __name__ == "__main__": + main() diff --git a/alg/delivery_v1.1/underframe_deflection/config.py b/alg/delivery_v1.2/underframe_deflection/config.py similarity index 94% rename from alg/delivery_v1.1/underframe_deflection/config.py rename to alg/delivery_v1.2/underframe_deflection/config.py index 43f30a1..c4b286c 100644 --- a/alg/delivery_v1.1/underframe_deflection/config.py +++ b/alg/delivery_v1.2/underframe_deflection/config.py @@ -118,6 +118,12 @@ DEFAULT_CONFIG: dict[str, Any] = { "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", @@ -166,6 +172,11 @@ DEFAULT_CONFIG: dict[str, Any] = { "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, diff --git a/alg/delivery_v1.1/underframe_deflection/excel_report.py b/alg/delivery_v1.2/underframe_deflection/excel_report.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/excel_report.py rename to alg/delivery_v1.2/underframe_deflection/excel_report.py diff --git a/alg/delivery_v1.1/underframe_deflection/exceptions.py b/alg/delivery_v1.2/underframe_deflection/exceptions.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/exceptions.py rename to alg/delivery_v1.2/underframe_deflection/exceptions.py diff --git a/alg/delivery_v1.1/underframe_deflection/fitting.py b/alg/delivery_v1.2/underframe_deflection/fitting.py similarity index 85% rename from alg/delivery_v1.1/underframe_deflection/fitting.py rename to alg/delivery_v1.2/underframe_deflection/fitting.py index 595babe..fec2168 100644 --- a/alg/delivery_v1.1/underframe_deflection/fitting.py +++ b/alg/delivery_v1.2/underframe_deflection/fitting.py @@ -190,6 +190,39 @@ def _fit_spline( return spline, residual_rms +def _reject_contaminated_representatives( + x: np.ndarray, + z: np.ndarray, + window_mm: float, + sigma: float, +) -> np.ndarray: + """Flag representative points that deviate from a robust local baseline. + + When the cloud is sampled directly from CAD, gussets and cross-members + intermittently raise the detected beam underside by tens of mm at + isolated X positions. A sliding-window median is robust to such localized + spikes (up to ~50% local contamination) while passing smooth, genuine + deflection through almost unchanged. Points whose height deviates from the + median baseline by more than ``sigma`` robust standard deviations are + rejected before the spline fit. + """ + n = len(x) + keep = np.ones(n, dtype=bool) + if n < 6: + return keep + baseline = np.empty(n, dtype=np.float64) + for index in range(n): + local = z[(x >= x[index] - window_mm) & (x <= x[index] + window_mm)] + baseline[index] = float(np.median(local)) + residual = z - baseline + mad = float(np.median(np.abs(residual - np.median(residual)))) + scale = 1.4826 * mad + 1e-6 + keep = np.abs(residual) <= sigma * scale + if np.count_nonzero(keep) < 4: + return np.ones(n, dtype=bool) + return keep + + def fit_representative_line( representatives: list[RepresentativePoint], config: dict, @@ -217,6 +250,16 @@ def fit_representative_line( rep_cfg = config["representative"] spacing = config["runtime"]["spacing_mm"] + + if rep_cfg.get("outlier_rejection_enabled", True) and len(x) >= 6: + window_mm = rep_cfg.get("outlier_window_slices", 2.5) * config["beam"]["slice_step_mm"] + sigma = rep_cfg.get("outlier_sigma", 2.5) + keep = _reject_contaminated_representatives(x, z, window_mm, sigma) + if np.count_nonzero(keep) >= 4: + x = x[keep] + y = y[keep] + z = z[keep] + reps_sorted = [item for item, flag in zip(reps_sorted, keep) if flag] y_spline, y_residual = _fit_spline( x, y, diff --git a/alg/delivery_v1.1/underframe_deflection/frame.py b/alg/delivery_v1.2/underframe_deflection/frame.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/frame.py rename to alg/delivery_v1.2/underframe_deflection/frame.py diff --git a/alg/delivery_v1.2/underframe_deflection/io.py b/alg/delivery_v1.2/underframe_deflection/io.py new file mode 100644 index 0000000..edcba93 --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/io.py @@ -0,0 +1,278 @@ +from __future__ import annotations + +import csv +import json +from pathlib import Path +from typing import Iterable + +import numpy as np + + +def read_point_cloud(path: str | Path) -> np.ndarray: + path = Path(path) + suffix = path.suffix.lower() + if suffix == ".ply": + return _read_ply(path) + if suffix in {".xyz", ".txt", ".csv"}: + return np.loadtxt(path, dtype=np.float64) + raise ValueError(f"Unsupported point cloud format: {path.suffix}") + + +# PLY scalar property type -> numpy dtype code. +_PLY_TYPE_TO_NP = { + "char": "i1", + "int8": "i1", + "uchar": "u1", + "uint8": "u1", + "short": "i2", + "int16": "i2", + "ushort": "u2", + "uint16": "u2", + "int": "i4", + "int32": "i4", + "uint": "u4", + "uint32": "u4", + "float": "f4", + "float32": "f4", + "double": "f8", + "float64": "f8", +} + + +def _parse_ply_header( + path: Path, +) -> tuple[str, int, int, int, list[tuple[str, str]]]: + """Parse a PLY header. + + Returns ``(format, header_lines, header_bytes, vertex_count, + vertex_props)`` where ``format`` is one of ``ascii`` / + ``binary_little_endian`` / ``binary_big_endian`` and ``vertex_props`` is + the ordered ``(type, name)`` list for the vertex element. The file is + read in binary so the byte offset of the data block is exact (needed for + binary PLY). + """ + header_lines = 0 + header_bytes = 0 + vertex_count: int | None = None + vertex_props: list[tuple[str, str]] = [] + in_vertex = False + fmt = "ascii" + with path.open("rb") as fh: + first = fh.readline() + header_bytes += len(first) + header_lines += 1 + if first.strip().lower() != b"ply": + raise ValueError(f"Not a PLY file (missing 'ply' magic): {path}") + while True: + raw = fh.readline() + if not raw: + raise ValueError(f"PLY header missing end_header: {path}") + header_bytes += len(raw) + header_lines += 1 + stripped = raw.decode("ascii", errors="replace").strip() + if not stripped: + continue + tokens = stripped.split() + keyword = tokens[0].lower() + if keyword == "format": + fmt = tokens[1].lower() + elif keyword == "element": + in_vertex = tokens[1].lower() == "vertex" + if in_vertex: + vertex_count = int(tokens[2]) + elif keyword == "property" and in_vertex: + if tokens[1].lower() == "list": + vertex_props.append(("list", tokens[-1])) + else: + vertex_props.append((tokens[1].lower(), tokens[-1])) + elif keyword == "end_header": + break + if vertex_count is None: + raise ValueError(f"PLY file has no vertex element: {path}") + return fmt, header_lines, header_bytes, vertex_count, vertex_props + + +def _read_ply(path: Path) -> np.ndarray: + """Read a PLY point cloud (ASCII or binary) into an ``(N, 3)`` array.""" + fmt, header_lines, header_bytes, vertex_count, vertex_props = _parse_ply_header(path) + if fmt == "ascii": + points = np.loadtxt(path, skiprows=header_lines, dtype=np.float64) + if points.ndim == 1: + points = points[None, :] + if points.shape[1] < 3: + raise ValueError(f"PLY file does not contain XYZ coordinates: {path}") + points = points[:, :3] + if len(points) != vertex_count: + raise ValueError( + f"PLY vertex count mismatch: header={vertex_count}, data={len(points)}" + ) + return points + if fmt in {"binary_little_endian", "binary_big_endian"}: + return _read_binary_ply(path, header_bytes, vertex_count, vertex_props, fmt) + raise ValueError(f"Unsupported PLY format '{fmt}': {path}") + + +def _read_binary_ply( + path: Path, + header_bytes: int, + vertex_count: int, + vertex_props: list[tuple[str, str]], + fmt: str, +) -> np.ndarray: + if any(ptype == "list" for ptype, _ in vertex_props): + raise ValueError( + f"Binary PLY with list properties in the vertex element is unsupported: {path}" + ) + missing = {"x", "y", "z"} - {name for _, name in vertex_props} + if missing: + raise ValueError( + f"PLY vertex element missing coordinate field(s) {sorted(missing)}: {path}" + ) + endian = "<" if fmt == "binary_little_endian" else ">" + try: + record_dtype = np.dtype( + [(name, endian + _PLY_TYPE_TO_NP[ptype]) for ptype, name in vertex_props] + ) + except KeyError as exc: + raise ValueError(f"Unsupported PLY property type {exc} in {path}") from exc + expected = record_dtype.itemsize * vertex_count + with path.open("rb") as fh: + fh.seek(header_bytes) + buffer = fh.read(expected) + if len(buffer) < expected: + raise ValueError( + f"Binary PLY truncated: expected {expected} bytes of vertex data, " + f"got {len(buffer)}: {path}" + ) + records = np.frombuffer(buffer, dtype=record_dtype, count=vertex_count) + return np.column_stack([records["x"], records["y"], records["z"]]).astype(np.float64) + + +# Backwards-compatible alias. ``cad_io.py`` imports this name; it now reads +# both ASCII and binary PLY transparently. +_read_ascii_ply = _read_ply + + +def read_point_cloud_cropped( + path: str | Path, + aabb: dict, + *, + chunk_vertices: int = 4_000_000, +) -> np.ndarray: + """Stream a (possibly huge) PLY and return only points inside ``aabb``. + + For multi-GB clouds (hundreds of millions / billions of points) loading + the whole file is infeasible. This reads the vertex block in chunks, + keeps only points within the axis-aligned bbox, and concatenates the + survivors. Falls back to a full read + mask for ASCII / text formats. + + ``aabb`` keys: ``x_min/x_max/y_min/y_max/z_min/z_max`` (same frame as the + file). Returns an ``(M, 3) float64`` array. + """ + path = Path(path) + suffix = path.suffix.lower() + bounds = ( + float(aabb["x_min"]), float(aabb["x_max"]), + float(aabb["y_min"]), float(aabb["y_max"]), + float(aabb["z_min"]), float(aabb["z_max"]), + ) + if suffix != ".ply": + pts = read_point_cloud(path) + return _crop_points(pts, bounds) + + fmt, _, header_bytes, vertex_count, vertex_props = _parse_ply_header(path) + if fmt == "ascii": + pts = _read_ply(path) + return _crop_points(pts, bounds) + if fmt not in {"binary_little_endian", "binary_big_endian"}: + raise ValueError(f"Unsupported PLY format '{fmt}': {path}") + if any(ptype == "list" for ptype, _ in vertex_props): + raise ValueError(f"Binary PLY with list vertex properties unsupported: {path}") + missing = {"x", "y", "z"} - {name for _, name in vertex_props} + if missing: + raise ValueError(f"PLY missing coordinate field(s) {sorted(missing)}: {path}") + + endian = "<" if fmt == "binary_little_endian" else ">" + record_dtype = np.dtype( + [(name, endian + _PLY_TYPE_TO_NP[ptype]) for ptype, name in vertex_props] + ) + record_bytes = record_dtype.itemsize + kept_chunks: list[np.ndarray] = [] + with path.open("rb") as fh: + fh.seek(header_bytes) + remaining = vertex_count + while remaining > 0: + n = min(chunk_vertices, remaining) + buf = fh.read(n * record_bytes) + if len(buf) < n * record_bytes: + n = len(buf) // record_bytes + if n == 0: + break + buf = buf[: n * record_bytes] + recs = np.frombuffer(buf, dtype=record_dtype, count=n) + xyz = np.column_stack( + [recs["x"], recs["y"], recs["z"]] + ).astype(np.float64) + crop = _crop_points(xyz, bounds) + if len(crop): + kept_chunks.append(crop) + remaining -= n + if not kept_chunks: + return np.zeros((0, 3), dtype=np.float64) + return np.concatenate(kept_chunks, axis=0) + + +def _crop_points(points: np.ndarray, bounds: tuple) -> np.ndarray: + x_lo, x_hi, y_lo, y_hi, z_lo, z_hi = bounds + mask = ( + (points[:, 0] >= x_lo) & (points[:, 0] <= x_hi) + & (points[:, 1] >= y_lo) & (points[:, 1] <= y_hi) + & (points[:, 2] >= z_lo) & (points[:, 2] <= z_hi) + ) + return points[mask] + + +def ensure_output_dir(path: str | Path) -> Path: + out_dir = Path(path) + out_dir.mkdir(parents=True, exist_ok=True) + return out_dir + + +def write_json(path: str | Path, payload: dict) -> None: + Path(path).write_text( + json.dumps(payload, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + + +def write_csv(path: str | Path, fieldnames: list[str], rows: Iterable[dict]) -> None: + with Path(path).open("w", encoding="utf-8", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=fieldnames) + writer.writeheader() + for row in rows: + writer.writerow(row) + + +def write_colored_ply( + path: str | Path, + points: np.ndarray, + colors: np.ndarray, +) -> None: + path = Path(path) + if len(points) != len(colors): + raise ValueError("Point/color length mismatch for PLY export.") + points = np.asarray(points, dtype=np.float64) + colors = np.asarray(colors, dtype=np.uint8) + with path.open("w", encoding="ascii", newline="\n") as fh: + fh.write("ply\n") + fh.write("format ascii 1.0\n") + fh.write(f"element vertex {len(points)}\n") + fh.write("property float x\n") + fh.write("property float y\n") + fh.write("property float z\n") + fh.write("property uchar red\n") + fh.write("property uchar green\n") + fh.write("property uchar blue\n") + fh.write("end_header\n") + for (x, y, z), (r, g, b) in zip(points, colors, strict=True): + fh.write(f"{x:.6f} {y:.6f} {z:.6f} {int(r)} {int(g)} {int(b)}\n") diff --git a/alg/delivery_v1.1/underframe_deflection/pipeline.py b/alg/delivery_v1.2/underframe_deflection/pipeline.py similarity index 88% rename from alg/delivery_v1.1/underframe_deflection/pipeline.py rename to alg/delivery_v1.2/underframe_deflection/pipeline.py index b7da5a9..44c6f56 100644 --- a/alg/delivery_v1.1/underframe_deflection/pipeline.py +++ b/alg/delivery_v1.2/underframe_deflection/pipeline.py @@ -41,6 +41,25 @@ from .roi import load_roi_aabb log = logging.getLogger(__name__) +def _swap_y_z(points: np.ndarray) -> np.ndarray: + """Reorder columns x,y,z -> x,z,y. + + Bridges between the CAD/scan convention (``y`` = height) and the + algorithm's canonical frame (``z`` = height). Pure column permutation; + no values are altered. + """ + return points[:, [0, 2, 1]] + + +def _swap_roi_y_z(roi: dict) -> dict: + """Swap the y/z bounds of an AABB to match :func:`_swap_y_z`.""" + return { + "x_min": roi["x_min"], "x_max": roi["x_max"], + "y_min": roi["z_min"], "y_max": roi["z_max"], + "z_min": roi["y_min"], "z_max": roi["y_max"], + } + + def run_measurement_pipeline( input_cloud: str | Path, out_dir: str | Path, @@ -52,6 +71,7 @@ def run_measurement_pipeline( skip_registration: bool = False, enable_bilateral: bool = True, skip_excel: bool = False, + input_axis: str = "y_up", ) -> dict: """Read inputs, run the algorithm, and write every artifact. @@ -80,6 +100,18 @@ def run_measurement_pipeline( if cad_path is not None: cad_points = read_cad_reference(cad_path) + if input_axis == "y_up": + # Bridge from the CAD/scan convention (y = height) into the + # algorithm's canonical frame (z = height). The core algorithm is + # unchanged; this is a pure input adapter at the I/O boundary. + scan_raw = _swap_y_z(scan_raw) + if cad_points is not None: + cad_points = _swap_y_z(cad_points) + if roi_aabb is not None: + roi_aabb = _swap_roi_y_z(roi_aabb) + elif input_axis != "z_up": + raise ValueError(f"input_axis must be 'z_up' or 'y_up', got {input_axis!r}") + registration_meta: dict[str, Any] = { "enabled": cad_points is not None and not skip_registration, "skipped": cad_points is not None and skip_registration, diff --git a/alg/delivery_v1.1/underframe_deflection/ply_overlay.py b/alg/delivery_v1.2/underframe_deflection/ply_overlay.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/ply_overlay.py rename to alg/delivery_v1.2/underframe_deflection/ply_overlay.py diff --git a/alg/delivery_v1.1/underframe_deflection/preprocess.py b/alg/delivery_v1.2/underframe_deflection/preprocess.py similarity index 82% rename from alg/delivery_v1.1/underframe_deflection/preprocess.py rename to alg/delivery_v1.2/underframe_deflection/preprocess.py index 175236f..c7f9867 100644 --- a/alg/delivery_v1.1/underframe_deflection/preprocess.py +++ b/alg/delivery_v1.2/underframe_deflection/preprocess.py @@ -110,12 +110,15 @@ def bilateral_filter_points( ) -> np.ndarray: """Point-cloud bilateral filter that smooths along estimated normals. - Each point is shifted along its own normal by the weighted average of - the signed normal-direction offsets from its k-nearest neighbours, - using a Gaussian on Euclidean distance (``sigma_d_mm``) and a - Gaussian on the angular deviation between point normals - (``sigma_n_rad``). The tangential position is preserved, so edges - survive while sensor noise along the surface normal is suppressed. + Each point is shifted *along its own normal* by the weighted average of + the signed normal-direction offsets to its k-nearest neighbours, using a + Gaussian on Euclidean distance (``sigma_d_mm``) and a Gaussian on the + angular deviation between point normals (``sigma_n_rad``). The tangential + position is preserved, so edges survive while noise along the surface + normal is suppressed. + + The per-point shift is clamped to ``sigma_d_mm`` so that degenerate + neighbourhoods or bad normals cannot collapse the cloud. """ if len(points) == 0 or iterations <= 0: return points.copy() @@ -124,31 +127,35 @@ def bilateral_filter_points( sigma_n = max(float(sigma_n_rad), 1e-6) inv_2sd2 = 1.0 / (2.0 * sigma_d * sigma_d) inv_2sn2 = 1.0 / (2.0 * sigma_n * sigma_n) + max_shift = sigma_d # clamp: a bilateral step never moves a point further than sigma_d current = points.astype(np.float64, copy=True) + unit_normals = normals.astype(np.float64, copy=True) + norm_len = np.linalg.norm(unit_normals, axis=1) + valid_normal = norm_len > 1e-9 + unit_normals[valid_normal] /= norm_len[valid_normal, None] + for _ in range(int(iterations)): tree = cKDTree(current) distances, indices = tree.query(current, k=effective_k) - if distances.ndim == 1: # k == 1 edge case + if distances.ndim == 1: distances = distances[:, None] indices = indices[:, None] - neighbor_points = current[indices] # (N, k, 3) - neighbor_normals = normals[indices] # (N, k, 3) - offsets = neighbor_points - current[:, None, :] # (N, k, 3) - own_normals = normals[:, None, :] # (N, 1, 3) - normal_offset = np.einsum("nkd,nkd->nk", offsets, own_normals) + own_n = unit_normals[:, None, :] # (N, 1, 3) + offsets = current[indices] - current[:, None, :] # (N, k, 3) + normal_offset = np.einsum("nkd,nkd->nk", offsets, own_n) spatial_w = np.exp(-(distances ** 2) * inv_2sd2) - dots = np.clip(np.einsum("nkd,nkd->nk", neighbor_normals, own_normals), -1.0, 1.0) + dots = np.clip(np.einsum("nkd,nkd->nk", unit_normals[indices], own_n), -1.0, 1.0) angle = np.arccos(np.abs(dots)) normal_w = np.exp(-(angle ** 2) * inv_2sn2) weights = spatial_w * normal_w weight_sum = weights.sum(axis=1) - valid = weight_sum > 1e-12 + move = weight_sum > 1e-12 + move &= valid_normal delta = np.zeros(len(current), dtype=np.float64) - delta[valid] = ( - (weights[valid] * normal_offset[valid]).sum(axis=1) / weight_sum[valid] - ) - current = current + delta[:, None] * normals + delta[move] = (weights[move] * normal_offset[move]).sum(axis=1) / weight_sum[move] + delta = np.clip(delta, -max_shift, max_shift) + current[move] = current[move] + delta[move, None] * unit_normals[move] return current @@ -188,6 +195,7 @@ def preprocess_point_cloud(points: np.ndarray, config: dict) -> PreprocessResult bilateral_cfg = preprocess.get("bilateral") or {} bilateral_count = len(radius_points) + bilateral_applied = False if bilateral_cfg.get("enabled") and len(radius_points) >= 8: k_requested = int(bilateral_cfg.get("k", 24)) k_effective = min(k_requested, len(radius_points)) @@ -207,6 +215,7 @@ def preprocess_point_cloud(points: np.ndarray, config: dict) -> PreprocessResult ), iterations=int(bilateral_cfg.get("iterations", 1)), ) + bilateral_applied = True work_points = voxel_downsample(radius_points, preprocess["voxel_mm"]) work_normals, work_curvature = estimate_normals( @@ -220,7 +229,7 @@ def preprocess_point_cloud(points: np.ndarray, config: dict) -> PreprocessResult "after_statistical_filter": int(len(stat_points)), "after_radius_filter": int(len(radius_points)), "after_bilateral": int(bilateral_count), - "bilateral_applied": bool(bilateral_cfg.get("enabled") and len(radius_points) >= 8), + "bilateral_applied": bilateral_applied, "downsampled_count": int(len(work_points)), "spacing_mm": float(spacing_estimate), "statistical_keep_ratio": float(np.mean(stat_mask)) if len(stat_mask) else 1.0, diff --git a/alg/delivery_v1.1/underframe_deflection/py.typed b/alg/delivery_v1.2/underframe_deflection/py.typed similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/py.typed rename to alg/delivery_v1.2/underframe_deflection/py.typed diff --git a/alg/delivery_v1.1/underframe_deflection/pyproject.toml b/alg/delivery_v1.2/underframe_deflection/pyproject.toml similarity index 87% rename from alg/delivery_v1.1/underframe_deflection/pyproject.toml rename to alg/delivery_v1.2/underframe_deflection/pyproject.toml index 7152f89..a354ba9 100644 --- a/alg/delivery_v1.1/underframe_deflection/pyproject.toml +++ b/alg/delivery_v1.2/underframe_deflection/pyproject.toml @@ -38,7 +38,13 @@ viz = ["matplotlib>=3.5,<4.0"] # Required by write_excel_report(); pipeline degrades gracefully if missing. excel = ["openpyxl>=3.1,<4.0"] -# Convenience meta-extra that pulls everything an integrator usually wants. +# ONLY for the standalone test-data generator under tools/cad_sampling/ +# (step_to_pointcloud.py). NOT needed by the measurement algorithm itself +# and deliberately kept out of `all` so the runtime stays light. +sampling = ["cadquery>=2.4"] + +# Convenience meta-extra that pulls everything an integrator usually wants +# at RUNTIME (does NOT include the heavy `sampling` toolchain). all = [ "matplotlib>=3.5,<4.0", "openpyxl>=3.1,<4.0", diff --git a/alg/delivery_v1.1/underframe_deflection/reference.py b/alg/delivery_v1.2/underframe_deflection/reference.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/reference.py rename to alg/delivery_v1.2/underframe_deflection/reference.py diff --git a/alg/delivery_v1.1/underframe_deflection/registration.py b/alg/delivery_v1.2/underframe_deflection/registration.py similarity index 81% rename from alg/delivery_v1.1/underframe_deflection/registration.py rename to alg/delivery_v1.2/underframe_deflection/registration.py index c4a3a11..982a296 100644 --- a/alg/delivery_v1.1/underframe_deflection/registration.py +++ b/alg/delivery_v1.2/underframe_deflection/registration.py @@ -65,6 +65,14 @@ def register_to_cad( idx = np.linspace(0, len(cad_sampled) - 1, target_n, dtype=int) cad_sampled = cad_sampled[idx] + max_corr = float(reg_cfg.get("icp_max_corr_mm", 25.0)) + max_iter = int(reg_cfg.get("icp_max_iter", 60)) + tol_mm = float(reg_cfg.get("icp_tol_mm", 0.05)) + tol_rel = float(reg_cfg.get("icp_tol_rel", 1.0e-4)) + trim = float(reg_cfg.get("trim_ratio", 0.2)) + + tree = cKDTree(cad_sampled) + R0, t0 = _coarse_align_pca( scan, cad_sampled, @@ -72,11 +80,17 @@ def register_to_cad( ) scan_coarse = scan @ R0.T + t0 - max_corr = float(reg_cfg.get("icp_max_corr_mm", 25.0)) - max_iter = int(reg_cfg.get("icp_max_iter", 60)) - tol_mm = float(reg_cfg.get("icp_tol_mm", 0.05)) - tol_rel = float(reg_cfg.get("icp_tol_rel", 1.0e-4)) - trim = float(reg_cfg.get("trim_ratio", 0.2)) + # Sanity guard: PCA-based coarse alignment can flip an axis when the scan + # is already close to the CAD frame (near-symmetric inertia). If the + # coarse step does not actually improve correspondence over the identity + # initialisation, fall back to identity so a bad flip cannot wreck ICP. + guard_max_corr = max(max_corr * 4.0, 0.05 * float(np.ptp(cad_sampled[:, 0]) + 1.0)) + rms_identity = _correspondence_rms(scan, tree, guard_max_corr) + rms_coarse = _correspondence_rms(scan_coarse, tree, guard_max_corr) + if rms_identity <= rms_coarse: + R0 = np.eye(3, dtype=np.float64) + t0 = np.zeros(3, dtype=np.float64) + scan_coarse = scan # ICP source downsampling — keeps query time bounded. src_target = target_n if target_n > 0 else len(scan_coarse) @@ -85,8 +99,6 @@ def register_to_cad( scan_for_icp = scan_coarse[idx] else: scan_for_icp = scan_coarse - - tree = cKDTree(cad_sampled) R = np.eye(3, dtype=np.float64) t = np.zeros(3, dtype=np.float64) prev_rms = float("inf") @@ -130,6 +142,27 @@ def register_to_cad( R_total = R @ R0 t_total = R @ t0 + t + + # Near-identity snap: when the scan is already aligned to the CAD (e.g. + # scan == cad, or a pre-registered scan), ICP can still drift by a few mm + # of translation / a fraction of a degree because it minimises mean + # nearest-neighbour distance on a *sparse* cloud (whose floor is ~spacing/2, + # not 0). That residual wobble shifts a thin ROI band enough to crop the + # beams. If the recovered transform is within tight identity tolerances and + # snapping does not worsen correspondence, return exact identity so an + # already-aligned input is left untouched. + identity_rot_tol_rad = float(reg_cfg.get("identity_rot_tol_deg", 0.5)) * np.pi / 180.0 + identity_trans_tol_mm = float(reg_cfg.get("identity_trans_tol_mm", 5.0)) + rot_angle = float(np.arccos(np.clip((np.trace(R_total) - 1.0) / 2.0, -1.0, 1.0))) + trans_norm = float(np.linalg.norm(t_total)) + if rot_angle <= identity_rot_tol_rad and trans_norm <= identity_trans_tol_mm: + rms_aligned = _correspondence_rms(scan, tree, max_corr) + rms_transformed = _correspondence_rms(scan @ R_total.T + t_total, tree, max_corr) + if rms_aligned <= rms_transformed + tol_mm: + R_total = np.eye(3, dtype=np.float64) + t_total = np.zeros(3, dtype=np.float64) + rms = rms_aligned + return RegistrationResult( R=R_total, t=t_total, diff --git a/alg/delivery_v1.1/underframe_deflection/requirements.txt b/alg/delivery_v1.2/underframe_deflection/requirements.txt similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/requirements.txt rename to alg/delivery_v1.2/underframe_deflection/requirements.txt diff --git a/alg/delivery_v1.1/underframe_deflection/roi.py b/alg/delivery_v1.2/underframe_deflection/roi.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/roi.py rename to alg/delivery_v1.2/underframe_deflection/roi.py diff --git a/alg/delivery_v1.2/underframe_deflection/single_beam.py b/alg/delivery_v1.2/underframe_deflection/single_beam.py new file mode 100644 index 0000000..604cf7c --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/single_beam.py @@ -0,0 +1,340 @@ +"""Direct single-beam deflection measurement. + +This module is an independent measurement path for the case where the input +ROI **already isolates one longitudinal beam** (left or right). It does not +use the dual-beam tracking algorithm in :mod:`beams` / :mod:`reference`; that +algorithm expects a car-bottom slab containing both beams and cannot resolve a +single, laterally-continuous beam surface. + +Geometry convention (CAD / scan native frame): + + x = longitudinal (car length) + y = vertical (height, up) + z = lateral (car width) + +Deflection is the vertical (y) departure of the beam underside from a straight +reference line along x. For a nominally straight beam the deflection is ~0. + +Method: + + 1. Crop the cloud to the ROI (axis-aligned bbox). + 2. Slice along x. + 3. Per slice, estimate the underside as the *dominant lower plane* height + (histogram peak of the lower half), robust to hangers / cables / noise + that hang below the real surface and to the upper flange above it. + 4. Fit a robust straight reference line through (x, underside). + 5. deflection(x) = underside(x) - reference(x). +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import numpy as np + +from .exceptions import InsufficientPointsError, InvalidPointCloudError +from .roi import normalize_aabb + + +# --------------------------------------------------------------------------- +# Result types +# --------------------------------------------------------------------------- + + +@dataclass +class SingleBeamResult: + """Result of :func:`measure_single_beam`. + + All distances are millimetres. ``deflection_mm`` is the underside height + minus the fitted straight reference, sampled at ``x_mm`` (slice centres). + ``selected_*`` is the point of maximum absolute deflection on the kept + (spike-rejected) profile. + """ + + side: str + available: bool + x_mm: np.ndarray + underside_height_mm: np.ndarray + reference_height_mm: np.ndarray + deflection_mm: np.ndarray + kept_mask: np.ndarray + selected_x_mm: float | None + selected_signed_mm: float | None + selected_absolute_mm: float | None + max_downward_mm: float | None + max_downward_x_mm: float | None + max_upward_mm: float | None + max_upward_x_mm: float | None + rms_mm: float | None + reference_slope_mm_per_m: float | None + n_slices: int + n_kept: int + roi_point_count: int + quality_flags: list[str] = field(default_factory=list) + report: dict = field(default_factory=dict) + + def to_dict(self) -> dict: + return self.report + + +# --------------------------------------------------------------------------- +# Public entry point +# --------------------------------------------------------------------------- + + +def measure_single_beam( + points: np.ndarray, + roi_aabb: dict, + *, + side: str = "beam", + config: dict | None = None, +) -> SingleBeamResult: + """Measure the vertical deflection of a single isolated beam. + + Args: + points: ``(N, 3)`` XYZ array, native frame (x=length, y=height, + z=width), millimetres. + roi_aabb: axis-aligned bbox dict with ``x_min/x_max/y_min/y_max/ + z_min/z_max`` (mm, native frame). Should isolate one beam. + side: label carried into the result / report (e.g. ``"left"``). + config: optional overrides; see :data:`DEFAULT_SINGLE_BEAM_CONFIG`. + + Returns: + :class:`SingleBeamResult`. + + Raises: + InvalidPointCloudError: ``points`` is not ``(N, 3+)``. + InsufficientPointsError: too few points in the ROI to measure. + """ + cfg = dict(DEFAULT_SINGLE_BEAM_CONFIG) + if config: + cfg.update(config) + + array = np.asarray(points, dtype=np.float64) + if array.ndim != 2 or array.shape[1] < 3: + raise InvalidPointCloudError( + f"Point cloud must be a 2D (N, 3+) array, got shape {array.shape}." + ) + array = array[:, :3] + + aabb = normalize_aabb(roi_aabb) + mask = ( + (array[:, 0] >= aabb["x_min"]) & (array[:, 0] <= aabb["x_max"]) + & (array[:, 1] >= aabb["y_min"]) & (array[:, 1] <= aabb["y_max"]) + & (array[:, 2] >= aabb["z_min"]) & (array[:, 2] <= aabb["z_max"]) + ) + beam = array[mask] + if len(beam) < cfg["min_roi_points"]: + raise InsufficientPointsError( + f"ROI for side '{side}' contains {len(beam)} points; need at least " + f"{cfg['min_roi_points']}." + ) + + x = beam[:, 0] + x_min, x_max = float(x.min()), float(x.max()) + slice_mm = float(cfg["slice_mm"]) + edges = np.arange(x_min, x_max + slice_mm, slice_mm) + + xs: list[float] = [] + under: list[float] = [] + support: list[int] = [] + for i in range(len(edges) - 1): + s = beam[(x >= edges[i]) & (x < edges[i + 1])] + if len(s) < cfg["min_slice_points"]: + continue + height = _dominant_underside_height(s[:, 1], cfg) + if height is None: + continue + xs.append(float(0.5 * (edges[i] + edges[i + 1]))) + under.append(height) + support.append(int(np.sum(np.abs(s[:, 1] - height) <= cfg["support_tol_mm"]))) + + flags: list[str] = [] + if len(xs) < cfg["min_valid_slices"]: + return _empty_result(side, len(beam), flags + ["insufficient_valid_slices"]) + + x_arr = np.asarray(xs, dtype=np.float64) + under_arr = np.asarray(under, dtype=np.float64) + + # Reject residual spikes on the underside profile before / via the fit. + slope, intercept, kept = _robust_line_fit(x_arr, under_arr, cfg) + reference = slope * x_arr + intercept + deflection = under_arr - reference + + n_kept = int(kept.sum()) + if n_kept < cfg["min_valid_slices"]: + return _empty_result(side, len(beam), flags + ["fit_unstable"]) + + dev_kept = deflection[kept] + x_kept = x_arr[kept] + down_i = int(np.argmin(dev_kept)) + up_i = int(np.argmax(dev_kept)) + abs_i = int(np.argmax(np.abs(dev_kept))) + selected_signed = float(dev_kept[abs_i]) + rms = float(np.sqrt(np.mean(dev_kept ** 2))) + + if int((~kept).sum()) > cfg["spike_warn_count"]: + flags.append("many_underside_spikes") + if np.median(support) < cfg["low_support_threshold"]: + flags.append("low_surface_support") + + report = { + "mode": "single_beam", + "side": side, + "available": True, + "roi_aabb": aabb, + "roi_point_count": int(len(beam)), + "n_slices": int(len(xs)), + "n_kept": n_kept, + "reference_slope_mm_per_m": float(slope * 1000.0), + "deflection": { + "selected_x_mm": float(x_kept[abs_i]), + "selected_signed_mm": selected_signed, + "selected_absolute_mm": float(abs(selected_signed)), + "max_downward_mm": float(dev_kept[down_i]), + "max_downward_x_mm": float(x_kept[down_i]), + "max_upward_mm": float(dev_kept[up_i]), + "max_upward_x_mm": float(x_kept[up_i]), + "rms_mm": rms, + }, + "quality_flags": flags, + "curve": { + "x_mm": x_arr.tolist(), + "underside_height_mm": under_arr.tolist(), + "reference_height_mm": reference.tolist(), + "deflection_mm": deflection.tolist(), + "kept": kept.tolist(), + }, + } + + return SingleBeamResult( + side=side, + available=True, + x_mm=x_arr, + underside_height_mm=under_arr, + reference_height_mm=reference, + deflection_mm=deflection, + kept_mask=kept, + selected_x_mm=float(x_kept[abs_i]), + selected_signed_mm=selected_signed, + selected_absolute_mm=float(abs(selected_signed)), + max_downward_mm=float(dev_kept[down_i]), + max_downward_x_mm=float(x_kept[down_i]), + max_upward_mm=float(dev_kept[up_i]), + max_upward_x_mm=float(x_kept[up_i]), + rms_mm=rms, + reference_slope_mm_per_m=float(slope * 1000.0), + n_slices=int(len(xs)), + n_kept=n_kept, + roi_point_count=int(len(beam)), + quality_flags=flags, + report=report, + ) + + +# --------------------------------------------------------------------------- +# Internals +# --------------------------------------------------------------------------- + + +DEFAULT_SINGLE_BEAM_CONFIG: dict = { + "slice_mm": 200.0, + "min_roi_points": 2000, + "min_slice_points": 30, + "min_valid_slices": 8, + # dominant-plane extraction + "hist_bin_mm": 3.0, + "plane_refine_mm": 8.0, + "support_tol_mm": 3.0, + # robust line fit + "fit_iters": 5, + "fit_tukey_c": 4.685, + "fit_reject_sigma": 4.0, + # warnings: flag only when a large *fraction* of slices are spikes + "spike_warn_count": 40, + "low_support_threshold": 30, +} + + +def _dominant_underside_height(height: np.ndarray, cfg: dict) -> float | None: + """Height of the dominant plane in the lower half of a slice. + + Robust to (a) hangers / cables / noise that float *below* the real + underside and (b) the upper flange / deck plate *above* it. We look only + at the lower half (<= median), take the densest histogram bin (the real + surface has the most points), then refine with the median of nearby + points. + """ + if len(height) < 10: + return None + lo = float(height.min()) + mid = float(np.median(height)) + if mid - lo < cfg["hist_bin_mm"]: + return float(mid) + lower = height[height <= mid] + bins = np.arange(lo, mid + cfg["hist_bin_mm"], cfg["hist_bin_mm"]) + if len(bins) < 2: + return float(mid) + hist, edges = np.histogram(lower, bins=bins) + if hist.sum() == 0: + return None + k = int(np.argmax(hist)) + peak = 0.5 * (edges[k] + edges[k + 1]) + near = height[np.abs(height - peak) <= cfg["plane_refine_mm"]] + return float(np.median(near)) if len(near) else float(peak) + + +def _robust_line_fit( + x: np.ndarray, y: np.ndarray, cfg: dict +) -> tuple[float, float, np.ndarray]: + """Tukey-reweighted straight-line fit; returns (slope, intercept, keep).""" + w = np.ones_like(y) + coef = np.array([0.0, float(np.median(y))]) + for _ in range(int(cfg["fit_iters"])): + A = np.vstack([x, np.ones_like(x)]).T + coef = np.linalg.lstsq(A * w[:, None], y * w, rcond=None)[0] + r = y - (coef[0] * x + coef[1]) + scale = float(np.median(np.abs(r - np.median(r))) * 1.4826) + 1e-6 + u = np.clip(np.abs(r) / (cfg["fit_tukey_c"] * scale), 0.0, 1.0) + w = (1.0 - u ** 2) ** 2 + r = y - (coef[0] * x + coef[1]) + med = float(np.median(r)) + mad = float(np.median(np.abs(r - med)) * 1.4826) + 1e-6 + keep = np.abs(r - med) <= cfg["fit_reject_sigma"] * mad + return float(coef[0]), float(coef[1]), keep + + +def _empty_result(side: str, roi_count: int, flags: list[str]) -> SingleBeamResult: + empty = np.zeros(0, dtype=np.float64) + return SingleBeamResult( + side=side, + available=False, + x_mm=empty, + underside_height_mm=empty, + reference_height_mm=empty, + deflection_mm=empty, + kept_mask=np.zeros(0, dtype=bool), + selected_x_mm=None, + selected_signed_mm=None, + selected_absolute_mm=None, + max_downward_mm=None, + max_downward_x_mm=None, + max_upward_mm=None, + max_upward_x_mm=None, + rms_mm=None, + reference_slope_mm_per_m=None, + n_slices=0, + n_kept=0, + roi_point_count=roi_count, + quality_flags=flags, + report={ + "mode": "single_beam", + "side": side, + "available": False, + "roi_point_count": roi_count, + "quality_flags": flags, + }, + ) + + +__all__ = ["SingleBeamResult", "measure_single_beam", "DEFAULT_SINGLE_BEAM_CONFIG"] diff --git a/alg/delivery_v1.2/underframe_deflection/single_beam_pipeline.py b/alg/delivery_v1.2/underframe_deflection/single_beam_pipeline.py new file mode 100644 index 0000000..80538dc --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/single_beam_pipeline.py @@ -0,0 +1,285 @@ +"""End-to-end single-beam measurement: stream-crop a cloud by ROI, measure, +write artifacts. This is the DEFAULT pipeline for the "ROI already isolates +one beam" workflow. + +Each call handles ONE beam (one ROI -> one result). For left+right, call it +twice with the two ROI files; outputs land in separate sub-directories. +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +import numpy as np + +from .io import ensure_output_dir, read_point_cloud_cropped, write_csv, write_json +from .roi import load_roi_aabb, normalize_aabb +from .single_beam import SingleBeamResult, measure_single_beam + +log = logging.getLogger(__name__) + + +def run_single_beam_pipeline( + input_cloud: str | Path, + roi_path: str | Path, + out_dir: str | Path, + *, + side: str | None = None, + config: dict | None = None, + cad_path: str | Path | None = None, + enable_registration: bool = False, + skip_excel: bool = False, + skip_plot: bool = False, + _registration: dict | None = None, +) -> dict: + """Measure one beam and write report.json/csv (+ xlsx/png if available). + + Args: + input_cloud: PLY/XYZ path. Large binary PLYs are stream-cropped. + roi_path: JSON AABB isolating the beam (native frame x=len, y=height, + z=width). + out_dir: output directory for this beam's artifacts. + side: label for the report; defaults to the ROI file stem. + cad_path: theoretical CAD reference. Recorded for provenance; parsed + and used only when ``enable_registration`` is True. + enable_registration: if True, the cloud is rigidly aligned to the CAD + before measurement (see :func:`_maybe_register`). Default False — + the supplied cloud is assumed already registered. + _registration: internal; precomputed registration metadata (used by + :func:`run_both_beams` to register once and reuse). + """ + out_dir_path = ensure_output_dir(out_dir) + roi_aabb = load_roi_aabb(roi_path) + aabb = normalize_aabb(roi_aabb) + if side is None: + stem = Path(roi_path).stem.lower() + side = "left" if "left" in stem else "right" if "right" in stem else stem + + reg_meta = _registration + transform = None + if reg_meta is None: + reg_meta, transform = _maybe_register(input_cloud, cad_path, enable_registration, config) + elif reg_meta.get("performed"): + transform = (np.asarray(reg_meta["R"], dtype=np.float64), np.asarray(reg_meta["t"], dtype=np.float64)) + + points = read_point_cloud_cropped(input_cloud, aabb) + if transform is not None: + from .registration import apply_transform + # ROI is defined in the registered frame; crop already-registered + # points would be wrong, so when registering we crop a slightly + # padded box, transform, then re-crop. Handled in _maybe_register's + # contract: here transform is applied then re-cropped. + points = apply_transform(points, transform[0], transform[1]) + from .io import _crop_points + points = _crop_points(points, ( + aabb["x_min"], aabb["x_max"], aabb["y_min"], aabb["y_max"], aabb["z_min"], aabb["z_max"])) + + result = measure_single_beam(points, aabb, side=side, config=config) + + result.report["input_cloud"] = str(Path(input_cloud).resolve()) + result.report["roi_source"] = str(Path(roi_path).resolve()) + result.report["cad_reference"] = str(Path(cad_path).resolve()) if cad_path else None + result.report["registration"] = reg_meta + + write_json(out_dir_path / "report.json", result.report) + _write_curve_csv(result, out_dir_path / "deflection_curve.csv") + + if not skip_plot: + _plot_safely(result, out_dir_path / "deflection_curve.png") + if not skip_excel: + _excel_safely(result, out_dir_path / "report.xlsx", input_cloud, roi_path, cad_path, reg_meta) + + return result.report + + +def run_both_beams( + input_cloud: str | Path, + left_roi: str | Path, + right_roi: str | Path, + out_dir: str | Path, + *, + config: dict | None = None, + cad_path: str | Path | None = None, + enable_registration: bool = False, + skip_excel: bool = False, + skip_plot: bool = False, +) -> dict: + """Measure left and right beams into ``out_dir/{left,right}``. + + Each side is a fully independent measurement (one call, one Excel each) — + per the integration contract. When ``enable_registration`` is True the + whole-train cloud is registered to the CAD once and the transform reused + for both sides. + """ + out_dir_path = ensure_output_dir(out_dir) + reg_meta, _ = _maybe_register(input_cloud, cad_path, enable_registration, config) + left = run_single_beam_pipeline( + input_cloud, left_roi, out_dir_path / "left", side="left", + config=config, cad_path=cad_path, enable_registration=enable_registration, + skip_excel=skip_excel, skip_plot=skip_plot, _registration=reg_meta, + ) + right = run_single_beam_pipeline( + input_cloud, right_roi, out_dir_path / "right", side="right", + config=config, cad_path=cad_path, enable_registration=enable_registration, + skip_excel=skip_excel, skip_plot=skip_plot, _registration=reg_meta, + ) + summary = {"left": left, "right": right} + write_json(out_dir_path / "summary.json", summary) + return summary + + +def _maybe_register(input_cloud, cad_path, enable_registration, config): + """Return (registration_meta, transform_or_None). + + When ``enable_registration`` is False (default), no transform is applied + and the meta records that the cloud is assumed pre-registered. When True, + the whole-train cloud (downsampled) is rigidly aligned to the CAD; the CAD + may be a point cloud / mesh (.ply/.xyz/.stl/.obj). STEP (.stp) is recorded + but must be converted to a sampled cloud first (see tools/cad_sampling/). + """ + if not enable_registration: + return {"performed": False, "reason": "disabled (cloud assumed pre-registered)", + "cad_reference": str(Path(cad_path).resolve()) if cad_path else None}, None + if cad_path is None: + return {"performed": False, "reason": "no cad_path supplied"}, None + + suffix = Path(cad_path).suffix.lower() + if suffix in {".stp", ".step", ".igs", ".iges"}: + return {"performed": False, + "reason": f"STEP not parsed; convert to sampled .ply/.stl first ({suffix})", + "cad_reference": str(Path(cad_path).resolve())}, None + + from .cad_io import read_cad_reference + from .config import load_config, resolve_numeric_parameters, cloud_geometry + from .preprocess import estimate_spacing, voxel_downsample + from .registration import register_to_cad + + cad = read_cad_reference(cad_path) + # Downsample the (possibly huge) train cloud for registration. + scan_full = read_point_cloud_cropped(input_cloud, { + "x_min": -1e12, "x_max": 1e12, "y_min": -1e12, "y_max": 1e12, "z_min": -1e12, "z_max": 1e12}) + base = load_config(None, "auto") + if config: + from .config import deep_merge + base = deep_merge(base, config) + spacing = estimate_spacing(scan_full, base["preprocess"]["spacing_sample_size"]) + rc = resolve_numeric_parameters(base, cloud_geometry(scan_full), spacing) + scan_ds = voxel_downsample(scan_full, max(rc["preprocess"]["voxel_mm"], 10.0)) + reg = register_to_cad(scan_ds, cad, rc) + meta = { + "performed": True, "rms_mm": reg.rms_mm, "iterations": reg.iterations, + "inlier_ratio": reg.inlier_ratio, "converged": reg.converged, + "R": reg.R.tolist(), "t": reg.t.tolist(), + "cad_reference": str(Path(cad_path).resolve()), + } + return meta, (reg.R, reg.t) + + +# --------------------------------------------------------------------------- +# Writers +# --------------------------------------------------------------------------- + + +def _write_curve_csv(result: SingleBeamResult, path: Path) -> None: + rows = [] + for i in range(len(result.x_mm)): + rows.append({ + "x_mm": float(result.x_mm[i]), + "underside_height_mm": float(result.underside_height_mm[i]), + "reference_height_mm": float(result.reference_height_mm[i]), + "deflection_mm": float(result.deflection_mm[i]), + "kept": bool(result.kept_mask[i]) if len(result.kept_mask) else False, + }) + write_csv( + path, + fieldnames=["x_mm", "underside_height_mm", "reference_height_mm", "deflection_mm", "kept"], + rows=rows, + ) + + +def _plot_safely(result: SingleBeamResult, path: Path) -> None: + if not result.available or len(result.x_mm) == 0: + return + try: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + except ImportError: + log.warning("matplotlib not installed; skipping single-beam plot") + return + x = result.x_mm + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 6), sharex=True) + ax1.scatter(x, result.underside_height_mm, s=8, c="tab:blue", label="underside") + ax1.plot(x, result.reference_height_mm, "r-", lw=1.5, label="reference line") + ax1.set_ylabel("height (mm)") + ax1.legend(loc="best") + ax1.set_title(f"{result.side} beam — underside vs straight reference") + ax2.axhline(0.0, color="k", lw=0.8) + ax2.plot(x, result.deflection_mm, "tab:green", lw=1.2) + if result.selected_x_mm is not None: + ax2.plot([result.selected_x_mm], [result.selected_signed_mm], "ro", + label=f"max |dev| = {result.selected_absolute_mm:.2f} mm") + ax2.legend(loc="best") + ax2.set_xlabel("x — longitudinal (mm)") + ax2.set_ylabel("deflection (mm)") + fig.tight_layout() + fig.savefig(path, dpi=150) + plt.close(fig) + + +def _excel_safely(result: SingleBeamResult, path: Path, input_cloud, roi_path, + cad_path=None, reg_meta=None) -> None: + try: + from openpyxl import Workbook + except ImportError: + log.warning("openpyxl not installed; skipping single-beam xlsx") + return + reg_meta = reg_meta or {} + wb = Workbook() + ws = wb.active + ws.title = "Summary" + d = result.report.get("deflection", {}) + rows = [ + ("mode", "single_beam"), + ("side", result.side), + ("available", result.available), + ("input_cloud", str(Path(input_cloud).resolve())), + ("roi_source", str(Path(roi_path).resolve())), + ("cad_reference", str(Path(cad_path).resolve()) if cad_path else ""), + ("registration_performed", reg_meta.get("performed", False)), + ("registration_rms_mm", reg_meta.get("rms_mm", "")), + ("registration_note", reg_meta.get("reason", "")), + ("roi_point_count", result.roi_point_count), + ("n_slices", result.n_slices), + ("n_kept", result.n_kept), + ("reference_slope_mm_per_m", result.reference_slope_mm_per_m), + ("selected_x_mm", d.get("selected_x_mm")), + ("selected_signed_deflection_mm", d.get("selected_signed_mm")), + ("selected_absolute_deflection_mm", d.get("selected_absolute_mm")), + ("max_downward_mm", d.get("max_downward_mm")), + ("max_downward_x_mm", d.get("max_downward_x_mm")), + ("max_upward_mm", d.get("max_upward_mm")), + ("max_upward_x_mm", d.get("max_upward_x_mm")), + ("rms_mm", d.get("rms_mm")), + ("quality_flags", "; ".join(result.quality_flags)), + ] + ws.append(["key", "value"]) + for k, v in rows: + ws.append([k, "" if v is None else v]) + ws.column_dimensions["A"].width = 28 + ws.column_dimensions["B"].width = 40 + + ws2 = wb.create_sheet("Deflection_Curve") + ws2.append(["x_mm", "underside_height_mm", "reference_height_mm", "deflection_mm", "kept"]) + for i in range(len(result.x_mm)): + ws2.append([ + float(result.x_mm[i]), float(result.underside_height_mm[i]), + float(result.reference_height_mm[i]), float(result.deflection_mm[i]), + bool(result.kept_mask[i]) if len(result.kept_mask) else False, + ]) + wb.save(path) + + +__all__ = ["run_single_beam_pipeline", "run_both_beams"] diff --git a/alg/delivery_v1.1/underframe_deflection/structures.py b/alg/delivery_v1.2/underframe_deflection/structures.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/structures.py rename to alg/delivery_v1.2/underframe_deflection/structures.py diff --git a/alg/delivery_v1.2/underframe_deflection/test_data/roi_left_beam.json b/alg/delivery_v1.2/underframe_deflection/test_data/roi_left_beam.json new file mode 100644 index 0000000..e3e6dea --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/test_data/roi_left_beam.json @@ -0,0 +1,8 @@ +{ + "x_min": -100000.0, + "x_max": 100000.0, + "y_min": 919.950, + "y_max": 1399.950, + "z_min": 948.623, + "z_max": 1752.212 +} diff --git a/alg/delivery_v1.2/underframe_deflection/test_data/roi_right_beam.json b/alg/delivery_v1.2/underframe_deflection/test_data/roi_right_beam.json new file mode 100644 index 0000000..eebaa2d --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/test_data/roi_right_beam.json @@ -0,0 +1,8 @@ +{ + "x_min": -100000.0, + "x_max": 100000.0, + "y_min": 919.950, + "y_max": 1399.950, + "z_min": -1752.210, + "z_max": -948.617 +} diff --git a/alg/delivery_v1.2/underframe_deflection/test_data/sample_roi.json b/alg/delivery_v1.2/underframe_deflection/test_data/sample_roi.json new file mode 100644 index 0000000..363be27 --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/test_data/sample_roi.json @@ -0,0 +1,8 @@ +{ + "x_min": -12500.0, + "x_max": 12500.0, + "y_min": 218.0, + "y_max": 1372.0, + "z_min": -1700.0, + "z_max": 1700.0 +} diff --git a/alg/delivery_v1.2/underframe_deflection/tools/cad_sampling/README.md b/alg/delivery_v1.2/underframe_deflection/tools/cad_sampling/README.md new file mode 100644 index 0000000..e6eb67f --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/tools/cad_sampling/README.md @@ -0,0 +1,54 @@ +# CAD 采样工具(测试数据生成 · 与主算法隔离) + +> **这是独立工具,不是主算法的一部分。** 它只用来把 CAD 的 STEP 模型采样成点云, +> 作为**模拟测试数据**。主测量算法 (`underframe_deflection`) **不会 import 本目录任何代码**, +> 也不依赖 `cadquery`。将来用真实扫描点云时,本工具完全可以不用。 + +## 作用 + +``` +STEP (.stp) ──► step_to_pointcloud.py ──► 整车点云 (.ply / .xyz) + metadata (.json) + (tessellate + 按三角面积加权采样) +``` + +输出坐标轴约定与主算法一致:`x=纵向, y=横向, z=高度`(脚本内部做了 `[0,2,1]` 轴重排, +因为 CAD 原始是 `x=纵向, y=高度, z=横向`)。 + +## 安装依赖 + +```bash +# 仅生成测试数据时才需要;主算法运行不需要它 +pip install -e .[sampling] # 安装 cadquery +``` + +## 用法 + +```bash +python tools/cad_sampling/step_to_pointcloud.py <模型.stp> \ + -n 800000 \ # 采样点数(越大越密) + --bottom-boost 4.0 \ # 对车底区域加密的权重(>1 时车底点更多) + --bottom-band-height-ratio 0.24 \ + --output-prefix test_data/dense_800k +``` + +常用参数: + +| 参数 | 默认 | 说明 | +| --- | --- | --- | +| `-n / --num-points` | 100000 | 采样点总数 | +| `--bottom-boost` | 1.0 | 车底加密倍数(挠度测量建议 ≥ 3) | +| `--bottom-band-height-ratio` | 0.24 | 车底带的高度占比 | +| `--linear-deflection` | 1.0 | tessellation 线性精度(越小三角越细,越慢) | +| `--angular-deflection` | 0.1 | tessellation 角度精度 | +| `--seed` | 42 | 随机种子(可复现) | + +## 与主算法的关系 + +``` +[本工具] STEP → 点云 [主算法] 点云 + CAD + ROI(json) → 挠度报告 + (cadquery,仅测试) (numpy+scipy,生产) + └── 解耦 ──────────────────────┘ +``` + +挠度测量对点云密度敏感:**车底纵梁区域过稀会导致样条拟合在端点不稳定**。 +生成模拟测试数据时建议 `--bottom-boost ≥ 3` 且 `-n ≥ 8e5`。 diff --git a/alg/delivery_v1.2/underframe_deflection/tools/cad_sampling/step_to_pointcloud.py b/alg/delivery_v1.2/underframe_deflection/tools/cad_sampling/step_to_pointcloud.py new file mode 100644 index 0000000..e83e5a7 --- /dev/null +++ b/alg/delivery_v1.2/underframe_deflection/tools/cad_sampling/step_to_pointcloud.py @@ -0,0 +1,332 @@ +import argparse +import json +import os +from pathlib import Path + +import cadquery as cq +import numpy as np + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Sample a STEP model surface into a point cloud." + ) + parser.add_argument("input_step", help="Path to the STEP file.") + parser.add_argument( + "-n", + "--num-points", + type=int, + default=100_000, + help="Number of points to sample from the surface.", + ) + parser.add_argument( + "--linear-deflection", + type=float, + default=1.0, + help="Linear deflection for tessellation.", + ) + parser.add_argument( + "--angular-deflection", + type=float, + default=0.1, + help="Angular deflection for tessellation.", + ) + parser.add_argument( + "--bottom-boost", + type=float, + default=1.0, + help="Extra sampling weight applied to the bottom band during triangle selection.", + ) + parser.add_argument( + "--bottom-band-height-ratio", + type=float, + default=0.24, + help="Height ratio used to define the bottom band for boosted sampling.", + ) + parser.add_argument( + "--seed", + type=int, + default=42, + help="Random seed for reproducible sampling.", + ) + parser.add_argument( + "--chunk-size", + type=int, + default=250_000, + help="Triangle chunk size used during weighted sampling.", + ) + parser.add_argument( + "--output-prefix", + default=None, + help="Output prefix. Defaults to _pts.", + ) + return parser.parse_args() + + +def load_shape(step_path: Path): + original_cwd = Path.cwd() + step_dir = step_path.resolve().parent + try: + os.chdir(step_dir) + return cq.importers.importStep(step_path.name).val() + finally: + os.chdir(original_cwd) + + +def vectors_to_array(vectors) -> np.ndarray: + return np.asarray([v.toTuple() for v in vectors], dtype=np.float64) + + +def triangle_areas(vertices: np.ndarray, tri_chunk: np.ndarray) -> np.ndarray: + p0 = vertices[tri_chunk[:, 0]] + p1 = vertices[tri_chunk[:, 1]] + p2 = vertices[tri_chunk[:, 2]] + cross = np.cross(p1 - p0, p2 - p0) + return 0.5 * np.linalg.norm(cross, axis=1) + + +def triangle_effective_weights( + vertices: np.ndarray, + tri_chunk: np.ndarray, + bottom_boost: float, + bottom_height_cut: float | None, +) -> np.ndarray: + areas = triangle_areas(vertices, tri_chunk) + if bottom_boost <= 1.0 or bottom_height_cut is None: + return areas + centroids = vertices[tri_chunk].mean(axis=1) + boost_mask = centroids[:, 1] <= bottom_height_cut + weights = areas.copy() + weights[boost_mask] *= bottom_boost + return weights + + +def assign_samples_per_chunk( + triangles, + vertices: np.ndarray, + num_points: int, + chunk_size: int, + seed: int, + bottom_boost: float, + bottom_height_cut: float | None, +) -> tuple[list[int], np.ndarray]: + chunk_area_sums = [] + non_empty_chunk_indices = [] + + for start in range(0, len(triangles), chunk_size): + tri_chunk = np.asarray(triangles[start : start + chunk_size], dtype=np.int32) + if len(tri_chunk) == 0: + continue + weights = triangle_effective_weights( + vertices, + tri_chunk, + bottom_boost=bottom_boost, + bottom_height_cut=bottom_height_cut, + ) + area_sum = float(weights.sum()) + chunk_area_sums.append(area_sum) + if area_sum > 0: + non_empty_chunk_indices.append(len(chunk_area_sums) - 1) + + if not chunk_area_sums: + raise ValueError("No tessellated triangles were produced from the STEP model.") + + chunk_area_sums = np.asarray(chunk_area_sums, dtype=np.float64) + total_area = float(chunk_area_sums.sum()) + if total_area <= 0: + raise ValueError("All tessellated triangle areas are zero.") + + probabilities = chunk_area_sums / total_area + rng = np.random.default_rng(seed) + chunk_counts = rng.multinomial(num_points, probabilities) + return chunk_counts.tolist(), chunk_area_sums + + +def sample_points( + triangles, + vertices: np.ndarray, + chunk_counts: list[int], + chunk_size: int, + seed: int, + bottom_boost: float, + bottom_height_cut: float | None, +) -> np.ndarray: + rng = np.random.default_rng(seed) + sampled_chunks = [] + + for chunk_index, start in enumerate(range(0, len(triangles), chunk_size)): + requested = chunk_counts[chunk_index] + if requested <= 0: + continue + + tri_chunk = np.asarray(triangles[start : start + chunk_size], dtype=np.int32) + weights = triangle_effective_weights( + vertices, + tri_chunk, + bottom_boost=bottom_boost, + bottom_height_cut=bottom_height_cut, + ) + positive_mask = weights > 0 + if not np.any(positive_mask): + continue + + tri_chunk = tri_chunk[positive_mask] + weights = weights[positive_mask] + probabilities = weights / weights.sum() + + chosen = rng.choice(len(tri_chunk), size=requested, p=probabilities) + chosen_triangles = tri_chunk[chosen] + + p0 = vertices[chosen_triangles[:, 0]] + p1 = vertices[chosen_triangles[:, 1]] + p2 = vertices[chosen_triangles[:, 2]] + + u = rng.random(requested) + v = rng.random(requested) + sqrt_u = np.sqrt(u) + w0 = 1.0 - sqrt_u + w1 = sqrt_u * (1.0 - v) + w2 = sqrt_u * v + points = ( + p0 * w0[:, None] + + p1 * w1[:, None] + + p2 * w2[:, None] + ) + sampled_chunks.append(points) + + if not sampled_chunks: + raise ValueError("Sampling produced no points.") + + points = np.vstack(sampled_chunks) + if len(points) != sum(chunk_counts): + raise RuntimeError("Point count mismatch after sampling.") + return points + + +def write_xyz(points: np.ndarray, output_path: Path) -> None: + np.savetxt(output_path, points, fmt="%.6f") + + +def write_ply(points: np.ndarray, output_path: Path) -> None: + with output_path.open("w", encoding="ascii", newline="\n") as fh: + fh.write("ply\n") + fh.write("format ascii 1.0\n") + fh.write(f"element vertex {len(points)}\n") + fh.write("property float x\n") + fh.write("property float y\n") + fh.write("property float z\n") + fh.write("end_header\n") + for x, y, z in points: + fh.write(f"{x:.6f} {y:.6f} {z:.6f}\n") + + +def write_metadata( + output_path: Path, + input_step: Path, + num_points: int, + linear_deflection: float, + angular_deflection: float, + seed: int, + num_vertices: int, + num_triangles: int, + bottom_boost: float, + bottom_band_height_ratio: float, + output_axis_convention: dict[str, str], +) -> None: + metadata = { + "input_step": str(input_step.resolve()), + "num_points": num_points, + "linear_deflection": linear_deflection, + "angular_deflection": angular_deflection, + "seed": seed, + "num_tessellated_vertices": num_vertices, + "num_tessellated_triangles": num_triangles, + "bottom_boost": bottom_boost, + "bottom_band_height_ratio": bottom_band_height_ratio, + "output_axis_convention": output_axis_convention, + } + output_path.write_text( + json.dumps(metadata, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + + +def main() -> None: + args = parse_args() + input_step = Path(args.input_step) + if not input_step.exists(): + raise FileNotFoundError(f"Input STEP file not found: {input_step}") + + if args.output_prefix: + output_prefix = Path(args.output_prefix) + else: + output_prefix = input_step.with_name( + f"{input_step.stem}_{args.num_points}pts" + ) + + shape = load_shape(input_step) + vertices, triangles = shape.tessellate( + args.linear_deflection, + args.angular_deflection, + ) + vertices_np = vectors_to_array(vertices) + height_min = float(vertices_np[:, 1].min()) + height_max = float(vertices_np[:, 1].max()) + bottom_height_cut = height_min + args.bottom_band_height_ratio * ( + height_max - height_min + ) + chunk_counts, _ = assign_samples_per_chunk( + triangles, + vertices_np, + args.num_points, + args.chunk_size, + args.seed, + args.bottom_boost, + bottom_height_cut, + ) + points = sample_points( + triangles, + vertices_np, + chunk_counts, + args.chunk_size, + args.seed, + args.bottom_boost, + bottom_height_cut, + ) + # CAD model coordinates are x=longitudinal, y=height, z=lateral. + # We deliberately KEEP the CAD-native axis order here (no y/z swap), so the + # sampled cloud matches the real scan/CAD coordinate convention. Any axis + # alignment the measurement algorithm needs is handled downstream, not here. + + xyz_path = output_prefix.with_suffix(".xyz") + ply_path = output_prefix.with_suffix(".ply") + metadata_path = output_prefix.with_suffix(".json") + + write_xyz(points, xyz_path) + write_ply(points, ply_path) + write_metadata( + metadata_path, + input_step, + len(points), + args.linear_deflection, + args.angular_deflection, + args.seed, + len(vertices), + len(triangles), + args.bottom_boost, + args.bottom_band_height_ratio, + { + "x": "longitudinal", + "y": "lateral", + "z": "height", + }, + ) + + print(f"Sampled {len(points)} points.") + print(f"PLY: {ply_path}") + print(f"XYZ: {xyz_path}") + print(f"Metadata: {metadata_path}") + + +if __name__ == "__main__": + main() diff --git a/alg/delivery_v1.1/underframe_deflection/visualization.py b/alg/delivery_v1.2/underframe_deflection/visualization.py similarity index 100% rename from alg/delivery_v1.1/underframe_deflection/visualization.py rename to alg/delivery_v1.2/underframe_deflection/visualization.py diff --git a/client/src/components/TaskManager.tsx b/client/src/components/TaskManager.tsx index 49bc1a5..b48dd6e 100644 --- a/client/src/components/TaskManager.tsx +++ b/client/src/components/TaskManager.tsx @@ -172,6 +172,17 @@ export default function TaskManager({ onLoadPointClouds(activeTaskId, ids); } + const allCadSelected = taskCadFiles.length > 0 && taskCadFiles.every((f) => selectedCadFileIds.has(f.id)); + const someCadSelected = taskCadFiles.some((f) => selectedCadFileIds.has(f.id)); + + function toggleAllCadFiles() { + if (allCadSelected) { + setSelectedCadFileIds(new Set()); + } else { + setSelectedCadFileIds(new Set(taskCadFiles.map((f) => f.id))); + } + } + function toggleCadFile(id: number) { setSelectedCadFileIds((prev) => { const next = new Set(prev); @@ -467,12 +478,26 @@ export default function TaskManager({ ({taskCadFiles.length}) - +
+ {taskCadFiles.length > 0 && ( + + )} + +
{uploadingCadFor === activeTaskId && ( @@ -549,9 +574,26 @@ export default function TaskManager({ {selectedCadFileIds.size > 0 && ( -

- 已选 {selectedCadFileIds.size} 个 CAD · 将作为算法输入 -

+
+

+ 已选 {selectedCadFileIds.size} 个 CAD · 将作为算法输入 +

+ {selectedCadFileIds.size === 1 && onSelectTaskCad && (() => { + const f = taskCadFiles.find(f => selectedCadFileIds.has(f.id)); + return f ? ( + + ) : null; + })()} +
)} )} diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 64dc87e..16b6a9d 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -3,7 +3,6 @@ import { useAuth } from '@/_core/hooks/useAuth'; import { getLoginUrl } from '@/const'; import PointCloudViewer from '@/components/PointCloudViewer'; import VehicleTree, { TreeSelection } from '@/components/VehicleTree'; -import ModelManager from '@/components/ModelManager'; import TaskManager from '@/components/TaskManager'; import AnalysisPanel from '@/components/AnalysisPanel'; import AlgorithmRunPanel from '@/components/AlgorithmRunPanel'; @@ -310,14 +309,6 @@ export default function Home() { <>
- -
int: out_dir.mkdir(parents=True, exist_ok=True) try: - emit_progress(0.05, "加载 coupler_panel v1.1 算法包") + emit_progress(0.05, "加载 coupler_panel v1.2 算法包") from coupler_panel.pipeline import run_coupler_measurement_pipeline # type: ignore pc_path = first_pointcloud(payload) diff --git a/server/plugins/adapters/underframe_deflection_run.py b/server/plugins/adapters/underframe_deflection_run.py index 6b2bf90..780319a 100644 --- a/server/plugins/adapters/underframe_deflection_run.py +++ b/server/plugins/adapters/underframe_deflection_run.py @@ -1,10 +1,13 @@ -"""underframe-deflection 算法适配器 (v1.1)。 +"""underframe-deflection 算法适配器 (v1.2)。 + +v1.2 变更: + - 算法包升级至 delivery_v1.2(新增 single_beam 单梁管线) + - PYTHONPATH 指向 delivery_v1.2 v1.1 变更: - 入口改为 run_measurement_pipeline(返回 dict,同时落盘 report.json / xlsx / PLY) - 新增 cad_path(CAD 基准 PLY/STL)和 roi_path(ROI JSON)可选输入 - 新增 skip_registration / enable_bilateral 配置项 - - PYTHONPATH 指向 delivery_v1.1 - 结果从 report dict 中读取,而非 DeflectionResult 对象字段 """ @@ -51,7 +54,7 @@ def main() -> int: out_dir.mkdir(parents=True, exist_ok=True) try: - emit_progress(0.05, "加载 underframe_deflection v1.1 算法包") + emit_progress(0.05, "加载 underframe_deflection v1.2 算法包") from underframe_deflection.pipeline import run_measurement_pipeline # type: ignore pc_path = first_pointcloud(payload) diff --git a/server/plugins/manifests/coupler-panel.json b/server/plugins/manifests/coupler-panel.json index 9396269..10a3d6c 100644 --- a/server/plugins/manifests/coupler-panel.json +++ b/server/plugins/manifests/coupler-panel.json @@ -2,8 +2,8 @@ "id": "coupler-panel", "name": "车钩面板检测", "shortName": "coupler-panel", - "description": "基于点云的车钩面板偏转角与最大偏差测量(v1.1,来自 alg/delivery_v1.1/coupler_panel_release)。输出 θ_right / θ_ref 偏转角、dx_y / dx_z 最大 x 偏差及 D_y / D_z 面板偏差。", - "version": "1.1.0", + "description": "基于点云的车钩面板偏转角与最大偏差测量(v1.2,来自 alg/delivery_v1.2/coupler_panel_release)。输出 θ_right / θ_ref 偏转角、dx_y / dx_z 最大 x 偏差及 D_y / D_z 面板偏差。v1.2 新增自动调参(autotune)与分层选点(layer_select)。", + "version": "1.2.0", "schemaVersion": 1, "category": "几何测量", "requiresPointCloud": true, @@ -42,7 +42,7 @@ "{inputJson}" ], "envExtra": { - "PYTHONPATH": "{algRoot}/delivery_v1.1/coupler_panel_release" + "PYTHONPATH": "{algRoot}/delivery_v1.2/coupler_panel_release" }, "timeoutMs": 1200000 } diff --git a/server/plugins/manifests/underframe-deflection.json b/server/plugins/manifests/underframe-deflection.json index a5fa250..1909e06 100644 --- a/server/plugins/manifests/underframe-deflection.json +++ b/server/plugins/manifests/underframe-deflection.json @@ -2,8 +2,8 @@ "id": "underframe-deflection", "name": "构架挠度测量", "shortName": "underframe-deflection", - "description": "基于点云的高铁底架纵梁挠度测量(v1.1,来自 alg/delivery_v1.1/underframe_deflection)。v1.1 新增 CAD 基准配准与 ROI 裁剪支持,输出左右梁挠度、配准 RMS 及挠度曲线 PLY 叠加层。", - "version": "1.1.0", + "description": "基于点云的高铁底架纵梁挠度测量(v1.2,来自 alg/delivery_v1.2/underframe_deflection)。支持 CAD 基准配准与 ROI 裁剪,输出左右梁挠度、配准 RMS 及挠度曲线 PLY 叠加层。v1.2 新增单梁管线(single_beam)支持。", + "version": "1.2.0", "schemaVersion": 1, "category": "几何测量", "requiresPointCloud": true, @@ -54,7 +54,7 @@ "{inputJson}" ], "envExtra": { - "PYTHONPATH": "{algRoot}/delivery_v1.1" + "PYTHONPATH": "{algRoot}/delivery_v1.2" }, "timeoutMs": 1200000 }