Files
nanocode/web-opencv/miniappB/index.html
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

286 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>尿检试纸识别 — 纯 JS 版</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f0f2f5; color: #333; min-height: 100vh; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { font-size: 24px; margin-bottom: 8px; color: #1a1a2e; }
.subtitle { font-size: 13px; color: #999; margin-bottom: 20px; }
.card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card h2 { font-size: 16px; color: #666; margin-bottom: 16px; }
.upload-zone { border: 2px dashed #ccc; border-radius: 12px; padding: 40px;
text-align: center; cursor: pointer; transition: all 0.2s; }
.upload-zone:hover, .upload-zone.dragover { border-color: #4a90d9; background: #f0f7ff; }
.upload-zone input { display: none; }
.upload-zone .icon { font-size: 48px; margin-bottom: 12px; }
.upload-zone .text { font-size: 14px; color: #999; }
.upload-zone .hint { font-size: 12px; color: #bbb; margin-top: 8px; }
.sample-bar { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; align-items: center; }
.sample-label { font-size: 12px; color: #999; }
.sample-btn { padding: 6px 16px; border: 1px solid #ddd; border-radius: 20px;
background: #fff; cursor: pointer; font-size: 12px; transition: all 0.2s; }
.sample-btn:hover { border-color: #4a90d9; color: #4a90d9; }
.sample-btn:active { background: #f0f7ff; }
.loading { display: none; align-items: center; gap: 8px; margin-top: 12px; }
.loading.active { display: flex; }
.spinner { width: 20px; height: 20px; border: 3px solid #e0e0e0;
border-top-color: #4a90d9; border-radius: 50%; animation: spin 0.6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.status-bar { padding: 12px 16px; border-radius: 8px; margin-bottom: 16px;
font-size: 14px; display: none; }
.status-bar.success { display: block; background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.status-bar.error { display: block; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 10px; margin-bottom: 20px; }
.info-item { background: #f8f9fa; border-radius: 8px; padding: 12px; text-align: center; }
.info-item .label { font-size: 11px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; }
.info-item .value { font-size: 18px; font-weight: 700; margin-top: 4px; }
.info-item .value.ok { color: #27ae60; }
.info-item .value.warn { color: #e67e22; }
.result-section { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px; }
@media (max-width: 768px) { .result-section { grid-template-columns: 1fr; } }
.image-box { background: #f8f9fa; border-radius: 8px; overflow: hidden; border: 1px solid #eee; }
.image-box img, .image-box canvas { width: 100%; height: auto; display: block; }
.image-box .label { padding: 8px 12px; font-size: 12px; color: #666;
background: #f5f5f5; border-bottom: 1px solid #eee; }
.colors-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.colors-table th { background: #f8f9fa; padding: 8px 10px; text-align: left;
font-weight: 600; font-size: 11px; color: #666; border-bottom: 2px solid #eee; }
.colors-table td { padding: 6px 10px; border-bottom: 1px solid #f0f0f0; font-variant-numeric: tabular-nums; }
.colors-table tr:hover { background: #f8f9fa; }
.color-dot { display: inline-block; width: 14px; height: 14px; border-radius: 50%;
vertical-align: middle; margin-right: 6px; border: 1px solid rgba(0,0,0,0.1); }
.json-box { background: #1a1a2e; border-radius: 8px; padding: 16px; margin-top: 12px; }
.json-box pre { color: #e8e8e8; font-size: 12px; line-height: 1.5;
max-height: 250px; overflow: auto; white-space: pre-wrap; }
.json-box .label { color: #999; font-size: 11px; margin-bottom: 8px; text-transform: uppercase; }
.footer { text-align: center; color: #bbb; font-size: 12px; padding: 20px 0; }
.footer a { color: #4a90d9; text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<h1>🧪 尿检试纸识别</h1>
<div class="subtitle">纯 JavaScript 实现 · 无外部依赖 · 浏览器端运行</div>
<div class="card">
<div class="upload-zone" id="uploadZone">
<div class="icon">📸</div>
<div class="text">点击或拖拽图片到此处</div>
<div class="hint">支持 JPG / PNG / WebP</div>
<input type="file" accept="image/*" id="fileInput">
</div>
<div class="sample-bar">
<span class="sample-label">试试示例:</span>
<button class="sample-btn" data-sample="1">Sample 1 (S型)</button>
<button class="sample-btn" data-sample="2">Sample 2 (L型)</button>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<span>识别中...</span>
</div>
</div>
<div class="status-bar" id="statusBar"></div>
<div class="card" id="resultCard" style="display:none">
<h2>识别结果</h2>
<div class="info-grid" id="infoGrid"></div>
<div class="result-section">
<div class="image-box">
<div class="label">原图</div>
<img id="originalImg" src="" alt="原图">
</div>
<div class="image-box">
<div class="label">标注图</div>
<canvas id="annotatedCanvas" style="width:100%;height:auto"></canvas>
</div>
</div>
<h2 style="margin-bottom:12px; font-size:16px; color:#666;">色块颜色数据</h2>
<div style="overflow-x:auto">
<table class="colors-table" id="colorsTable">
<thead>
<tr><th>#</th><th>色样</th><th>R</th><th>G</th><th>B</th><th>H</th><th>S</th><th>V</th><th>L</th><th>a</th><th>b</th></tr>
</thead>
<tbody id="colorsBody"></tbody>
</table>
</div>
<div class="json-box">
<div class="label">原始数据 (JSON)</div>
<pre id="jsonOutput"></pre>
</div>
</div>
<div class="footer">
PaperRecognizer · 基于 HarmonyOS 原版 C++ 逻辑移植 · 开源实现
</div>
</div>
<script src="paper-recognizer.js"></script>
<script>
(function() {
'use strict';
const fileInput = document.getElementById('fileInput');
const uploadZone = document.getElementById('uploadZone');
const loading = document.getElementById('loading');
const statusBar = document.getElementById('statusBar');
const resultCard = document.getElementById('resultCard');
const infoGrid = document.getElementById('infoGrid');
const originalImg = document.getElementById('originalImg');
const annotatedCanvas = document.getElementById('annotatedCanvas');
const colorsBody = document.getElementById('colorsBody');
const jsonOutput = document.getElementById('jsonOutput');
const { PaperRecognizer, PST_NAMES } = window;
// Events
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('dragover'); });
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
uploadZone.addEventListener('drop', e => {
e.preventDefault(); uploadZone.classList.remove('dragover');
if (e.dataTransfer.files.length) processFile(e.dataTransfer.files[0]);
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length) processFile(fileInput.files[0]);
});
document.querySelectorAll('.sample-btn').forEach(btn => {
btn.addEventListener('click', () => loadSample(btn.dataset.sample));
});
async function loadSample(n) {
loading.classList.add('active');
hideStatus();
resultCard.style.display = 'none';
try {
const resp = await fetch(`samples/sample${n}.jpg`);
if (!resp.ok) throw new Error(`示例加载失败 (HTTP ${resp.status})`);
const blob = await resp.blob();
await processBlob(blob, `Sample ${n}`);
} catch (e) {
showError(`加载失败: ${e.message}`);
} finally {
loading.classList.remove('active');
}
}
function processFile(file) {
if (!file) return;
loading.classList.add('active');
hideStatus();
resultCard.style.display = 'none';
processBlob(file, file.name).finally(() => loading.classList.remove('active'));
}
function hideStatus() { statusBar.className = 'status-bar'; }
function showError(msg) { statusBar.className = 'status-bar error'; statusBar.textContent = msg; }
function showSuccess(msg) { statusBar.className = 'status-bar success'; statusBar.textContent = msg; }
async function processBlob(blob, name) {
try {
const bitmap = await createImageBitmap(blob);
const w = bitmap.width, h = bitmap.height;
if (w * h > 4000000) {
showError(`图片过大 (${w}×${h}),建议缩小后重试`);
bitmap.close();
return;
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
bitmap.close();
const imageData = ctx.getImageData(0, 0, w, h);
originalImg.src = URL.createObjectURL(blob);
// Run recognition
const startTime = performance.now();
const recognizer = new PaperRecognizer();
const result = recognizer.recognize(imageData.data, w, h);
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
if (!result.success) {
showError(`识别失败 [${result.errorCode}] ${result.errorMessage} (${elapsed}s)`);
return;
}
showSuccess(`✅ 识别成功 — ${elapsed}s`);
// Info grid
const blurNames = ['正常', '轻微模糊', '严重模糊'];
const colorNames = ['未知', '红', '绿', '蓝'];
infoGrid.innerHTML = `
<div class="info-item"><div class="label">试纸类型</div><div class="value ok">${PST_NAMES[result.paperType] || '未知'}</div></div>
<div class="info-item"><div class="label">色块数</div><div class="value">${result.itemCount}</div></div>
<div class="info-item"><div class="label">定位块颜色</div><div class="value">${colorNames[result.locatorColor] || '未知'}</div></div>
<div class="info-item"><div class="label">小定位块方向</div><div class="value">${result.smallLocatorOnLeft ? '← 左侧' : '→ 右侧'}</div></div>
<div class="info-item"><div class="label">清晰度</div><div class="value ${result.blurLevel > 0 ? 'warn' : ''}">${blurNames[result.blurLevel] || '未知'} (${(result.blurScore||0).toFixed(0)})</div></div>
<div class="info-item"><div class="label">原图尺寸</div><div class="value">${w}×${h}</div></div>
`;
// Annotated canvas
const ap = recognizer.getAnnotatedPixels();
const aw = recognizer.getAnnotatedWidth();
const ah = recognizer.getAnnotatedHeight();
if (ap && aw > 0 && ah > 0) {
annotatedCanvas.width = aw;
annotatedCanvas.height = ah;
annotatedCanvas.style.width = '100%';
annotatedCanvas.style.height = 'auto';
const actx = annotatedCanvas.getContext('2d');
const imgData = actx.createImageData(aw, ah);
imgData.data.set(ap);
actx.putImageData(imgData, 0, 0);
}
// Colors table
const colors = result.colors || [];
colorsBody.innerHTML = colors.map(c => `
<tr>
<td><b>${c.id}</b></td>
<td><span class="color-dot" style="background:rgb(${Math.round(c.r)},${Math.round(c.g)},${Math.round(c.b_val)})"></span></td>
<td>${c.r.toFixed(0)}</td>
<td>${c.g.toFixed(0)}</td>
<td>${c.b_val.toFixed(0)}</td>
<td>${c.h.toFixed(1)}</td>
<td>${c.s.toFixed(0)}</td>
<td>${c.v.toFixed(0)}</td>
<td>${c.l.toFixed(1)}</td>
<td>${c.a.toFixed(1)}</td>
<td>${c.b_val.toFixed(1)}</td>
</tr>
`).join('');
jsonOutput.textContent = recognizer.getResultJson();
resultCard.style.display = 'block';
} catch (e) {
showError(`处理错误: ${e.message}`);
}
}
})();
</script>
</body>
</html>