feat: update algorithm integrations and point cloud overlays
This commit is contained in:
@@ -5,14 +5,14 @@
|
||||
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/)
|
||||
- 内置配置文件改为 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_0618.yaml 作为模板。
|
||||
否则使用算法仓库自带的 config/measurements.yaml(v3)作为模板。
|
||||
- 所有任务结果合成成 measurements[] 列表,附带可视化 PLY 路径到 extra。
|
||||
"""
|
||||
|
||||
@@ -33,6 +33,57 @@ from _plugin_io import (
|
||||
)
|
||||
|
||||
|
||||
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 {}
|
||||
@@ -41,14 +92,13 @@ def main() -> int:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
emit_progress(0.05, "加载 train_measurement v2.2 算法包")
|
||||
emit_progress(0.05, "加载 train_measurement v3.2 算法包")
|
||||
|
||||
# TRAIN_MEAS_ROOT 由 manifest envExtra 注入,指向 train_measurement_system_v2.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 推导
|
||||
alg_root = Path(os.environ.get("ALG_ROOT") or "/alg")
|
||||
train_root = alg_root / "train_measurement_system_v2.2" / "train_measurement_system_v2.2"
|
||||
train_root = alg_root / "train_measurement_system_v3.2_0629"
|
||||
|
||||
if not train_root.exists():
|
||||
raise RuntimeError(
|
||||
@@ -78,8 +128,8 @@ def main() -> int:
|
||||
# 已经是 YAML 格式,直接写盘
|
||||
yaml_path.write_text(custom_yaml_str, encoding="utf-8")
|
||||
else:
|
||||
# 使用算法仓库内置的 config/measurements_0618.yaml
|
||||
yaml_path = train_root / "config" / "measurements_0618.yaml"
|
||||
# 使用算法仓库内置的 config/measurements.yaml(v3)
|
||||
yaml_path = train_root / "config" / "measurements.yaml"
|
||||
if not yaml_path.exists():
|
||||
raise RuntimeError(f"内置配置文件不存在:{yaml_path}")
|
||||
|
||||
@@ -87,16 +137,25 @@ def main() -> int:
|
||||
|
||||
# ── 初始化 TaskManager ────────────────────────────────────────────────
|
||||
tm = TaskManager()
|
||||
# 先注入点云路径,让 load_config 跳过 YAML 里的 pointcloud_file(开发测试路径)
|
||||
tm.pointcloud_path_override = pc_path
|
||||
# 把输出目录重定向到 outDir,避免写到只读的 /alg 目录
|
||||
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)
|
||||
@@ -104,10 +163,37 @@ def main() -> int:
|
||||
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)} 个测量任务")
|
||||
|
||||
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] = []
|
||||
@@ -133,39 +219,41 @@ def main() -> int:
|
||||
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": float(value) if isinstance(value, (int, float)) else float("nan"),
|
||||
"value": fval if fval is not None else float("nan"),
|
||||
"unit": "mm",
|
||||
"status": "info",
|
||||
"tolerance": tolerance,
|
||||
"status": meas_status,
|
||||
"passLabel": "通过",
|
||||
"failLabel": "不通过",
|
||||
}
|
||||
)
|
||||
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(
|
||||
{
|
||||
@@ -176,6 +264,17 @@ def main() -> int:
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user