整合去雾网页工具

This commit is contained in:
admin
2026-06-10 17:42:11 +08:00
commit 6db15ebc3f
101 changed files with 10167 additions and 0 deletions

265
web_dehaze/static/app.js Normal file
View File

@@ -0,0 +1,265 @@
const state = {
images: [],
capabilities: null,
selectedImage: null,
currentJob: null,
pollTimer: null,
};
const $ = (id) => document.getElementById(id);
function assetUrl(path) {
return `/asset?path=${encodeURIComponent(path)}&t=${Date.now()}`;
}
async function api(path, options = {}) {
const response = await fetch(path, options);
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || response.statusText);
}
return payload;
}
function formatBytes(bytes) {
if (!Number.isFinite(bytes)) return "";
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
function renderImages() {
const box = $("imageList");
box.innerHTML = "";
state.images.forEach((image) => {
const item = document.createElement("button");
item.className = `image-item ${state.selectedImage === image.name ? "active" : ""}`;
item.type = "button";
item.innerHTML = `
<span>${image.name}</span>
<span class="image-meta">${image.width || "?"}x${image.height || "?"} · ${formatBytes(image.size)}</span>
`;
item.addEventListener("click", () => selectImage(image.name));
box.appendChild(item);
});
}
function renderMethods() {
const box = $("methodList");
box.innerHTML = "";
const methods = state.capabilities?.methods || {};
Object.entries(methods).forEach(([key, info]) => {
const label = document.createElement("label");
label.className = `check-item ${info.available ? "" : "disabled"}`;
label.innerHTML = `
<span>
<strong>${info.label}</strong>
<span class="method-meta">${info.available ? "ready" : `${info.module_ok ? "模型" : info.module}`}</span>
</span>
<input type="checkbox" value="${key}" ${info.available || key === "Baidu_API" || key === "DCP" ? "" : "disabled"} ${key === "DCP" ? "checked" : ""} />
`;
box.appendChild(label);
});
}
function renderPostprocessors() {
const box = $("postList");
box.innerHTML = "";
const processors = state.capabilities?.postprocessors || {};
Object.entries(processors).forEach(([key, info]) => {
const label = document.createElement("label");
label.className = "check-item";
label.innerHTML = `
<span><strong>${info.label}</strong></span>
<input type="checkbox" value="${key}" ${key === "manual_sv" ? "checked" : ""} />
`;
box.appendChild(label);
});
}
function renderReferenceOptions() {
const select = $("referenceImage");
select.innerHTML = "";
state.images.forEach((image) => {
const option = document.createElement("option");
option.value = image.name;
option.textContent = image.name;
option.selected = image.name === state.selectedImage;
select.appendChild(option);
});
}
function setJobState(text, status = "") {
const box = $("jobState");
box.textContent = text;
box.className = `job-state ${status}`;
}
async function selectImage(name) {
state.selectedImage = name;
$("currentTitle").textContent = name;
renderImages();
renderReferenceOptions();
await refreshResults();
}
function resultCard(title, path, exists = true) {
const card = document.createElement("article");
card.className = "result-card";
const badge = exists ? '<span class="badge ok">ready</span>' : '<span class="badge pending">未生成</span>';
const body = exists
? `<div class="image-frame"><img src="${assetUrl(path)}" alt="${title}" loading="lazy" /></div>`
: '<div class="image-frame"><span class="missing">暂无结果</span></div>';
card.innerHTML = `<header><h3>${title}</h3>${badge}</header>${body}`;
return card;
}
function updatePostSourceOptions(results) {
const select = $("postSource");
const previous = select.value;
select.innerHTML = "";
const original = document.createElement("option");
original.value = "original";
original.textContent = "原图";
select.appendChild(original);
results.dehaze.filter((item) => item.exists).forEach((item) => {
const option = document.createElement("option");
option.value = item.method;
option.textContent = item.label;
select.appendChild(option);
});
if ([...select.options].some((option) => option.value === previous)) {
select.value = previous;
} else if (results.dehaze.some((item) => item.method === "DCP" && item.exists)) {
select.value = "DCP";
}
}
async function refreshResults() {
if (!state.selectedImage) return;
const results = await api(`/api/results?image=${encodeURIComponent(state.selectedImage)}`);
const grid = $("resultGrid");
grid.innerHTML = "";
grid.appendChild(resultCard(results.original.label, results.original.path, true));
results.dehaze.forEach((item) => {
grid.appendChild(resultCard(item.label, item.path, item.exists));
});
results.postprocess.forEach((item) => {
grid.appendChild(resultCard(item.name, item.path, true));
});
updatePostSourceOptions(results);
}
function selectedValues(containerId) {
return [...$(containerId).querySelectorAll("input[type=checkbox]:checked")].map((input) => input.value);
}
function postParams() {
return {
manual_sv: {
s_gain: Number($("sGain").value),
v_gain: Number($("vGain").value),
},
hsv_hist: {
match_hue: $("matchHue").checked,
},
hist_auto_sv: {
match_hue: $("matchHue").checked,
},
};
}
async function startJob(endpoint, payload) {
const response = await api(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
state.currentJob = response.job.id;
$("logBox").textContent = "";
setJobState("Running", "running");
pollJob();
}
async function pollJob() {
if (!state.currentJob) return;
clearTimeout(state.pollTimer);
try {
const job = await api(`/api/job?id=${encodeURIComponent(state.currentJob)}`);
$("logBox").textContent = (job.logs || []).join("\n");
$("logBox").scrollTop = $("logBox").scrollHeight;
if (job.status === "running") {
setJobState("Running", "running");
state.pollTimer = setTimeout(pollJob, 1000);
} else {
setJobState(job.status === "done" ? "Done" : "Error", job.status);
await refreshResults();
}
} catch (error) {
setJobState("Error", "error");
$("logBox").textContent = error.message;
}
}
async function runSelectedModels() {
if (!state.selectedImage) return;
const methods = selectedValues("methodList");
const payload = {
image: state.selectedImage,
methods,
options: {
DCP: {
sz: Number($("dcpSz").value),
tx: Number($("dcpTx").value),
},
},
};
await startJob("/api/run", payload);
}
async function runPostprocess() {
if (!state.selectedImage) return;
const processors = selectedValues("postList");
const payload = {
image: state.selectedImage,
source: $("postSource").value,
reference: $("referenceImage").value || state.selectedImage,
processors,
params: postParams(),
};
await startJob("/api/postprocess", payload);
}
function bindControls() {
$("runBtn").addEventListener("click", runSelectedModels);
$("postBtn").addEventListener("click", runPostprocess);
$("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)}%`;
});
}
async function init() {
bindControls();
const [capabilities, images] = await Promise.all([api("/api/capabilities"), api("/api/images")]);
state.capabilities = capabilities;
state.images = images.images || [];
$("envLine").textContent = capabilities.results_dir;
renderMethods();
renderPostprocessors();
renderImages();
if (state.images.length) {
await selectImage(state.images[0].name);
} else {
$("currentTitle").textContent = "待去雾图片为空";
}
setJobState("Idle");
}
init().catch((error) => {
setJobState("Error", "error");
$("logBox").textContent = error.stack || error.message;
});

View File

@@ -0,0 +1,93 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dehaze Console</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<main class="shell">
<aside class="control-panel">
<div class="brand">
<span class="brand-mark"></span>
<div>
<h1>Dehaze Console</h1>
<p id="envLine">Loading...</p>
</div>
</div>
<section class="panel-section">
<h2>待去雾图片</h2>
<div id="imageList" class="image-list"></div>
</section>
<section class="panel-section">
<h2>模型</h2>
<div id="methodList" class="check-grid"></div>
<div class="param-grid">
<label>
<span>DCP 窗口</span>
<input id="dcpSz" type="number" min="3" max="99" step="1" value="10" />
</label>
<label>
<span>DCP tx</span>
<input id="dcpTx" type="number" min="0.01" max="1" step="0.01" value="0.20" />
</label>
</div>
<button id="runBtn" class="primary-btn">运行选中模型</button>
</section>
<section class="panel-section">
<h2>后处理</h2>
<label class="field">
<span>源图</span>
<select id="postSource"></select>
</label>
<label class="field">
<span>参考图</span>
<select id="referenceImage"></select>
</label>
<div id="postList" class="check-grid compact"></div>
<div class="slider-row">
<span>S</span>
<input id="sGain" type="range" min="0" max="2.5" step="0.01" value="1" />
<strong id="sGainValue">100%</strong>
</div>
<div class="slider-row">
<span>V</span>
<input id="vGain" type="range" min="0" max="2.5" step="0.01" value="1" />
<strong id="vGainValue">100%</strong>
</div>
<label class="toggle-line">
<input id="matchHue" type="checkbox" />
<span>匹配 H 通道</span>
</label>
<button id="postBtn" class="secondary-btn">生成后处理</button>
</section>
</aside>
<section class="workspace">
<header class="topbar">
<div>
<p class="eyebrow">当前图片</p>
<h2 id="currentTitle">未选择</h2>
</div>
<div id="jobState" class="job-state">Idle</div>
</header>
<section id="resultGrid" class="result-grid"></section>
<section class="log-panel">
<div class="log-head">
<h2>日志</h2>
<button id="refreshBtn" class="text-btn">刷新</button>
</div>
<pre id="logBox"></pre>
</section>
</section>
</main>
<script src="/static/app.js"></script>
</body>
</html>

428
web_dehaze/static/style.css Normal file
View File

@@ -0,0 +1,428 @@
:root {
--paper: #f4f0e8;
--panel: #fffaf1;
--ink: #1e2520;
--muted: #6d756f;
--line: #d8d0c3;
--green: #1f6b57;
--green-dark: #124839;
--cobalt: #295f9f;
--amber: #c1842d;
--red: #b3473f;
--shadow: 0 18px 60px rgba(33, 37, 31, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
background:
linear-gradient(90deg, rgba(31, 107, 87, 0.06) 1px, transparent 1px),
linear-gradient(0deg, rgba(31, 107, 87, 0.05) 1px, transparent 1px),
var(--paper);
background-size: 28px 28px;
font-family: "Aptos", "Noto Sans SC", "Microsoft YaHei", sans-serif;
}
button,
input,
select {
font: inherit;
}
.shell {
display: grid;
grid-template-columns: minmax(300px, 360px) 1fr;
min-height: 100vh;
}
.control-panel {
padding: 24px 20px;
border-right: 1px solid var(--line);
background: rgba(255, 250, 241, 0.94);
box-shadow: var(--shadow);
overflow-y: auto;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
padding-bottom: 22px;
border-bottom: 1px solid var(--line);
}
.brand-mark {
width: 42px;
height: 42px;
border: 2px solid var(--ink);
background:
linear-gradient(135deg, transparent 46%, var(--ink) 47%, var(--ink) 53%, transparent 54%),
linear-gradient(45deg, var(--green) 0 48%, var(--amber) 48% 100%);
}
h1,
h2,
p {
margin: 0;
}
h1 {
font-size: 24px;
line-height: 1;
}
.brand p,
.eyebrow {
margin-top: 6px;
color: var(--muted);
font-size: 12px;
}
.panel-section {
padding: 20px 0;
border-bottom: 1px solid var(--line);
}
.panel-section h2,
.log-head h2 {
margin-bottom: 12px;
font-size: 14px;
letter-spacing: 0;
}
.image-list,
.check-grid {
display: grid;
gap: 8px;
}
.image-item,
.check-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 42px;
padding: 9px 10px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.55);
}
.image-item {
cursor: pointer;
}
.image-item.active {
border-color: var(--green);
background: rgba(31, 107, 87, 0.12);
}
.image-meta,
.method-meta {
color: var(--muted);
font-size: 11px;
}
.check-item.disabled {
color: var(--muted);
background: rgba(216, 208, 195, 0.34);
}
.check-item input {
width: 18px;
height: 18px;
accent-color: var(--green);
}
.compact {
grid-template-columns: 1fr;
}
.param-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 12px;
}
.param-grid label,
.field {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 12px;
}
.param-grid input,
.field select {
width: 100%;
min-height: 38px;
border: 1px solid var(--line);
background: #fffdf8;
color: var(--ink);
padding: 7px 8px;
}
.field {
margin-bottom: 10px;
}
.primary-btn,
.secondary-btn,
.text-btn {
min-height: 42px;
border: 1px solid transparent;
cursor: pointer;
}
.primary-btn,
.secondary-btn {
width: 100%;
margin-top: 12px;
color: #fff;
}
.primary-btn {
background: var(--green);
}
.primary-btn:hover {
background: var(--green-dark);
}
.secondary-btn {
background: var(--cobalt);
}
.secondary-btn:hover {
background: #1e4d83;
}
.text-btn {
padding: 0 12px;
border-color: var(--line);
background: #fffdf8;
color: var(--ink);
}
.slider-row {
display: grid;
grid-template-columns: 20px 1fr 52px;
align-items: center;
gap: 8px;
margin-top: 12px;
color: var(--muted);
font-size: 12px;
}
.slider-row input {
accent-color: var(--green);
}
.slider-row strong {
color: var(--ink);
font-size: 12px;
text-align: right;
}
.toggle-line {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
color: var(--muted);
font-size: 12px;
}
.toggle-line input {
accent-color: var(--green);
}
.workspace {
min-width: 0;
padding: 26px;
overflow: hidden;
}
.topbar {
display: flex;
align-items: end;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.topbar h2 {
margin-top: 4px;
font-size: clamp(22px, 3vw, 38px);
line-height: 1.05;
word-break: break-all;
}
.job-state {
min-width: 92px;
padding: 8px 12px;
border: 1px solid var(--line);
background: rgba(255, 250, 241, 0.82);
text-align: center;
color: var(--muted);
}
.job-state.running {
color: var(--cobalt);
border-color: rgba(41, 95, 159, 0.35);
}
.job-state.error {
color: var(--red);
border-color: rgba(179, 71, 63, 0.4);
}
.job-state.done {
color: var(--green);
border-color: rgba(31, 107, 87, 0.4);
}
.result-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 14px;
max-height: calc(100vh - 300px);
overflow-y: auto;
padding-right: 4px;
}
.result-card {
display: grid;
grid-template-rows: auto 1fr;
min-height: 230px;
border: 1px solid var(--line);
background: rgba(255, 250, 241, 0.82);
}
.result-card header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 42px;
padding: 10px;
border-bottom: 1px solid var(--line);
}
.result-card h3 {
margin: 0;
font-size: 13px;
word-break: break-word;
}
.badge {
flex: 0 0 auto;
padding: 3px 7px;
border: 1px solid var(--line);
color: var(--muted);
font-size: 11px;
}
.badge.ok {
color: var(--green);
border-color: rgba(31, 107, 87, 0.4);
}
.badge.pending {
color: var(--amber);
border-color: rgba(193, 132, 45, 0.42);
}
.image-frame {
display: grid;
place-items: center;
min-height: 188px;
padding: 8px;
background:
linear-gradient(45deg, rgba(30, 37, 32, 0.05) 25%, transparent 25% 75%, rgba(30, 37, 32, 0.05) 75%),
linear-gradient(45deg, rgba(30, 37, 32, 0.05) 25%, transparent 25% 75%, rgba(30, 37, 32, 0.05) 75%);
background-position: 0 0, 8px 8px;
background-size: 16px 16px;
}
.image-frame img {
display: block;
max-width: 100%;
max-height: 38vh;
object-fit: contain;
background: #fff;
}
.missing {
color: var(--muted);
font-size: 13px;
}
.log-panel {
margin-top: 18px;
border: 1px solid var(--line);
background: rgba(30, 37, 32, 0.92);
color: #e9eadf;
}
.log-head {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 44px;
padding: 8px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}
.log-head h2 {
margin: 0;
color: #f7f2e8;
}
.log-panel .text-btn {
min-height: 30px;
background: transparent;
color: #f7f2e8;
border-color: rgba(255, 255, 255, 0.22);
}
#logBox {
height: 172px;
margin: 0;
padding: 12px;
overflow: auto;
white-space: pre-wrap;
font-family: "Cascadia Mono", "Noto Sans Mono", monospace;
font-size: 12px;
line-height: 1.5;
}
@media (max-width: 900px) {
.shell {
grid-template-columns: 1fr;
}
.control-panel {
border-right: 0;
border-bottom: 1px solid var(--line);
}
.workspace {
padding: 20px;
}
.topbar {
align-items: start;
flex-direction: column;
}
.result-grid {
max-height: none;
}
}