Files
massive-cloud/server/plugins/adapters/train_measurement_run.py
lipan 29f1d45d66 feat: 算法升级、整车配准集成与性能分析文档
算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3
适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新
后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动
前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复
文档: 新增5份算法性能分析报告
2026-06-25 08:22:20 +00:00

207 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""train-measurement 算法适配器 (v2.2)。
读 input.json → 调 TaskManager(来自 alg/train_measurement_system_v2.2)→ 写统一 result.json。
v2.2 变更:
- 算法路径从 alg/点云测距v1.1 → alg/train_measurement_system_v2.2
- 新增孔中心拟合(hole_center_fitter)、测量日志、性能日志
- 内置配置文件改为 config/measurements_0618.yaml
- TRAIN_MEAS_ROOT 指向 train_measurement_system_v2.2/(用于定位 config/)
- 算法内置 _read_binary_ply,直接支持二进制 PLY,无需 ASCII 转换
约定:
- 第一个点云路径作为 pointcloud_file 注入到测量配置 YAML 中;
- 若用户在 UI 选择「自定义 YAML」并提供 customYaml,则使用之;
否则使用算法仓库自带的 config/measurements_0618.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,
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 v2.2 算法包")
# TRAIN_MEAS_ROOT 由 manifest envExtra 注入,指向 train_measurement_system_v2.2/
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 / "train_measurement_system_v2.2" / "train_measurement_system_v2.2"
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)}")
# ── 准备 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_0618.yaml
yaml_path = train_root / "config" / "measurements_0618.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
# 把输出目录重定向到 outDir,避免写到只读的 /alg 目录
tm.output_dir_override = str(out_dir)
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())