Spaces:
Running
Running
Download index.html from Sssunset/MedPLIB-BRISC: direct link, hf CLI and curl.
- Browser
- Download file 26.2 kB
-
https://huggingface.co/spaces/Sssunset/MedPLIB-BRISC/resolve/main/index.html
- Command line
-
hf download hf://spaces/Sssunset/MedPLIB-BRISC/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Sssunset/MedPLIB-BRISC/resolve/main/index.html
26.2 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>MedPLIB-BRISC: Joint Brain Tumor Classification and Segmentation</title> | |
| <meta name="description" content="One multimodal large language model that names the brain tumor type and draws the tumor mask in the same response."> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&family=Source+Serif+4:wght@600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css"> | |
| <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script> | |
| <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"></script> | |
| <script>document.addEventListener("DOMContentLoaded", () => renderMathInElement(document.getElementById("method"), {delimiters: [{left: "\\[", right: "\\]", display: true}, {left: "\\(", right: "\\)", display: false}]}));</script> | |
| <style> | |
| :root { | |
| --ink: #1d2330; --ink-2: #4a5263; --ink-3: #7b8394; | |
| --line: #e3e6ec; --soft: #f5f7fa; | |
| --accent: #2f5fb3; --accent-soft: #e8eef9; | |
| --good: #1f7a4d; --bad: #b3261e; --seg: #2f5fb3; | |
| } | |
| * { box-sizing: border-box; } | |
| html { scroll-behavior: smooth; } | |
| body { margin: 0; background: #fff; color: var(--ink); font: 400 17px/1.65 "Noto Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } | |
| a { color: var(--accent); text-decoration: none; } | |
| a:hover { text-decoration: underline; } | |
| .wrap { max-width: 1000px; margin: 0 auto; padding: 0 20px; } | |
| .narrow { max-width: 820px; margin-left: auto; margin-right: auto; } | |
| code, .mono { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 0.9em; } | |
| .seg { color: var(--seg); font-weight: 600; } | |
| header { padding: 60px 0 24px; text-align: center; } | |
| h1 { font: 700 clamp(28px, 4.6vw, 44px)/1.18 "Source Serif 4", Georgia, serif; margin: 0 auto 18px; max-width: 900px; letter-spacing: -0.01em; } | |
| .authors { margin: 0 0 6px; } | |
| .authors span { white-space: nowrap; margin: 0 9px; } | |
| .affil { color: var(--ink-2); font-size: 16px; margin: 0 0 22px; } | |
| .buttons { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; } | |
| .btn { display: inline-flex; align-items: center; gap: 8px; padding: 9px 18px; border-radius: 999px; background: #22262e; color: #fff; font-weight: 500; font-size: 15px; } | |
| .btn:hover { background: #3a404b; text-decoration: none; } | |
| .btn svg { width: 17px; height: 17px; fill: currentColor; } | |
| section { padding: 38px 0 6px; } | |
| h2 { font: 700 28px/1.25 "Source Serif 4", Georgia, serif; text-align: center; margin: 0 0 8px; } | |
| .sub { text-align: center; color: var(--ink-2); margin: 0 auto 22px; max-width: 720px; } | |
| h3 { font-size: 18px; margin: 22px 0 8px; } | |
| p { margin: 0 0 14px; } | |
| figure { margin: 22px 0; } | |
| .eq { overflow-x: auto; margin: 4px 0 14px; } | |
| figure img { display: block; width: 100%; height: auto; } | |
| figcaption { color: var(--ink-2); font-size: 15px; line-height: 1.55; margin-top: 10px; } | |
| figcaption b { color: var(--ink); } | |
| /* demo */ | |
| .demo { display: grid; grid-template-columns: 120px minmax(0, 1fr) minmax(0, 1.15fr); gap: 22px; align-items: start; border: 1px solid var(--line); border-radius: 12px; padding: 20px; background: #fff; } | |
| .thumbs { display: grid; gap: 10px; } | |
| .thumb { padding: 0; border: 2px solid transparent; border-radius: 8px; background: none; cursor: pointer; overflow: hidden; } | |
| .thumb img { display: block; width: 100%; aspect-ratio: 1; } | |
| .thumb[aria-pressed="true"] { border-color: var(--accent); } | |
| .thumb span { display: block; font-size: 12px; color: var(--ink-2); padding: 3px 0; background: var(--soft); } | |
| .viewer img { width: 100%; border-radius: 8px; display: block; aspect-ratio: 1; background: #000; } | |
| .viewer .cap { font-size: 14px; color: var(--ink-2); margin-top: 8px; } | |
| .prompt { background: var(--soft); border: 1px dashed #b9c0cc; border-radius: 8px; padding: 10px 12px; font-size: 14.5px; margin-bottom: 14px; } | |
| .prompt .who { display: block; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3); margin-bottom: 4px; } | |
| .cands { display: grid; gap: 8px; } | |
| .cand { display: grid; grid-template-columns: 150px minmax(0, 1fr) 74px; gap: 10px; align-items: center; font-size: 14.5px; padding: 6px 8px; border-radius: 8px; border: 1px solid var(--line); } | |
| .cand.win { border-color: var(--accent); background: var(--accent-soft); font-weight: 600; } | |
| .bar { height: 10px; background: var(--line); border-radius: 5px; overflow: hidden; } | |
| .bar i { display: block; height: 100%; background: var(--accent); border-radius: 5px; } | |
| .cand .p { text-align: right; font-family: "JetBrains Mono", monospace; font-size: 13px; } | |
| .result { margin-top: 14px; font-size: 15px; } | |
| .runbtn { margin-top: 14px; padding: 8px 16px; border-radius: 999px; border: 0; background: var(--accent); color: #fff; font: 500 15px "Noto Sans", sans-serif; cursor: pointer; } | |
| @media (max-width: 820px) { .demo { grid-template-columns: minmax(0, 1fr); } .thumbs { grid-template-columns: repeat(4, minmax(0, 1fr)); } .cand { grid-template-columns: 120px minmax(0, 1fr) 66px; } } | |
| /* language */ | |
| .chats { display: grid; gap: 14px; margin-top: 18px; } | |
| .chat { border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; } | |
| .chat .q { font-size: 14.5px; margin-bottom: 10px; } | |
| .chat .q b { color: var(--ink-3); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; display: block; margin-bottom: 3px; } | |
| .pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; } | |
| .ans { background: var(--soft); border-radius: 8px; padding: 10px 12px; font-size: 14.5px; } | |
| .ans .who { display: block; font-size: 12px; color: var(--ink-3); margin-bottom: 4px; } | |
| .ans.after { background: var(--accent-soft); } | |
| .tag { display: inline-block; font-size: 12px; font-weight: 600; padding: 1px 8px; border-radius: 999px; margin-left: 6px; } | |
| .tag.ok { background: #e3f3ea; color: var(--good); } | |
| .tag.no { background: #fbe7e5; color: var(--bad); } | |
| @media (max-width: 700px) { .pair { grid-template-columns: minmax(0, 1fr); } } | |
| /* tables */ | |
| .table-wrap { overflow-x: auto; margin: 16px 0 8px; } | |
| table { border-collapse: collapse; width: 100%; font-size: 15px; } | |
| th, td { padding: 8px 10px; border-bottom: 1px solid var(--line); text-align: center; white-space: nowrap; } | |
| th { font-weight: 600; border-bottom: 2px solid var(--ink); white-space: normal; vertical-align: bottom; line-height: 1.3; } | |
| td:first-child { white-space: normal; min-width: 190px; } | |
| @media (max-width: 700px) { table { font-size: 13.5px; } th, td { padding: 7px 6px; } } | |
| td:first-child, th:first-child { text-align: left; } | |
| tr.ours td { background: var(--accent-soft); font-weight: 600; } | |
| tbody tr:last-child td { border-bottom: 2px solid var(--ink); } | |
| .note { color: var(--ink-3); font-size: 14px; line-height: 1.5; } | |
| /* gallery */ | |
| .tabs { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin: 6px 0 16px; } | |
| .tab { border: 1px solid var(--line); background: #fff; border-radius: 999px; padding: 7px 16px; font: 500 15px "Noto Sans", sans-serif; cursor: pointer; color: var(--ink-2); } | |
| .tab[aria-selected="true"] { background: var(--ink); color: #fff; border-color: var(--ink); } | |
| .legend { display: flex; gap: 18px; justify-content: center; font-size: 14px; color: var(--ink-2); margin-bottom: 12px; flex-wrap: wrap; } | |
| .legend i { display: inline-block; width: 22px; height: 0; border-top: 3px solid #00C853; vertical-align: middle; margin-right: 6px; } | |
| .legend i.pred { height: 12px; border: 1.5px solid #FF1744; background: rgba(255, 23, 68, 0.45); border-radius: 2px; width: 16px; } | |
| .case { margin-bottom: 18px; } | |
| .case h4 { margin: 0 0 8px; font-size: 15px; font-weight: 600; } | |
| .row4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } | |
| .cell img { display: block; width: 100%; aspect-ratio: 1; border-radius: 6px; background: #000; } | |
| .cell .t { font-size: 12.5px; color: var(--ink-2); margin-top: 4px; line-height: 1.35; text-align: center; } | |
| .cell .t b { color: var(--ink); } | |
| .cell .t .bad { color: var(--bad); } | |
| .colhead { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; font-size: 13px; font-weight: 600; text-align: center; margin-bottom: 6px; } | |
| @media (max-width: 640px) { .row4, .colhead { grid-template-columns: repeat(2, minmax(0, 1fr)); } .colhead { display: none; } } | |
| .team { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 10px; } | |
| .member { border: 1px solid var(--line); border-radius: 10px; padding: 14px; text-align: center; } | |
| .member .n { font-weight: 600; } | |
| .member .s { color: var(--ink-2); font-size: 14px; } | |
| @media (max-width: 760px) { .team { grid-template-columns: repeat(2, minmax(0, 1fr)); } } | |
| pre { background: var(--soft); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; overflow-x: auto; font-size: 13px; line-height: 1.5; } | |
| footer { border-top: 1px solid var(--line); margin-top: 48px; padding: 22px 0 40px; color: var(--ink-3); font-size: 14px; text-align: center; } | |
| </style> | |
| </head> | |
| <body> | |
| <header class="wrap"> | |
| <h1>Joint Brain Tumor Classification and Segmentation from MRI with an Adapted Multimodal Large Language Model</h1> | |
| <p class="authors"> | |
| <span>Simon Tong Sing Hee</span><span>Zeng Yi</span><span>Feng Peilin</span> | |
| <span>Ye Xiaomeng</span><span>Lyu Muyang</span><span>Timothy Aw Bang Hao</span> | |
| </p> | |
| <p class="affil">Nanyang Technological University · GP8001 Group Project, Group 3</p> | |
| <div class="buttons"> | |
| <a class="btn" href="https://github.com/Peilin-FF/Tumor"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>Code</a> | |
| <a class="btn" href="https://huggingface.co/Sssunset/MedPLIB-BRISC"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2a10 10 0 100 20 10 10 0 000-20zm-3.5 7a1.5 1.5 0 110 3 1.5 1.5 0 010-3zm7 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3zM12 18c-2.6 0-4.6-1.5-5.3-3.5h10.6C16.6 16.5 14.6 18 12 18z"/></svg>Model</a> | |
| <a class="btn" href="#demo"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>Try the demo</a> | |
| </div> | |
| </header> | |
| <main class="wrap"> | |
| <section class="narrow"> | |
| <p>Brain tumor MRI analysis asks two questions: which type of tumor is present, and where it is. They are usually answered by a classifier and a separate segmentation network that never share information, so their answers can contradict each other. We adapt MedPLIB-7b-2e, a biomedical multimodal large language model, so that one model answers both questions in one response. It writes the tumor type followed by a special <span class="seg"><SEG></span> token, and the hidden state of that token is decoded into the tumor mask.</p> | |
| </section> | |
| <section class="narrow" id="method"> | |
| <h2>Method</h2> | |
| <p class="sub">One model, one answer, two outputs.</p> | |
| <h3>The class and the mask come from one answer</h3> | |
| <p>Given an MRI slice \(x\) and a fixed instruction \(q\), <a href="https://arxiv.org/abs/2412.09278">MedPLIB</a> writes an answer of the form “\(c\) <span class="seg"><SEG></span>”, where \(c\) is one of the four classes. A projector \(P\) maps the hidden state \(h\) of the <span class="seg"><SEG></span> token to a prompt vector. The <a href="https://arxiv.org/abs/2308.16184">SAM-Med2D</a> mask decoder \(D\) combines this prompt with the image features \(F = E(x)\):</p> | |
| <div class="eq">\[ S = \sigma\big(D(F, P(h))\big), \qquad \hat{M}_{ij} = \mathbb{I}\big[S_{ij} > 0.5\big]. \]</div> | |
| <p>The prompt takes the place of the click or box that a user would normally give to SAM. The mask is therefore drawn from what the language model has just decided.</p> | |
| <h3>The answer is chosen by scoring</h3> | |
| <p>Only four answers are valid, so we do not parse free text. Each candidate \(a^{(c)}\) is scored by its log-likelihood and the best one is kept:</p> | |
| <div class="eq">\[ \hat{y} = \arg\max_{c \in \mathcal{C}} \sum_{t=1}^{T_c} \log p_\theta\big(a^{(c)}_t \mid a^{(c)}_{<t}, x, q\big). \]</div> | |
| <p>The mask comes from the <span class="seg"><SEG></span> state of the winning answer. No rule removes it afterwards, so an empty mask on a non-tumorous slice has to come from the model itself.</p> | |
| <h3>Two-stage adaptation</h3> | |
| <p>Stage A trains only the modules that connect the parts: the image projector, the router, the <span class="seg"><SEG></span> projector and the mask decoder. Stage B adds LoRA, \(W = W_0 + \tfrac{\alpha}{r}BA\) with \(r = 16\), to all 32 language model layers and switches on the adapters of the SAM-Med2D encoder. It minimizes</p> | |
| <div class="eq">\[ \mathcal{L} = \mathcal{L}_{\mathrm{CE}} + 2\,\mathcal{L}_{\mathrm{BCE}} + 2\,\mathcal{L}_{\mathrm{Dice}} + \mathcal{L}_{\mathrm{IoU}}, \]</div> | |
| <p>where the first term trains the answer and the other three train the mask. Non-tumorous slices are trained with an empty mask. Half of the training prompts ask for the class and the mask, a quarter for the class only and a quarter for the mask only.</p> | |
| <figure> | |
| <img src="assets/page/train_curves.png" alt="Training losses per epoch"> | |
| <figcaption><b>Training losses per epoch.</b> The answer loss is close to zero after a few epochs, while the mask loss keeps falling through Stage B. Learning the mask is the harder part of the task.</figcaption> | |
| </figure> | |
| </section> | |
| <section id="demo"> | |
| <h2>Ask the model</h2> | |
| <p class="sub">Pick a test slice. The model scores each of the four valid answers, keeps the most likely one, and draws the mask from that answer's <span class="seg"><SEG></span> token.</p> | |
| <div class="demo"> | |
| <div class="thumbs" id="thumbs"></div> | |
| <div class="viewer"> | |
| <img id="view" alt="Selected MRI slice"> | |
| <div class="cap" id="viewcap"></div> | |
| </div> | |
| <div> | |
| <div class="prompt"><span class="who">Instruction</span>Classify this contrast-enhanced T1 brain MRI as glioma, meningioma, pituitary tumor, or non-tumorous, and segment the tumor region.</div> | |
| <div class="cands" id="cands"></div> | |
| <button class="runbtn" id="run">Answer</button> | |
| <div class="result" id="result"></div> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="narrow"> | |
| <h2>Results at a glance</h2> | |
| <div class="table-wrap"> | |
| <table> | |
| <thead><tr><th>Model</th><th>Accuracy</th><th>Tumor Dice</th><th>Dice, all slices</th><th>False masks</th><th>Contradictions</th></tr></thead> | |
| <tbody> | |
| <tr><td>MedPLIB, no adaptation</td><td>0.294</td><td>0.040</td><td>0.085</td><td>63.6%</td><td>7.0%</td></tr> | |
| <tr class="ours"><td>MedPLIB, unified, ours</td><td>0.980</td><td>0.854</td><td>0.874</td><td>0.0%</td><td>0.0%</td></tr> | |
| <tr><td>EfficientNet-B0 + U-Net</td><td>0.993</td><td>0.881</td><td>0.812</td><td>61.4%</td><td>9.0%</td></tr> | |
| <tr><td>EfficientNet-B0 + U-Net, with a hand-written rule</td><td>0.993</td><td>0.881</td><td>0.898</td><td>0.0%</td><td>0.4%</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <p class="note">Dice over all slices also scores the 140 non-tumorous slices, where an empty mask counts as correct. The hand-written rule removes the U-Net mask whenever EfficientNet-B0 predicts no tumor. Accuracy of the unified model is about the same on all three imaging planes: 0.980 on axial, 0.984 on coronal and 0.976 on sagittal slices. The full comparison is in the <a href="https://github.com/Peilin-FF/Tumor">README</a>.</p> | |
| </section> | |
| <section class="narrow"> | |
| <h2>What adaptation changes</h2> | |
| <p class="sub">The same model on the same 1,000 test slices, before and after our two training stages.</p> | |
| <figure> | |
| <img src="assets/page/confusion.png" alt="Confusion matrices before and after adaptation"> | |
| <figcaption><b>Confusion matrices.</b> Without adaptation, MedPLIB calls almost every slice a meningioma and never answers non-tumorous. After adaptation, all 140 non-tumorous slices are recognized, and the 20 remaining errors are all between tumor types. No tumor is ever called non-tumorous.</figcaption> | |
| </figure> | |
| <h3>Real answers before and after adaptation</h3> | |
| <div class="chats"> | |
| <div class="chat"> | |
| <div class="q"><b>Main instruction, glioma slice</b>Classify this contrast-enhanced T1 brain MRI as glioma, meningioma, pituitary tumor, or non-tumorous, and segment the tumor region.</div> | |
| <div class="pair"> | |
| <div class="ans"><span class="who">No adaptation</span>Non-enhancing tumor.<span class="tag no">no class, no mask</span></div> | |
| <div class="ans after"><span class="who">Unified model</span>glioma <span class="seg"><SEG></span><span class="tag ok">class and mask</span></div> | |
| </div> | |
| </div> | |
| <div class="chat"> | |
| <div class="q"><b>Question it was never trained on, non-tumorous slice</b>Describe this contrast-enhanced T1 brain MRI. Is there a tumor? If so, what type is it and where is it located?</div> | |
| <div class="pair"> | |
| <div class="ans"><span class="who">No adaptation</span>The tumor appears as areas of increased signal intensity on the image, indicating abnormal growth in the head and neck region.<span class="tag no">invents a tumor</span></div> | |
| <div class="ans after"><span class="who">Unified model</span>No<span class="tag ok">correct</span></div> | |
| </div> | |
| </div> | |
| <div class="chat"> | |
| <div class="q"><b>Imaging plane question, axial slice</b>Which imaging plane is this brain MRI: axial, coronal, or sagittal?</div> | |
| <div class="pair"> | |
| <div class="ans"><span class="who">No adaptation</span>axial<span class="tag ok">correct</span></div> | |
| <div class="ans after"><span class="who">Unified model</span>coronal<span class="tag no">wrong</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <p class="note" style="margin-top:12px">The first two cases show the gain from adaptation. The third shows its cost: training on one narrow task made the model lose some general skills, and its free descriptions shrank to a single word. Mixing general question and answer data into training is one way to keep these skills.</p> | |
| </section> | |
| <section class="narrow"> | |
| <h2>The model knows when it is unsure</h2> | |
| <p class="sub">The four answer scores give a confidence for every prediction, at no extra cost.</p> | |
| <figure> | |
| <img src="assets/page/confidence.png" alt="Answer confidence"> | |
| <figcaption><b>Confidence of the chosen answer.</b> Correct answers have a median confidence of 0.9997, while the 20 wrong answers average 0.78. Keeping only answers with a confidence of 0.99 or more covers 84.3% of the slices at 99.9% accuracy, with a single error. The expected calibration error is 0.009. In practice, the remaining slices could be sent to a radiologist for review.</figcaption> | |
| </figure> | |
| </section> | |
| <section> | |
| <h2>Case gallery</h2> | |
| <p class="sub">For each tumor type, the test slices where the unified model scored its best, median and worst Dice, and the two non-tumorous slices with the largest false masks before adaptation. Nothing is hand picked.</p> | |
| <div class="tabs" role="tablist" id="tabs"></div> | |
| <div class="legend"><span><i></i>ground truth</span><span><i class="pred"></i>predicted mask</span></div> | |
| <div class="colhead"><div>Input</div><div>MedPLIB, no adaptation</div><div>EfficientNet-B0 + U-Net</div><div>MedPLIB, unified, ours</div></div> | |
| <div id="gallery"></div> | |
| </section> | |
| <section class="narrow"> | |
| <h2>Team</h2> | |
| <div class="team"> | |
| <div class="member"><div class="n">Simon Tong Sing Hee</div><div class="s">Asian School of the Environment</div></div> | |
| <div class="member"><div class="n">Zeng Yi</div><div class="s">College of Computing and Data Science</div></div> | |
| <div class="member"><div class="n">Feng Peilin</div><div class="s">School of Electrical and Electronic Engineering</div></div> | |
| <div class="member"><div class="n">Ye Xiaomeng</div><div class="s">Lee Kong Chian School of Medicine</div></div> | |
| <div class="member"><div class="n">Lyu Muyang</div><div class="s">School of Mechanical and Aerospace Engineering</div></div> | |
| <div class="member"><div class="n">Timothy Aw Bang Hao</div><div class="s">School of Social Sciences</div></div> | |
| </div> | |
| </section> | |
| </main> | |
| <footer class="wrap">GP8001 Group 3 · Nanyang Technological University · 2026</footer> | |
| <script> | |
| (async () => { | |
| const LABEL = {glioma: "glioma", meningioma: "meningioma", pituitary: "pituitary tumor", non_tumorous: "non-tumorous"}; | |
| const ORDER = ["glioma", "meningioma", "pituitary", "non_tumorous"]; | |
| const esc = s => String(s).replace(/[&<>]/g, c => ({"&": "&", "<": "<", ">": ">"}[c])); | |
| let data; | |
| try { data = await (await fetch("assets/page/page_data.json")).json(); } catch (e) { return; } | |
| // ---- demo ---- | |
| const thumbs = document.getElementById("thumbs"), view = document.getElementById("view"), | |
| viewcap = document.getElementById("viewcap"), cands = document.getElementById("cands"), | |
| result = document.getElementById("result"), run = document.getElementById("run"); | |
| let cur = 0, answered = false; | |
| const pct = v => v >= 0.9995 ? ">99.9%" : v < 0.0005 ? "<0.1%" : (v * 100).toFixed(1) + "%"; | |
| function draw() { | |
| const d = data.demo[cur]; | |
| view.src = "assets/page/demo/" + d.id + (answered ? "_answer.png" : "_input.png"); | |
| viewcap.textContent = answered ? "Green outline: ground truth. Red area: predicted mask." : "Test slice " + d.id.slice(15, 20) + ", true class hidden until you ask."; | |
| const win = ORDER.indexOf(d.pred); | |
| cands.innerHTML = ORDER.map((c, i) => `<div class="cand${answered && i === win ? " win" : ""}"> | |
| <span>${esc(LABEL[c])} <span class="seg"><SEG></span></span> | |
| <div class="bar"><i style="width:${answered ? Math.max(d.prob[i] * 100, 0.6) : 0}%"></i></div> | |
| <span class="p">${answered ? pct(d.prob[i]) : "?"}</span></div>`).join(""); | |
| run.style.display = answered ? "none" : "inline-block"; | |
| if (!answered) { result.innerHTML = ""; return; } | |
| const ok = d.pred === d.true; | |
| result.innerHTML = `Answer: <b>${esc(LABEL[d.pred])}</b> <span class="seg"><SEG></span>. True class: <b>${esc(LABEL[d.true])}</b>. ` + | |
| (d.true === "non_tumorous" ? (d.mask ? "A mask was drawn." : "The decoded mask is empty, as it should be.") : `Dice with the expert mask: <b>${d.dice.toFixed(2)}</b>.`) + | |
| (ok ? "" : " The class is wrong."); | |
| } | |
| data.demo.forEach((d, i) => { | |
| const b = document.createElement("button"); | |
| b.className = "thumb"; b.setAttribute("aria-pressed", i === 0); | |
| b.innerHTML = `<img src="assets/page/demo/${d.id}_input.png" alt="Slice ${i + 1}"><span>Slice ${i + 1}</span>`; | |
| b.onclick = () => { cur = i; answered = false; [...thumbs.children].forEach((t, j) => t.setAttribute("aria-pressed", j === i)); draw(); }; | |
| thumbs.appendChild(b); | |
| }); | |
| run.onclick = () => { answered = true; draw(); }; | |
| draw(); | |
| // ---- gallery ---- | |
| const tabs = document.getElementById("tabs"), gal = document.getElementById("gallery"); | |
| const runs = [["zeroshot", "No adaptation"], ["unet_r34_seed42", "EfficientNet-B0 + U-Net"], ["stageB_v2_seed42", "Unified, ours"]]; | |
| const ROLE = {"Best": "Best case for the unified model", "Median": "Median case for the unified model", "Worst": "Worst case for the unified model", | |
| "Largest zero-shot false alarm": "Largest false mask before adaptation", "Median zero-shot false alarm": "Typical false mask before adaptation"}; | |
| const roleText = r => ROLE[r] || r; | |
| function show(cls) { | |
| [...tabs.children].forEach(t => t.setAttribute("aria-selected", t.dataset.c === cls)); | |
| gal.innerHTML = data.gallery.filter(g => g.class === cls).map(g => { | |
| const cells = [`<div class="cell"><img src="assets/page/gallery/${g.id}__input.png" alt="Input"><div class="t">true class: <b>${esc(LABEL[g.class])}</b></div></div>`] | |
| .concat(runs.map(([r, name]) => { | |
| const m = g.models[r], okc = m.pred === g.class; | |
| const second = g.class === "non_tumorous" ? (m.mask ? '<span class="bad">mask drawn</span>' : "no mask") : | |
| (m.dice < 0.5 ? `<span class="bad">Dice ${m.dice.toFixed(2)}</span>` : `Dice ${m.dice.toFixed(2)}`); | |
| return `<div class="cell"><img src="assets/page/gallery/${g.id}__${r}.png" alt="${esc(name)}"><div class="t">` + | |
| (okc ? `<b>${esc(LABEL[m.pred])}</b>` : `<span class="bad">${esc(LABEL[m.pred])}</span>`) + `<br>${second}</div></div>`; | |
| })); | |
| return `<div class="case"><h4>${esc(roleText(g.role))}</h4><div class="row4">${cells.join("")}</div></div>`; | |
| }).join(""); | |
| } | |
| ORDER.forEach((c, i) => { | |
| const t = document.createElement("button"); | |
| t.className = "tab"; t.dataset.c = c; t.setAttribute("role", "tab"); | |
| t.textContent = c === "non_tumorous" ? "Non-tumorous" : LABEL[c][0].toUpperCase() + LABEL[c].slice(1); | |
| t.onclick = () => show(c); | |
| tabs.appendChild(t); | |
| }); | |
| show("glioma"); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |