"""train-measurement 算法适配器 (v1.1)。 读 input.json → 调 TaskManager(来自 alg/点云测距v1.1)→ 写统一 result.json。 v1.1 变更: - 路径从 260505列车点云测距v1 → 点云测距v1.1 - 算法包现在是可安装的 Python 包(有 __init__.py),PYTHONPATH 指向 src/ - TRAIN_MEAS_ROOT 指向 train_measurement_system/(用于定位 config/measurements.yaml) - TaskManager.run_all_tasks() 返回结构不变,data 字段含 mean_distance / std_distance 等 约定: - 第一个点云路径作为 pointcloud_file 注入到测量配置 YAML 中; - 若用户在 UI 选择「自定义 YAML」并提供 customYaml,则使用之; 否则使用算法仓库自带的 config/measurements.yaml 作为模板。 - 所有任务结果合成成 measurements[] 列表,附带可视化 PLY 路径到 extra。 """ from __future__ import annotations import os import sys import traceback from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from _plugin_io import ( emit_progress, ensure_ascii_ply, first_pointcloud, load_input, write_error_result, write_result, ) def main() -> int: payload = load_input(sys.argv) inputs = payload.get("inputs") or {} config = inputs.get("config") or {} out_dir = Path(payload["outDir"]) out_dir.mkdir(parents=True, exist_ok=True) try: emit_progress(0.05, "加载 train_measurement v1.1 算法包") # TRAIN_MEAS_ROOT 由 manifest envExtra 注入,指向 train_measurement_system/ train_root = Path(os.environ.get("TRAIN_MEAS_ROOT") or "") if not train_root.exists(): # 回退:从 ALG_ROOT 推导 alg_root = Path(os.environ.get("ALG_ROOT") or "/alg") train_root = alg_root / "点云测距v1.1" / "train_measurement_system" if not train_root.exists(): raise RuntimeError( f"找不到 train_measurement_system 目录:{train_root}。" "请确认 TRAIN_MEAS_ROOT 或 ALG_ROOT 环境变量已正确设置。" ) from task_manager import TaskManager # type: ignore pc_path = first_pointcloud(payload) emit_progress(0.15, f"读取点云:{os.path.basename(pc_path)}") # 若是二进制 PLY,先转成 ASCII PLY(算法内置 reader 不依赖 open3d) pc_path = ensure_ascii_ply(pc_path, out_dir / "_ply_cache") # ── 准备 YAML 配置 ──────────────────────────────────────────────────── config_preset = str(config.get("configPreset") or "builtin") custom_yaml_str = str(config.get("customYaml") or "").strip() if config_preset == "custom" and custom_yaml_str: # 支持 JSON 格式(前端传来)和 YAML 格式 yaml_path = out_dir / "custom_measurements.yaml" try: import json as _json parsed = _json.loads(custom_yaml_str) # JSON 格式:转成 YAML 写盘 import yaml as _yaml yaml_path.write_text(_yaml.dump(parsed, allow_unicode=True, default_flow_style=False), encoding="utf-8") except Exception: # 已经是 YAML 格式,直接写盘 yaml_path.write_text(custom_yaml_str, encoding="utf-8") else: # 使用算法仓库内置的 config/measurements.yaml yaml_path = train_root / "config" / "measurements.yaml" if not yaml_path.exists(): raise RuntimeError(f"内置配置文件不存在:{yaml_path}") emit_progress(0.20, f"加载配置:{yaml_path.name}") # ── 初始化 TaskManager ──────────────────────────────────────────────── tm = TaskManager() # 先注入点云路径,让 load_config 跳过 YAML 里的 pointcloud_file(开发测试路径) tm.pointcloud_path_override = pc_path if not tm.load_config(str(yaml_path)): raise RuntimeError(f"TaskManager 加载配置失败:{yaml_path}") # 注入点云路径(覆盖 YAML 中的 pointcloud_file) tm.config["pointcloud_file"] = pc_path # 注入可视化输出目录 viz_dir = out_dir / "visualizations" viz_dir.mkdir(parents=True, exist_ok=True) for task in tm.tasks: if hasattr(task, "visualization_dir"): task.visualization_dir = str(viz_dir) emit_progress(0.30, "加载点云(已在 load_config 中完成)") emit_progress(0.40, f"执行 {len(tm.tasks)} 个测量任务") results = tm.run_all_tasks() emit_progress(0.92, "整理结果") measurements: list[dict] = [] clean_raw: list[dict] = [] for r in results: name = str(r.get("name") or "未命名任务") status_raw = r.get("status") data = r.get("data") if isinstance(r.get("data"), dict) else {} # 剥掉以 '_' 开头的私有 numpy 数组(不可 JSON 序列化) clean_data = {k: v for k, v in data.items() if not k.startswith("_")} if data else {} clean_raw.append( { "name": name, "type": r.get("type"), "status": status_raw, "data": clean_data, "error": r.get("error"), } ) if status_raw == "success": value = clean_data.get("mean_distance") if value is None: value = clean_data.get("distance") measurements.append( { "name": name, "value": float(value) if isinstance(value, (int, float)) else float("nan"), "unit": "mm", "status": "info", } ) std = clean_data.get("std_distance") if isinstance(std, (int, float)): measurements.append( { "name": f"{name} · 标准差", "value": float(std), "unit": "mm", "status": "info", } ) # v1.1 新增字段:min/max distance for field_name, label in [ ("min_distance", "最小距离"), ("max_distance", "最大距离"), ]: v = clean_data.get(field_name) if isinstance(v, (int, float)): measurements.append( { "name": f"{name} · {label}", "value": float(v), "unit": "mm", "status": "info", } ) else: measurements.append( { "name": name, "value": float("nan"), "unit": "mm", "status": "fail", } ) success_count = sum(1 for r in results if r.get("status") == "success") payload_out = { "schemaVersion": 1, "status": "completed", "message": f"共 {len(results)} 个任务,成功 {success_count} 个。", "measurements": measurements, "overlay3d": {"kind": "none"}, "extra": { "rawTaskResults": clean_raw, "visualizationDir": "visualizations", }, } write_result(out_dir, payload_out) emit_progress(1.0, "完成") return 0 except Exception as exc: traceback.print_exc() write_error_result( out_dir, f"train-measurement 计算失败:{exc}", extra={"traceback": traceback.format_exc()}, ) return 1 if __name__ == "__main__": sys.exit(main())