From ec5b25d2d00628a6c93fd5feae4df2ecde587c99 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 10 Jun 2026 17:50:42 +0800 Subject: [PATCH] Add batch default flow and downloads --- README.md | 2 + web_dehaze/README.md | 2 + web_dehaze/pipeline.py | 37 ++++++++++++++ web_dehaze/server.py | 95 +++++++++++++++++++++++++++++++++++- web_dehaze/static/app.js | 23 ++++++++- web_dehaze/static/index.html | 13 ++++- web_dehaze/static/style.css | 13 +++++ 使用手册.md | 8 ++- 8 files changed, 187 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index feed32c..f143aa5 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ http://192.168.3.11:7860/ - GCANet - RefineDNet - 多种 HSV/SV 后处理方式 +- 一键批量默认流程:全部图片运行 `Baidu_API + 自动 S/V` +- 网页端批量下载当前图片或全部图片的 ZIP 结果包 ## 验证 diff --git a/web_dehaze/README.md b/web_dehaze/README.md index 5036236..b69bddb 100644 --- a/web_dehaze/README.md +++ b/web_dehaze/README.md @@ -2,6 +2,8 @@ 本地网页端入口,用于选择 `待去雾图片/` 中的图片,运行 AOD、Baidu_API、DCP、DehazeNet、GCANet、RefineDNet,并统一展示结果与后处理结果。 +网页左侧提供“批量默认流程”,会对全部图片运行 `Baidu_API -> 自动 S/V`;下载区可将当前图片或全部图片已有结果打包为 ZIP。 + ```bash bash run_dehaze_web.sh ``` diff --git a/web_dehaze/pipeline.py b/web_dehaze/pipeline.py index 9cdb2c6..2584826 100644 --- a/web_dehaze/pipeline.py +++ b/web_dehaze/pipeline.py @@ -216,6 +216,43 @@ def get_results(filename: str) -> dict[str, Any]: } +def collect_download_items(filenames: list[str] | None = None, include_original: bool = True) -> list[tuple[Path, str]]: + names = filenames or [item["name"] for item in list_images()] + items: list[tuple[Path, str]] = [] + used_names: set[str] = set() + + def add_file(path: Path, archive_name: str) -> None: + if not path.exists() or not path.is_file(): + return + archive_name = archive_name.replace("\\", "/") + if archive_name in used_names: + stem = Path(archive_name).with_suffix("").as_posix() + suffix = Path(archive_name).suffix + index = 2 + while f"{stem}_{index}{suffix}" in used_names: + index += 1 + archive_name = f"{stem}_{index}{suffix}" + used_names.add(archive_name) + items.append((path, archive_name)) + + for filename in names: + source = source_image_path(filename) + folder = image_id(filename) + if include_original: + add_file(source, f"{folder}/original/{source.name}") + + for method in DEHAZE_METHODS: + result = dehaze_result_path(filename, method) + add_file(result, f"{folder}/dehaze/{_safe_slug(method)}.png") + + post_dir = image_result_dir(filename) / "postprocess" + 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}") + + return items + + def _reset_dir(path: Path) -> None: if path.exists(): shutil.rmtree(path) diff --git a/web_dehaze/server.py b/web_dehaze/server.py index ad6bdd6..ac71275 100644 --- a/web_dehaze/server.py +++ b/web_dehaze/server.py @@ -1,10 +1,12 @@ from __future__ import annotations import argparse +import io import json import mimetypes import threading import traceback +import zipfile from http import HTTPStatus from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from itertools import count @@ -112,16 +114,71 @@ def _run_post_job(log, payload: dict[str, Any]) -> dict[str, Any]: } +def _run_default_batch_job(log, payload: dict[str, Any]) -> dict[str, Any]: + requested = payload.get("images") or [item["name"] for item in pipeline.list_images()] + skip_existing = bool(payload.get("skip_existing", True)) + completed = [] + failed = [] + + for filename in requested: + try: + log(f"[默认流程] {filename}: Baidu_API -> 自动 S/V") + baidu_output = pipeline.dehaze_result_path(filename, "Baidu_API") + if skip_existing and baidu_output.exists(): + log(f"[Baidu_API] 已存在,跳过: {pipeline.relpath(baidu_output)}") + else: + 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(): + log(f"[自动 S/V] 已存在,跳过: {pipeline.relpath(auto_output)}") + else: + outputs = pipeline.run_postprocessors_for_source( + filename, + "Baidu_API", + ["auto_sv"], + params={"auto_sv": {}}, + reference_filename=filename, + log=log, + ) + auto_output = outputs[-1] + + completed.append( + { + "image": filename, + "baidu": pipeline.relpath(baidu_output), + "auto_sv": pipeline.relpath(auto_output), + } + ) + except Exception as exc: + failed.append({"image": filename, "error": str(exc)}) + log(f"[默认流程] {filename} 失败: {exc}") + + return { + "completed": completed, + "failed": failed, + "results": {item["image"]: pipeline.get_results(item["image"]) for item in completed}, + } + + class DehazeRequestHandler(BaseHTTPRequestHandler): server_version = "DehazeWeb/1.0" def log_message(self, fmt: str, *args) -> None: print("[%s] %s" % (self.log_date_time_string(), fmt % args)) - def _send(self, status: int, body: bytes, content_type: str = "application/json; charset=utf-8") -> None: - self.send_response(status) + def _send( + self, + status: int, + body: bytes, + content_type: str = "application/json; charset=utf-8", + headers: dict[str, str] | None = None, + ) -> None: + self.send_response(int(status)) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(body))) + for key, value in (headers or {}).items(): + self.send_header(key, value) self.end_headers() self.wfile.write(body) @@ -179,6 +236,8 @@ class DehazeRequestHandler(BaseHTTPRequestHandler): elif path == "/api/results": filename = query.get("image", [""])[0] self._send_json(pipeline.get_results(filename)) + elif path == "/api/download": + self._serve_download(query) elif path == "/api/job": job_id = query.get("id", [""])[0] with JOB_LOCK: @@ -200,6 +259,9 @@ class DehazeRequestHandler(BaseHTTPRequestHandler): if parsed.path == "/api/run": job = _new_job("dehaze", _run_dehaze_job, payload) self._send_json({"job": job}) + elif parsed.path == "/api/run-default": + job = _new_job("default_batch", _run_default_batch_job, payload) + self._send_json({"job": job}) elif parsed.path == "/api/postprocess": job = _new_job("postprocess", _run_post_job, payload) self._send_json({"job": job}) @@ -230,6 +292,35 @@ class DehazeRequestHandler(BaseHTTPRequestHandler): content_type = mimetypes.guess_type(target.name)[0] or "application/octet-stream" self._send(HTTPStatus.OK, target.read_bytes(), content_type) + def _serve_download(self, query: dict[str, list[str]]) -> None: + scope = query.get("scope", ["all"])[0] + if scope == "current": + filename = query.get("image", [""])[0] + if not filename: + self._send_error_json(HTTPStatus.BAD_REQUEST, "missing image") + return + items = pipeline.collect_download_items([filename]) + archive_name = f"dehaze_{pipeline.image_id(filename)}.zip" + else: + items = pipeline.collect_download_items() + archive_name = "dehaze_results_all.zip" + + if not items: + self._send_error_json(HTTPStatus.NOT_FOUND, "no downloadable files") + return + + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for path, inner_name in items: + archive.write(path, inner_name) + + self._send( + HTTPStatus.OK, + buffer.getvalue(), + "application/zip", + headers={"Content-Disposition": f'attachment; filename="{archive_name}"'}, + ) + def main() -> None: parser = argparse.ArgumentParser(description="Local dehaze web console") diff --git a/web_dehaze/static/app.js b/web_dehaze/static/app.js index 0e0b0a2..ce9920d 100644 --- a/web_dehaze/static/app.js +++ b/web_dehaze/static/app.js @@ -49,6 +49,7 @@ function renderMethods() { box.innerHTML = ""; const methods = state.capabilities?.methods || {}; Object.entries(methods).forEach(([key, info]) => { + const checked = key === "Baidu_API" || key === "DCP"; const label = document.createElement("label"); label.className = `check-item ${info.available ? "" : "disabled"}`; label.innerHTML = ` @@ -56,7 +57,7 @@ function renderMethods() { ${info.label} ${info.available ? "ready" : `缺 ${info.module_ok ? "模型" : info.module}`} - + `; box.appendChild(label); }); @@ -71,7 +72,7 @@ function renderPostprocessors() { label.className = "check-item"; label.innerHTML = ` ${info.label} - + `; box.appendChild(label); }); @@ -130,6 +131,8 @@ function updatePostSourceOptions(results) { }); if ([...select.options].some((option) => option.value === previous)) { select.value = previous; + } else if (results.dehaze.some((item) => item.method === "Baidu_API" && item.exists)) { + select.value = "Baidu_API"; } else if (results.dehaze.some((item) => item.method === "DCP" && item.exists)) { select.value = "DCP"; } @@ -217,6 +220,10 @@ async function runSelectedModels() { await startJob("/api/run", payload); } +async function runDefaultBatch() { + await startJob("/api/run-default", { skip_existing: true }); +} + async function runPostprocess() { if (!state.selectedImage) return; const processors = selectedValues("postList"); @@ -230,9 +237,21 @@ async function runPostprocess() { await startJob("/api/postprocess", payload); } +function downloadCurrent() { + if (!state.selectedImage) return; + window.location.href = `/api/download?scope=current&image=${encodeURIComponent(state.selectedImage)}`; +} + +function downloadAll() { + window.location.href = "/api/download?scope=all"; +} + function bindControls() { $("runBtn").addEventListener("click", runSelectedModels); + $("runDefaultBatchBtn").addEventListener("click", runDefaultBatch); $("postBtn").addEventListener("click", runPostprocess); + $("downloadCurrentBtn").addEventListener("click", downloadCurrent); + $("downloadAllBtn").addEventListener("click", downloadAll); $("refreshBtn").addEventListener("click", refreshResults); $("sGain").addEventListener("input", () => { $("sGainValue").textContent = `${Math.round(Number($("sGain").value) * 100)}%`; diff --git a/web_dehaze/static/index.html b/web_dehaze/static/index.html index 1a3c8de..36baf48 100644 --- a/web_dehaze/static/index.html +++ b/web_dehaze/static/index.html @@ -35,7 +35,10 @@ - +
+ + +
@@ -65,6 +68,14 @@
+ +
+

下载

+
+ + +
+
diff --git a/web_dehaze/static/style.css b/web_dehaze/static/style.css index 38ecb1b..3350db7 100644 --- a/web_dehaze/static/style.css +++ b/web_dehaze/static/style.css @@ -210,6 +210,19 @@ h1 { color: var(--ink); } +.button-stack { + display: grid; + gap: 8px; + margin-top: 12px; +} + +.button-stack .primary-btn, +.button-stack .secondary-btn, +.button-stack .text-btn { + width: 100%; + margin-top: 0; +} + .slider-row { display: grid; grid-template-columns: 20px 1fr 52px; diff --git a/使用手册.md b/使用手册.md index e93f2a5..df26f3f 100644 --- a/使用手册.md +++ b/使用手册.md @@ -11,6 +11,8 @@ - GCANet - RefineDNet - 后处理:手动 S/V、HSV 直方图匹配、自动 S/V、直方图匹配 + 自动 S/V +- 一键批量默认流程:所有图片运行 `Baidu_API + 自动 S/V` +- 网页端 ZIP 下载:当前图片或全部图片结果打包下载 ## 2. 启动网页 @@ -64,10 +66,14 @@ BAIDU_SECRET_KEY=... 3. 在左侧选择图片。 4. 勾选需要运行的模型,点击“运行选中模型”。 5. 在后处理区域选择源图、参考图和后处理方法,点击“生成后处理”。 -6. 结果会保存到 `web_results/`,网页会自动刷新显示。 +6. 需要批量处理时,点击“批量默认流程”,会对 `待去雾图片/` 中所有图片执行 `Baidu_API -> 自动 S/V`。已有的百度结果和自动 S/V 结果会跳过,避免重复消耗 API。 +7. 需要导出结果时,在“下载”区域点击“下载当前图片”或“批量下载全部”,网页会生成 ZIP 包。 +8. 结果会保存到 `web_results/`,网页会自动刷新显示。 页面中“未生成”表示对应结果文件还不存在,并不是任务卡住。 +批量下载包中包含对应图片的原图、已生成的模型去雾结果和已生成的后处理结果。 + ## 6. 命令行验证 验证全部图片和全部模型: