Add batch default flow and downloads

This commit is contained in:
admin
2026-06-10 17:50:42 +08:00
parent 6db15ebc3f
commit ec5b25d2d0
8 changed files with 187 additions and 6 deletions

View File

@@ -25,6 +25,8 @@ http://192.168.3.11:7860/
- GCANet - GCANet
- RefineDNet - RefineDNet
- 多种 HSV/SV 后处理方式 - 多种 HSV/SV 后处理方式
- 一键批量默认流程:全部图片运行 `Baidu_API + 自动 S/V`
- 网页端批量下载当前图片或全部图片的 ZIP 结果包
## 验证 ## 验证

View File

@@ -2,6 +2,8 @@
本地网页端入口,用于选择 `待去雾图片/` 中的图片,运行 AOD、Baidu_API、DCP、DehazeNet、GCANet、RefineDNet并统一展示结果与后处理结果。 本地网页端入口,用于选择 `待去雾图片/` 中的图片,运行 AOD、Baidu_API、DCP、DehazeNet、GCANet、RefineDNet并统一展示结果与后处理结果。
网页左侧提供“批量默认流程”,会对全部图片运行 `Baidu_API -> 自动 S/V`;下载区可将当前图片或全部图片已有结果打包为 ZIP。
```bash ```bash
bash run_dehaze_web.sh bash run_dehaze_web.sh
``` ```

View File

@@ -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: def _reset_dir(path: Path) -> None:
if path.exists(): if path.exists():
shutil.rmtree(path) shutil.rmtree(path)

View File

@@ -1,10 +1,12 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import io
import json import json
import mimetypes import mimetypes
import threading import threading
import traceback import traceback
import zipfile
from http import HTTPStatus from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from itertools import count 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): class DehazeRequestHandler(BaseHTTPRequestHandler):
server_version = "DehazeWeb/1.0" server_version = "DehazeWeb/1.0"
def log_message(self, fmt: str, *args) -> None: def log_message(self, fmt: str, *args) -> None:
print("[%s] %s" % (self.log_date_time_string(), fmt % args)) 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: def _send(
self.send_response(status) 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-Type", content_type)
self.send_header("Content-Length", str(len(body))) self.send_header("Content-Length", str(len(body)))
for key, value in (headers or {}).items():
self.send_header(key, value)
self.end_headers() self.end_headers()
self.wfile.write(body) self.wfile.write(body)
@@ -179,6 +236,8 @@ class DehazeRequestHandler(BaseHTTPRequestHandler):
elif path == "/api/results": elif path == "/api/results":
filename = query.get("image", [""])[0] filename = query.get("image", [""])[0]
self._send_json(pipeline.get_results(filename)) self._send_json(pipeline.get_results(filename))
elif path == "/api/download":
self._serve_download(query)
elif path == "/api/job": elif path == "/api/job":
job_id = query.get("id", [""])[0] job_id = query.get("id", [""])[0]
with JOB_LOCK: with JOB_LOCK:
@@ -200,6 +259,9 @@ class DehazeRequestHandler(BaseHTTPRequestHandler):
if parsed.path == "/api/run": if parsed.path == "/api/run":
job = _new_job("dehaze", _run_dehaze_job, payload) job = _new_job("dehaze", _run_dehaze_job, payload)
self._send_json({"job": job}) 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": elif parsed.path == "/api/postprocess":
job = _new_job("postprocess", _run_post_job, payload) job = _new_job("postprocess", _run_post_job, payload)
self._send_json({"job": job}) self._send_json({"job": job})
@@ -230,6 +292,35 @@ class DehazeRequestHandler(BaseHTTPRequestHandler):
content_type = mimetypes.guess_type(target.name)[0] or "application/octet-stream" content_type = mimetypes.guess_type(target.name)[0] or "application/octet-stream"
self._send(HTTPStatus.OK, target.read_bytes(), content_type) 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: def main() -> None:
parser = argparse.ArgumentParser(description="Local dehaze web console") parser = argparse.ArgumentParser(description="Local dehaze web console")

View File

@@ -49,6 +49,7 @@ function renderMethods() {
box.innerHTML = ""; box.innerHTML = "";
const methods = state.capabilities?.methods || {}; const methods = state.capabilities?.methods || {};
Object.entries(methods).forEach(([key, info]) => { Object.entries(methods).forEach(([key, info]) => {
const checked = key === "Baidu_API" || key === "DCP";
const label = document.createElement("label"); const label = document.createElement("label");
label.className = `check-item ${info.available ? "" : "disabled"}`; label.className = `check-item ${info.available ? "" : "disabled"}`;
label.innerHTML = ` label.innerHTML = `
@@ -56,7 +57,7 @@ function renderMethods() {
<strong>${info.label}</strong> <strong>${info.label}</strong>
<span class="method-meta">${info.available ? "ready" : `${info.module_ok ? "模型" : info.module}`}</span> <span class="method-meta">${info.available ? "ready" : `${info.module_ok ? "模型" : info.module}`}</span>
</span> </span>
<input type="checkbox" value="${key}" ${info.available || key === "Baidu_API" || key === "DCP" ? "" : "disabled"} ${key === "DCP" ? "checked" : ""} /> <input type="checkbox" value="${key}" ${info.available || checked ? "" : "disabled"} ${checked ? "checked" : ""} />
`; `;
box.appendChild(label); box.appendChild(label);
}); });
@@ -71,7 +72,7 @@ function renderPostprocessors() {
label.className = "check-item"; label.className = "check-item";
label.innerHTML = ` label.innerHTML = `
<span><strong>${info.label}</strong></span> <span><strong>${info.label}</strong></span>
<input type="checkbox" value="${key}" ${key === "manual_sv" ? "checked" : ""} /> <input type="checkbox" value="${key}" ${key === "auto_sv" ? "checked" : ""} />
`; `;
box.appendChild(label); box.appendChild(label);
}); });
@@ -130,6 +131,8 @@ function updatePostSourceOptions(results) {
}); });
if ([...select.options].some((option) => option.value === previous)) { if ([...select.options].some((option) => option.value === previous)) {
select.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)) { } else if (results.dehaze.some((item) => item.method === "DCP" && item.exists)) {
select.value = "DCP"; select.value = "DCP";
} }
@@ -217,6 +220,10 @@ async function runSelectedModels() {
await startJob("/api/run", payload); await startJob("/api/run", payload);
} }
async function runDefaultBatch() {
await startJob("/api/run-default", { skip_existing: true });
}
async function runPostprocess() { async function runPostprocess() {
if (!state.selectedImage) return; if (!state.selectedImage) return;
const processors = selectedValues("postList"); const processors = selectedValues("postList");
@@ -230,9 +237,21 @@ async function runPostprocess() {
await startJob("/api/postprocess", payload); 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() { function bindControls() {
$("runBtn").addEventListener("click", runSelectedModels); $("runBtn").addEventListener("click", runSelectedModels);
$("runDefaultBatchBtn").addEventListener("click", runDefaultBatch);
$("postBtn").addEventListener("click", runPostprocess); $("postBtn").addEventListener("click", runPostprocess);
$("downloadCurrentBtn").addEventListener("click", downloadCurrent);
$("downloadAllBtn").addEventListener("click", downloadAll);
$("refreshBtn").addEventListener("click", refreshResults); $("refreshBtn").addEventListener("click", refreshResults);
$("sGain").addEventListener("input", () => { $("sGain").addEventListener("input", () => {
$("sGainValue").textContent = `${Math.round(Number($("sGain").value) * 100)}%`; $("sGainValue").textContent = `${Math.round(Number($("sGain").value) * 100)}%`;

View File

@@ -35,7 +35,10 @@
<input id="dcpTx" type="number" min="0.01" max="1" step="0.01" value="0.20" /> <input id="dcpTx" type="number" min="0.01" max="1" step="0.01" value="0.20" />
</label> </label>
</div> </div>
<button id="runBtn" class="primary-btn">运行选中模型</button> <div class="button-stack">
<button id="runBtn" class="primary-btn">运行选中模型</button>
<button id="runDefaultBatchBtn" class="secondary-btn">批量默认流程</button>
</div>
</section> </section>
<section class="panel-section"> <section class="panel-section">
@@ -65,6 +68,14 @@
</label> </label>
<button id="postBtn" class="secondary-btn">生成后处理</button> <button id="postBtn" class="secondary-btn">生成后处理</button>
</section> </section>
<section class="panel-section">
<h2>下载</h2>
<div class="button-stack">
<button id="downloadCurrentBtn" class="text-btn">下载当前图片</button>
<button id="downloadAllBtn" class="text-btn">批量下载全部</button>
</div>
</section>
</aside> </aside>
<section class="workspace"> <section class="workspace">

View File

@@ -210,6 +210,19 @@ h1 {
color: var(--ink); 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 { .slider-row {
display: grid; display: grid;
grid-template-columns: 20px 1fr 52px; grid-template-columns: 20px 1fr 52px;

View File

@@ -11,6 +11,8 @@
- GCANet - GCANet
- RefineDNet - RefineDNet
- 后处理:手动 S/V、HSV 直方图匹配、自动 S/V、直方图匹配 + 自动 S/V - 后处理:手动 S/V、HSV 直方图匹配、自动 S/V、直方图匹配 + 自动 S/V
- 一键批量默认流程:所有图片运行 `Baidu_API + 自动 S/V`
- 网页端 ZIP 下载:当前图片或全部图片结果打包下载
## 2. 启动网页 ## 2. 启动网页
@@ -64,10 +66,14 @@ BAIDU_SECRET_KEY=...
3. 在左侧选择图片。 3. 在左侧选择图片。
4. 勾选需要运行的模型,点击“运行选中模型”。 4. 勾选需要运行的模型,点击“运行选中模型”。
5. 在后处理区域选择源图、参考图和后处理方法,点击“生成后处理”。 5. 在后处理区域选择源图、参考图和后处理方法,点击“生成后处理”。
6. 结果会保存到 `web_results/`,网页会自动刷新显示 6. 需要批量处理时,点击“批量默认流程”,会对 `待去雾图片/` 中所有图片执行 `Baidu_API -> 自动 S/V`。已有的百度结果和自动 S/V 结果会跳过,避免重复消耗 API
7. 需要导出结果时,在“下载”区域点击“下载当前图片”或“批量下载全部”,网页会生成 ZIP 包。
8. 结果会保存到 `web_results/`,网页会自动刷新显示。
页面中“未生成”表示对应结果文件还不存在,并不是任务卡住。 页面中“未生成”表示对应结果文件还不存在,并不是任务卡住。
批量下载包中包含对应图片的原图、已生成的模型去雾结果和已生成的后处理结果。
## 6. 命令行验证 ## 6. 命令行验证
验证全部图片和全部模型: 验证全部图片和全部模型: