File size: 7,282 Bytes
ebfb673 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | <!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> |