162 lines
5.0 KiB
Python
162 lines
5.0 KiB
Python
"""``coupler-measure`` command-line entry point.
|
|
|
|
Three modes::
|
|
|
|
# measure an existing point cloud (production-like flow)
|
|
coupler-measure --input-cloud scan.ply --out-dir runs/demo
|
|
|
|
# research: synthesise a cloud from a STEP file and measure it
|
|
coupler-measure --stp panel.stp --out-dir runs/from_cad
|
|
|
|
# research: parameter sweep
|
|
coupler-measure --sweep matrix.yaml --out-dir runs/sweep_xx
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
log = logging.getLogger("coupler_panel.cli")
|
|
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
p = argparse.ArgumentParser(
|
|
prog="coupler-measure",
|
|
description="Coupler-panel CAD-STP to point-cloud measurement & validation.",
|
|
)
|
|
src = p.add_mutually_exclusive_group(required=True)
|
|
src.add_argument("--input-cloud", type=Path, help="Path to a PLY/XYZ/CSV/PCD point cloud.")
|
|
src.add_argument("--stp", type=Path, help="Path to a STEP file (research mode).")
|
|
src.add_argument("--sweep", type=Path, help="Path to a sweep matrix YAML.")
|
|
|
|
p.add_argument("--out-dir", type=Path, required=True, help="Output directory.")
|
|
p.add_argument("--config", type=Path, default=None, help="YAML config overrides.")
|
|
p.add_argument(
|
|
"--profile",
|
|
type=str,
|
|
default="auto",
|
|
help="auto | default | high_density | low_density | custom",
|
|
)
|
|
p.add_argument(
|
|
"--target-holes",
|
|
type=Path,
|
|
default=None,
|
|
help="DEPRECATED — the main pipeline no longer performs 4-hole Kabsch "
|
|
"registration (it cancels the panel deflection signal). The flag "
|
|
"is accepted for backward compatibility but is ignored.",
|
|
)
|
|
p.add_argument(
|
|
"--render",
|
|
action="store_true",
|
|
help="After measurement, render PNG diagnostics into <out-dir>/images/.",
|
|
)
|
|
p.add_argument("-v", "--verbose", action="count", default=0, help="-v: INFO, -vv: DEBUG")
|
|
return p
|
|
|
|
|
|
def _setup_logging(verbose: int) -> None:
|
|
level = logging.WARNING
|
|
if verbose == 1:
|
|
level = logging.INFO
|
|
elif verbose >= 2:
|
|
level = logging.DEBUG
|
|
logging.basicConfig(
|
|
level=level,
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
)
|
|
|
|
|
|
def _on_progress(stage: str, fraction: float) -> None:
|
|
log.info("[%5.1f%%] %s", fraction * 100, stage)
|
|
|
|
|
|
def _maybe_render(out_dir: Path) -> None:
|
|
try:
|
|
from .viz import render_all
|
|
|
|
render_all(out_dir)
|
|
except ImportError:
|
|
log.warning("matplotlib not installed; skip --render")
|
|
except Exception as exc:
|
|
log.warning("render_all failed: %s", exc)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = _build_parser().parse_args(argv)
|
|
_setup_logging(args.verbose)
|
|
|
|
try:
|
|
if args.input_cloud is not None:
|
|
from .pipeline import run_coupler_measurement_pipeline
|
|
|
|
if args.target_holes is not None:
|
|
log.warning(
|
|
"--target-holes is deprecated and ignored; the main pipeline "
|
|
"no longer performs Kabsch registration."
|
|
)
|
|
result = run_coupler_measurement_pipeline(
|
|
input_cloud=args.input_cloud,
|
|
out_dir=args.out_dir,
|
|
config_path=args.config,
|
|
profile=args.profile,
|
|
progress=_on_progress,
|
|
)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"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,
|
|
"quality_flags": result.quality_flags,
|
|
},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
|
|
elif args.stp is not None:
|
|
from .pipeline import run_from_cad_pipeline
|
|
|
|
summary = run_from_cad_pipeline(
|
|
stp_path=args.stp,
|
|
out_dir=args.out_dir,
|
|
config_path=args.config,
|
|
profile=args.profile,
|
|
progress=_on_progress,
|
|
)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"diff": summary["diff"],
|
|
"quality_flags": summary["measured"]["quality_flags"],
|
|
},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
|
|
elif args.sweep is not None:
|
|
from importlib import import_module
|
|
|
|
mod = import_module("experiments.sweep_runner")
|
|
mod.main(matrix_path=args.sweep, out_dir=args.out_dir, config_path=args.config)
|
|
|
|
if args.render:
|
|
_maybe_render(args.out_dir)
|
|
|
|
except Exception as exc:
|
|
log.exception("pipeline failed: %s", exc)
|
|
return 2
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|