Files
massive-cloud/server/plugins/adapters/train_measurement_run.py

306 lines
12 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.yaml(v3.2)
- TRAIN_MEAS_ROOT 指向 train_measurement_system_v3.2_0629/(用于定位 config/)
- 算法内置 _read_binary_ply,直接支持二进制 PLY,无需 ASCII 转换
约定:
- 第一个点云路径作为 pointcloud_file 注入到测量配置 YAML 中;
- 若用户在 UI 选择「自定义 YAML」并提供 customYaml,则使用之;
否则使用算法仓库自带的 config/measurements.yaml(v3)作为模板。
- 所有任务结果合成成 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 _to_float(value):
if value is None:
return None
if isinstance(value, (int, float)):
return float(value)
try:
return float(str(value).strip())
except (TypeError, ValueError):
return None
def _load_excel_measurement_rows(out_dir: Path) -> dict[str, dict]:
"""读取算法导出的 Excel 结论,作为前端 measurements 的权威来源。"""
try:
from openpyxl import load_workbook
excel_files = sorted(
out_dir.glob("measurement_results_*.xlsx"),
key=lambda p: p.stat().st_mtime,
)
if not excel_files:
return {}
wb = load_workbook(excel_files[-1], data_only=True, read_only=True)
ws = wb.active
rows: dict[str, dict] = {}
for row in ws.iter_rows(min_row=2, values_only=True):
name = str(row[0] or "").strip()
if not name:
continue
conclusion = str(row[6] or "").strip()
status = None
if conclusion == "通过":
status = "pass"
elif conclusion in ("不通过", "失败"):
status = "fail"
elif conclusion == "未配置":
status = "info"
rows[name] = {
"value": _to_float(row[3]),
"toleranceLabel": str(row[2]) if row[2] is not None else None,
"status": status,
}
return rows
except Exception as exc:
print(f"读取 Excel 结论失败,继续使用适配器判定:{exc}", file=sys.stderr)
return {}
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 v3.2 算法包")
# TRAIN_MEAS_ROOT 由 manifest envExtra 注入,指向 train_measurement_system_v3.2_0629/
train_root = Path(os.environ.get("TRAIN_MEAS_ROOT") or "")
if not train_root.exists():
alg_root = Path(os.environ.get("ALG_ROOT") or "/alg")
train_root = alg_root / "train_measurement_system_v3.2_0629"
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.yaml(v3)
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()
tm.pointcloud_path_override = pc_path
tm.output_dir_override = str(out_dir)
emit_progress(0.25, f"读取点云({os.path.basename(pc_path)},可能需要 1~2 分钟)")
if not tm.load_config(str(yaml_path)):
raise RuntimeError(f"TaskManager 加载配置失败:{yaml_path}")
# 注入点云路径(覆盖 YAML 中的 pointcloud_file)
tm.config["pointcloud_file"] = pc_path
# 过滤 enabled=False 的任务(v3 TaskManager 不处理 enabled 字段)
import yaml as _yaml_check
_raw = _yaml_check.safe_load(yaml_path.read_text(encoding="utf-8"))
_enabled_names = {
t["name"] for t in (_raw.get("tasks") or _raw.get("measurements") or [])
if t.get("enabled", True)
}
tm.tasks = [t for t in tm.tasks if t.name in _enabled_names]
# 注入可视化输出目录
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.40, f"点云已加载,开始执行 {len(tm.tasks)} 个测量任务")
# ── 从 YAML 构建公差映射(name → {theoretical, upper, lower}) ─────────
import yaml as _yaml
try:
raw_yaml = _yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
# v3 用 'tasks' key,v2 用 'measurements' key,兼容两者
task_cfgs = raw_yaml.get("tasks") or raw_yaml.get("measurements") or []
tol_map: dict[str, dict] = {}
for tc in task_cfgs:
tname = tc.get("name")
if not tname:
continue
tol = tc.get("tolerance")
theoretical = tc.get("theoretical_dimension")
if isinstance(tol, dict):
tol_map[tname] = {
"theoretical": theoretical,
"upper": float(tol.get("upper", 0)),
"lower": float(tol.get("lower", 0)),
}
elif tol is not None:
tol_map[tname] = {
"theoretical": theoretical,
"upper": float(tol),
"lower": float(tol),
}
except Exception:
tol_map = {}
results = tm.run_all_tasks()
report_rows = _load_excel_measurement_rows(out_dir)
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")
fval = float(value) if isinstance(value, (int, float)) else None
# v3.2 data 里直接有 is_qualified 判定结果(line_to_plane/line_to_line 也支持)
data_pass = clean_data.get("is_qualified")
if data_pass is None:
data_pass = clean_data.get("pass") # 兼容旧字段名
tol_cfg = tol_map.get(name)
tolerance = None
meas_status = "info"
if data_pass is not None:
# 算法已判定,直接用
meas_status = "pass" if data_pass else "fail"
if tol_cfg:
tolerance = tol_cfg.get("upper")
elif tol_cfg and fval is not None:
theoretical = tol_cfg.get("theoretical")
upper = tol_cfg["upper"]
lower = tol_cfg["lower"]
tolerance = upper
if theoretical is not None:
error = fval - float(theoretical)
meas_status = "pass" if (-lower <= error <= upper) else "fail"
measurements.append(
{
"name": name,
"value": fval if fval is not None else float("nan"),
"unit": "mm",
"tolerance": tolerance,
"status": meas_status,
"passLabel": "通过",
"failLabel": "不通过",
}
)
else:
measurements.append(
{
"name": name,
"value": float("nan"),
"unit": "mm",
"status": "fail",
}
)
for measurement in measurements:
report_row = report_rows.get(measurement["name"])
if not report_row:
continue
if report_row.get("value") is not None:
measurement["value"] = report_row["value"]
if report_row.get("toleranceLabel"):
measurement["toleranceLabel"] = report_row["toleranceLabel"]
if report_row.get("status"):
measurement["status"] = report_row["status"]
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())