Show automatic pose adjustment deltas
This commit is contained in:
@@ -943,6 +943,7 @@ function buildPoseControls() {
|
|||||||
markDirty();
|
markDirty();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
updateAutoPoseResult(state.pose, state.pose);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireHoldNudge(button) {
|
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 `
|
||||||
|
<span class="${changed ? "changed" : "muted"}">
|
||||||
|
<b>${escapeHtml(label)}</b>
|
||||||
|
<strong>${escapeHtml(deltaText)}</strong>
|
||||||
|
<small>${escapeHtml(currentText)}</small>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function updateAutoPoseResult(before = state.lastAutoPose?.before || state.pose, after = state.pose) {
|
function updateAutoPoseResult(before = state.lastAutoPose?.before || state.pose, after = state.pose) {
|
||||||
const delta = poseDelta(before, after);
|
const delta = poseDelta(before, after);
|
||||||
const el = $("autoPoseResult");
|
const el = $("autoPoseResult");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.innerHTML = [
|
el.innerHTML = [
|
||||||
`<span>平移 X ${delta.translateX}</span>`,
|
autoPoseResultItem("平移 X", `Δ ${signedValue(delta.translateX)}`, `当前 ${Number(after.translateX || 0).toFixed(3)}`, Math.abs(delta.translateX) > 0.0005),
|
||||||
`<span>平移 Y ${delta.translateY}</span>`,
|
autoPoseResultItem("平移 Y", `Δ ${signedValue(delta.translateY)}`, `当前 ${Number(after.translateY || 0).toFixed(3)}`, Math.abs(delta.translateY) > 0.0005),
|
||||||
`<span>平移 Z ${delta.translateZ}</span>`,
|
autoPoseResultItem("平移 Z", `Δ ${signedValue(delta.translateZ)}`, `当前 ${Number(after.translateZ || 0).toFixed(3)}`, Math.abs(delta.translateZ) > 0.0005),
|
||||||
`<span>缩放 ${delta.scale}</span>`,
|
autoPoseResultItem("缩放", `×${Number(delta.scale || 1).toFixed(3)}`, `当前 ${Number(after.scale || 1).toFixed(3)}`, Math.abs((delta.scale || 1) - 1) > 0.0005),
|
||||||
].join("");
|
].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;
|
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() {
|
function runAutoCoarse() {
|
||||||
if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) {
|
if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) {
|
||||||
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
|
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
|
||||||
@@ -2430,14 +2481,21 @@ function runAutoCoarse() {
|
|||||||
}
|
}
|
||||||
const before = { ...state.pose };
|
const before = { ...state.pose };
|
||||||
const settings = readAutoSettings();
|
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();
|
renderPoseControls();
|
||||||
applyPose();
|
applyPose();
|
||||||
fitCamera();
|
fitCamera();
|
||||||
state.autoResult = { score: candidateScore(state.pose, settings), pose: { ...state.pose }, evaluated: 1, settings };
|
state.autoResult = { score: candidateScore(state.pose, settings), pose: { ...state.pose }, evaluated: 1, settings };
|
||||||
state.lastAutoPose = { before, after: { ...state.pose } };
|
state.lastAutoPose = { before, after: { ...state.pose } };
|
||||||
setAutoState("粗配准完成", "ok");
|
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);
|
updateAutoPoseResult(before, state.pose);
|
||||||
markDirty();
|
markDirty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1974,17 +1974,50 @@ input[type="range"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auto-pose-result span {
|
.auto-pose-result span {
|
||||||
min-height: 36px;
|
min-height: 52px;
|
||||||
display: inline-flex;
|
display: grid;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
row-gap: 2px;
|
||||||
border: 1px solid rgba(25, 214, 195, 0.38);
|
border: 1px solid rgba(25, 214, 195, 0.38);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: rgba(20, 184, 166, 0.09);
|
background: rgba(20, 184, 166, 0.09);
|
||||||
color: #ccfbf1;
|
color: #ccfbf1;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
font-weight: 900;
|
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 {
|
.login-overlay {
|
||||||
|
|||||||
Reference in New Issue
Block a user