From 75ab895ba2fd6e660474976752fbe7c7584f00a8 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 10 Jun 2026 17:57:54 +0800 Subject: [PATCH] Show SV metadata for postprocess results --- web_dehaze/pipeline.py | 48 +++++++++++++++++++++- web_dehaze/server.py | 5 ++- web_dehaze/static/app.js | 79 ++++++++++++++++++++++++++++++++----- web_dehaze/static/style.css | 37 +++++++++++++++++ 使用手册.md | 5 ++- 5 files changed, 160 insertions(+), 14 deletions(-) diff --git a/web_dehaze/pipeline.py b/web_dehaze/pipeline.py index 2584826..a83bb7d 100644 --- a/web_dehaze/pipeline.py +++ b/web_dehaze/pipeline.py @@ -99,6 +99,25 @@ def post_result_path(filename: str, source: str, processor: str, params: dict[st return image_result_dir(filename) / "postprocess" / f"{_safe_slug(source)}__{_safe_slug(processor)}{suffix}.png" +def post_metadata_path(output_path: Path) -> Path: + return output_path.with_suffix(".json") + + +def read_post_metadata(output_path: Path) -> dict[str, Any]: + metadata_path = post_metadata_path(output_path) + if not metadata_path.exists(): + return {} + try: + return json.loads(metadata_path.read_text(encoding="utf-8")) + except Exception: + return {} + + +def write_post_metadata(output_path: Path, payload: dict[str, Any]) -> None: + metadata_path = post_metadata_path(output_path) + metadata_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + + def relpath(path: Path) -> str: return path.resolve().relative_to(ROOT).as_posix() @@ -206,7 +225,20 @@ def get_results(filename: str) -> dict[str, Any]: post_dir = image_result_dir(filename) / "postprocess" if post_dir.exists(): for path in sorted(post_dir.glob("*.png"), key=lambda p: p.name.lower()): - post.append({"name": path.stem, "exists": True, "path": relpath(path)}) + metadata = read_post_metadata(path) + post.append( + { + "name": path.stem, + "exists": True, + "path": relpath(path), + "meta": metadata.get("meta", {}), + "processor": metadata.get("processor", ""), + "source": metadata.get("source", ""), + "reference": metadata.get("reference", ""), + "params": metadata.get("params", {}), + "metadata_path": relpath(post_metadata_path(path)) if post_metadata_path(path).exists() else "", + } + ) return { "image": filename, @@ -249,6 +281,7 @@ def collect_download_items(filenames: list[str] | None = None, include_original: if post_dir.exists(): for result in sorted(post_dir.glob("*.png"), key=lambda p: p.name.lower()): add_file(result, f"{folder}/postprocess/{result.name}") + add_file(post_metadata_path(result), f"{folder}/postprocess/{post_metadata_path(result).name}") return items @@ -523,6 +556,19 @@ def run_postprocessors_for_source( proc_params = params.get(processor, params) output = post_result_path(filename, source_name, processor, proc_params) meta = run_postprocess(processor, source_path, output, reference_path=reference_path, params=proc_params) + write_post_metadata( + output, + { + "image": filename, + "source": source_name, + "processor": processor, + "reference": reference_path.name, + "params": proc_params, + "meta": meta, + "output": relpath(output), + "created_at": time.strftime("%Y-%m-%d %H:%M:%S"), + }, + ) outputs.append(output) log(f"后处理完成 {source_name} / {processor}: {json.dumps(meta, ensure_ascii=False)}") return outputs diff --git a/web_dehaze/server.py b/web_dehaze/server.py index ac71275..73fc6d5 100644 --- a/web_dehaze/server.py +++ b/web_dehaze/server.py @@ -130,9 +130,12 @@ def _run_default_batch_job(log, payload: dict[str, Any]) -> dict[str, Any]: baidu_output = pipeline.run_dehaze_method(filename, "Baidu_API", {}, log) auto_output = pipeline.post_result_path(filename, "Baidu_API", "auto_sv", {}) - if skip_existing and auto_output.exists(): + auto_meta = pipeline.post_metadata_path(auto_output) + if skip_existing and auto_output.exists() and auto_meta.exists(): log(f"[自动 S/V] 已存在,跳过: {pipeline.relpath(auto_output)}") else: + if auto_output.exists() and not auto_meta.exists(): + log("[自动 S/V] 已有图片但缺少 S/V 元数据,重新计算") outputs = pipeline.run_postprocessors_for_source( filename, "Baidu_API", diff --git a/web_dehaze/static/app.js b/web_dehaze/static/app.js index ce9920d..e41fee4 100644 --- a/web_dehaze/static/app.js +++ b/web_dehaze/static/app.js @@ -28,6 +28,56 @@ function formatBytes(bytes) { return `${(bytes / 1024 / 1024).toFixed(1)} MB`; } +function formatGain(value) { + const number = Number(value); + return Number.isFinite(number) ? number.toFixed(3) : ""; +} + +function svSummary(meta = {}) { + const s = formatGain(meta.s_gain); + const v = formatGain(meta.v_gain); + if (!s || !v) return ""; + return `S ${s} · V ${v}`; +} + +function updateGainLabels() { + $("sGainValue").textContent = formatGain($("sGain").value); + $("vGainValue").textContent = formatGain($("vGain").value); +} + +function setOnlyPostprocessor(processor) { + [...$("postList").querySelectorAll("input[type=checkbox]")].forEach((input) => { + input.checked = input.value === processor; + }); +} + +function setSelectIfOption(selectId, value) { + const select = $(selectId); + if ([...select.options].some((option) => option.value === value)) { + select.value = value; + } +} + +function applySvFromResult(item) { + const meta = item.meta || {}; + const sGain = Number(meta.s_gain); + const vGain = Number(meta.v_gain); + if (!Number.isFinite(sGain) || !Number.isFinite(vGain)) return; + + $("sGain").value = Math.min(2.5, Math.max(0, sGain)); + $("vGain").value = Math.min(2.5, Math.max(0, vGain)); + updateGainLabels(); + setOnlyPostprocessor("manual_sv"); + if (item.source) { + setSelectIfOption("postSource", item.source); + } + if (item.reference) { + setSelectIfOption("referenceImage", item.reference); + } + $("logBox").textContent = `已载入 ${item.name} 的参数:S=${formatGain(sGain)},V=${formatGain(vGain)}\n可在左侧手动微调后点击“生成后处理”。`; + setJobState("SV Ready", "done"); +} + function renderImages() { const box = $("imageList"); box.innerHTML = ""; @@ -104,14 +154,26 @@ async function selectImage(name) { await refreshResults(); } -function resultCard(title, path, exists = true) { +function resultCard(title, path, exists = true, item = null) { const card = document.createElement("article"); - card.className = "result-card"; + const summary = item ? svSummary(item.meta) : ""; + card.className = `result-card ${summary ? "clickable" : ""}`; const badge = exists ? 'ready' : '未生成'; const body = exists - ? `
${title}
` + ? `
${title}
${summary ? `
${summary}点击载入
` : ""}` : '
暂无结果
'; card.innerHTML = `

${title}

${badge}
${body}`; + if (summary && item) { + card.tabIndex = 0; + card.title = "点击将这组 S/V 载入左侧手动调整"; + card.addEventListener("click", () => applySvFromResult(item)); + card.addEventListener("keydown", (event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + applySvFromResult(item); + } + }); + } return card; } @@ -148,7 +210,7 @@ async function refreshResults() { grid.appendChild(resultCard(item.label, item.path, item.exists)); }); results.postprocess.forEach((item) => { - grid.appendChild(resultCard(item.name, item.path, true)); + grid.appendChild(resultCard(item.name, item.path, true, item)); }); updatePostSourceOptions(results); } @@ -253,12 +315,9 @@ function bindControls() { $("downloadCurrentBtn").addEventListener("click", downloadCurrent); $("downloadAllBtn").addEventListener("click", downloadAll); $("refreshBtn").addEventListener("click", refreshResults); - $("sGain").addEventListener("input", () => { - $("sGainValue").textContent = `${Math.round(Number($("sGain").value) * 100)}%`; - }); - $("vGain").addEventListener("input", () => { - $("vGainValue").textContent = `${Math.round(Number($("vGain").value) * 100)}%`; - }); + $("sGain").addEventListener("input", updateGainLabels); + $("vGain").addEventListener("input", updateGainLabels); + updateGainLabels(); } async function init() { diff --git a/web_dehaze/static/style.css b/web_dehaze/static/style.css index 3350db7..dadd5a2 100644 --- a/web_dehaze/static/style.css +++ b/web_dehaze/static/style.css @@ -318,6 +318,17 @@ h1 { background: rgba(255, 250, 241, 0.82); } +.result-card.clickable { + cursor: pointer; +} + +.result-card.clickable:hover, +.result-card.clickable:focus { + outline: 2px solid rgba(31, 107, 87, 0.36); + outline-offset: 2px; + border-color: rgba(31, 107, 87, 0.55); +} + .result-card header { display: flex; align-items: center; @@ -372,6 +383,32 @@ h1 { background: #fff; } +.sv-strip { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-height: 36px; + padding: 7px 10px; + border-top: 1px solid var(--line); + background: rgba(31, 107, 87, 0.09); + color: var(--green-dark); + font-size: 12px; + font-variant-numeric: tabular-nums; +} + +.sv-strip span { + min-width: 0; + overflow-wrap: anywhere; + font-weight: 700; +} + +.sv-strip small { + flex: 0 0 auto; + color: var(--muted); + font-size: 11px; +} + .missing { color: var(--muted); font-size: 13px; diff --git a/使用手册.md b/使用手册.md index df26f3f..8e483c8 100644 --- a/使用手册.md +++ b/使用手册.md @@ -67,8 +67,9 @@ BAIDU_SECRET_KEY=... 4. 勾选需要运行的模型,点击“运行选中模型”。 5. 在后处理区域选择源图、参考图和后处理方法,点击“生成后处理”。 6. 需要批量处理时,点击“批量默认流程”,会对 `待去雾图片/` 中所有图片执行 `Baidu_API -> 自动 S/V`。已有的百度结果和自动 S/V 结果会跳过,避免重复消耗 API。 -7. 需要导出结果时,在“下载”区域点击“下载当前图片”或“批量下载全部”,网页会生成 ZIP 包。 -8. 结果会保存到 `web_results/`,网页会自动刷新显示。 +7. 自动 S/V、手动 S/V 等后处理卡片会显示对应的 S/V 参数;点击带有 S/V 的结果卡片,会把参数载入左侧手动 S/V 滑杆,方便继续人工微调。 +8. 需要导出结果时,在“下载”区域点击“下载当前图片”或“批量下载全部”,网页会生成 ZIP 包。 +9. 结果会保存到 `web_results/`,网页会自动刷新显示。 页面中“未生成”表示对应结果文件还不存在,并不是任务卡住。