Show SV metadata for postprocess results
This commit is contained in:
@@ -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"
|
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:
|
def relpath(path: Path) -> str:
|
||||||
return path.resolve().relative_to(ROOT).as_posix()
|
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"
|
post_dir = image_result_dir(filename) / "postprocess"
|
||||||
if post_dir.exists():
|
if post_dir.exists():
|
||||||
for path in sorted(post_dir.glob("*.png"), key=lambda p: p.name.lower()):
|
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 {
|
return {
|
||||||
"image": filename,
|
"image": filename,
|
||||||
@@ -249,6 +281,7 @@ def collect_download_items(filenames: list[str] | None = None, include_original:
|
|||||||
if post_dir.exists():
|
if post_dir.exists():
|
||||||
for result in sorted(post_dir.glob("*.png"), key=lambda p: p.name.lower()):
|
for result in sorted(post_dir.glob("*.png"), key=lambda p: p.name.lower()):
|
||||||
add_file(result, f"{folder}/postprocess/{result.name}")
|
add_file(result, f"{folder}/postprocess/{result.name}")
|
||||||
|
add_file(post_metadata_path(result), f"{folder}/postprocess/{post_metadata_path(result).name}")
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
@@ -523,6 +556,19 @@ def run_postprocessors_for_source(
|
|||||||
proc_params = params.get(processor, params)
|
proc_params = params.get(processor, params)
|
||||||
output = post_result_path(filename, source_name, processor, proc_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)
|
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)
|
outputs.append(output)
|
||||||
log(f"后处理完成 {source_name} / {processor}: {json.dumps(meta, ensure_ascii=False)}")
|
log(f"后处理完成 {source_name} / {processor}: {json.dumps(meta, ensure_ascii=False)}")
|
||||||
return outputs
|
return outputs
|
||||||
|
|||||||
@@ -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)
|
baidu_output = pipeline.run_dehaze_method(filename, "Baidu_API", {}, log)
|
||||||
|
|
||||||
auto_output = pipeline.post_result_path(filename, "Baidu_API", "auto_sv", {})
|
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)}")
|
log(f"[自动 S/V] 已存在,跳过: {pipeline.relpath(auto_output)}")
|
||||||
else:
|
else:
|
||||||
|
if auto_output.exists() and not auto_meta.exists():
|
||||||
|
log("[自动 S/V] 已有图片但缺少 S/V 元数据,重新计算")
|
||||||
outputs = pipeline.run_postprocessors_for_source(
|
outputs = pipeline.run_postprocessors_for_source(
|
||||||
filename,
|
filename,
|
||||||
"Baidu_API",
|
"Baidu_API",
|
||||||
|
|||||||
@@ -28,6 +28,56 @@ function formatBytes(bytes) {
|
|||||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
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() {
|
function renderImages() {
|
||||||
const box = $("imageList");
|
const box = $("imageList");
|
||||||
box.innerHTML = "";
|
box.innerHTML = "";
|
||||||
@@ -104,14 +154,26 @@ async function selectImage(name) {
|
|||||||
await refreshResults();
|
await refreshResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resultCard(title, path, exists = true) {
|
function resultCard(title, path, exists = true, item = null) {
|
||||||
const card = document.createElement("article");
|
const card = document.createElement("article");
|
||||||
card.className = "result-card";
|
const summary = item ? svSummary(item.meta) : "";
|
||||||
|
card.className = `result-card ${summary ? "clickable" : ""}`;
|
||||||
const badge = exists ? '<span class="badge ok">ready</span>' : '<span class="badge pending">未生成</span>';
|
const badge = exists ? '<span class="badge ok">ready</span>' : '<span class="badge pending">未生成</span>';
|
||||||
const body = exists
|
const body = exists
|
||||||
? `<div class="image-frame"><img src="${assetUrl(path)}" alt="${title}" loading="lazy" /></div>`
|
? `<div class="image-frame"><img src="${assetUrl(path)}" alt="${title}" loading="lazy" /></div>${summary ? `<div class="sv-strip"><span>${summary}</span><small>点击载入</small></div>` : ""}`
|
||||||
: '<div class="image-frame"><span class="missing">暂无结果</span></div>';
|
: '<div class="image-frame"><span class="missing">暂无结果</span></div>';
|
||||||
card.innerHTML = `<header><h3>${title}</h3>${badge}</header>${body}`;
|
card.innerHTML = `<header><h3>${title}</h3>${badge}</header>${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;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +210,7 @@ async function refreshResults() {
|
|||||||
grid.appendChild(resultCard(item.label, item.path, item.exists));
|
grid.appendChild(resultCard(item.label, item.path, item.exists));
|
||||||
});
|
});
|
||||||
results.postprocess.forEach((item) => {
|
results.postprocess.forEach((item) => {
|
||||||
grid.appendChild(resultCard(item.name, item.path, true));
|
grid.appendChild(resultCard(item.name, item.path, true, item));
|
||||||
});
|
});
|
||||||
updatePostSourceOptions(results);
|
updatePostSourceOptions(results);
|
||||||
}
|
}
|
||||||
@@ -253,12 +315,9 @@ function bindControls() {
|
|||||||
$("downloadCurrentBtn").addEventListener("click", downloadCurrent);
|
$("downloadCurrentBtn").addEventListener("click", downloadCurrent);
|
||||||
$("downloadAllBtn").addEventListener("click", downloadAll);
|
$("downloadAllBtn").addEventListener("click", downloadAll);
|
||||||
$("refreshBtn").addEventListener("click", refreshResults);
|
$("refreshBtn").addEventListener("click", refreshResults);
|
||||||
$("sGain").addEventListener("input", () => {
|
$("sGain").addEventListener("input", updateGainLabels);
|
||||||
$("sGainValue").textContent = `${Math.round(Number($("sGain").value) * 100)}%`;
|
$("vGain").addEventListener("input", updateGainLabels);
|
||||||
});
|
updateGainLabels();
|
||||||
$("vGain").addEventListener("input", () => {
|
|
||||||
$("vGainValue").textContent = `${Math.round(Number($("vGain").value) * 100)}%`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
|||||||
@@ -318,6 +318,17 @@ h1 {
|
|||||||
background: rgba(255, 250, 241, 0.82);
|
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 {
|
.result-card header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -372,6 +383,32 @@ h1 {
|
|||||||
background: #fff;
|
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 {
|
.missing {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
5
使用手册.md
5
使用手册.md
@@ -67,8 +67,9 @@ BAIDU_SECRET_KEY=...
|
|||||||
4. 勾选需要运行的模型,点击“运行选中模型”。
|
4. 勾选需要运行的模型,点击“运行选中模型”。
|
||||||
5. 在后处理区域选择源图、参考图和后处理方法,点击“生成后处理”。
|
5. 在后处理区域选择源图、参考图和后处理方法,点击“生成后处理”。
|
||||||
6. 需要批量处理时,点击“批量默认流程”,会对 `待去雾图片/` 中所有图片执行 `Baidu_API -> 自动 S/V`。已有的百度结果和自动 S/V 结果会跳过,避免重复消耗 API。
|
6. 需要批量处理时,点击“批量默认流程”,会对 `待去雾图片/` 中所有图片执行 `Baidu_API -> 自动 S/V`。已有的百度结果和自动 S/V 结果会跳过,避免重复消耗 API。
|
||||||
7. 需要导出结果时,在“下载”区域点击“下载当前图片”或“批量下载全部”,网页会生成 ZIP 包。
|
7. 自动 S/V、手动 S/V 等后处理卡片会显示对应的 S/V 参数;点击带有 S/V 的结果卡片,会把参数载入左侧手动 S/V 滑杆,方便继续人工微调。
|
||||||
8. 结果会保存到 `web_results/`,网页会自动刷新显示。
|
8. 需要导出结果时,在“下载”区域点击“下载当前图片”或“批量下载全部”,网页会生成 ZIP 包。
|
||||||
|
9. 结果会保存到 `web_results/`,网页会自动刷新显示。
|
||||||
|
|
||||||
页面中“未生成”表示对应结果文件还不存在,并不是任务卡住。
|
页面中“未生成”表示对应结果文件还不存在,并不是任务卡住。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user