MedPLIB-BRISC / index.html
Sssunset's picture
Focus project page on method and results
4422330 verified
Raw History Blame Contribute Delete
26.2 kB
<!doctype html>
<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">&lt;SEG&gt;</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 &ldquo;\(c\) <span class="seg">&lt;SEG&gt;</span>&rdquo;, where \(c\) is one of the four classes. A projector \(P\) maps the hidden state \(h\) of the <span class="seg">&lt;SEG&gt;</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)}_{&lt;t}, x, q\big). \]</div>
<p>The mask comes from the <span class="seg">&lt;SEG&gt;</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">&lt;SEG&gt;</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">&lt;SEG&gt;</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">&lt;SEG&gt;</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 => ({"&": "&amp;", "<": "&lt;", ">": "&gt;"}[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">&lt;SEG&gt;</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">&lt;SEG&gt;</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>