ISISeg
-介入导丝视频分割工作台
+ISISeg Console
+From 77b8ecdfbe4165d19e9c544f6fffe58010062df2 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Mon, 18 May 2026 19:11:58 +0800 Subject: [PATCH] =?UTF-8?q?2026-05-18-19-06-50=20=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E7=BD=91=E9=A1=B5=E7=AB=AF=E5=88=86=E5=89=B2=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + backend/main.py | 18 + frontend/app.js | 193 +++++++- frontend/index.html | 175 +++++--- frontend/styles.css | 535 ++++++++++++++++++----- tests/test_api.py | 3 + 工程分析/实现方案-2026-05-18-19-06-50.md | 33 ++ 工程分析/测试方案-2026-05-18-19-06-50.md | 34 ++ 工程分析/经验记录.md | 12 + 工程分析/需求分析-2026-05-18-19-06-50.md | 22 + 10 files changed, 851 insertions(+), 175 deletions(-) create mode 100644 工程分析/实现方案-2026-05-18-19-06-50.md create mode 100644 工程分析/测试方案-2026-05-18-19-06-50.md create mode 100644 工程分析/需求分析-2026-05-18-19-06-50.md diff --git a/README.md b/README.md index 98fe224..d985673 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ## 功能 - Web 上传图片或视频。 +- Web 端支持拖拽上传、样例视频一键加载、算法卡片选择、参数调节、输入预览和结果详情弹窗。 - 支持 `hessian_ridge`、`edge_morphology`、`temporal_difference`、`fusion`、`compare` 五种模式。 - 显示原图、导丝叠加结果、掩膜、覆盖率、骨架长度和连通域数量。 - 提供合成导丝视频生成脚本,便于快速验证。 diff --git a/backend/main.py b/backend/main.py index 7230905..38f8db8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -52,6 +52,24 @@ def methods() -> dict[str, Any]: return {"methods": METHOD_DESCRIPTIONS} +@app.get("/api/samples") +def samples() -> dict[str, Any]: + ensure_dirs() + items = [] + for path in sorted(SAMPLE_DIR.glob("*")): + suffix = path.suffix.lower() + if suffix in IMAGE_SUFFIXES | VIDEO_SUFFIXES: + items.append( + { + "name": path.name, + "url": _public(path), + "kind": "image" if suffix in IMAGE_SUFFIXES else "video", + "size": path.stat().st_size, + } + ) + return {"samples": items} + + def _public(path: Path) -> str: return "/" + path.relative_to(ROOT).as_posix() diff --git a/frontend/app.js b/frontend/app.js index 5ca09ee..2c1dc5a 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1,21 +1,79 @@ const form = document.querySelector("#segmentForm"); const methodSelect = document.querySelector("#method"); +const methodGrid = document.querySelector("#methodGrid"); const sensitivity = document.querySelector("#sensitivity"); const sensitivityValue = document.querySelector("#sensitivityValue"); const fileInput = document.querySelector("#file"); const fileName = document.querySelector("#fileName"); +const dropZone = document.querySelector("#dropZone"); +const sampleButton = document.querySelector("#sampleButton"); +const clearButton = document.querySelector("#clearButton"); const resultGrid = document.querySelector("#resultGrid"); const emptyState = document.querySelector("#emptyState"); const videoLink = document.querySelector("#videoLink"); const health = document.querySelector("#health"); const template = document.querySelector("#resultCardTemplate"); +const previewEmpty = document.querySelector("#previewEmpty"); +const videoPreview = document.querySelector("#videoPreview"); +const imagePreview = document.querySelector("#imagePreview"); +const progressWrap = document.querySelector("#progressWrap"); +const progressText = document.querySelector("#progressText"); +const summaryStrip = document.querySelector("#summaryStrip"); +const summaryJob = document.querySelector("#summaryJob"); +const summaryFrames = document.querySelector("#summaryFrames"); +const summaryCoverage = document.querySelector("#summaryCoverage"); +const summarySkeleton = document.querySelector("#summarySkeleton"); +const resultCount = document.querySelector("#resultCount"); +const jobMeta = document.querySelector("#jobMeta"); +const detailDialog = document.querySelector("#detailDialog"); +const closeDialog = document.querySelector("#closeDialog"); const methodLabels = new Map(); +const methodDescriptions = new Map(); +let selectedFile = null; +let currentObjectUrl = null; +let lastFrames = []; -function setBusy(isBusy) { - const button = form.querySelector("button"); +function setBusy(isBusy, text = "运行分割") { + const button = form.querySelector(".primary"); button.disabled = isBusy; - button.querySelector("span").textContent = isBusy ? "分割中" : "开始分割"; + button.querySelector("span").textContent = isBusy ? "分割中" : text; + progressWrap.hidden = !isBusy; + if (isBusy) { + progressText.textContent = "正在上传、抽帧并执行导丝分割"; + } +} + +function setFile(file) { + selectedFile = file; + const transfer = new DataTransfer(); + transfer.items.add(file); + fileInput.files = transfer.files; + fileName.textContent = `${file.name} · ${(file.size / 1024 / 1024).toFixed(2)} MB`; + renderPreview(file); +} + +function revokePreview() { + if (currentObjectUrl) { + URL.revokeObjectURL(currentObjectUrl); + currentObjectUrl = null; + } +} + +function renderPreview(file) { + revokePreview(); + currentObjectUrl = URL.createObjectURL(file); + previewEmpty.hidden = true; + videoPreview.hidden = true; + imagePreview.hidden = true; + if (file.type.startsWith("video/")) { + videoPreview.src = currentObjectUrl; + videoPreview.hidden = false; + } else { + imagePreview.src = currentObjectUrl; + imagePreview.hidden = false; + } + jobMeta.textContent = `已选择 ${file.name}`; } async function loadHealth() { @@ -31,53 +89,171 @@ async function loadHealth() { } } +function activateMethod(key) { + methodSelect.value = key; + [...methodGrid.querySelectorAll(".method-option")].forEach((node) => { + node.classList.toggle("is-active", node.dataset.method === key); + }); +} + async function loadMethods() { const response = await fetch("/api/methods"); const data = await response.json(); methodSelect.innerHTML = ""; + methodGrid.innerHTML = ""; Object.entries(data.methods).forEach(([key, value]) => { methodLabels.set(key, value.label); + methodDescriptions.set(key, value.description); + const option = document.createElement("option"); option.value = key; option.textContent = value.label; - if (key === "fusion") option.selected = true; methodSelect.appendChild(option); + + const card = document.createElement("button"); + card.type = "button"; + card.className = "method-option"; + card.dataset.method = key; + card.innerHTML = `${value.label}${value.description}`; + card.addEventListener("click", () => activateMethod(key)); + methodGrid.appendChild(card); }); + activateMethod("fusion"); +} + +function updateSummary(data) { + const frames = data.frames || []; + const coverage = frames.reduce((sum, frame) => sum + frame.metrics.coverage, 0) / Math.max(1, frames.length); + const skeleton = frames.reduce((sum, frame) => sum + frame.metrics.skeleton_length, 0) / Math.max(1, frames.length); + summaryJob.textContent = data.job_id; + summaryFrames.textContent = frames.length; + summaryCoverage.textContent = `${(coverage * 100).toFixed(3)}%`; + summarySkeleton.textContent = Math.round(skeleton); + summaryStrip.hidden = false; + resultCount.textContent = `${frames.length} 个结果`; + jobMeta.textContent = `${data.kind === "video" ? "视频" : "图像"} · ${methodLabels.get(data.method) || data.method}`; +} + +function openDetail(frame) { + document.querySelector("#detailMethod").textContent = methodLabels.get(frame.method) || frame.method; + document.querySelector("#detailTitle").textContent = `帧 ${frame.frame_index} 分割详情`; + document.querySelector("#detailOriginal").src = frame.original_url; + document.querySelector("#detailOverlay").src = frame.overlay_url; + document.querySelector("#detailMask").src = frame.mask_url; + const metrics = document.querySelector("#detailMetrics"); + metrics.innerHTML = ` +
ISISeg
-ISISeg Console
+