feat: 算法升级、整车配准集成与性能分析文档
算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3 适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新 后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动 前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复 文档: 新增5份算法性能分析报告
This commit is contained in:
@@ -123,6 +123,11 @@ async function startServer() {
|
||||
// Configure body parser with larger size limit for file uploads
|
||||
app.use(express.json({ limit: "50mb" }));
|
||||
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
||||
|
||||
// 文件上传路由单独设置超长超时(50 GB 点云,按 100 MB/s 约需 8 分钟,留 60 分钟余量)
|
||||
server.timeout = 0; // 禁用 Node http.Server 全局超时
|
||||
server.keepAliveTimeout = 0; // 长连接不超时
|
||||
server.headersTimeout = 0; // 读请求头不超时
|
||||
// OAuth callback under /api/oauth/callback
|
||||
registerOAuthRoutes(app);
|
||||
// 禁止代理(NPS 等)缓存 API 响应
|
||||
|
||||
@@ -35,7 +35,7 @@ class ConcurrencyPool {
|
||||
private running = 0;
|
||||
private queue: PendingJob[] = [];
|
||||
private readonly limit: number;
|
||||
private readonly activeChildren = new Set<ChildProcess>();
|
||||
private readonly activeChildren = new Map<number, ChildProcess>(); // resultId → child
|
||||
|
||||
constructor(limit: number) {
|
||||
this.limit = Math.max(1, limit);
|
||||
@@ -52,6 +52,49 @@ class ConcurrencyPool {
|
||||
});
|
||||
}
|
||||
|
||||
/** 取消指定 resultId 的 job:若在队列中直接移除;若已在运行则 kill 子进程。 */
|
||||
cancel(resultId: number): boolean {
|
||||
// 先从队列里找(还未开始执行)
|
||||
const idx = this.queue.findIndex((j) => j.resultId === resultId);
|
||||
if (idx !== -1) {
|
||||
const [job] = this.queue.splice(idx, 1);
|
||||
job.resolve(); // 让等待方正常退出
|
||||
return true;
|
||||
}
|
||||
// 再找正在运行的子进程
|
||||
const child = this.activeChildren.get(resultId);
|
||||
if (child) {
|
||||
child.kill('SIGKILL');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 取消某个 taskId 下所有 job(删除任务时用)。返回被取消的 resultId 数组。 */
|
||||
cancelByTask(taskId: number): number[] {
|
||||
const cancelled: number[] = [];
|
||||
// 队列中移除
|
||||
const keep: PendingJob[] = [];
|
||||
for (const job of this.queue) {
|
||||
if (job.taskId === taskId) {
|
||||
job.resolve();
|
||||
cancelled.push(job.resultId);
|
||||
} else {
|
||||
keep.push(job);
|
||||
}
|
||||
}
|
||||
this.queue = keep;
|
||||
// 运行中 kill
|
||||
for (const [resultId, child] of this.activeChildren) {
|
||||
const jobTaskId = (child as ChildProcess & { _taskId?: number })._taskId;
|
||||
if (jobTaskId === taskId) {
|
||||
child.kill('SIGKILL');
|
||||
cancelled.push(resultId);
|
||||
}
|
||||
}
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
private pump(): void {
|
||||
while (this.running < this.limit && this.queue.length > 0) {
|
||||
const next = this.queue.shift()!;
|
||||
@@ -79,14 +122,16 @@ class ConcurrencyPool {
|
||||
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
||||
execArgv: isProd ? [] : ['--import', 'tsx'],
|
||||
});
|
||||
this.activeChildren.add(child);
|
||||
// 把 taskId 附到 child 上,cancelByTask 用
|
||||
(child as ChildProcess & { _taskId?: number })._taskId = job.taskId;
|
||||
this.activeChildren.set(job.resultId, child);
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
child.on('error', (err) => {
|
||||
console.error('[AlgorithmJob] fork 失败:', err);
|
||||
});
|
||||
child.on('exit', (code, signal) => {
|
||||
this.activeChildren.delete(child);
|
||||
this.activeChildren.delete(job.resultId);
|
||||
if (code !== 0 && code != null) {
|
||||
console.error(
|
||||
`[AlgorithmJob] 子进程退出 code=${code} signal=${signal ?? ''} pluginId=${job.pluginId} resultId=${job.resultId}`,
|
||||
@@ -128,3 +173,13 @@ export function enqueueAlgorithmJob(
|
||||
export function algorithmJobStats() {
|
||||
return pool.stats;
|
||||
}
|
||||
|
||||
/** 取消单个分析结果对应的 job(删除分析结果时调用)。 */
|
||||
export function cancelAlgorithmJob(resultId: number): boolean {
|
||||
return pool.cancel(resultId);
|
||||
}
|
||||
|
||||
/** 取消某检测任务下所有 job(删除任务时调用)。 */
|
||||
export function cancelAlgorithmJobsByTask(taskId: number): number[] {
|
||||
return pool.cancelByTask(taskId);
|
||||
}
|
||||
|
||||
@@ -321,6 +321,7 @@ export async function runAlgorithmJob(
|
||||
errorStats: result?.errorStats,
|
||||
overlay3d: result?.overlay3d,
|
||||
images: result?.images,
|
||||
files: result?.files,
|
||||
extra: result?.extra,
|
||||
error: finalStatus === 'error'
|
||||
? result?.message ?? `子进程退出码 ${exitCode}`
|
||||
@@ -334,6 +335,35 @@ export async function runAlgorithmJob(
|
||||
resultData,
|
||||
});
|
||||
|
||||
// ── 回写配准产出点云 ─────────────────────────────────────────────────────
|
||||
// 若 result.json 包含 derivedPointClouds,把对应 PLY 注册为任务点云,
|
||||
// 供后续测量算法直接选用,无需用户手动上传。
|
||||
if (finalStatus === 'completed') {
|
||||
const derived = (result as Record<string, unknown> | null)?.derivedPointClouds;
|
||||
if (Array.isArray(derived) && derived.length > 0) {
|
||||
for (const item of derived) {
|
||||
const d = item as { file?: string; name?: string };
|
||||
if (!d.file) continue;
|
||||
const absPath = path.join(workDir, d.file);
|
||||
if (!fs.existsSync(absPath)) {
|
||||
console.warn(`[AlgorithmJobRunner] derivedPointCloud not found: ${absPath}`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await db.createRegisteredPointcloudFile(
|
||||
taskId,
|
||||
d.name ?? d.file,
|
||||
absPath,
|
||||
resultId,
|
||||
);
|
||||
console.log(`[AlgorithmJobRunner] registered derived pointcloud: ${d.name ?? d.file}`);
|
||||
} catch (err) {
|
||||
console.error('[AlgorithmJobRunner] failed to register derived pointcloud:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (finalStatus === 'completed') {
|
||||
console.log(
|
||||
`[AlgorithmJobRunner] ok pluginId=${pluginId} resultId=${resultId} taskId=${taskId}`,
|
||||
|
||||
21
server/db.ts
21
server/db.ts
@@ -307,6 +307,27 @@ export async function createTaskPointcloudFile(data: InsertTaskPointcloudFile):
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 把配准算法产出的点云注册为任务点云(source='registered')。
|
||||
* filePath 必须是绝对路径,文件必须已存在于磁盘。
|
||||
*/
|
||||
export async function createRegisteredPointcloudFile(
|
||||
taskId: number,
|
||||
name: string,
|
||||
filePath: string,
|
||||
sourceResultId: number,
|
||||
): Promise<TaskPointcloudFile> {
|
||||
const fs = await import('node:fs');
|
||||
const fileSize = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
|
||||
return createTaskPointcloudFile({
|
||||
taskId,
|
||||
name,
|
||||
filePath,
|
||||
fileSize,
|
||||
source: 'registered',
|
||||
sourceResultId,
|
||||
} as InsertTaskPointcloudFile);
|
||||
}
|
||||
|
||||
export async function listTaskPointcloudFiles(taskId: number): Promise<TaskPointcloudFile[]> {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
@@ -43,7 +43,7 @@ const diskStorage = multer.diskStorage({
|
||||
|
||||
const diskUpload = multer({
|
||||
storage: diskStorage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 * 1024 }, // 5 GB
|
||||
limits: { fileSize: 50 * 1024 * 1024 * 1024 }, // 50 GB
|
||||
});
|
||||
|
||||
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -43,19 +43,49 @@ def emit_progress(percent: float, message: str = "") -> None:
|
||||
def write_result(out_dir: str | os.PathLike, payload: dict[str, Any]) -> Path:
|
||||
"""写 result.json 到 outDir,并强制 schemaVersion / status 字段存在。
|
||||
NaN/Infinity 会被替换为 null,确保输出合法 JSON。
|
||||
|
||||
自动发现 outDir 下的 Excel 产物(*.xlsx / *.xls),以相对路径写入
|
||||
payload["files"],前端据此展示下载入口(适配器也可自行预置 files)。
|
||||
"""
|
||||
import math
|
||||
|
||||
def _sanitize(obj: Any) -> Any:
|
||||
# numpy 标量 → Python 原生类型
|
||||
try:
|
||||
import numpy as np # type: ignore
|
||||
if isinstance(obj, np.ndarray):
|
||||
return _sanitize(obj.tolist())
|
||||
if isinstance(obj, (np.floating,)):
|
||||
f = float(obj)
|
||||
return None if (math.isnan(f) or math.isinf(f)) else f
|
||||
if isinstance(obj, (np.integer,)):
|
||||
return int(obj)
|
||||
if isinstance(obj, (np.bool_,)):
|
||||
return bool(obj)
|
||||
except ImportError:
|
||||
pass
|
||||
if isinstance(obj, float) and (math.isnan(obj) or math.isinf(obj)):
|
||||
return None
|
||||
if isinstance(obj, dict):
|
||||
return {k: _sanitize(v) for k, v in obj.items()}
|
||||
if isinstance(obj, list):
|
||||
return [_sanitize(v) for v in obj]
|
||||
if isinstance(obj, tuple):
|
||||
return [_sanitize(v) for v in obj]
|
||||
return obj
|
||||
|
||||
out_path = Path(out_dir) / "result.json"
|
||||
out_dir = Path(out_dir)
|
||||
|
||||
excel_files: list[str] = list(payload.get("files") or [])
|
||||
for pattern in ("*.xlsx", "*.xls"):
|
||||
for f in sorted(out_dir.rglob(pattern)):
|
||||
rel = str(f.relative_to(out_dir))
|
||||
if rel not in excel_files:
|
||||
excel_files.append(rel)
|
||||
if excel_files:
|
||||
payload["files"] = excel_files
|
||||
|
||||
out_path = out_dir / "result.json"
|
||||
payload.setdefault("schemaVersion", 1)
|
||||
payload.setdefault("status", "completed")
|
||||
out_path.write_text(json.dumps(_sanitize(payload), ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
"""coupler-panel 算法适配器 (v1.2)。
|
||||
"""coupler-panel 算法适配器 (v1.2.1)。
|
||||
|
||||
v1.2.1 变更:
|
||||
- 算法包升级至 delivery_v1.2.1
|
||||
- 新增项点 Excel 报表(write_inspection_report):
|
||||
项点名称/理论尺寸/公差要求/测量结果/误差结果/测量结论
|
||||
- PYTHONPATH 指向 delivery_v1.2.1/coupler_panel_release
|
||||
|
||||
v1.2 变更:
|
||||
- 算法包升级至 delivery_v1.2(新增 autotune 自动调参与 layer_select 分层选点)
|
||||
- PYTHONPATH 指向 delivery_v1.2/coupler_panel_release
|
||||
- 新增 autotune 自动调参与 layer_select 分层选点
|
||||
|
||||
v1.1 变更:
|
||||
- 入口从 compute_coupler_measurement → run_coupler_measurement_pipeline
|
||||
@@ -20,7 +25,6 @@ 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,
|
||||
@@ -46,23 +50,45 @@ def main() -> int:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
emit_progress(0.05, "加载 coupler_panel v1.2 算法包")
|
||||
emit_progress(0.05, "加载 coupler_panel v1.2.1 算法包")
|
||||
from coupler_panel.pipeline import run_coupler_measurement_pipeline # 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")
|
||||
|
||||
profile = str(config.get("profile") or "auto")
|
||||
right_axis_sign = int(config.get("rightAxisSign") or 1)
|
||||
|
||||
# 可选 CAD STP 路径(前端传入 cadPath 时使用)
|
||||
cad_stp_path = inputs.get("cadPath") or None
|
||||
|
||||
# 可选 ROI JSON 路径(前端传入 roiJson 时使用)
|
||||
# ROI 裁剪:优先用 roiJson 文件,其次用前端填的 6 个坐标参数
|
||||
roi_json = inputs.get("roiJson") or None
|
||||
roi_spec = None
|
||||
if not roi_json:
|
||||
def _f(key: str) -> float | None:
|
||||
v = config.get(key)
|
||||
try:
|
||||
return float(v) if v is not None and str(v).strip() != "" else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
x_min, x_max = _f("roiXMin"), _f("roiXMax")
|
||||
y_min, y_max = _f("roiYMin"), _f("roiYMax")
|
||||
z_min, z_max = _f("roiZMin"), _f("roiZMax")
|
||||
# 至少有一对轴的范围被填写时才构建 ROI
|
||||
if any(v is not None for v in (x_min, x_max, y_min, y_max, z_min, z_max)):
|
||||
from coupler_panel.roi import ROISpec # type: ignore
|
||||
|
||||
roi_spec = ROISpec(
|
||||
x_min=x_min if x_min is not None else float("-inf"),
|
||||
x_max=x_max if x_max is not None else float("inf"),
|
||||
y_min=y_min if y_min is not None else float("-inf"),
|
||||
y_max=y_max if y_max is not None else float("inf"),
|
||||
z_min=z_min if z_min is not None else float("-inf"),
|
||||
z_max=z_max if z_max is not None else float("inf"),
|
||||
)
|
||||
emit_progress(0.20, f"ROI 裁剪:x[{x_min},{x_max}] y[{y_min},{y_max}] z[{z_min},{z_max}]")
|
||||
|
||||
emit_progress(0.25, "调用 run_coupler_measurement_pipeline")
|
||||
|
||||
@@ -74,7 +100,8 @@ def main() -> int:
|
||||
out_dir=str(out_dir),
|
||||
profile=profile,
|
||||
cad_stp_path=cad_stp_path,
|
||||
roi_json=roi_json,
|
||||
roi_spec=roi_spec, # 优先用坐标参数构建的 ROISpec
|
||||
roi_json=roi_json, # 备选:直接传 ROI JSON 文件
|
||||
progress=_on_progress,
|
||||
)
|
||||
|
||||
@@ -83,6 +110,41 @@ def main() -> int:
|
||||
# right_axis_sign 影响 theta_right_deg 的符号约定
|
||||
theta_right = float(result.theta_right_deg) * right_axis_sign
|
||||
theta_ref = float(result.theta_ref_deg)
|
||||
|
||||
# ── 项点 Excel 报表(v1.2.1 新增)────────────────────────────────────
|
||||
# 用未截断的拟合平面法向量算原始角度(避免微小角被钳为 0),
|
||||
# 公差按斜率形式给出:参考面 0.8/375mm,纵向中心 1/700mm。
|
||||
# 产物 xlsx 由 write_result 自动发现并写入 files 字段。
|
||||
try:
|
||||
import math
|
||||
|
||||
from coupler_panel.reporting import InspectionItem, write_inspection_report # type: ignore
|
||||
|
||||
raw_right, raw_ref = theta_right, theta_ref
|
||||
report_dict = result.report if isinstance(result.report, dict) else {}
|
||||
normal = (report_dict.get("plane") or {}).get("normal")
|
||||
if normal and len(normal) == 3:
|
||||
nx, ny, nz = (float(v) for v in normal)
|
||||
raw_right = math.degrees(math.atan2(ny, nx)) * right_axis_sign
|
||||
raw_ref = math.degrees(math.atan2(nz, nx))
|
||||
|
||||
item_ref = InspectionItem(
|
||||
name="车钩面板距参考面的右偏转角(头车一位端处车钩)",
|
||||
theta_deg=raw_ref,
|
||||
baseline_mm=375.0,
|
||||
tol_value_mm=0.8,
|
||||
)
|
||||
item_long = InspectionItem(
|
||||
name="车钩面板距纵向中心的右偏转角(头车一位端处车钩)",
|
||||
theta_deg=raw_right,
|
||||
baseline_mm=700.0,
|
||||
tol_value_mm=1.0,
|
||||
)
|
||||
write_inspection_report(item_ref, out_dir / "report_ref_plane.xlsx")
|
||||
write_inspection_report(item_long, out_dir / "report_longitudinal.xlsx")
|
||||
except Exception:
|
||||
# Excel 报表失败不影响主结果
|
||||
traceback.print_exc()
|
||||
dx_y = float(result.dx_y_max_mm)
|
||||
dx_z = float(result.dx_z_max_mm)
|
||||
D_y = float(result.D_y_max_mm)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""flatness-suite 算法适配器 (v1.2)。
|
||||
"""flatness-suite 算法适配器 (v1.5)。
|
||||
|
||||
v1.2 变更:
|
||||
- 算法目录从 alg/平整度、平面度、轮廓测量/ → alg/point_porj/
|
||||
- 入口从独立脚本(importlib 加载)→ MeasurementPipeline 类
|
||||
- run_plane / run_curvature / run_contour 统一通过 MeasurementPipeline 调用
|
||||
- 点云路径通过 config["paths"]["pointcloud"] 注入,不再依赖 open3d 直接读取
|
||||
v1.5 变更:
|
||||
- 算法目录从 alg/point_porj → alg/平整度、平面度、轮廓测量v1.5
|
||||
- 新增 car_part_v1/ 多车型 STEP CAD 模板
|
||||
- run_contour 新增 Excel 报表输出(excel_output_path)
|
||||
- pipeline 新增 result_template_path / combined_context_pointcloud_path 参数
|
||||
|
||||
约定:
|
||||
- module=all:plane + curvature(contour 需要 CAD STL,缺失时跳过)
|
||||
@@ -25,7 +25,6 @@ from typing import Any
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from _plugin_io import (
|
||||
emit_progress,
|
||||
ensure_ascii_ply,
|
||||
first_pointcloud,
|
||||
load_input,
|
||||
write_error_result,
|
||||
@@ -41,8 +40,9 @@ def _safe_float(v: object) -> float | None:
|
||||
|
||||
|
||||
def _find_alg_dir(alg_root: str) -> Path:
|
||||
"""定位 point_porj 目录,支持多种可能的路径。"""
|
||||
"""定位平整度算法目录,支持多个历史版本路径,优先使用最新版。"""
|
||||
candidates = [
|
||||
Path(alg_root) / "平整度、平面度、轮廓测量v1.5",
|
||||
Path(alg_root) / "point_porj",
|
||||
Path(alg_root) / "平整度、平面度、轮廓测量v1.2" / "point_porj",
|
||||
Path(alg_root) / "平整度、平面度、轮廓测量" / "point_porj",
|
||||
@@ -51,9 +51,9 @@ def _find_alg_dir(alg_root: str) -> Path:
|
||||
if c.is_dir() and (c / "pipeline.py").exists():
|
||||
return c
|
||||
raise RuntimeError(
|
||||
f"找不到 point_porj 算法目录,已尝试:\n"
|
||||
f"找不到平整度算法目录,已尝试:\n"
|
||||
+ "\n".join(f" {c}" for c in candidates)
|
||||
+ "\n请确认 alg/ 下已解压 point_porj/。"
|
||||
+ "\n请确认 alg/ 下已解压 平整度、平面度、轮廓测量v1.5/。"
|
||||
)
|
||||
|
||||
|
||||
@@ -105,8 +105,47 @@ def main() -> int:
|
||||
pc_path_raw = first_pointcloud(payload)
|
||||
emit_progress(0.10, f"读取点云:{os.path.basename(pc_path_raw)}")
|
||||
|
||||
# 二进制 PLY → ASCII PLY(算法内置 reader 不依赖 open3d)
|
||||
pc_path = ensure_ascii_ply(pc_path_raw, out_dir / "_ply_cache")
|
||||
# ── ROI 裁剪(整车点云场景下必须填写,否则算法极慢)──────────────────────
|
||||
def _f(key: str) -> float | None:
|
||||
v = config.get(key)
|
||||
try:
|
||||
return float(v) if v is not None and str(v).strip() != "" else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
x_min, x_max = _f("roiXMin"), _f("roiXMax")
|
||||
y_min, y_max = _f("roiYMin"), _f("roiYMax")
|
||||
z_min, z_max = _f("roiZMin"), _f("roiZMax")
|
||||
has_roi = any(v is not None for v in (x_min, x_max, y_min, y_max, z_min, z_max))
|
||||
|
||||
if has_roi:
|
||||
emit_progress(0.12, "ROI 裁剪中…")
|
||||
import numpy as np
|
||||
import open3d as o3d # type: ignore
|
||||
|
||||
pcd = o3d.io.read_point_cloud(pc_path_raw)
|
||||
pts = np.asarray(pcd.points)
|
||||
mask = np.ones(len(pts), dtype=bool)
|
||||
if x_min is not None: mask &= pts[:, 0] >= x_min
|
||||
if x_max is not None: mask &= pts[:, 0] <= x_max
|
||||
if y_min is not None: mask &= pts[:, 1] >= y_min
|
||||
if y_max is not None: mask &= pts[:, 1] <= y_max
|
||||
if z_min is not None: mask &= pts[:, 2] >= z_min
|
||||
if z_max is not None: mask &= pts[:, 2] <= z_max
|
||||
n_total, n_crop = len(pts), int(mask.sum())
|
||||
if n_crop == 0:
|
||||
raise RuntimeError(
|
||||
f"ROI 裁剪后无点(原始 {n_total} 点),请检查坐标范围是否正确。"
|
||||
)
|
||||
cropped = pcd.select_by_index(np.where(mask)[0])
|
||||
roi_ply = out_dir / "_roi_cropped.ply"
|
||||
# 二进制写出,算法用 o3d 读所以支持二进制,比 ASCII 快很多
|
||||
o3d.io.write_point_cloud(str(roi_ply), cropped, write_ascii=False)
|
||||
pc_path_raw = str(roi_ply)
|
||||
emit_progress(0.14, f"ROI 裁剪完成:{n_total} → {n_crop} 点")
|
||||
|
||||
# v1.5 用 o3d.io.read_point_cloud 读取,直接支持二进制 PLY,无需转 ASCII
|
||||
pc_path = pc_path_raw
|
||||
|
||||
module = str(config.get("module") or "all")
|
||||
emit_progress(0.15, f"初始化 MeasurementPipeline(module={module})")
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"""train-measurement 算法适配器 (v1.1)。
|
||||
"""train-measurement 算法适配器 (v2.2)。
|
||||
|
||||
读 input.json → 调 TaskManager(来自 alg/点云测距v1.1)→ 写统一 result.json。
|
||||
读 input.json → 调 TaskManager(来自 alg/train_measurement_system_v2.2)→ 写统一 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 等
|
||||
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.yaml 作为模板。
|
||||
否则使用算法仓库自带的 config/measurements_0618.yaml 作为模板。
|
||||
- 所有任务结果合成成 measurements[] 列表,附带可视化 PLY 路径到 extra。
|
||||
"""
|
||||
|
||||
@@ -25,7 +26,6 @@ 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,
|
||||
@@ -41,14 +41,14 @@ def main() -> int:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
emit_progress(0.05, "加载 train_measurement v1.1 算法包")
|
||||
emit_progress(0.05, "加载 train_measurement v2.2 算法包")
|
||||
|
||||
# TRAIN_MEAS_ROOT 由 manifest envExtra 注入,指向 train_measurement_system/
|
||||
# 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 / "点云测距v1.1" / "train_measurement_system"
|
||||
train_root = alg_root / "train_measurement_system_v2.2" / "train_measurement_system_v2.2"
|
||||
|
||||
if not train_root.exists():
|
||||
raise RuntimeError(
|
||||
@@ -61,9 +61,6 @@ def main() -> int:
|
||||
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()
|
||||
@@ -81,8 +78,8 @@ def main() -> int:
|
||||
# 已经是 YAML 格式,直接写盘
|
||||
yaml_path.write_text(custom_yaml_str, encoding="utf-8")
|
||||
else:
|
||||
# 使用算法仓库内置的 config/measurements.yaml
|
||||
yaml_path = train_root / "config" / "measurements.yaml"
|
||||
# 使用算法仓库内置的 config/measurements_0618.yaml
|
||||
yaml_path = train_root / "config" / "measurements_0618.yaml"
|
||||
if not yaml_path.exists():
|
||||
raise RuntimeError(f"内置配置文件不存在:{yaml_path}")
|
||||
|
||||
@@ -92,6 +89,8 @@ def main() -> int:
|
||||
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}")
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
"""underframe-deflection 算法适配器 (v1.2)。
|
||||
"""underframe-deflection 算法适配器 (v1.2.1)。
|
||||
|
||||
v1.2.1 变更:
|
||||
- 算法包升级至 delivery_v1.2.1(单梁结果新增项点检验字段:公差/测量/误差/结论)
|
||||
- report.xlsx 等 Excel 产物通过 result.json 的 files 字段暴露给前端下载
|
||||
- PYTHONPATH 指向 delivery_v1.2.1
|
||||
|
||||
v1.2 变更:
|
||||
- 算法包升级至 delivery_v1.2(新增 single_beam 单梁管线)
|
||||
- PYTHONPATH 指向 delivery_v1.2
|
||||
- 新增 single_beam 单梁管线
|
||||
|
||||
v1.1 变更:
|
||||
- 入口改为 run_measurement_pipeline(返回 dict,同时落盘 report.json / xlsx / PLY)
|
||||
@@ -21,7 +25,6 @@ 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,
|
||||
@@ -54,15 +57,12 @@ def main() -> int:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
emit_progress(0.05, "加载 underframe_deflection v1.2 算法包")
|
||||
emit_progress(0.05, "加载 underframe_deflection v1.2.1 算法包")
|
||||
from underframe_deflection.pipeline import run_measurement_pipeline # 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")
|
||||
|
||||
profile = str(config.get("profile") or "auto")
|
||||
skip_registration = bool(config.get("skipRegistration") or False)
|
||||
enable_bilateral = bool(config.get("enableBilateral") if "enableBilateral" in config else True)
|
||||
|
||||
181
server/plugins/adapters/vehicle_registration_run.py
Normal file
181
server/plugins/adapters/vehicle_registration_run.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""vehicle-registration 整车配准适配器 (v1.3)。
|
||||
|
||||
调用 alg/整车配准v1.3 的 translate_align_by_front_tail:
|
||||
- 输入:整车点云 PLY + 标准 STEP CAD
|
||||
- 原理:车头/车尾各采样少量点算质心差,融合成整体平移矩阵
|
||||
- v1.3 新增:ICP 精配准、CAD 采样缓存、配准误差统计(RMSE/p95/p99)
|
||||
- 输出:
|
||||
aligned_pointcloud_translation_only.ply 配准后的完整点云
|
||||
T_translation_only.txt 4x4 平移矩阵
|
||||
|
||||
result.json 额外约定 derivedPointClouds 字段:runner 会把列出的 PLY
|
||||
回写为该任务的新点云记录(source='registered'),供后续测量算法选用。
|
||||
"""
|
||||
|
||||
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, "加载整车配准算法包 v1.3")
|
||||
from translate_align_pointcloud_to_cad import translate_align_by_front_tail # type: ignore
|
||||
|
||||
pc_path = first_pointcloud(payload)
|
||||
cad_path = inputs.get("cadPath")
|
||||
if not cad_path:
|
||||
raise RuntimeError("整车配准需要 STEP CAD 文件,请在任务下上传并选中 CAD。")
|
||||
|
||||
axis = str(config.get("axis") or "x")
|
||||
crop_ratio = float(config.get("cropRatio") or 0.01)
|
||||
mesh_deflection = float(config.get("meshDeflection") or 0.1)
|
||||
cad_sample_points = int(config.get("cadSamplePoints") or 30000)
|
||||
local_sample_points = int(config.get("localSamplePoints") or 1000)
|
||||
scale_pointcloud = float(config.get("scalePointcloud") or 1.0)
|
||||
|
||||
# v1.3 新增参数
|
||||
use_icp = bool(config.get("useIcp") if config.get("useIcp") is not None else True)
|
||||
icp_sample_points = int(config.get("icpSamplePoints") or 200000)
|
||||
compute_error_stats = bool(config.get("computeErrorStats") if config.get("computeErrorStats") is not None else True)
|
||||
use_cad_cache = bool(config.get("useCadCache") if config.get("useCadCache") is not None else True)
|
||||
|
||||
emit_progress(
|
||||
0.15,
|
||||
f"开始配准:{os.path.basename(pc_path)} ← {os.path.basename(str(cad_path))}"
|
||||
+ (" + ICP" if use_icp else ""),
|
||||
)
|
||||
|
||||
result = translate_align_by_front_tail(
|
||||
cad_path=str(cad_path),
|
||||
pointcloud_path=pc_path,
|
||||
output_dir=str(out_dir),
|
||||
axis=axis,
|
||||
crop_ratio=crop_ratio,
|
||||
mesh_deflection=mesh_deflection,
|
||||
cad_sample_points=cad_sample_points,
|
||||
local_sample_points=local_sample_points,
|
||||
scale_pointcloud=scale_pointcloud,
|
||||
visualize=False,
|
||||
use_cad_cache=use_cad_cache,
|
||||
use_icp=use_icp,
|
||||
icp_sample_points=icp_sample_points,
|
||||
compute_error_stats=compute_error_stats,
|
||||
save_debug_samples=False,
|
||||
)
|
||||
|
||||
emit_progress(0.92, "整理结果")
|
||||
|
||||
t_final = [float(v) for v in result["translation"]]
|
||||
ft_diff = float(result["front_tail_translation_difference"])
|
||||
elapsed = float(result.get("elapsed_seconds") or 0)
|
||||
|
||||
measurements = [
|
||||
{"name": "最终平移量 Tx", "value": t_final[0], "unit": "mm", "status": "info"},
|
||||
{"name": "最终平移量 Ty", "value": t_final[1], "unit": "mm", "status": "info"},
|
||||
{"name": "最终平移量 Tz", "value": t_final[2], "unit": "mm", "status": "info"},
|
||||
{
|
||||
"name": "前后端平移差",
|
||||
"value": ft_diff,
|
||||
"unit": "mm",
|
||||
"tolerance": 50.0,
|
||||
"status": "pass" if ft_diff <= 50.0 else "warning",
|
||||
},
|
||||
]
|
||||
|
||||
# ICP 结果指标
|
||||
icp_stats = result.get("icp_stats")
|
||||
if icp_stats:
|
||||
measurements.append({
|
||||
"name": "ICP fitness",
|
||||
"value": float(icp_stats.get("fitness", 0)),
|
||||
"unit": "",
|
||||
"status": "pass" if float(icp_stats.get("fitness", 0)) >= 0.9 else "warning",
|
||||
})
|
||||
measurements.append({
|
||||
"name": "ICP inlier RMSE",
|
||||
"value": float(icp_stats.get("inlier_rmse", 0)),
|
||||
"unit": "mm",
|
||||
"status": "info",
|
||||
})
|
||||
|
||||
# 配准误差统计
|
||||
error_stats = result.get("error_stats")
|
||||
if error_stats:
|
||||
rmse = float(error_stats.get("rmse", 0))
|
||||
p95 = float(error_stats.get("p95", 0))
|
||||
measurements += [
|
||||
{"name": "配准 RMSE", "value": rmse, "unit": "mm",
|
||||
"status": "pass" if rmse <= 5.0 else "warning"},
|
||||
{"name": "配准误差 p95", "value": p95, "unit": "mm", "status": "info"},
|
||||
{"name": "配准误差 p99", "value": float(error_stats.get("p99", 0)), "unit": "mm", "status": "info"},
|
||||
]
|
||||
|
||||
aligned_name = Path(result["aligned_path"]).name
|
||||
src_stem = Path(pc_path).stem
|
||||
|
||||
# 下载文件:矩阵 + 误差统计(如有)
|
||||
files = [Path(result["matrix_path"]).name]
|
||||
error_stats_path = result.get("error_stats_path")
|
||||
if error_stats_path and Path(error_stats_path).exists():
|
||||
files.append(Path(error_stats_path).name)
|
||||
|
||||
payload_out = {
|
||||
"schemaVersion": 1,
|
||||
"status": "completed",
|
||||
"message": (
|
||||
f"整车配准完成({'平移 + ICP' if use_icp else '仅平移'},耗时 {elapsed:.1f}s)。"
|
||||
"配准后的点云已加入任务点云列表,请在后续算法中选用。"
|
||||
),
|
||||
"measurements": measurements,
|
||||
"overlay3d": {"kind": "none"},
|
||||
"files": files,
|
||||
"derivedPointClouds": [
|
||||
{"file": aligned_name, "name": f"{src_stem}_已配准.ply"}
|
||||
],
|
||||
"extra": {
|
||||
"initialTranslation": [float(v) for v in result.get("initial_translation", result["translation"])],
|
||||
"finalTranslation": t_final,
|
||||
"tFront": [float(v) for v in result["t_front"]],
|
||||
"tTail": [float(v) for v in result["t_tail"]],
|
||||
"matrix": [[float(v) for v in row] for row in result["T_final"]],
|
||||
"icpStats": icp_stats,
|
||||
"elapsedSeconds": elapsed,
|
||||
"sourcePointCloud": os.path.basename(pc_path),
|
||||
"cad": os.path.basename(str(cad_path)),
|
||||
},
|
||||
}
|
||||
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"整车配准失败:{exc}",
|
||||
extra={"traceback": traceback.format_exc()},
|
||||
)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -2,8 +2,8 @@
|
||||
"id": "coupler-panel",
|
||||
"name": "车钩面板检测",
|
||||
"shortName": "coupler-panel",
|
||||
"description": "基于点云的车钩面板偏转角与最大偏差测量(v1.2,来自 alg/delivery_v1.2/coupler_panel_release)。输出 θ_right / θ_ref 偏转角、dx_y / dx_z 最大 x 偏差及 D_y / D_z 面板偏差。v1.2 新增自动调参(autotune)与分层选点(layer_select)。",
|
||||
"version": "1.2.0",
|
||||
"description": "基于点云的车钩面板偏转角与最大偏差测量(v1.2.1,来自 alg/delivery_v1.2.1/coupler_panel_release)。输出 θ_right / θ_ref 偏转角、dx_y / dx_z 最大 x 偏差及 D_y / D_z 面板偏差。v1.2.1 新增项点检验 Excel 报表(项点名称/理论尺寸/公差要求/测量结果/误差结果/测量结论)。",
|
||||
"version": "1.2.1",
|
||||
"schemaVersion": 1,
|
||||
"category": "几何测量",
|
||||
"requiresPointCloud": true,
|
||||
@@ -32,7 +32,16 @@
|
||||
{ "value": "-1", "label": "-1(车体右 = -y)" }
|
||||
],
|
||||
"description": "若车体「右」方向为 +y 保持 +1;若为 -y 改为 -1。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "roiXMin", "label": "ROI X 最小值 (mm)", "type": "number", "default": null,
|
||||
"description": "车钩面板局部 ROI 裁剪范围(相对于配准后 CAD 坐标系)。整车点云时必填,否则算法处理全部点云会极慢。留空 = 不裁剪。"
|
||||
},
|
||||
{ "name": "roiXMax", "label": "ROI X 最大值 (mm)", "type": "number", "default": null },
|
||||
{ "name": "roiYMin", "label": "ROI Y 最小值 (mm)", "type": "number", "default": null },
|
||||
{ "name": "roiYMax", "label": "ROI Y 最大值 (mm)", "type": "number", "default": null },
|
||||
{ "name": "roiZMin", "label": "ROI Z 最小值 (mm)", "type": "number", "default": null },
|
||||
{ "name": "roiZMax", "label": "ROI Z 最大值 (mm)", "type": "number", "default": null }
|
||||
],
|
||||
"runtime": {
|
||||
"kind": "process",
|
||||
@@ -42,8 +51,8 @@
|
||||
"{inputJson}"
|
||||
],
|
||||
"envExtra": {
|
||||
"PYTHONPATH": "{algRoot}/delivery_v1.2/coupler_panel_release"
|
||||
"PYTHONPATH": "{algRoot}/delivery_v1.2.1/coupler_panel_release"
|
||||
},
|
||||
"timeoutMs": 1200000
|
||||
"timeoutMs": 5400000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"id": "flatness-suite",
|
||||
"name": "平整度 / 平面度 / 轮廓测量",
|
||||
"shortName": "flatness-suite",
|
||||
"description": "平整度 / 平面度 / 轮廓 / 曲率 套件(直接调用 alg/平整度、平面度、轮廓测量/ 下的脚本:Plane_Estimation / Contour_Flatness / Curvature_Estimation)。运行需要 numpy + open3d;contour 子模块还需要 torch + trimesh + STL CAD。任意子模块缺依赖时自动跳过并在 message 里说明。",
|
||||
"version": "1.0.0",
|
||||
"description": "平整度 / 平面度 / 轮廓 / 曲率 套件(v1.5,来自 alg/平整度、平面度、轮廓测量v1.5)。v1.5 新增多车型 STEP CAD 模板(car_part_v1)与 contour Excel 报表。任意子模块缺依赖时自动跳过并在 message 里说明。",
|
||||
"version": "1.5.0",
|
||||
"schemaVersion": 1,
|
||||
"category": "几何测量",
|
||||
"requiresPointCloud": true,
|
||||
@@ -30,7 +30,16 @@
|
||||
"max": 10.0,
|
||||
"step": 0.1,
|
||||
"description": "平面拟合的距离阈值(同时也作为曲率搜索半径的基准 ×2)。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "roiXMin", "label": "ROI X 最小值 (mm)", "type": "number", "default": null,
|
||||
"description": "待测区域裁剪范围(配准后 CAD 坐标系,mm)。整车点云时必填以加速计算,留空 = 不裁剪。"
|
||||
},
|
||||
{ "name": "roiXMax", "label": "ROI X 最大值 (mm)", "type": "number", "default": null },
|
||||
{ "name": "roiYMin", "label": "ROI Y 最小值 (mm)", "type": "number", "default": null },
|
||||
{ "name": "roiYMax", "label": "ROI Y 最大值 (mm)", "type": "number", "default": null },
|
||||
{ "name": "roiZMin", "label": "ROI Z 最小值 (mm)", "type": "number", "default": null },
|
||||
{ "name": "roiZMax", "label": "ROI Z 最大值 (mm)", "type": "number", "default": null }
|
||||
],
|
||||
"runtime": {
|
||||
"kind": "process",
|
||||
@@ -42,6 +51,6 @@
|
||||
"envExtra": {
|
||||
"ALG_ROOT": "{algRoot}"
|
||||
},
|
||||
"timeoutMs": 1200000
|
||||
"timeoutMs": 5400000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"id": "train-measurement",
|
||||
"name": "列车点云测距",
|
||||
"shortName": "train-measurement",
|
||||
"description": "基于配置 YAML 驱动的列车点云批量测距套件(v1.1)。支持「平面到平面 / 点到平面 / 点到点」三类任务,可在 3D 视图中拖拽 ROI 包围盒可视化配置。",
|
||||
"version": "1.1.0",
|
||||
"description": "基于配置 YAML 驱动的列车点云批量测距套件(v2.2)。v2.2 新增孔中心拟合(hole_center_fitter)、测量日志(measurement_logger)、性能日志(performance_logger)。支持「平面到平面 / 点到平面 / 点到点 / 带孔」四类任务,可在 3D 视图中拖拽 ROI 包围盒可视化配置。",
|
||||
"version": "2.2.0",
|
||||
"schemaVersion": 1,
|
||||
"category": "尺寸测量",
|
||||
"requiresPointCloud": true,
|
||||
@@ -37,9 +37,9 @@
|
||||
"{inputJson}"
|
||||
],
|
||||
"envExtra": {
|
||||
"PYTHONPATH": "{algRoot}/点云测距v1.1/train_measurement_system/src",
|
||||
"TRAIN_MEAS_ROOT": "{algRoot}/点云测距v1.1/train_measurement_system"
|
||||
"PYTHONPATH": "{algRoot}/train_measurement_system_v2.2/train_measurement_system_v2.2/src",
|
||||
"TRAIN_MEAS_ROOT": "{algRoot}/train_measurement_system_v2.2/train_measurement_system_v2.2"
|
||||
},
|
||||
"timeoutMs": 1200000
|
||||
"timeoutMs": 5400000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"id": "underframe-deflection",
|
||||
"name": "构架挠度测量",
|
||||
"shortName": "underframe-deflection",
|
||||
"description": "基于点云的高铁底架纵梁挠度测量(v1.2,来自 alg/delivery_v1.2/underframe_deflection)。支持 CAD 基准配准与 ROI 裁剪,输出左右梁挠度、配准 RMS 及挠度曲线 PLY 叠加层。v1.2 新增单梁管线(single_beam)支持。",
|
||||
"version": "1.2.0",
|
||||
"description": "基于点云的高铁底架纵梁挠度测量(v1.2.1,来自 alg/delivery_v1.2.1/underframe_deflection)。支持 CAD 基准配准与 ROI 裁剪,输出左右梁挠度、配准 RMS 及挠度曲线 PLY 叠加层。v1.2.1 新增项点检验字段(公差/测量/误差/结论)与 Excel 报表导出。",
|
||||
"version": "1.2.1",
|
||||
"schemaVersion": 1,
|
||||
"category": "几何测量",
|
||||
"requiresPointCloud": true,
|
||||
@@ -54,8 +54,8 @@
|
||||
"{inputJson}"
|
||||
],
|
||||
"envExtra": {
|
||||
"PYTHONPATH": "{algRoot}/delivery_v1.2"
|
||||
"PYTHONPATH": "{algRoot}/delivery_v1.2.1"
|
||||
},
|
||||
"timeoutMs": 1200000
|
||||
"timeoutMs": 5400000
|
||||
}
|
||||
}
|
||||
|
||||
105
server/plugins/manifests/vehicle-registration.json
Normal file
105
server/plugins/manifests/vehicle-registration.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"id": "vehicle-registration",
|
||||
"name": "整车配准",
|
||||
"shortName": "vehicle-registration",
|
||||
"description": "将整车扫描点云与标准 STEP CAD 进行配准(v1.3)。v1.3 新增 ICP 精配准、CAD 采样缓存加速、配准误差统计(RMSE/p95)。配准完成后,对齐点云自动加入任务点云列表,供后续测量算法选用。",
|
||||
"version": "1.3.0",
|
||||
"schemaVersion": 1,
|
||||
"category": "配准",
|
||||
"requiresPointCloud": true,
|
||||
"requiresCad": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "axis",
|
||||
"label": "车辆长度轴",
|
||||
"type": "select",
|
||||
"default": "x",
|
||||
"options": [
|
||||
{ "value": "x", "label": "X 轴" },
|
||||
{ "value": "y", "label": "Y 轴" },
|
||||
{ "value": "z", "label": "Z 轴" }
|
||||
],
|
||||
"description": "扫描坐标系中车辆纵向(车头→车尾)对应的轴。"
|
||||
},
|
||||
{
|
||||
"name": "cropRatio",
|
||||
"label": "车头/车尾截取比例",
|
||||
"type": "number",
|
||||
"default": 0.01,
|
||||
"min": 0.005,
|
||||
"max": 0.15,
|
||||
"step": 0.005,
|
||||
"description": "从点云两端各取此比例的长度范围用于质心估算。数据干净时 0.01 即可;噪声多时可适当加大。"
|
||||
},
|
||||
{
|
||||
"name": "scalePointcloud",
|
||||
"label": "点云缩放系数",
|
||||
"type": "number",
|
||||
"default": 1.0,
|
||||
"min": 0.001,
|
||||
"max": 1000.0,
|
||||
"step": 0.001,
|
||||
"description": "点云单位与 CAD 单位不一致时使用。例如点云为米、CAD 为毫米,填 1000。"
|
||||
},
|
||||
{
|
||||
"name": "cadSamplePoints",
|
||||
"label": "CAD 采样点数",
|
||||
"type": "integer",
|
||||
"default": 30000,
|
||||
"min": 5000,
|
||||
"max": 500000,
|
||||
"description": "从 CAD mesh 均匀采样的点数。点数越多精度越高,耗时也越长。"
|
||||
},
|
||||
{
|
||||
"name": "localSamplePoints",
|
||||
"label": "端部局部采样点数",
|
||||
"type": "integer",
|
||||
"default": 1000,
|
||||
"min": 100,
|
||||
"max": 20000,
|
||||
"description": "车头/车尾区域用于质心计算的最远点采样数。"
|
||||
},
|
||||
{
|
||||
"name": "useIcp",
|
||||
"label": "ICP 精配准",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "平移对齐后再做一轮 ICP 精配准(v1.3 新增)。通常可进一步减小配准误差,耗时约增加 30–60 秒。"
|
||||
},
|
||||
{
|
||||
"name": "icpSamplePoints",
|
||||
"label": "ICP 采样点数",
|
||||
"type": "integer",
|
||||
"default": 200000,
|
||||
"min": 10000,
|
||||
"max": 1000000,
|
||||
"description": "ICP 配准时使用的点数。越多精度越高,耗时也越长。"
|
||||
},
|
||||
{
|
||||
"name": "computeErrorStats",
|
||||
"label": "计算配准误差统计",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "配准后抽样计算点云到 CAD 的距离误差(RMSE / p95 / p99),用于评估配准质量。"
|
||||
},
|
||||
{
|
||||
"name": "useCadCache",
|
||||
"label": "缓存 CAD 采样",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "缓存 CAD 三角化采样结果,同一 CAD 文件第二次配准时跳过耗时的 STEP 解析,大幅加速。"
|
||||
}
|
||||
],
|
||||
"runtime": {
|
||||
"kind": "process",
|
||||
"cmd": [
|
||||
"{pythonBin}",
|
||||
"{adapterDir}/vehicle_registration_run.py",
|
||||
"{inputJson}"
|
||||
],
|
||||
"envExtra": {
|
||||
"PYTHONPATH": "{algRoot}/整车配准v1.3"
|
||||
},
|
||||
"timeoutMs": 1800000
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,12 @@ export interface PluginResult {
|
||||
/** 相对 outDir 的图片文件名(如 plot.png 或 images/foo.png)。
|
||||
* 前端通过 /api/files/read/analysis/:taskId/:resultId/:filename 读取 */
|
||||
images?: string[];
|
||||
/** 相对 outDir 的可下载产物(如 report.xlsx)。
|
||||
* 前端在结果面板展示下载入口。 */
|
||||
files?: string[];
|
||||
/** 配准算法专用:产出的衍生点云,runner 自动回写为任务点云记录。
|
||||
* file 相对 outDir;name 是在任务点云列表中显示的名称。 */
|
||||
derivedPointClouds?: { file: string; name: string }[];
|
||||
/** 算法专属字段;前端折叠在「原始字段」里展示 */
|
||||
extra?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { enqueueDeviationHeatmapJob } from "./deviationHeatmapJob.js";
|
||||
import { enqueueSmoothHeatmapJob } from "./smoothHeatmapJob.js";
|
||||
import { stepParseCacheDir } from "./stepParse.js";
|
||||
import { listPlugins, getPlugin } from "./plugins/registry.js";
|
||||
import { enqueueAlgorithmJob, algorithmJobStats } from "./algorithmJob.js";
|
||||
import { enqueueAlgorithmJob, algorithmJobStats, cancelAlgorithmJob, cancelAlgorithmJobsByTask } from "./algorithmJob.js";
|
||||
|
||||
export const appRouter = router({
|
||||
system: systemRouter,
|
||||
@@ -214,6 +214,11 @@ export const appRouter = router({
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
await db.deleteInspectionTask(input.id);
|
||||
// 取消该任务下所有正在运行或排队的算法 job
|
||||
const cancelled = cancelAlgorithmJobsByTask(input.id);
|
||||
if (cancelled.length > 0) {
|
||||
console.log(`[routers] 删除任务 ${input.id},取消 ${cancelled.length} 个算法 job:`, cancelled);
|
||||
}
|
||||
return { success: true };
|
||||
}),
|
||||
}),
|
||||
@@ -398,6 +403,8 @@ export const appRouter = router({
|
||||
delete: protectedProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
// 先取消正在运行或排队的 job,再删数据库记录
|
||||
cancelAlgorithmJob(input.id);
|
||||
await db.deleteTaskAnalysisResult(input.id);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user