diff --git a/DICOM_and_UPP配准/static/app.js b/DICOM_and_UPP配准/static/app.js index 63be57a..5112116 100644 --- a/DICOM_and_UPP配准/static/app.js +++ b/DICOM_and_UPP配准/static/app.js @@ -58,6 +58,13 @@ const POSE_SIGNATURE_KEYS = [ "flipY", "flipZ", ]; +const AUTO_PROGRESS_STEPS = [ + { id: "save", label: "保存" }, + { id: "bone", label: "骨窗" }, + { id: "sample", label: "采样" }, + { id: "score", label: "评分" }, + { id: "apply", label: "应用" }, +]; const state = { token: localStorage.getItem("dicom_upp_registration_token") || "", @@ -101,6 +108,7 @@ const state = { lastAutoPose: null, autoDeltaBaseline: null, autoFineRunning: false, + poseControlsLocked: false, dirty: false, dirtyReason: "", savedPoseSignature: "", @@ -967,6 +975,7 @@ function buildPoseControls() { const number = row.querySelector("input[type='number']"); const option = POSE_CONTROLS.find((item) => item.key === key); const sync = (value, source) => { + if (state.poseControlsLocked) return; const numeric = Number(value); state.pose[key] = Number.isFinite(numeric) ? Number(numeric.toFixed(option?.decimals ?? 3)) : DEFAULT_POSE[key]; if (source !== range) range.value = state.pose[key]; @@ -980,6 +989,7 @@ function buildPoseControls() { document.querySelectorAll("[data-nudge]").forEach((button) => wireHoldNudge(button)); document.querySelectorAll("[data-flip]").forEach((button) => { button.addEventListener("click", () => { + if (state.poseControlsLocked) return; const key = button.dataset.flip; state.pose[key] = !state.pose[key]; renderPoseControls(); @@ -988,6 +998,7 @@ function buildPoseControls() { }); }); updateAutoPoseResult(state.pose, state.pose); + applyPoseLockState(); } function wireHoldNudge(button) { @@ -997,6 +1008,7 @@ function wireHoldNudge(button) { return; } const start = (event) => { + if (state.poseControlsLocked || button.disabled) return; if (event.button !== undefined && event.button !== 0) return; event.preventDefault(); const key = button.dataset.nudge; @@ -1028,6 +1040,7 @@ function clearNudgeTimers() { } function nudgePose(key, delta) { + if (state.poseControlsLocked) return; if (!key || !Number.isFinite(delta)) return; const option = POSE_CONTROLS.find((item) => item.key === key); const current = Number(state.pose[key] ?? DEFAULT_POSE[key]); @@ -1051,6 +1064,7 @@ function renderPoseControls() { }); renderActiveHeader(); updateStretchButtons(); + applyPoseLockState(); } function isRightAngleRotation(value) { @@ -1063,17 +1077,55 @@ function canStretchByAxis() { } function updateStretchButtons() { - const enabled = canStretchByAxis(); + const enabled = canStretchByAxis() && !state.poseControlsLocked; ["x", "y", "z"].forEach((axis) => { const button = $(`stretch${axis.toUpperCase()}Btn`); if (!button) return; button.disabled = !enabled; button.title = enabled ? `按 ${axis.toUpperCase()} 方向自动等比例拉伸模型` - : "仅当旋转 X/Y/Z 均为 0 或 90° 的倍数时可用"; + : state.poseControlsLocked + ? "自动迭代运行中,暂不可调整位姿" + : "仅当旋转 X/Y/Z 均为 0 或 90° 的倍数时可用"; }); } +function applyPoseLockState() { + const locked = Boolean(state.poseControlsLocked); + const selectors = [ + "#poseGrid input", + "#poseGrid button", + "[data-flip]", + "#resetRotationBtn", + "#resetTransformBtn", + "#resetFlipBtn", + "#autoSettingsPanel input", + "#autoSettingsPanel button", + "#savePoseBtn", + "#saveIterateBtn", + "#restorePoseBtn", + "#saveBtn", + "#statusBtn", + "#autoCoarseBtn", + "#autoFineBtn", + ]; + selectors.forEach((selector) => { + document.querySelectorAll(selector).forEach((element) => { + element.disabled = locked; + }); + }); + document.querySelectorAll(".pose-control, .flip-row, .pose-actions, .auto-settings-panel").forEach((element) => { + element.classList.toggle("locked", locked); + }); + updateStretchButtons(); +} + +function setPoseEditingLocked(locked) { + state.poseControlsLocked = Boolean(locked); + if (locked) clearNudgeTimers(); + applyPoseLockState(); +} + function updateSliceControl() { const selected = currentSeries(); const count = Number(selected?.count || 0); @@ -2364,7 +2416,7 @@ async function buildStlModels(volume, onProgress = null) { onProgress?.(100); } -function applyPose(poseInput = state.pose) { +function applyPose(poseInput = state.pose, options = {}) { if (!state.modelGroup) return; const pose = { ...DEFAULT_POSE, ...poseInput }; state.modelGroup.rotation.set( @@ -2380,7 +2432,7 @@ function applyPose(poseInput = state.pose) { scale * (pose.flipZ ? -1 : 1), ); state.modelGroup.updateMatrixWorld(true); - scheduleMappingDraw(); + if (options.scheduleMapping !== false) scheduleMappingDraw(); } function fitCamera() { @@ -2402,6 +2454,7 @@ function resetSceneView() { } function resetPose(kind) { + if (state.poseControlsLocked) return; if (kind === "rotation") { state.pose = { ...state.pose, rotateX: 0, rotateY: 0, rotateZ: 0 }; } else if (kind === "transform") { @@ -2417,6 +2470,10 @@ function resetPose(kind) { } function stretchAxis(axis) { + if (state.poseControlsLocked) { + $("autoResult").textContent = "自动迭代运行中,暂不可调整位姿。"; + return; + } if (!canStretchByAxis()) { const message = "仅当旋转 X/Y/Z 均为 0 或 90° 的倍数时可使用 XYZ 拉伸。"; setAutoState("XYZ 拉伸不可用", "warn"); @@ -2541,27 +2598,165 @@ function resetAutoDeltaBaseline() { updateAutoPoseResult(state.pose, state.pose); } -function setAutoProgress(visible, value = 0, text = "") { +function renderAutoProgressSteps(activeStep = "") { + const stepsEl = $("autoProgressSteps"); + if (!stepsEl) return; + const activeIndex = AUTO_PROGRESS_STEPS.findIndex((step) => step.id === activeStep); + stepsEl.innerHTML = AUTO_PROGRESS_STEPS.map((step, index) => { + const cls = index < activeIndex ? "done" : step.id === activeStep ? "active" : ""; + return `${escapeHtml(step.label)}`; + }).join(""); +} + +function setAutoProgress(visible, value = 0, title = "", detail = "", activeStep = "") { 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; - }); + $("autoProgressText").textContent = `${Math.round(safeValue)}%`; + $("autoProgressTitle").textContent = title || "自动微调处理中"; + $("autoProgressDetail").textContent = detail || "正在准备自动微调任务。"; + renderAutoProgressSteps(activeStep); } function waitForUiFrame() { return new Promise((resolve) => window.requestAnimationFrame(() => resolve())); } +function autoSettingsForStorage(settings) { + const { imageContext, ...rest } = settings || {}; + return rest; +} + +function chooseAutoVolumeEntries(settings) { + const volume = state.volumeMeta; + const frames = volume?.frames || []; + const indices = volume?.indices || []; + if (!frames.length || !indices.length) return []; + const entries = frames.map((frame, index) => ({ frame, slice: Number(indices[index] ?? 0) })); + const limit = Math.max(3, Math.min(entries.length, Number(settings.sampleSlices || 9))); + if (entries.length <= limit) return entries; + const selected = new Map(); + for (let index = 0; index < limit; index += 1) { + const entryIndex = Math.round((index * (entries.length - 1)) / Math.max(limit - 1, 1)); + selected.set(entryIndex, entries[entryIndex]); + } + return [...selected.values()].sort((left, right) => left.slice - right.slice); +} + +function collectAutoSamplePoints(settings, maxPoints = 5200) { + const records = autoReferenceRecords(settings); + const totalVertices = records.reduce((sum, record) => sum + (record.mesh.geometry.attributes.position?.count || 0), 0); + if (!totalVertices) return []; + const stride = Math.max(1, Math.ceil(totalVertices / maxPoints)); + const samples = []; + records.forEach((record) => { + const attribute = record.mesh.geometry.attributes.position; + if (!attribute) return; + for (let index = 0; index < attribute.count; index += stride) { + samples.push({ + x: attribute.getX(index), + y: attribute.getY(index), + z: attribute.getZ(index), + }); + } + }); + return samples; +} + +async function prepareAutoFineImageContext(settings) { + if (!state.volumeMeta || !state.volumeScene) return null; + const entries = chooseAutoVolumeEntries(settings); + const samples = collectAutoSamplePoints(settings); + if (!entries.length || !samples.length) return null; + const frames = []; + for (const [index, entry] of entries.entries()) { + const progress = 20 + (index / Math.max(entries.length, 1)) * 16; + setAutoProgress(true, progress, "读取骨窗采样切片", `正在读取第 ${index + 1}/${entries.length} 张骨窗切片,切片 ${entry.slice + 1}。`, "bone"); + await waitForUiFrame(); + frames.push({ ...entry, image: await loadImageData(entry.frame, 320) }); + } + const sortedSlices = frames.map((entry) => entry.slice).sort((left, right) => left - right); + const gaps = sortedSlices.slice(1).map((slice, index) => Math.abs(slice - sortedSlices[index])); + const sliceTolerance = Math.max(1, Math.ceil((gaps.length ? Math.max(...gaps) : 1) / 2)); + return { + frames, + samples, + sliceTolerance, + volumeScene: { ...state.volumeScene }, + }; +} + +function scorePoseAgainstBoneImages(pose, context, settings) { + if (!context?.frames?.length || !context.samples?.length || !context.volumeScene) return NaN; + const current = { ...state.pose }; + applyPose(pose, { scheduleMapping: false }); + const matrix = modelPoseMatrix(); + applyPose(current, { scheduleMapping: false }); + const volume = context.volumeScene; + const point = new THREE.Vector3(); + let hitReward = 0; + let missPenalty = 0; + let contributors = 0; + for (const sample of context.samples) { + point.set(sample.x, sample.y, sample.z).applyMatrix4(matrix); + if ( + point.x < -volume.width / 2 || point.x > volume.width / 2 + || point.y < -volume.height / 2 || point.y > volume.height / 2 + || point.z < -volume.depth / 2 || point.z > volume.depth / 2 + ) { + missPenalty += 0.85; + contributors += 1; + continue; + } + const slice = volume.total <= 1 ? 0 : ((point.z + volume.depth / 2) / Math.max(volume.depth, 0.001)) * (volume.total - 1); + let nearest = context.frames[0]; + let nearestDistance = Infinity; + for (const frame of context.frames) { + const distance = Math.abs(frame.slice - slice); + if (distance < nearestDistance) { + nearest = frame; + nearestDistance = distance; + } + } + if (nearestDistance > context.sliceTolerance) { + missPenalty += 0.28; + contributors += 1; + continue; + } + const image = nearest.image; + const x = Math.round(((point.x + volume.width / 2) / Math.max(volume.width, 0.001)) * (image.width - 1)); + const y = Math.round(((volume.height / 2 - point.y) / Math.max(volume.height, 0.001)) * (image.height - 1)); + if (x < 0 || x >= image.width || y < 0 || y >= image.height) { + missPenalty += 0.75; + contributors += 1; + continue; + } + const lum = image.data[(y * image.width + x) * 4] || 0; + if (lum >= 170) hitReward += 1; + else if (lum >= 115) hitReward += 0.45; + else if (lum >= 80) hitReward += 0.16; + else missPenalty += 0.9 - lum / 255; + contributors += 1; + } + const safeContributors = Math.max(contributors, 1); + const basePose = settings.basePose || state.autoDeltaBaseline || state.pose; + const movement = Math.sqrt( + ((pose.translateX || 0) - (basePose.translateX || 0)) ** 2 + + ((pose.translateY || 0) - (basePose.translateY || 0)) ** 2 + + ((pose.translateZ || 0) - (basePose.translateZ || 0)) ** 2, + ); + const movementPenalty = movement * Number(settings.movePenalty || 0); + const scalePenalty = Math.abs((pose.scale || 1) - (basePose.scale || 1)) * Number(settings.scalePenalty || 0); + return (hitReward / safeContributors) * Number(settings.boneReward || 1) + - (missPenalty / safeContributors) * (0.25 + Number(settings.outsidePenalty || 0)) + - movementPenalty + - scalePenalty; +} + function restoreAutoPose() { + if (state.poseControlsLocked) return; if (!state.lastAutoPose?.before) { $("autoResult").textContent = "暂无可退回的自动调整位姿。"; return; @@ -2581,9 +2776,9 @@ function candidateScore(pose, settings = null) { const records = autoReferenceRecords(config); if (!records.length) return -Infinity; const current = { ...state.pose }; - applyPose(pose); + applyPose(pose, { scheduleMapping: false }); const modelBox = visibleModelBox(records); - applyPose(current); + applyPose(current, { scheduleMapping: false }); if (!modelBox) return -Infinity; const overlap = modelBox.clone().intersect(state.dicomSceneBox); const overlapSize = new THREE.Vector3(); @@ -2601,7 +2796,12 @@ function candidateScore(pose, settings = null) { const centerPenalty = modelCenter.distanceTo(dicomCenter) / Math.max(dicomSize.length(), 0.001); const scalePenalty = Math.abs((pose.scale || 1) - 1) * (0.03 + Number(config.scalePenalty || 0)); const movePenalty = (Math.abs(pose.translateX || 0) + Math.abs(pose.translateY || 0) + Math.abs(pose.translateZ || 0)) * Number(config.movePenalty || 0); - return (overlapVolume / modelVolume) * Number(config.boneReward || 1) - centerPenalty - scalePenalty - movePenalty - Number(config.outsidePenalty || 0) * 0.01; + const geometryScore = (overlapVolume / modelVolume) * Number(config.boneReward || 1) - centerPenalty - scalePenalty - movePenalty - Number(config.outsidePenalty || 0) * 0.01; + if (config.imageContext) { + const imageScore = scorePoseAgainstBoneImages(pose, config.imageContext, config); + if (Number.isFinite(imageScore)) return imageScore + geometryScore * 0.12; + } + return geometryScore; } function estimateCoarsePose(settings) { @@ -2626,9 +2826,9 @@ function estimateCoarsePose(settings) { } } const previous = { ...state.pose }; - applyPose(candidate); + applyPose(candidate, { scheduleMapping: false }); const nextBox = visibleModelBox(records); - applyPose(previous); + applyPose(previous, { scheduleMapping: false }); if (!nextBox) return candidate; const modelCenter = new THREE.Vector3(); nextBox.getCenter(modelCenter); @@ -2640,6 +2840,7 @@ function estimateCoarsePose(settings) { } function runAutoCoarse() { + if (state.poseControlsLocked) return; if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) { $("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。"; return; @@ -2668,23 +2869,35 @@ function runAutoCoarse() { markDirty(); } -async function runAutoFine() { +async function runAutoFine(options = {}) { if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) { $("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。"; return; } - if (state.autoFineRunning) return; + if (state.autoFineRunning && !options.lockAlreadyHeld) return; const before = { ...state.pose }; - const settings = readAutoSettings(); + const settings = { ...readAutoSettings(), basePose: before }; const adjustable = settings.adjustable; + const lockAlreadyHeld = Boolean(options.lockAlreadyHeld); state.autoFineRunning = true; - setAutoControlsBusy(true); + if (!lockAlreadyHeld) setPoseEditingLocked(true); setAutoState("运行中", "warn"); - setAutoProgress(true, 2, "2%"); - if (state.windowMode !== "bone") setWindowMode("bone", true); - $("autoResult").textContent = "已切换骨窗显示,正在按参考 STL 与 DICOM 三维盒体重叠做微调..."; + setAutoProgress(true, options.fromSaveIterate ? 12 : 4, "锁定位姿参数", "自动微调运行中,位姿输入、XYZ 拉伸和迭代按钮已临时锁定。", options.fromSaveIterate ? "save" : "bone"); + $("autoResult").textContent = "正在准备自动微调:锁定位姿控件并切换骨窗采样。"; await waitForUiFrame(); try { + setAutoProgress(true, 14, "加载骨窗参考", "读取骨窗 DICOM 切片范围,并重建三维参考显示。", "bone"); + if (state.windowMode !== "bone") await setWindowMode("bone", true); + await waitForUiFrame(); + + setAutoProgress(true, 22, "采样参考 STL", "正在从勾选的参考 STL 和骨窗切片中构建评分样本。", "sample"); + settings.imageContext = await prepareAutoFineImageContext(settings); + const imageContextText = settings.imageContext + ? `已采样 ${settings.imageContext.samples.length} 个 STL 点、${settings.imageContext.frames.length} 张骨窗切片。` + : "未能建立骨窗像素样本,将退回 DICOM/STL 三维盒体评分。"; + setAutoProgress(true, 38, "建立候选队列", imageContextText, "score"); + await waitForUiFrame(); + let best = { pose: { ...state.pose }, score: candidateScore(state.pose, settings), mode: "初始位姿" }; let evaluated = 1; const coarsePose = estimateCoarsePose(settings); @@ -2699,24 +2912,51 @@ async function runAutoFine() { }); 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 enabledKeys = []; + if (adjustable.translateX) enabledKeys.push("translateX"); + if (adjustable.translateY) enabledKeys.push("translateY"); + if (adjustable.translateZ) enabledKeys.push("translateZ"); + if (adjustable.scale) enabledKeys.push("scale"); + const stepForKey = (key, ratio = 1) => (key === "scale" ? round.scale : round.move) * ratio; + const poseWithDelta = (sourcePose, key, delta) => { + const baseValue = Number(sourcePose[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)) }; + return { ...sourcePose, [key]: Number(nextValue.toFixed(key === "scale" ? 3 : 4)) }; + }; + enabledKeys.forEach((key) => { + candidates.push({ pose: poseWithDelta(best.pose, key, stepForKey(key)), mode: `${key} +` }); + candidates.push({ pose: poseWithDelta(best.pose, key, -stepForKey(key)), mode: `${key} -` }); + }); + for (let left = 0; left < enabledKeys.length; left += 1) { + for (let right = left + 1; right < enabledKeys.length; right += 1) { + [-1, 1].forEach((leftSign) => { + [-1, 1].forEach((rightSign) => { + const leftKey = enabledKeys[left]; + const rightKey = enabledKeys[right]; + const pose = poseWithDelta( + poseWithDelta(best.pose, leftKey, stepForKey(leftKey, 0.65) * leftSign), + rightKey, + stepForKey(rightKey, 0.65) * rightSign, + ); + candidates.push({ pose, mode: `${leftKey}/${rightKey} 联合` }); + }); + }); + } + } + for (const candidate of candidates.slice(0, settings.candidates)) { + const pose = candidate.pose; const score = candidateScore(pose, settings); evaluated += 1; - if (score > best.score) best = { pose, score, mode: `${key} ${delta > 0 ? "+" : "-"}` }; + if (score > best.score) best = { pose, score, mode: candidate.mode }; } - const progress = 8 + ((index + 1) / Math.max(rounds.length, 1)) * 88; - setAutoProgress(true, progress, `${Math.round(progress)}%`); + const progress = 40 + ((index + 1) / Math.max(rounds.length, 1)) * 48; + const scoreText = Number.isFinite(best.score) ? best.score.toFixed(4) : "不可用"; + setAutoProgress(true, progress, `候选评分 ${index + 1}/${rounds.length}`, `本轮评估 ${candidates.length} 个候选,累计 ${evaluated} 个;当前最佳 ${best.mode},score ${scoreText}。`, "score"); await waitForUiFrame(); } + setAutoProgress(true, 94, "应用最佳位姿", "正在把本轮最高分候选写回位姿控件和融合视图。", "apply"); state.pose = { ...best.pose }; - state.autoResult = { ...best, evaluated, settings: { ...settings, scoring: "reference-stl-box-overlap", window: "bone-display" } }; + state.autoResult = { ...best, evaluated, settings: { ...autoSettingsForStorage(settings), scoring: settings.imageContext ? "bone-window-stl-samples" : "reference-stl-box-overlap", window: "bone" } }; state.lastAutoPose = { before, after: { ...state.pose } }; state.autoDeltaBaseline = { ...before }; renderPoseControls(); @@ -2726,10 +2966,10 @@ async function runAutoFine() { 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 盒体评分,若图像上仍错位,需要先粗配准或手动调整旋转后再微调。`; + ? `最佳候选:${best.mode};score ${scoreText};评估 ${evaluated} 个候选;${settings.imageContext ? "骨窗像素采样 + STL 参考评分" : "参考 STL 盒体评分"}。` + : `已完成评估但最佳仍为初始位姿;score ${scoreText};评估 ${evaluated} 个候选。说明本轮候选没有比当前位姿更高分;若图像上仍错位,通常需要先调整大角度旋转或粗配准后再微调。`; updateAutoPoseResult(before, state.pose); - setAutoProgress(true, 100, "100%"); + setAutoProgress(true, 100, changed ? "微调完成" : "未找到更优候选", changed ? `已应用 ${best.mode}。` : "候选均未超过当前位姿,因此没有强行移动模型。", "apply"); markDirty(); } catch (error) { setAutoState("微调失败", "error"); @@ -2737,10 +2977,27 @@ async function runAutoFine() { setAutoProgress(false); } finally { state.autoFineRunning = false; - setAutoControlsBusy(false); + setPoseEditingLocked(false); } } +async function saveAndIteratePose() { + if (state.autoFineRunning || state.poseControlsLocked) return; + state.autoFineRunning = true; + setPoseEditingLocked(true); + setAutoState("保存中", "warn"); + setAutoProgress(true, 4, "保存当前位姿", "先保存当前人工位姿,并以保存后的位姿作为本轮自动微调起点。", "save"); + const saved = await saveRegistration(); + if (!saved) { + state.autoFineRunning = false; + setPoseEditingLocked(false); + setAutoProgress(false); + return; + } + state.autoFineRunning = false; + await runAutoFine({ lockAlreadyHeld: true, fromSaveIterate: true }); +} + async function saveRegistration(nextStatus = null) { if (!state.activeCase || !currentSeries() || state.saving) return false; state.saving = true; @@ -2813,8 +3070,12 @@ async function saveRegistration(nextStatus = null) { return false; } finally { state.saving = false; - $("saveBtn").disabled = false; - $("statusBtn").disabled = false; + if (state.poseControlsLocked) { + applyPoseLockState(); + } else { + $("saveBtn").disabled = false; + $("statusBtn").disabled = false; + } } } @@ -2853,7 +3114,7 @@ function setWindowMode(mode, reload = true) { state.windowMode = mode || "default"; document.querySelectorAll("[data-window]").forEach((item) => item.classList.toggle("active", item.dataset.window === state.windowMode)); document.querySelectorAll("[data-map-window]").forEach((item) => item.classList.toggle("active", item.dataset.mapWindow === state.windowMode)); - if (reload) loadFusion(); + return reload ? loadFusion() : Promise.resolve(); } function setMappingMode(mode) { @@ -2908,12 +3169,7 @@ function wireEvents() { $("autoCoarseBtn").addEventListener("click", runAutoCoarse); $("autoFineBtn").addEventListener("click", runAutoFine); $("savePoseBtn").addEventListener("click", () => saveRegistration()); - $("saveIterateBtn").addEventListener("click", async () => { - const saved = await saveRegistration(); - if (!saved) return; - await runAutoFine(); - resetAutoDeltaBaseline(); - }); + $("saveIterateBtn").addEventListener("click", saveAndIteratePose); $("restorePoseBtn").addEventListener("click", restoreAutoPose); const syncSampleSlices = (source) => { const range = $("autoSampleSlices"); diff --git a/DICOM_and_UPP配准/static/index.html b/DICOM_and_UPP配准/static/index.html index 3e31b8d..13363b2 100644 --- a/DICOM_and_UPP配准/static/index.html +++ b/DICOM_and_UPP配准/static/index.html @@ -257,8 +257,13 @@
选择 DICOM 序列和 STL 后可运行。
diff --git a/DICOM_and_UPP配准/static/styles.css b/DICOM_and_UPP配准/static/styles.css index ba184a6..2fe9f99 100644 --- a/DICOM_and_UPP配准/static/styles.css +++ b/DICOM_and_UPP配准/static/styles.css @@ -1874,24 +1874,39 @@ input[type="range"] { .auto-progress { display: grid; - grid-template-columns: minmax(0, 1fr) 42px; - align-items: center; - gap: 10px; - min-height: 28px; - border: 1px solid rgba(59, 130, 246, 0.3); + gap: 8px; + min-height: 92px; + border: 1px solid rgba(59, 130, 246, 0.34); border-radius: 8px; background: rgba(7, 10, 15, 0.72); - padding: 7px 8px; + padding: 9px 10px; } -.auto-progress div { +.auto-progress-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.auto-progress-head b { + min-width: 0; + overflow: hidden; + color: #dbeafe; + font-size: 12px; + font-weight: 950; + text-overflow: ellipsis; + white-space: nowrap; +} + +.auto-progress-track { height: 7px; overflow: hidden; border-radius: 99px; background: rgba(148, 163, 184, 0.18); } -.auto-progress i { +.auto-progress-track i { display: block; width: 0%; height: 100%; @@ -1900,7 +1915,7 @@ input[type="range"] { transition: width 160ms ease; } -.auto-progress span { +.auto-progress-head span { color: #c7d2fe; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; @@ -1908,6 +1923,60 @@ input[type="range"] { text-align: right; } +.auto-progress-steps { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 4px; +} + +.auto-progress-steps span { + min-width: 0; + border: 1px solid rgba(148, 163, 184, 0.2); + border-radius: 7px; + background: rgba(15, 23, 42, 0.62); + color: #8ea3bb; + font-size: 10px; + font-weight: 900; + line-height: 1.2; + padding: 5px 3px; + text-align: center; +} + +.auto-progress-steps span.active { + border-color: rgba(34, 211, 238, 0.68); + background: rgba(8, 145, 178, 0.2); + color: #cffafe; +} + +.auto-progress-steps span.done { + border-color: rgba(52, 211, 153, 0.42); + background: rgba(20, 184, 166, 0.12); + color: #a7f3d0; +} + +.auto-progress p { + margin: 0; + color: #9fb3c8; + font-size: 11px; + line-height: 1.45; +} + +.pose-control.locked, +.flip-row.locked, +.pose-actions.locked, +.auto-settings-panel.locked { + opacity: 0.58; +} + +.pose-control button:disabled, +.pose-control input:disabled, +.pose-actions button:disabled, +.flip-row button:disabled, +.auto-box button:disabled, +.auto-box input:disabled { + cursor: not-allowed; +} + .compact-title { min-height: 28px; margin-top: 4px;