From e2be49677fabf66b3ebd652ee11a70285c118672 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 31 May 2026 01:27:32 +0800 Subject: [PATCH] Compact STL list and clarify auto iteration --- DICOM_and_UPP配准/static/app.js | 114 +++++++++++++++++++++------- DICOM_and_UPP配准/static/index.html | 4 +- DICOM_and_UPP配准/static/styles.css | 42 ++++++---- 3 files changed, 112 insertions(+), 48 deletions(-) diff --git a/DICOM_and_UPP配准/static/app.js b/DICOM_and_UPP配准/static/app.js index 871a17a..0fbc169 100644 --- a/DICOM_and_UPP配准/static/app.js +++ b/DICOM_and_UPP配准/static/app.js @@ -900,6 +900,11 @@ function stlPresentation(file) { }; } +function compactStlName(file) { + return String(file.segment_name || file.family || file.file_name || `STL_${file.id || ""}`) + .replace(/\.stl$/i, ""); +} + function stlFileIndex(file) { const id = normalizedStlId(file); const index = state.stlFiles.findIndex((item) => normalizedStlId(item) === id); @@ -976,6 +981,37 @@ async function setAllStlMode(mode) { await applyStlSelectionChange(); } +function aggregateStlMode() { + if (!state.stlFiles.length || !state.selectedStlIds.size) return "hidden"; + const modes = state.stlFiles.map((file) => stlMode(file)); + if (modes.every((mode) => mode === "solid")) return "solid"; + if (modes.every((mode) => mode === "translucent")) return "translucent"; + if (modes.every((mode) => mode === "hidden")) return "hidden"; + return "mixed"; +} + +function renderStlVisibilityAction() { + const button = $("cycleAllStlModeBtn"); + if (!button) return; + const mode = aggregateStlMode(); + button.classList.remove("hidden-eye", "solid-eye", "translucent-eye", "mixed-eye"); + button.classList.add(`${mode === "mixed" ? "mixed" : mode}-eye`); + const label = { + hidden: "当前全部隐藏,点击切换为全部实体", + solid: "当前全部实体,点击切换为全部半透明", + translucent: "当前全部半透明,点击切换为全部隐藏", + mixed: "当前为混合显示,点击切换为全部实体", + }[mode]; + button.title = label; + button.setAttribute("aria-label", label); +} + +async function cycleAllStlMode() { + const mode = aggregateStlMode(); + const next = mode === "solid" ? "translucent" : mode === "translucent" ? "hidden" : "solid"; + await setAllStlMode(next); +} + function currentSeries() { return state.series.find((item) => item.series_uid === state.selectedSeriesUid) || null; } @@ -1078,6 +1114,7 @@ async function selectSeries(uid, options = {}) { function renderStl() { $("stlCount").textContent = state.stlFiles.length ? `${state.selectedStlIds.size}/${state.stlFiles.length}` : "0"; + renderStlVisibilityAction(); const models = [...new Set(state.stlFiles.map((item) => item.algorithm_model || state.algorithmModel || "未指定模型"))]; $("modelRail").innerHTML = models.map((model) => `${escapeHtml(model)}`).join(""); if (!state.stlFiles.length) { @@ -1124,9 +1161,7 @@ function renderStl() { return ` @@ -3122,6 +3157,14 @@ function candidateScore(pose, settings = null) { return geometryScore; } +function describeAutoReference(settings) { + const records = autoReferenceRecords(settings); + if (!records.length) return "当前没有可用参考 STL"; + const names = records.slice(0, 4).map((record) => compactStlName(record.file)); + const suffix = records.length > names.length ? ` 等 ${records.length} 个` : ""; + return `参考 STL:${names.join("、")}${suffix}`; +} + function estimateCoarsePose(settings) { const records = autoReferenceRecords(settings); const modelBox = visibleModelBox(records); @@ -3219,6 +3262,7 @@ async function runAutoFine(options = {}) { let best = { pose: { ...state.pose }, score: candidateScore(state.pose, settings), mode: "初始位姿" }; let evaluated = 1; const coarsePose = estimateCoarsePose(settings); + const coarseSeed = coarsePose ? { pose: coarsePose, mode: "粗配准种子" } : null; if (coarsePose) { const coarseScore = candidateScore(coarsePose, settings); evaluated += 1; @@ -3241,27 +3285,40 @@ async function runAutoFine(options = {}) { const nextValue = key === "scale" ? Math.max(0.2, Math.min(3, baseValue + delta)) : baseValue + delta; 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} 联合` }); + const searchBases = [{ pose: best.pose, mode: best.mode }]; + if (coarseSeed && poseSignature(coarseSeed.pose) !== poseSignature(best.pose)) searchBases.push(coarseSeed); + const seenCandidates = new Set(); + const addCandidate = (pose, mode) => { + const signature = poseSignature(pose); + if (seenCandidates.has(signature)) return; + seenCandidates.add(signature); + candidates.push({ pose, mode }); + }; + searchBases.forEach((base) => { + enabledKeys.forEach((key) => { + addCandidate(poseWithDelta(base.pose, key, stepForKey(key)), `${base.mode} · ${key} +`); + addCandidate(poseWithDelta(base.pose, key, -stepForKey(key)), `${base.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(base.pose, leftKey, stepForKey(leftKey, 0.65) * leftSign), + rightKey, + stepForKey(rightKey, 0.65) * rightSign, + ); + addCandidate(pose, `${base.mode} · ${leftKey}/${rightKey} 联合`); + }); }); - }); + } } - } - for (const candidate of candidates.slice(0, settings.candidates)) { + }); + const candidateLimit = Math.max(settings.candidates, settings.candidates * searchBases.length); + const candidatesToEvaluate = candidates.slice(0, candidateLimit); + for (const candidate of candidatesToEvaluate) { const pose = candidate.pose; const score = candidateScore(pose, settings); evaluated += 1; @@ -3269,7 +3326,7 @@ async function runAutoFine(options = {}) { } 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"); + setAutoProgress(true, progress, `候选评分 ${index + 1}/${rounds.length}`, `本轮评估 ${candidatesToEvaluate.length}/${candidates.length} 个候选,累计 ${evaluated} 个;当前最高分候选 ${best.mode},score ${scoreText}。`, "score"); await waitForUiFrame(); } setAutoProgress(true, 94, "应用最佳位姿", "正在把本轮最高分候选写回位姿控件和融合视图。", "apply"); @@ -3283,11 +3340,12 @@ async function runAutoFine(options = {}) { const changed = poseSignature(best.pose) !== poseSignature(before); setAutoState(changed ? "微调完成" : "微调无变化", changed ? "ok" : "warn"); const scoreText = Number.isFinite(best.score) ? best.score.toFixed(4) : "不可用"; + const referenceText = describeAutoReference(settings); $("autoResult").textContent = changed - ? `最佳候选:${best.mode};score ${scoreText};评估 ${evaluated} 个候选;${settings.imageContext ? "骨窗像素采样 + STL 参考评分" : "参考 STL 盒体评分"}。` - : `已完成评估但最佳仍为初始位姿;score ${scoreText};评估 ${evaluated} 个候选。说明本轮候选没有比当前位姿更高分;若图像上仍错位,通常需要先调整大角度旋转或粗配准后再微调。`; + ? `本轮最高分候选:${best.mode};score ${scoreText};评估 ${evaluated} 个候选;${settings.imageContext ? "骨窗像素采样 + STL 参考评分" : "参考 STL 盒体评分"};${referenceText}。` + : `本轮未找到比当前位姿更高分的候选;score ${scoreText};评估 ${evaluated} 个候选。这不代表已配准正确,只说明当前搜索范围和参考 STL 下没有更高分;${referenceText};自动微调目前只调整平移/缩放,明显旋转错位需要先手动校正。`; updateAutoPoseResult(before, state.pose); - setAutoProgress(true, 100, changed ? "微调完成" : "未找到更优候选", changed ? `已应用 ${best.mode}。` : "候选均未超过当前位姿,因此没有强行移动模型。", "apply"); + setAutoProgress(true, 100, changed ? "微调完成" : "本轮未改动", changed ? `已应用 ${best.mode}。` : "没有强行移动模型;请检查参考 STL 和大角度旋转。", "apply"); markDirty(); } catch (error) { setAutoState("微调失败", "error"); @@ -3480,9 +3538,7 @@ function wireEvents() { $("saveBtn").addEventListener("click", () => saveRegistration()); $("statusBtn").addEventListener("click", () => saveRegistration(state.registrationStatus === "registered" ? "unregistered" : "registered")); $("notes").addEventListener("input", () => markDirty("notes")); - $("hideAllStlBtn").addEventListener("click", () => setAllStlMode("hidden")); - $("solidAllStlBtn").addEventListener("click", () => setAllStlMode("solid")); - $("translucentAllStlBtn").addEventListener("click", () => setAllStlMode("translucent")); + $("cycleAllStlModeBtn").addEventListener("click", cycleAllStlMode); $("resetRotationBtn").addEventListener("click", () => resetPose("rotation")); $("resetTransformBtn").addEventListener("click", () => resetPose("transform")); $("resetFlipBtn").addEventListener("click", () => resetPose("flip")); diff --git a/DICOM_and_UPP配准/static/index.html b/DICOM_and_UPP配准/static/index.html index 01ac339..2d6b432 100644 --- a/DICOM_and_UPP配准/static/index.html +++ b/DICOM_and_UPP配准/static/index.html @@ -170,9 +170,7 @@
STL
- - - +
0
diff --git a/DICOM_and_UPP配准/static/styles.css b/DICOM_and_UPP配准/static/styles.css index 2a00773..de87d04 100644 --- a/DICOM_and_UPP配准/static/styles.css +++ b/DICOM_and_UPP配准/static/styles.css @@ -817,7 +817,7 @@ button { } .tool-section-title button.model-eye-btn { - width: 30px; + width: 34px; height: 28px; display: inline-flex; align-items: center; @@ -861,6 +861,15 @@ button { rgba(45, 212, 191, 0.22); } +.mixed-eye span::before { + border-color: #93c5fd; + background: linear-gradient(90deg, rgba(45, 212, 191, 0.26) 0 50%, rgba(15, 23, 42, 0.74) 50% 100%); +} + +.mixed-eye span::after { + background: #93c5fd; +} + .hidden-eye span::after { top: 5px; left: -2px; @@ -1804,7 +1813,8 @@ input[type="range"] { grid-template-columns: minmax(0, 1fr) 30px; gap: 12px; align-items: center; - padding: 10px; + min-height: 38px; + padding: 7px 10px; cursor: pointer; transition: border-color 0.16s ease, background 0.16s ease, opacity 0.16s ease; } @@ -1818,7 +1828,7 @@ input[type="range"] { display: block; overflow: hidden; color: #e8f1fb; - font-size: 13px; + font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } @@ -1828,12 +1838,12 @@ input[type="range"] { } .stl-family { - margin-bottom: 10px; + margin-bottom: 7px; } .stl-family-head { width: 100%; - min-height: 32px; + min-height: 30px; display: grid; grid-template-columns: minmax(0, 1fr) auto 14px; align-items: center; @@ -1885,8 +1895,8 @@ input[type="range"] { } .stl-hierarchy-group .stl-family-head { - min-height: 36px; - margin-bottom: 6px; + min-height: 32px; + margin-bottom: 5px; border-color: rgba(148, 163, 184, 0.22); border-radius: 7px; background: linear-gradient(180deg, rgba(31, 41, 59, 0.94), rgba(15, 23, 42, 0.94)); @@ -1899,20 +1909,20 @@ input[type="range"] { } .stl-hierarchy-group .stl-family-list { - margin: -2px 0 10px 12px; - padding-left: 10px; + margin: -2px 0 8px 12px; + padding-left: 8px; border-left: 1px solid rgba(45, 212, 191, 0.22); } .stl-hierarchy-group .stl-row { - min-height: 58px; - margin-bottom: 7px; - border-radius: 10px; + min-height: 38px; + margin-bottom: 5px; + border-radius: 8px; } .stl-visibility-swatch { - width: 24px; - height: 24px; + width: 22px; + height: 22px; justify-self: end; border: 2px solid rgba(226, 232, 240, 0.68); border-radius: 5px; @@ -1940,9 +1950,9 @@ input[type="range"] { .stl-visibility-swatch.hidden::after { content: ""; display: block; - width: 26px; + width: 24px; height: 2px; - margin: 9px 0 0 -3px; + margin: 8px 0 0 -3px; border-radius: 99px; background: rgba(226, 232, 240, 0.76); transform: rotate(-45deg);