HsuHH's picture
Upload index.html
ebfb673 verified
Raw
History Blame Contribute Delete
7.28 kB
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MS COCO邊緣端物件偵測 (Web Accelerator)</title>
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 20px auto; text-align: center; background-color: #f5f5f5; }
.container { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
#canvasContainer { position: relative; display: inline-block; margin-top: 20px; }
canvas { position: absolute; left: 0; top: 0; }
img { max-width: 100%; height: auto; display: block; }
.status { color: #666; margin: 10px 0; font-style: italic; }
/* 新增:Loading 遮罩與動畫樣式 */
#loadingOverlay {
display: none; /* 預設隱藏 */
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(255, 255, 255, 0.8);
z-index: 10;
justify-content: center;
align-items: center;
flex-direction: column;
border-radius: 4px;
}
.spinner {
border: 5px solid #f3f3f3;
border-top: 5px solid #007bff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="container">
<h2>MS COCO 邊緣端物件偵測 ( Web Accelerator)</h2>
<p class="status" id="status">正在初始化模型環境...</p>
<input type="file" id="imageLoader" accept="image/*" disabled>
<br>
<div id="canvasContainer">
<!-- 新增:載入中的遮罩 UI -->
<div id="loadingOverlay">
<div class="spinner"></div>
<p style="font-weight: bold; color: #333; margin-top: 15px;">神經網路推論中,請稍候...</p>
</div>
<img id="inputImage" src="" alt="">
<canvas id="outputCanvas"></canvas>
</div>
</div>
<script>
let session = null;
const statusText = document.getElementById('status');
const imageLoader = document.getElementById('imageLoader');
const imgElement = document.getElementById('inputImage');
const canvas = document.getElementById('outputCanvas');
const loadingOverlay = document.getElementById('loadingOverlay'); // 取得 Loading DOM
const ctx = canvas.getContext('2d');
// 1. 異步載入 ONNX 模型
async function initModel() {
try {
statusText.innerText = "正在下載並載入 best.onnx (這可能需要一點時間)...";
session = await ort.InferenceSession.create('./best.onnx', { executionProviders: ['wasm'] });
statusText.innerText = "模型載入成功!請上傳一張圖片進行物件偵測。";
imageLoader.disabled = false;
} catch (e) {
statusText.innerText = "模型載入失敗: " + e.message;
console.error(e);
}
}
// 2. 監聽圖片上傳
imageLoader.addEventListener('change', handleImage, false);
function handleImage(e) {
const reader = new FileReader();
reader.onload = function(event) {
imgElement.src = event.target.result;
imgElement.onload = async function() {
canvas.width = imgElement.clientWidth;
canvas.height = imgElement.clientHeight;
ctx.clearRect(0, 0, canvas.width, canvas.height);
statusText.innerText = "準備執行推論...";
// ★ 關鍵:顯示 Loading 動畫
loadingOverlay.style.display = 'flex';
// ★ 關鍵技巧:強迫暫停 50 毫秒,讓瀏覽器有時間把 Loading 畫面畫出來,再進入繁重的推論
await new Promise(resolve => setTimeout(resolve, 50));
runInference();
}
}
reader.readAsDataURL(e.target.files[0]);
}
// 3. 執行推論與畫框
async function runInference() {
if (!session) return;
try {
// --- 影像前處理開始 ---
const tmpCanvas = document.createElement('canvas');
const targetSize = 640;
tmpCanvas.width = targetSize;
tmpCanvas.height = targetSize;
const tctx = tmpCanvas.getContext('2d');
tctx.drawImage(imgElement, 0, 0, targetSize, targetSize);
const imgData = tctx.getImageData(0, 0, targetSize, targetSize);
const data = imgData.data;
const hw = targetSize * targetSize;
const input = new Float32Array(3 * hw);
for (let i = 0; i < hw; i++) {
const dataIdx = i * 4;
input[i] = data[dataIdx] / 255.0; // R 通道
input[hw + i] = data[dataIdx + 1] / 255.0; // G 通道
input[2 * hw + i] = data[dataIdx + 2] / 255.0; // B 通道
}
// --- 影像前處理結束 ---
const tensor = new ort.Tensor('float32', input, [1, 3, targetSize, targetSize]);
const feeds = { input_tensor: tensor };
const results = await session.run(feeds);
const boxes = results.boxes.data;
const scores = results.scores.data;
const labels = results.labels.data;
// 繪製 Bounding Boxes
let detectedCount = 0;
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 3;
ctx.font = '18px Arial'; // 字體稍微調大一點
ctx.fillStyle = '#00ff00';
for (let i = 0; i < scores.length; i++) {
if (scores[i] > 0.5) {
detectedCount++;
const xmin = boxes[i*4] * (canvas.width / 640);
const ymin = boxes[i*4+1] * (canvas.height / 640);
const xmax = boxes[i*4+2] * (canvas.width / 640);
const ymax = boxes[i*4+3] * (canvas.height / 640);
ctx.strokeRect(xmin, ymin, xmax - xmin, ymax - ymin);
ctx.fillText(`Obj ID: ${labels[i]} (${(scores[i]*100).toFixed(1)}%)`, xmin, ymin - 5);
}
}
statusText.innerText = `偵測結束,共發現 ${detectedCount} 個置信度大於 50% 的物件。`;
} catch (error) {
statusText.innerText = "推論發生錯誤: " + error.message;
console.error(error);
} finally {
// ★ 無論成功或失敗,最後一定要把 Loading 遮罩關閉
loadingOverlay.style.display = 'none';
}
}
// 啟動初始化
initModel();
</script>
</body>
</html>