Add batch default flow and downloads
This commit is contained in:
@@ -25,6 +25,8 @@ http://192.168.3.11:7860/
|
|||||||
- GCANet
|
- GCANet
|
||||||
- RefineDNet
|
- RefineDNet
|
||||||
- 多种 HSV/SV 后处理方式
|
- 多种 HSV/SV 后处理方式
|
||||||
|
- 一键批量默认流程:全部图片运行 `Baidu_API + 自动 S/V`
|
||||||
|
- 网页端批量下载当前图片或全部图片的 ZIP 结果包
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)}%`;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
8
使用手册.md
8
使用手册.md
@@ -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. 命令行验证
|
||||||
|
|
||||||
验证全部图片和全部模型:
|
验证全部图片和全部模型:
|
||||||
|
|||||||
Reference in New Issue
Block a user