776 lines
24 KiB
HTML
776 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>OpenCV WASM 测试平台</title>
|
|
<style>
|
|
/* ===== Reset & Base ===== */
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
background: #FEF9E7;
|
|
color: #1a1a1a;
|
|
padding: 40px 20px;
|
|
min-height: 100vh;
|
|
}
|
|
.container {
|
|
max-width: 960px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* ===== Header ===== */
|
|
h1 {
|
|
font-size: 28px;
|
|
font-weight: 800;
|
|
margin-bottom: 8px;
|
|
}
|
|
.subtitle {
|
|
font-size: 14px;
|
|
color: #555;
|
|
margin-bottom: 24px;
|
|
}
|
|
.subtitle code {
|
|
background: #eee;
|
|
padding: 2px 6px;
|
|
border: 1px solid #ccc;
|
|
}
|
|
|
|
/* ===== Tabs ===== */
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0;
|
|
border-bottom: 3px solid #000;
|
|
margin-bottom: 24px;
|
|
}
|
|
.tab-btn {
|
|
padding: 12px 24px;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
background: #e8e0d0;
|
|
border: 2px solid #000;
|
|
border-bottom: none;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
position: relative;
|
|
margin-right: 4px;
|
|
}
|
|
.tab-btn:hover { background: #ddd5c5; }
|
|
.tab-btn.active {
|
|
background: #FEF9E7;
|
|
transform: translateY(3px);
|
|
border-bottom: 3px solid #FEF9E7;
|
|
}
|
|
.tab-btn:focus-visible { outline: 3px solid #000; outline-offset: -3px; }
|
|
|
|
/* ===== Tab Content ===== */
|
|
.tab-panel { display: none; }
|
|
.tab-panel.active { display: block; }
|
|
|
|
/* ===== Card ===== */
|
|
.card {
|
|
background: #fff;
|
|
border: 3px solid #000;
|
|
box-shadow: 6px 6px 0 #000;
|
|
padding: 24px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
/* ===== Buttons ===== */
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 20px;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
border: 3px solid #000;
|
|
box-shadow: 4px 4px 0 #000;
|
|
cursor: pointer;
|
|
transition: all 0.1s;
|
|
background: #fff;
|
|
}
|
|
.btn:hover { transform: translate(-2px, -2px); box-shadow: 6px 6px 0 #000; }
|
|
.btn:active { transform: translate(2px, 2px); box-shadow: 2px 2px 0 #000; }
|
|
.btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: 4px 4px 0 #000;
|
|
}
|
|
.btn-primary { background: #ffd43b; }
|
|
.btn-success { background: #51cf66; }
|
|
.btn-danger { background: #ff6b6b; color: #fff; }
|
|
.btn-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; align-items: center; }
|
|
|
|
/* ===== File Input ===== */
|
|
.file-label {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 20px;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
border: 3px solid #000;
|
|
box-shadow: 4px 4px 0 #000;
|
|
cursor: pointer;
|
|
background: #d0ebff;
|
|
transition: all 0.1s;
|
|
}
|
|
.file-label:hover { transform: translate(-2px, -2px); box-shadow: 6px 6px 0 #000; }
|
|
.file-label input[type="file"] { display: none; }
|
|
|
|
/* ===== Output Area ===== */
|
|
.output-box {
|
|
background: #1a1a1a;
|
|
color: #4af626;
|
|
font-family: "Courier New", monospace;
|
|
font-size: 14px;
|
|
padding: 16px;
|
|
border: 3px solid #000;
|
|
min-height: 80px;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
/* ===== Image Compare ===== */
|
|
.image-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
}
|
|
.image-box {
|
|
border: 3px solid #000;
|
|
overflow: hidden;
|
|
background: #f0f0f0;
|
|
}
|
|
.image-box .label {
|
|
background: #000;
|
|
color: #fff;
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
padding: 6px 12px;
|
|
text-align: center;
|
|
}
|
|
.image-box canvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: auto;
|
|
min-height: 200px;
|
|
}
|
|
.image-box .placeholder {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 200px;
|
|
color: #999;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* ===== Status Badge ===== */
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 4px 10px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
border: 2px solid #000;
|
|
}
|
|
.badge-success { background: #51cf66; }
|
|
.badge-wait { background: #ffd43b; }
|
|
.badge-error { background: #ff6b6b; color: #fff; }
|
|
|
|
/* ===== Logos / Status ===== */
|
|
.status-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* ===== Responsive ===== */
|
|
@media (max-width: 640px) {
|
|
.image-grid { grid-template-columns: 1fr; }
|
|
.tab-btn { padding: 10px 16px; font-size: 13px; }
|
|
.card { padding: 16px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🧪 OpenCV WASM 测试</h1>
|
|
<p class="subtitle">测试编译产物:<code>opencv_version.wasm</code> (core+imgproc+imgcodecs) + 自动白平衡</p>
|
|
|
|
<!-- Tabs -->
|
|
<div class="tabs" role="tablist">
|
|
<button class="tab-btn active" role="tab" aria-selected="true" data-tab="version">📟 opencv_version</button>
|
|
<button class="tab-btn" role="tab" aria-selected="false" data-tab="awb">🎨 自动白平衡</button>
|
|
<button class="tab-btn" role="tab" aria-selected="false" data-tab="opencv-awb">🧠 OpenCV AWB</button>
|
|
</div>
|
|
|
|
<!-- ===== Panel: opencv_version ===== -->
|
|
<div class="tab-panel active" id="panel-version" role="tabpanel">
|
|
<div class="card">
|
|
<div class="status-bar">
|
|
<span class="badge badge-wait" id="version-status">等待运行</span>
|
|
<span id="version-info" style="font-size:13px;color:#666;">
|
|
WASM: opencv_version.wasm (230K) · 依赖: libopencv_*.so (3 个动态库)
|
|
</span>
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<button class="btn btn-primary" id="btn-run-version">▶ 运行 opencv_version</button>
|
|
<button class="btn" id="btn-reset-version" disabled>↻ 重置</button>
|
|
</div>
|
|
|
|
<p style="font-size:13px;color:#555;margin-bottom:8px;">
|
|
点击运行,通过 Emscripten 动态加载 opencv_version.wasm,输出 OpenCV 版本信息。
|
|
</p>
|
|
|
|
<div class="output-box" id="version-output">等待运行…</div>
|
|
</div>
|
|
|
|
<div class="card" style="background:#fff3e0;">
|
|
<p style="font-weight:700;margin-bottom:8px;">📋 编译信息</p>
|
|
<pre style="font-size:13px;line-height:1.6;white-space:pre-wrap;">
|
|
OpenCV 4.10.0
|
|
构建模块: core, imgproc, imgcodecs
|
|
编译模式: MinSizeRel
|
|
SIMD: 启用 (-msimd128)
|
|
编译器: Emscripten (emcc)
|
|
动态库: libopencv_core.so (1.7M), libopencv_imgproc.so (2.6M), libopencv_imgcodecs.so (1.5M)
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== Panel: 自动白平衡 ===== -->
|
|
<div class="tab-panel" id="panel-awb" role="tabpanel">
|
|
<div class="card">
|
|
<div class="status-bar">
|
|
<span class="badge badge-wait" id="awb-status">等待操作</span>
|
|
<span style="font-size:13px;color:#666;">
|
|
灰度世界假设 (Gray World) 算法 · 无外部依赖
|
|
</span>
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<label class="file-label">
|
|
📁 选择图片
|
|
<input type="file" id="image-input" accept="image/jpeg,image/png,image/webp">
|
|
</label>
|
|
<button class="btn btn-success" id="btn-awb" disabled>✨ 白平衡处理</button>
|
|
<button class="btn btn-danger" id="btn-reset-awb" disabled>↺ 重置</button>
|
|
</div>
|
|
|
|
<p style="font-size:13px;color:#555;margin-bottom:16px;">
|
|
选择一张图片,点击「白平衡处理」即可看到灰度世界算法自动校正色偏的效果。
|
|
</p>
|
|
|
|
<div class="image-grid">
|
|
<div class="image-box">
|
|
<div class="label">📷 原图</div>
|
|
<canvas id="canvas-original"></canvas>
|
|
<div class="placeholder" id="placeholder-original">
|
|
请选择一张图片
|
|
</div>
|
|
</div>
|
|
<div class="image-box">
|
|
<div class="label">✨ 白平衡处理后</div>
|
|
<canvas id="canvas-result"></canvas>
|
|
<div class="placeholder" id="placeholder-result">
|
|
处理后显示在此处
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== Panel: OpenCV AWB ===== -->
|
|
<div class="tab-panel" id="panel-opencv-awb" role="tabpanel">
|
|
<div class="card">
|
|
<div class="status-bar">
|
|
<span class="badge badge-wait" id="ocv-awb-status">等待上传</span>
|
|
<span style="font-size:13px;color:#666;">
|
|
OpenCV 灰度世界算法 · Server-side WASM
|
|
</span>
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<label class="file-label">
|
|
📁 选择图片
|
|
<input type="file" id="ocv-image-input" accept="image/jpeg,image/png,image/webp">
|
|
</label>
|
|
<button class="btn btn-success" id="btn-ocv-awb" disabled>🧠 OpenCV 白平衡</button>
|
|
<button class="btn btn-danger" id="btn-ocv-reset" disabled>↺ 重置</button>
|
|
</div>
|
|
|
|
<p style="font-size:13px;color:#555;margin-bottom:16px;">
|
|
上传图片后,发送到服务器由 <strong>OpenCV C++ 灰度世界算法</strong>(静态编译 WASM)处理,
|
|
返回校正后的图片。基于真实的 OpenCV 实现,与纯 JS 算法对比。
|
|
</p>
|
|
|
|
<div class="image-grid">
|
|
<div class="image-box">
|
|
<div class="label">📷 原图</div>
|
|
<canvas id="ocv-canvas-original"></canvas>
|
|
<div class="placeholder" id="ocv-placeholder-original">
|
|
请选择一张图片
|
|
</div>
|
|
</div>
|
|
<div class="image-box">
|
|
<div class="label">🧠 OpenCV 处理后</div>
|
|
<canvas id="ocv-canvas-result"></canvas>
|
|
<div class="placeholder" id="ocv-placeholder-result">
|
|
处理中...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="ocv-meta-box" class="output-box" style="display:none;margin-top:12px;">
|
|
<p style="font-weight:700;margin-bottom:8px;">📊 处理元数据</p>
|
|
<pre id="ocv-meta-content" style="font-size:13px;line-height:1.6;white-space:pre-wrap;"></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="background:#fff3e0;">
|
|
<p style="font-weight:700;margin-bottom:8px;">📋 编译信息</p>
|
|
<pre style="font-size:13px;line-height:1.6;white-space:pre-wrap;">
|
|
OpenCV 4.10.0 · 静态编译 (STATIC_LIBS)
|
|
构建模块: core, imgproc, imgcodecs + libpng/libjpeg/libwebp
|
|
AWB 实现: Gray World 灰度世界算法 (C++ 实现)
|
|
编译: em++ -O2 -msimd128 · 输出 awb_static.wasm (2.5M)
|
|
运行方式: Server-side Node.js,调用 POST /api/awb 处理
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== Footer ===== -->
|
|
<p style="text-align:center;font-size:12px;color:#999;margin-top:32px;border-top:2px solid #ddd;padding-top:16px;">
|
|
OpenCV WASM Test · 编译于 Ubuntu 24.04 · Emscripten + CMake
|
|
</p>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ===== Tab Logic =====
|
|
const tabBtns = document.querySelectorAll('.tab-btn');
|
|
const panels = {
|
|
version: document.getElementById('panel-version'),
|
|
awb: document.getElementById('panel-awb'),
|
|
'opencv-awb': document.getElementById('panel-opencv-awb')
|
|
};
|
|
|
|
tabBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tab = btn.dataset.tab;
|
|
tabBtns.forEach(b => {
|
|
b.classList.remove('active');
|
|
b.setAttribute('aria-selected', 'false');
|
|
});
|
|
btn.classList.add('active');
|
|
btn.setAttribute('aria-selected', 'true');
|
|
Object.keys(panels).forEach(k => panels[k].classList.remove('active'));
|
|
panels[tab].classList.add('active');
|
|
});
|
|
});
|
|
|
|
// ===== opencv_version Runner =====
|
|
const btnRun = document.getElementById('btn-run-version');
|
|
const btnReset = document.getElementById('btn-reset-version');
|
|
const outputEl = document.getElementById('version-output');
|
|
const statusEl = document.getElementById('version-status');
|
|
|
|
let versionRunOnce = false;
|
|
|
|
function setVersionStatus(text, type) {
|
|
statusEl.textContent = text;
|
|
statusEl.className = 'badge badge-' + type;
|
|
}
|
|
|
|
btnRun.addEventListener('click', async () => {
|
|
if (versionRunOnce) return;
|
|
|
|
btnRun.disabled = true;
|
|
outputEl.textContent = '正在加载 opencv_version.wasm …';
|
|
setVersionStatus('加载中', 'wait');
|
|
|
|
const capturedOutput = [];
|
|
|
|
try {
|
|
// Step 1: override console.log/error to capture output
|
|
const origLog = console.log;
|
|
const origErr = console.error;
|
|
console.log = function() {
|
|
const msg = Array.from(arguments).join(' ');
|
|
capturedOutput.push(msg);
|
|
};
|
|
console.error = function() {
|
|
const msg = Array.from(arguments).join(' ');
|
|
capturedOutput.push(msg);
|
|
};
|
|
|
|
// Step 2: set up Module for Emscripten, with locateFile for .so files
|
|
window.Module = {
|
|
noInitialRun: true,
|
|
arguments: [],
|
|
locateFile: function(path, prefix) {
|
|
// Redirect .so files to lib/ directory
|
|
if (path.endsWith('.so') || path.includes('.so.')) {
|
|
return prefix + 'lib/' + path;
|
|
}
|
|
return prefix + path;
|
|
},
|
|
onAbort: function() {
|
|
// Module aborted (after exit)
|
|
},
|
|
onRuntimeInitialized: function() {
|
|
// Runtime initialized, now we can call main
|
|
try {
|
|
Module.callMain([]);
|
|
} catch(e) {
|
|
// exit() throws, that's expected
|
|
}
|
|
}
|
|
};
|
|
|
|
// Step 3: dynamically load opencv_version.js
|
|
await new Promise((resolve, reject) => {
|
|
const script = document.createElement('script');
|
|
script.src = 'opencv_version.js';
|
|
script.onload = resolve;
|
|
script.onerror = () => reject(new Error('加载 opencv_version.js 失败'));
|
|
document.head.appendChild(script);
|
|
});
|
|
|
|
// Step 4: restore console
|
|
console.log = origLog;
|
|
console.error = origErr;
|
|
|
|
// Step 5: display output
|
|
const text = capturedOutput.join('\n').trim();
|
|
if (text) {
|
|
outputEl.textContent = text;
|
|
setVersionStatus('运行成功', 'success');
|
|
} else {
|
|
outputEl.textContent = '⚠️ 未捕获到输出,尝试通过 fallback 获取版本号…';
|
|
// Fallback: try calling the WASM main directly
|
|
try {
|
|
if (window.Module && Module._main) {
|
|
Module.callMain([]);
|
|
}
|
|
} catch(e2) {}
|
|
const text2 = capturedOutput.join('\n').trim();
|
|
if (text2) {
|
|
outputEl.textContent = text2;
|
|
setVersionStatus('运行成功', 'success');
|
|
} else {
|
|
setVersionStatus('⚠️ 无输出', 'error');
|
|
}
|
|
}
|
|
|
|
versionRunOnce = true;
|
|
btnReset.disabled = false;
|
|
|
|
} catch (err) {
|
|
console.log = console.log; // restore might have failed, ensure
|
|
outputEl.textContent = '❌ 错误: ' + err.message;
|
|
setVersionStatus('加载失败', 'error');
|
|
btnRun.disabled = false;
|
|
}
|
|
});
|
|
|
|
btnReset.addEventListener('click', () => {
|
|
versionRunOnce = false;
|
|
btnRun.disabled = false;
|
|
btnReset.disabled = true;
|
|
outputEl.textContent = '等待运行…';
|
|
setVersionStatus('等待运行', 'wait');
|
|
});
|
|
|
|
// ===== Auto White Balance (Gray World) =====
|
|
const fileInput = document.getElementById('image-input');
|
|
const btnAwb = document.getElementById('btn-awb');
|
|
const btnResetAwb = document.getElementById('btn-reset-awb');
|
|
const canvasOrig = document.getElementById('canvas-original');
|
|
const canvasResult = document.getElementById('canvas-result');
|
|
const placeOrig = document.getElementById('placeholder-original');
|
|
const placeResult = document.getElementById('placeholder-result');
|
|
const awbStatus = document.getElementById('awb-status');
|
|
|
|
let loadedImage = null; // HTMLImageElement
|
|
let processedData = null; // ImageData
|
|
|
|
function setAwbStatus(text, type) {
|
|
awbStatus.textContent = text;
|
|
awbStatus.className = 'badge badge-' + type;
|
|
}
|
|
|
|
// Load image from file
|
|
fileInput.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
loadedImage = img;
|
|
processedData = null;
|
|
|
|
// Draw original image
|
|
canvasOrig.width = img.naturalWidth;
|
|
canvasOrig.height = img.naturalHeight;
|
|
const ctx = canvasOrig.getContext('2d');
|
|
ctx.drawImage(img, 0, 0);
|
|
placeOrig.style.display = 'none';
|
|
|
|
// Clear result
|
|
canvasResult.width = 0;
|
|
canvasResult.height = 0;
|
|
placeResult.style.display = 'flex';
|
|
placeResult.textContent = '已加载图片,点击「白平衡处理」';
|
|
|
|
btnAwb.disabled = false;
|
|
btnResetAwb.disabled = false;
|
|
setAwbStatus('图片已加载', 'wait');
|
|
};
|
|
img.src = ev.target.result;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
// Gray World Auto White Balance
|
|
function grayWorldAWB(imageData) {
|
|
const data = imageData.data; // Uint8ClampedArray, RGBA
|
|
const len = data.length;
|
|
|
|
let sumR = 0, sumG = 0, sumB = 0;
|
|
for (let i = 0; i < len; i += 4) {
|
|
sumR += data[i]; // R
|
|
sumG += data[i + 1]; // G
|
|
sumB += data[i + 2]; // B
|
|
}
|
|
|
|
const count = len / 4;
|
|
const meanR = sumR / count;
|
|
const meanG = sumG / count;
|
|
const meanB = sumB / count;
|
|
const grayMean = (meanR + meanG + meanB) / 3;
|
|
|
|
// Avoid division by zero
|
|
const scaleR = grayMean / (meanR || 1);
|
|
const scaleG = grayMean / (meanG || 1);
|
|
const scaleB = grayMean / (meanB || 1);
|
|
|
|
// Apply scaling, clamp to [0, 255]
|
|
for (let i = 0; i < len; i += 4) {
|
|
data[i] = Math.min(255, Math.max(0, Math.round(data[i] * scaleR)));
|
|
data[i + 1] = Math.min(255, Math.max(0, Math.round(data[i + 1] * scaleG)));
|
|
data[i + 2] = Math.min(255, Math.max(0, Math.round(data[i + 2] * scaleB)));
|
|
// data[i + 3] = alpha, unchanged
|
|
}
|
|
|
|
return imageData;
|
|
}
|
|
|
|
// Process AWB
|
|
btnAwb.addEventListener('click', () => {
|
|
if (!loadedImage) return;
|
|
|
|
btnAwb.disabled = true;
|
|
setAwbStatus('处理中…', 'wait');
|
|
|
|
// Use requestAnimationFrame for UI responsiveness
|
|
requestAnimationFrame(() => {
|
|
try {
|
|
// Get original image data
|
|
const ctx = canvasOrig.getContext('2d');
|
|
const srcData = ctx.getImageData(0, 0, canvasOrig.width, canvasOrig.height);
|
|
|
|
// Apply Gray World AWB
|
|
const resultData = grayWorldAWB(srcData);
|
|
|
|
// Draw result
|
|
canvasResult.width = canvasOrig.width;
|
|
canvasResult.height = canvasOrig.height;
|
|
const rctx = canvasResult.getContext('2d');
|
|
rctx.putImageData(resultData, 0, 0);
|
|
placeResult.style.display = 'none';
|
|
|
|
processedData = resultData;
|
|
setAwbStatus('处理完成', 'success');
|
|
btnAwb.disabled = false;
|
|
} catch (err) {
|
|
setAwbStatus('处理出错', 'error');
|
|
placeResult.style.display = 'flex';
|
|
placeResult.textContent = '❌ 处理失败: ' + err.message;
|
|
btnAwb.disabled = false;
|
|
}
|
|
});
|
|
});
|
|
|
|
// Reset AWB
|
|
btnResetAwb.addEventListener('click', () => {
|
|
loadedImage = null;
|
|
processedData = null;
|
|
fileInput.value = '';
|
|
|
|
canvasOrig.width = 0;
|
|
canvasOrig.height = 0;
|
|
canvasResult.width = 0;
|
|
canvasResult.height = 0;
|
|
|
|
placeOrig.style.display = 'flex';
|
|
placeOrig.textContent = '请选择一张图片';
|
|
placeResult.style.display = 'flex';
|
|
placeResult.textContent = '处理后显示在此处';
|
|
|
|
btnAwb.disabled = true;
|
|
btnResetAwb.disabled = true;
|
|
setAwbStatus('等待操作', 'wait');
|
|
});
|
|
|
|
// ===== OpenCV AWB (Server-side WASM) =====
|
|
const ocvFileInput = document.getElementById('ocv-image-input');
|
|
const btnOcvAwb = document.getElementById('btn-ocv-awb');
|
|
const btnOcvReset = document.getElementById('btn-ocv-reset');
|
|
const ocvCanvasOrig = document.getElementById('ocv-canvas-original');
|
|
const ocvCanvasResult = document.getElementById('ocv-canvas-result');
|
|
const ocvPlaceOrig = document.getElementById('ocv-placeholder-original');
|
|
const ocvPlaceResult = document.getElementById('ocv-placeholder-result');
|
|
const ocvMetaBox = document.getElementById('ocv-meta-box');
|
|
const ocvMetaContent = document.getElementById('ocv-meta-content');
|
|
const ocvStatus = document.getElementById('ocv-awb-status');
|
|
|
|
let ocvLoadedImage = null;
|
|
|
|
function setOcvStatus(text, type) {
|
|
ocvStatus.textContent = text;
|
|
ocvStatus.className = 'badge badge-' + type;
|
|
}
|
|
|
|
ocvFileInput.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
ocvLoadedImage = img;
|
|
|
|
ocvCanvasOrig.width = img.naturalWidth;
|
|
ocvCanvasOrig.height = img.naturalHeight;
|
|
const ctx = ocvCanvasOrig.getContext('2d');
|
|
ctx.drawImage(img, 0, 0);
|
|
ocvPlaceOrig.style.display = 'none';
|
|
|
|
ocvCanvasResult.width = 0;
|
|
ocvCanvasResult.height = 0;
|
|
ocvPlaceResult.style.display = 'flex';
|
|
ocvPlaceResult.textContent = '点击「OpenCV 白平衡」发送到服务器处理';
|
|
ocvMetaBox.style.display = 'none';
|
|
|
|
btnOcvAwb.disabled = false;
|
|
btnOcvReset.disabled = false;
|
|
setOcvStatus('图片已加载', 'wait');
|
|
};
|
|
img.src = ev.target.result;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
btnOcvAwb.addEventListener('click', async () => {
|
|
if (!ocvLoadedImage) return;
|
|
|
|
btnOcvAwb.disabled = true;
|
|
setOcvStatus('编码中…', 'wait');
|
|
ocvPlaceResult.textContent = '处理中...';
|
|
ocvPlaceResult.style.display = 'flex';
|
|
|
|
try {
|
|
// Convert canvas to PNG base64
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = ocvLoadedImage.naturalWidth;
|
|
canvas.height = ocvLoadedImage.naturalHeight;
|
|
canvas.getContext('2d').drawImage(ocvLoadedImage, 0, 0);
|
|
const pngB64 = canvas.toDataURL('image/png').split(',')[1];
|
|
|
|
setOcvStatus('发送到服务器…', 'wait');
|
|
|
|
// POST to server
|
|
const resp = await fetch('/api/awb', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ image: pngB64 })
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
throw new Error('服务器错误: ' + resp.status);
|
|
}
|
|
|
|
const result = await resp.json();
|
|
|
|
if (!result.success || !result.image) {
|
|
throw new Error(result.error || '处理失败');
|
|
}
|
|
|
|
// Decode base64 result and display
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
ocvCanvasResult.width = img.naturalWidth;
|
|
ocvCanvasResult.height = img.naturalHeight;
|
|
const ctx = ocvCanvasResult.getContext('2d');
|
|
ctx.drawImage(img, 0, 0);
|
|
ocvPlaceResult.style.display = 'none';
|
|
|
|
// Show metadata
|
|
if (result.metadata) {
|
|
ocvMetaContent.textContent = result.metadata;
|
|
ocvMetaBox.style.display = 'block';
|
|
}
|
|
|
|
setOcvStatus('处理完成 ✅', 'success');
|
|
btnOcvAwb.disabled = false;
|
|
};
|
|
img.onerror = () => {
|
|
throw new Error('无法解码返回的图片');
|
|
};
|
|
img.src = 'data:image/png;base64,' + result.image;
|
|
|
|
} catch (err) {
|
|
setOcvStatus('处理出错', 'error');
|
|
ocvPlaceResult.style.display = 'flex';
|
|
ocvPlaceResult.textContent = '❌ ' + err.message;
|
|
btnOcvAwb.disabled = false;
|
|
}
|
|
});
|
|
|
|
btnOcvReset.addEventListener('click', () => {
|
|
ocvLoadedImage = null;
|
|
ocvFileInput.value = '';
|
|
|
|
ocvCanvasOrig.width = 0;
|
|
ocvCanvasOrig.height = 0;
|
|
ocvCanvasResult.width = 0;
|
|
ocvCanvasResult.height = 0;
|
|
|
|
ocvPlaceOrig.style.display = 'flex';
|
|
ocvPlaceOrig.textContent = '请选择一张图片';
|
|
ocvPlaceResult.style.display = 'flex';
|
|
ocvPlaceResult.textContent = '处理中...';
|
|
ocvMetaBox.style.display = 'none';
|
|
|
|
btnOcvAwb.disabled = true;
|
|
btnOcvReset.disabled = true;
|
|
setOcvStatus('等待上传', 'wait');
|
|
});
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|