From 5cac15fbed3e156998a34ce9c3df0aed1546b0cb Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 31 May 2026 01:03:50 +0800 Subject: [PATCH] Group liver STL models by anatomy --- DICOM_and_UPP配准/static/app.js | 227 +++++++++++++++++++++++----- DICOM_and_UPP配准/static/styles.css | 30 ++++ 2 files changed, 221 insertions(+), 36 deletions(-) diff --git a/DICOM_and_UPP配准/static/app.js b/DICOM_and_UPP配准/static/app.js index 225caa5..871a17a 100644 --- a/DICOM_and_UPP配准/static/app.js +++ b/DICOM_and_UPP配准/static/app.js @@ -71,6 +71,46 @@ const STL_DISPLAY_LABELS = { solid: "实体显示", translucent: "半透明", }; +const LIVER_STL_GROUPS = [ + { key: "liver_segments", label: "肝段", order: 10 }, + { key: "liver_body", label: "肝脏", order: 20 }, + { key: "liver_vessels", label: "肝血管", order: 30 }, + { key: "liver_lesions", label: "肿瘤和囊肿", order: 40 }, + { key: "body_surface", label: "骨骼皮肤", order: 50 }, + { key: "other", label: "其他", order: 90 }, +]; +const LIVER_STL_GROUP_LOOKUP = Object.fromEntries(LIVER_STL_GROUPS.map((group) => [group.key, group])); +const LIVER_STL_LABELS = { + bile_duct: "胆管", + cholecyst: "胆囊", + gallbladder: "胆囊", + hipbone: "髋骨", + liver: "肝脏整体", + liver_artery: "肝动脉", + liver_left: "左肝叶", + liver_right: "右肝叶", + liver_vein: "肝静脉", + pancreas: "胰腺", + portal_vein: "门静脉", + rib: "肋骨", + ribs: "肋骨", + sacrum: "骶骨", + skin: "皮肤", + spleen: "脾脏", + sternum: "胸骨", + vertebra: "椎骨", + vertebrae: "椎骨", +}; +const ROMAN_NUMERALS = { + 1: "I", + 2: "II", + 3: "III", + 4: "IV", + 5: "V", + 6: "VI", + 7: "VII", + 8: "VIII", +}; const state = { token: localStorage.getItem("dicom_upp_registration_token") || "", @@ -760,6 +800,106 @@ function stlFamilyLabel(file) { return String(file.family || "未分类"); } +function normalizedStlText(file) { + return [ + file.family, + file.segment_name, + file.file_name, + file.category, + ].map((value) => String(value || "").toLowerCase()).join(" "); +} + +function normalizedStlName(file) { + return String(file.segment_name || file.family || file.file_name || "") + .replace(/\.stl$/i, "") + .toLowerCase(); +} + +function stlSegmentNumber(file) { + const text = normalizedStlText(file); + const match = text.match(/(?:segment[_\s-]*s?|肝段)\s*([0-9]+)/i) || text.match(/_s([0-9]+)/i); + return match ? Number(match[1]) : 999; +} + +function lesionNumber(file) { + const text = normalizedStlText(file); + const match = text.match(/(?:^|[_\s-])(?:liver[_\s-]*)?(?:tumor|lesion|cyst)[_\s-]*([0-9]+)/i) + || text.match(/(?:肿瘤|囊肿)[_\s-]*([0-9]+)/i); + return match ? Number(match[1]) : 99; +} + +function isTumorStl(file, category = String(file.category || "")) { + const text = normalizedStlText(file); + return /(?:^|[_\s-])(?:liver[_\s-]*)?(?:tumor|lesion)(?:[_\s-]|\d|$)/i.test(text) || category.includes("肿瘤"); +} + +function isCystStl(file, category = String(file.category || "")) { + const text = normalizedStlText(file); + return /(?:^|[_\s-])(?:liver[_\s-]*)?cyst(?:[_\s-]|\d|$)/i.test(text) || category.includes("囊肿"); +} + +function numberedLesionLabel(file, baseLabel) { + const number = lesionNumber(file); + return number < 99 ? `${baseLabel} ${number}` : baseLabel; +} + +function liverStlPresentation(file) { + const text = normalizedStlText(file); + const name = normalizedStlName(file); + const category = String(file.category || ""); + let groupKey = "other"; + let itemOrder = 900; + let label = LIVER_STL_LABELS[name] || file.segment_name || file.family || file.file_name; + + if (text.includes("liver_segment") || category.includes("肝段")) { + const segment = stlSegmentNumber(file); + groupKey = "liver_segments"; + itemOrder = segment; + label = `肝段 ${ROMAN_NUMERALS[segment] || segment}`; + } else if (["liver", "liver_left", "liver_right"].includes(name) || category.includes("肝脏主体")) { + groupKey = "liver_body"; + itemOrder = { liver: 1, liver_left: 2, liver_right: 3 }[name] || 99; + } else if (["liver_artery", "liver_vein", "portal_vein"].includes(name)) { + groupKey = "liver_vessels"; + itemOrder = { liver_artery: 1, liver_vein: 2, portal_vein: 3 }[name] || 99; + } else if (isTumorStl(file, category) || isCystStl(file, category)) { + const tumor = isTumorStl(file, category); + groupKey = "liver_lesions"; + itemOrder = (tumor ? 1 : 20) + lesionNumber(file) / 100; + label = tumor + ? numberedLesionLabel(file, "肝脏肿瘤") + : numberedLesionLabel(file, "肝囊肿"); + } else if (category.includes("体表骨骼") || ["hipbone", "rib", "ribs", "sacrum", "skin", "sternum", "vertebra", "vertebrae"].includes(name)) { + groupKey = "body_surface"; + itemOrder = { vertebrae: 1, vertebra: 1, rib: 2, ribs: 2, sternum: 3, sacrum: 4, hipbone: 5, skin: 6 }[name] || 99; + } else { + itemOrder = { bile_duct: 1, cholecyst: 2, gallbladder: 2, pancreas: 3, spleen: 4 }[name] || 99; + } + + const group = LIVER_STL_GROUP_LOOKUP[groupKey] || LIVER_STL_GROUP_LOOKUP.other; + return { + key: group.key, + label: group.label, + order: group.order, + itemOrder, + itemLabel: label, + hierarchy: true, + }; +} + +function stlPresentation(file) { + if (isLiverBiliaryModel(file.algorithm_model || state.algorithmModel)) return liverStlPresentation(file); + const label = stlFamilyLabel(file); + return { + key: label, + label, + order: 100, + itemOrder: 100, + itemLabel: file.segment_name || file.file_name, + hierarchy: false, + }; +} + function stlFileIndex(file) { const id = normalizedStlId(file); const index = state.stlFiles.findIndex((item) => normalizedStlId(item) === id); @@ -947,45 +1087,60 @@ function renderStl() { } const grouped = new Map(); state.stlFiles.forEach((file, index) => { - const family = stlFamilyLabel(file); - if (!grouped.has(family)) grouped.set(family, []); - grouped.get(family).push({ file, index }); + const presentation = stlPresentation(file); + if (!grouped.has(presentation.key)) { + grouped.set(presentation.key, { + key: presentation.key, + label: presentation.label, + order: presentation.order, + hierarchy: presentation.hierarchy, + rows: [], + }); + } + grouped.get(presentation.key).rows.push({ file, index, presentation }); }); - $("stlList").innerHTML = [...grouped.entries()].map(([family, rows]) => { - const visibleCount = rows.filter(({ file }) => stlMode(file) !== "hidden").length; - const collapsed = state.collapsedStlFamilies.has(family); - return ` -
- -
- ${rows.map(({ file, index }) => { - const id = normalizedStlId(file); - const mode = stlMode(file); - const color = stlColor(file, index); - return ` - - `; - }).join("")} -
-
- `; - }).join(""); + $("stlList").innerHTML = [...grouped.values()] + .sort((a, b) => a.order - b.order || a.label.localeCompare(b.label)) + .map((group) => { + const rows = group.rows.sort((a, b) => ( + a.presentation.itemOrder - b.presentation.itemOrder + || String(a.presentation.itemLabel).localeCompare(String(b.presentation.itemLabel)) + || a.index - b.index + )); + const visibleCount = rows.filter(({ file }) => stlMode(file) !== "hidden").length; + const collapsed = state.collapsedStlFamilies.has(group.key); + return ` +
+ +
+ ${rows.map(({ file, index, presentation }) => { + const id = normalizedStlId(file); + const mode = stlMode(file); + const color = stlColor(file, index); + return ` + + `; + }).join("")} +
+
+ `; + }).join(""); $("stlList").querySelectorAll("[data-family-toggle]").forEach((button) => { button.addEventListener("click", () => { - const family = button.dataset.familyToggle; - if (state.collapsedStlFamilies.has(family)) state.collapsedStlFamilies.delete(family); - else state.collapsedStlFamilies.add(family); + const groupKey = button.dataset.familyToggle; + if (state.collapsedStlFamilies.has(groupKey)) state.collapsedStlFamilies.delete(groupKey); + else state.collapsedStlFamilies.add(groupKey); renderStl(); }); }); diff --git a/DICOM_and_UPP配准/static/styles.css b/DICOM_and_UPP配准/static/styles.css index 7863fbe..2a00773 100644 --- a/DICOM_and_UPP配准/static/styles.css +++ b/DICOM_and_UPP配准/static/styles.css @@ -1880,6 +1880,36 @@ input[type="range"] { transform: rotate(-45deg); } +.stl-hierarchy-group { + margin-bottom: 8px; +} + +.stl-hierarchy-group .stl-family-head { + min-height: 36px; + margin-bottom: 6px; + 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)); + color: #eef7ff; +} + +.stl-hierarchy-group:not(.collapsed) .stl-family-head { + border-color: rgba(45, 212, 191, 0.58); + box-shadow: inset 3px 0 0 rgba(45, 212, 191, 0.85); +} + +.stl-hierarchy-group .stl-family-list { + margin: -2px 0 10px 12px; + padding-left: 10px; + border-left: 1px solid rgba(45, 212, 191, 0.22); +} + +.stl-hierarchy-group .stl-row { + min-height: 58px; + margin-bottom: 7px; + border-radius: 10px; +} + .stl-visibility-swatch { width: 24px; height: 24px;