From 86e25d0a51c96f2789eff0ac1e06bd5fdcb39d29 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 29 May 2026 17:16:02 +0800 Subject: [PATCH] Show automatic pose adjustment deltas --- DICOM_and_UPP配准/static/app.js | 70 ++++++++++++++++++++++++++--- DICOM_and_UPP配准/static/styles.css | 39 ++++++++++++++-- 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/DICOM_and_UPP配准/static/app.js b/DICOM_and_UPP配准/static/app.js index de094a3..6a6eafd 100644 --- a/DICOM_and_UPP配准/static/app.js +++ b/DICOM_and_UPP配准/static/app.js @@ -943,6 +943,7 @@ function buildPoseControls() { markDirty(); }); }); + updateAutoPoseResult(state.pose, state.pose); } function wireHoldNudge(button) { @@ -2370,15 +2371,31 @@ function poseDelta(from, to) { }; } +function signedValue(value, decimals = 3) { + const numeric = Number(value || 0); + if (Math.abs(numeric) < 10 ** -decimals) return "0"; + return `${numeric > 0 ? "+" : ""}${numeric.toFixed(decimals)}`; +} + +function autoPoseResultItem(label, deltaText, currentText, changed = true) { + return ` + + ${escapeHtml(label)} + ${escapeHtml(deltaText)} + ${escapeHtml(currentText)} + + `; +} + function updateAutoPoseResult(before = state.lastAutoPose?.before || state.pose, after = state.pose) { const delta = poseDelta(before, after); const el = $("autoPoseResult"); if (!el) return; el.innerHTML = [ - `平移 X ${delta.translateX}`, - `平移 Y ${delta.translateY}`, - `平移 Z ${delta.translateZ}`, - `缩放 ${delta.scale}`, + autoPoseResultItem("平移 X", `Δ ${signedValue(delta.translateX)}`, `当前 ${Number(after.translateX || 0).toFixed(3)}`, Math.abs(delta.translateX) > 0.0005), + autoPoseResultItem("平移 Y", `Δ ${signedValue(delta.translateY)}`, `当前 ${Number(after.translateY || 0).toFixed(3)}`, Math.abs(delta.translateY) > 0.0005), + autoPoseResultItem("平移 Z", `Δ ${signedValue(delta.translateZ)}`, `当前 ${Number(after.translateZ || 0).toFixed(3)}`, Math.abs(delta.translateZ) > 0.0005), + autoPoseResultItem("缩放", `×${Number(delta.scale || 1).toFixed(3)}`, `当前 ${Number(after.scale || 1).toFixed(3)}`, Math.abs((delta.scale || 1) - 1) > 0.0005), ].join(""); } @@ -2423,6 +2440,40 @@ function candidateScore(pose, settings = null) { return (overlapVolume / modelVolume) * Number(config.boneReward || 1) - centerPenalty - scalePenalty - movePenalty - Number(config.outsidePenalty || 0) * 0.01; } +function estimateCoarsePose(settings) { + const modelBox = visibleModelBox(); + if (!modelBox || !state.dicomSceneBox) return null; + const adjustable = settings.adjustable || {}; + const dicomSize = new THREE.Vector3(); + const modelSize = new THREE.Vector3(); + const dicomCenter = new THREE.Vector3(); + state.dicomSceneBox.getSize(dicomSize); + state.dicomSceneBox.getCenter(dicomCenter); + modelBox.getSize(modelSize); + const candidate = { ...state.pose }; + if (adjustable.scale) { + const ratios = ["x", "y", "z"] + .map((axis) => dicomSize[axis] / Math.max(modelSize[axis], 0.001)) + .filter((value) => Number.isFinite(value) && value > 0); + if (ratios.length) { + const fitRatio = Math.min(...ratios) * 0.96; + candidate.scale = Number(clamp((candidate.scale || 1) * fitRatio, 0.2, 3).toFixed(3)); + } + } + const previous = { ...state.pose }; + applyPose(candidate); + const nextBox = visibleModelBox(); + applyPose(previous); + if (!nextBox) return candidate; + const modelCenter = new THREE.Vector3(); + nextBox.getCenter(modelCenter); + const offset = dicomCenter.clone().sub(modelCenter); + if (adjustable.translateX) candidate.translateX = Number(((candidate.translateX || 0) + offset.x).toFixed(3)); + if (adjustable.translateY) candidate.translateY = Number(((candidate.translateY || 0) + offset.y).toFixed(3)); + if (adjustable.translateZ) candidate.translateZ = Number(((candidate.translateZ || 0) + offset.z).toFixed(3)); + return candidate; +} + function runAutoCoarse() { if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) { $("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。"; @@ -2430,14 +2481,21 @@ function runAutoCoarse() { } const before = { ...state.pose }; const settings = readAutoSettings(); - state.pose = { ...state.pose, translateX: 0, translateY: 0, translateZ: 0, scale: 1 }; + const estimated = estimateCoarsePose(settings); + if (!estimated) { + $("autoResult").textContent = "当前缺少 DICOM 或 STL 盒体信息,无法计算粗配准。"; + setAutoState("粗配准失败", "error"); + return; + } + state.pose = estimated; renderPoseControls(); applyPose(); fitCamera(); state.autoResult = { score: candidateScore(state.pose, settings), pose: { ...state.pose }, evaluated: 1, settings }; state.lastAutoPose = { before, after: { ...state.pose } }; setAutoState("粗配准完成", "ok"); - $("autoResult").textContent = `已按 DICOM 物理尺寸复位平移/缩放,score ${state.autoResult.score.toFixed(4)};采样 ${settings.sampleSlices} 张。`; + const delta = poseDelta(before, state.pose); + $("autoResult").textContent = `已按 DICOM/STL 盒体估算中心与缩放:ΔX ${signedValue(delta.translateX)},ΔY ${signedValue(delta.translateY)},ΔZ ${signedValue(delta.translateZ)},缩放 ×${delta.scale.toFixed(3)};score ${state.autoResult.score.toFixed(4)}。`; updateAutoPoseResult(before, state.pose); markDirty(); } diff --git a/DICOM_and_UPP配准/static/styles.css b/DICOM_and_UPP配准/static/styles.css index f69af20..4259d82 100644 --- a/DICOM_and_UPP配准/static/styles.css +++ b/DICOM_and_UPP配准/static/styles.css @@ -1974,17 +1974,50 @@ input[type="range"] { } .auto-pose-result span { - min-height: 36px; - display: inline-flex; + min-height: 52px; + display: grid; + grid-template-rows: auto auto auto; align-items: center; justify-content: center; + row-gap: 2px; border: 1px solid rgba(25, 214, 195, 0.38); border-radius: 10px; background: rgba(20, 184, 166, 0.09); color: #ccfbf1; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 12px; + font-size: 11px; font-weight: 900; + text-align: center; + white-space: nowrap; +} + +.auto-pose-result span.changed { + border-color: rgba(52, 211, 153, 0.72); + background: rgba(20, 184, 166, 0.18); + box-shadow: inset 0 0 0 1px rgba(20, 184, 166, 0.12); +} + +.auto-pose-result span.muted { + border-color: rgba(148, 163, 184, 0.28); + background: rgba(15, 23, 42, 0.58); + color: #9fb3c8; +} + +.auto-pose-result b, +.auto-pose-result strong, +.auto-pose-result small { + display: block; + line-height: 1.05; +} + +.auto-pose-result strong { + color: #e6fffb; + font-size: 13px; +} + +.auto-pose-result small { + color: #8fb3c9; + font-size: 10px; } .login-overlay {