diff --git a/DICOM_and_UPP配准/static/app.js b/DICOM_and_UPP配准/static/app.js index 6cad9ff..63be57a 100644 --- a/DICOM_and_UPP配准/static/app.js +++ b/DICOM_and_UPP配准/static/app.js @@ -99,6 +99,8 @@ const state = { dicomPreviewTimer: 0, autoResult: null, lastAutoPose: null, + autoDeltaBaseline: null, + autoFineRunning: false, dirty: false, dirtyReason: "", savedPoseSignature: "", @@ -223,6 +225,7 @@ function markDirty(reason = "pose") { state.dirty = true; state.dirtyReason = reason; setSaveState("未保存", "warn"); + if (reason === "pose") updateAutoPoseResult(); } function poseSignature(pose = state.pose) { @@ -243,6 +246,7 @@ function resetDirty() { state.dirtyReason = ""; state.savedPoseSignature = poseSignature(state.pose); setSaveState("已保存", "ok"); + resetAutoDeltaBaseline(); } async function api(path, options = {}) { @@ -530,8 +534,7 @@ function modelPoseMatrix() { return state.modelGroup.matrix.clone(); } -function visibleModelBox() { - const records = visibleModelRecords(); +function visibleModelBox(records = visibleModelRecords()) { if (!records.length) return null; const box = new THREE.Box3(); const matrix = modelPoseMatrix(); @@ -543,6 +546,14 @@ function visibleModelBox() { return box; } +function autoReferenceRecords(settings = null) { + const visibleRecords = visibleModelRecords(); + const referenceIds = new Set((settings?.referenceIds || []).map((id) => Number(id))); + if (!referenceIds.size) return visibleRecords; + const referenceRecords = visibleRecords.filter((record) => referenceIds.has(Number(record.file?.id))); + return referenceRecords.length ? referenceRecords : visibleRecords; +} + function bodyPartTags(row) { const labels = Array.isArray(row?.body_part_labels) ? row.body_part_labels : []; const keys = Array.isArray(row?.body_part_keys) ? row.body_part_keys : []; @@ -637,6 +648,7 @@ async function selectCase(ctNumber, algorithmModel = "未指定模型") { state.modelDetail = registration.model_reference?.model_detail || state.modelDetail || "ultra"; state.autoResult = null; state.lastAutoPose = null; + state.autoDeltaBaseline = null; state.sliceIndex = 0; $("notes").value = registration.notes || ""; @@ -2460,6 +2472,9 @@ function readAutoSettings() { const value = Number($(id)?.value); return Number.isFinite(value) ? value : fallback; }; + const referenceIds = [...document.querySelectorAll("#autoBoneOptions input:checked")] + .map((input) => Number(input.value)) + .filter(Number.isFinite); return { adjustable: { translateX: $("autoX")?.checked, @@ -2467,6 +2482,7 @@ function readAutoSettings() { translateZ: $("autoZ")?.checked, scale: $("autoScale")?.checked, }, + referenceIds, sampleSlices: Math.max(3, Math.min(60, Math.round(numericValue("autoSampleSlicesNumber", 9)))), boneReward: numericValue("autoBoneReward", 1), outsidePenalty: numericValue("autoOutsidePenalty", 0.1), @@ -2479,6 +2495,9 @@ function readAutoSettings() { function poseDelta(from, to) { return { + rotateX: Number(((to.rotateX || 0) - (from.rotateX || 0)).toFixed(3)), + rotateY: Number(((to.rotateY || 0) - (from.rotateY || 0)).toFixed(3)), + rotateZ: Number(((to.rotateZ || 0) - (from.rotateZ || 0)).toFixed(3)), translateX: Number(((to.translateX || 0) - (from.translateX || 0)).toFixed(3)), translateY: Number(((to.translateY || 0) - (from.translateY || 0)).toFixed(3)), translateZ: Number(((to.translateZ || 0) - (from.translateZ || 0)).toFixed(3)), @@ -2502,11 +2521,14 @@ function autoPoseResultItem(label, deltaText, currentText, changed = true) { `; } -function updateAutoPoseResult(before = state.lastAutoPose?.before || state.pose, after = state.pose) { +function updateAutoPoseResult(before = state.autoDeltaBaseline || state.lastAutoPose?.before || state.pose, after = state.pose) { const delta = poseDelta(before, after); const el = $("autoPoseResult"); if (!el) return; el.innerHTML = [ + autoPoseResultItem("旋转 X", `Δ ${signedValue(delta.rotateX, 1)}°`, `当前 ${Number(after.rotateX || 0).toFixed(1)}°`, Math.abs(delta.rotateX) > 0.0005), + autoPoseResultItem("旋转 Y", `Δ ${signedValue(delta.rotateY, 1)}°`, `当前 ${Number(after.rotateY || 0).toFixed(1)}°`, Math.abs(delta.rotateY) > 0.0005), + autoPoseResultItem("旋转 Z", `Δ ${signedValue(delta.rotateZ, 1)}°`, `当前 ${Number(after.rotateZ || 0).toFixed(1)}°`, Math.abs(delta.rotateZ) > 0.0005), 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), @@ -2514,6 +2536,31 @@ function updateAutoPoseResult(before = state.lastAutoPose?.before || state.pose, ].join(""); } +function resetAutoDeltaBaseline() { + state.autoDeltaBaseline = { ...state.pose }; + updateAutoPoseResult(state.pose, state.pose); +} + +function setAutoProgress(visible, value = 0, text = "") { + const wrap = $("autoProgress"); + if (!wrap) return; + wrap.classList.toggle("hidden", !visible); + const safeValue = Math.max(0, Math.min(100, Number(value) || 0)); + $("autoProgressFill").style.width = `${safeValue}%`; + $("autoProgressText").textContent = text || `${Math.round(safeValue)}%`; +} + +function setAutoControlsBusy(busy) { + ["autoCoarseBtn", "autoFineBtn", "saveIterateBtn"].forEach((id) => { + const button = $(id); + if (button) button.disabled = busy; + }); +} + +function waitForUiFrame() { + return new Promise((resolve) => window.requestAnimationFrame(() => resolve())); +} + function restoreAutoPose() { if (!state.lastAutoPose?.before) { $("autoResult").textContent = "暂无可退回的自动调整位姿。"; @@ -2523,7 +2570,7 @@ function restoreAutoPose() { renderPoseControls(); applyPose(); fitCamera(); - updateAutoPoseResult(state.lastAutoPose.after, state.pose); + resetAutoDeltaBaseline(); $("autoResult").textContent = "已退回到自动调整前的位姿。"; markDirty(); } @@ -2531,9 +2578,11 @@ function restoreAutoPose() { function candidateScore(pose, settings = null) { if (!state.dicomSceneBox || !state.modelGroup || !visibleModelRecords().length) return -Infinity; const config = settings || readAutoSettings(); + const records = autoReferenceRecords(config); + if (!records.length) return -Infinity; const current = { ...state.pose }; applyPose(pose); - const modelBox = visibleModelBox(); + const modelBox = visibleModelBox(records); applyPose(current); if (!modelBox) return -Infinity; const overlap = modelBox.clone().intersect(state.dicomSceneBox); @@ -2556,7 +2605,8 @@ function candidateScore(pose, settings = null) { } function estimateCoarsePose(settings) { - const modelBox = visibleModelBox(); + const records = autoReferenceRecords(settings); + const modelBox = visibleModelBox(records); if (!modelBox || !state.dicomSceneBox) return null; const adjustable = settings.adjustable || {}; const dicomSize = new THREE.Vector3(); @@ -2577,7 +2627,7 @@ function estimateCoarsePose(settings) { } const previous = { ...state.pose }; applyPose(candidate); - const nextBox = visibleModelBox(); + const nextBox = visibleModelBox(records); applyPose(previous); if (!nextBox) return candidate; const modelCenter = new THREE.Vector3(); @@ -2594,6 +2644,8 @@ function runAutoCoarse() { $("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。"; return; } + if (state.autoFineRunning) return; + setAutoProgress(false); const before = { ...state.pose }; const settings = readAutoSettings(); const estimated = estimateCoarsePose(settings); @@ -2608,6 +2660,7 @@ function runAutoCoarse() { fitCamera(); state.autoResult = { score: candidateScore(state.pose, settings), pose: { ...state.pose }, evaluated: 1, settings }; state.lastAutoPose = { before, after: { ...state.pose } }; + state.autoDeltaBaseline = { ...before }; setAutoState("粗配准完成", "ok"); 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)}。`; @@ -2615,49 +2668,81 @@ function runAutoCoarse() { markDirty(); } -function runAutoFine() { +async function runAutoFine() { if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) { $("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。"; return; } - setAutoState("运行中", "warn"); - $("autoResult").textContent = "正在进行几何重叠微调..."; + if (state.autoFineRunning) return; const before = { ...state.pose }; const settings = readAutoSettings(); const adjustable = settings.adjustable; - let best = { pose: { ...state.pose }, score: candidateScore(state.pose, settings), mode: "初始位姿" }; - let evaluated = 1; - const rounds = Array.from({ length: settings.iterations }, (_, index) => { - const t = Math.max(0.18, 1 - index / Math.max(settings.iterations, 1)); - return { move: 0.34 * t, scale: 0.12 * t }; - }); - for (const round of rounds) { - const candidates = []; - if (adjustable.translateX) candidates.push(["translateX", round.move], ["translateX", -round.move]); - if (adjustable.translateY) candidates.push(["translateY", round.move], ["translateY", -round.move]); - if (adjustable.translateZ) candidates.push(["translateZ", round.move], ["translateZ", -round.move]); - if (adjustable.scale) candidates.push(["scale", round.scale], ["scale", -round.scale]); - for (const [key, delta] of candidates.slice(0, settings.candidates)) { - const pose = { ...best.pose, [key]: key === "scale" ? Math.max(0.2, Math.min(3, best.pose.scale + delta)) : best.pose[key] + delta }; - const score = candidateScore(pose, settings); + state.autoFineRunning = true; + setAutoControlsBusy(true); + setAutoState("运行中", "warn"); + setAutoProgress(true, 2, "2%"); + if (state.windowMode !== "bone") setWindowMode("bone", true); + $("autoResult").textContent = "已切换骨窗显示,正在按参考 STL 与 DICOM 三维盒体重叠做微调..."; + await waitForUiFrame(); + try { + let best = { pose: { ...state.pose }, score: candidateScore(state.pose, settings), mode: "初始位姿" }; + let evaluated = 1; + const coarsePose = estimateCoarsePose(settings); + if (coarsePose) { + const coarseScore = candidateScore(coarsePose, settings); evaluated += 1; - if (score > best.score) best = { pose, score, mode: `${key} ${delta > 0 ? "+" : "-"}` }; + if (coarseScore > best.score) best = { pose: coarsePose, score: coarseScore, mode: "粗配准种子" }; } + const rounds = Array.from({ length: settings.iterations }, (_, index) => { + const t = Math.max(0.18, 1 - index / Math.max(settings.iterations, 1)); + return { move: 0.34 * t, scale: 0.12 * t }; + }); + for (const [index, round] of rounds.entries()) { + const candidates = []; + if (adjustable.translateX) candidates.push(["translateX", round.move], ["translateX", -round.move]); + if (adjustable.translateY) candidates.push(["translateY", round.move], ["translateY", -round.move]); + if (adjustable.translateZ) candidates.push(["translateZ", round.move], ["translateZ", -round.move]); + if (adjustable.scale) candidates.push(["scale", round.scale], ["scale", -round.scale]); + for (const [key, delta] of candidates.slice(0, settings.candidates)) { + const baseValue = Number(best.pose[key] ?? DEFAULT_POSE[key]); + const nextValue = key === "scale" ? Math.max(0.2, Math.min(3, baseValue + delta)) : baseValue + delta; + const pose = { ...best.pose, [key]: Number(nextValue.toFixed(key === "scale" ? 3 : 4)) }; + const score = candidateScore(pose, settings); + evaluated += 1; + if (score > best.score) best = { pose, score, mode: `${key} ${delta > 0 ? "+" : "-"}` }; + } + const progress = 8 + ((index + 1) / Math.max(rounds.length, 1)) * 88; + setAutoProgress(true, progress, `${Math.round(progress)}%`); + await waitForUiFrame(); + } + state.pose = { ...best.pose }; + state.autoResult = { ...best, evaluated, settings: { ...settings, scoring: "reference-stl-box-overlap", window: "bone-display" } }; + state.lastAutoPose = { before, after: { ...state.pose } }; + state.autoDeltaBaseline = { ...before }; + renderPoseControls(); + applyPose(); + fitCamera(); + const changed = poseSignature(best.pose) !== poseSignature(before); + setAutoState(changed ? "微调完成" : "微调无变化", changed ? "ok" : "warn"); + const scoreText = Number.isFinite(best.score) ? best.score.toFixed(4) : "不可用"; + $("autoResult").textContent = changed + ? `最佳候选:${best.mode};score ${scoreText};评估 ${evaluated} 个候选;骨窗显示 + 参考 STL 盒体评分。` + : `已完成评估但最佳仍为初始位姿;score ${scoreText};评估 ${evaluated} 个候选。当前微调使用骨窗显示和参考 STL 盒体评分,若图像上仍错位,需要先粗配准或手动调整旋转后再微调。`; + updateAutoPoseResult(before, state.pose); + setAutoProgress(true, 100, "100%"); + markDirty(); + } catch (error) { + setAutoState("微调失败", "error"); + $("autoResult").textContent = error.message || "自动微调失败。"; + setAutoProgress(false); + } finally { + state.autoFineRunning = false; + setAutoControlsBusy(false); } - state.pose = { ...best.pose }; - state.autoResult = { ...best, evaluated, settings }; - state.lastAutoPose = { before, after: { ...state.pose } }; - renderPoseControls(); - applyPose(); - fitCamera(); - setAutoState("微调完成", "ok"); - $("autoResult").textContent = `最佳候选:${best.mode};score ${best.score.toFixed(4)};评估 ${evaluated} 个候选;采样 ${settings.sampleSlices} 张。`; - updateAutoPoseResult(before, state.pose); - markDirty(); } async function saveRegistration(nextStatus = null) { - if (!state.activeCase || !currentSeries() || state.saving) return; + if (!state.activeCase || !currentSeries() || state.saving) return false; state.saving = true; $("saveBtn").disabled = true; $("statusBtn").disabled = true; @@ -2722,8 +2807,10 @@ async function saveRegistration(nextStatus = null) { renderCases(); renderSeries(); renderActiveHeader(); + return true; } catch (error) { setSaveState(error.message, "error"); + return false; } finally { state.saving = false; $("saveBtn").disabled = false; @@ -2822,8 +2909,10 @@ function wireEvents() { $("autoFineBtn").addEventListener("click", runAutoFine); $("savePoseBtn").addEventListener("click", () => saveRegistration()); $("saveIterateBtn").addEventListener("click", async () => { - await saveRegistration(); - runAutoFine(); + const saved = await saveRegistration(); + if (!saved) return; + await runAutoFine(); + resetAutoDeltaBaseline(); }); $("restorePoseBtn").addEventListener("click", restoreAutoPose); const syncSampleSlices = (source) => { diff --git a/DICOM_and_UPP配准/static/index.html b/DICOM_and_UPP配准/static/index.html index c0d1e0f..3e31b8d 100644 --- a/DICOM_and_UPP配准/static/index.html +++ b/DICOM_and_UPP配准/static/index.html @@ -236,23 +236,30 @@ -