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 {