Spaces:
Running
Running
| <html lang="zh-TW"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>腦部腫瘤 MRI 分類系統</title> | |
| <!-- 引入 ONNX Runtime Web,讓瀏覽器可以直接跑模型 --> | |
| <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script> | |
| <style> | |
| body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; text-align: center; background-color: #f7f9fa; } | |
| .container { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } | |
| input[type="file"] { margin: 20px 0; } | |
| #preview { max-width: 100%; max-height: 300px; margin-top: 15px; border-radius: 8px; display: none; } | |
| #result { margin-top: 25px; font-weight: bold; text-align: left; background: #f0f4f8; padding: 15px; border-radius: 6px; display: none; } | |
| .bar-container { background-color: #e0e0e0; border-radius: 4px; margin-top: 5px; height: 15px; width: 100%; } | |
| .bar { background-color: #3b82f6; height: 100%; border-radius: 4px; width: 0%; transition: width 0.3s; } | |
| #loading { color: #666; font-style: italic; display: none; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h2>🧠 腦部腫瘤 MRI 分類系統 (純前端版)</h2> | |
| <p style="color: #666;">模型完全在您的瀏覽器中執行,安全且完全免費</p> | |
| <input type="file" id="imageLoader" accept="image/*"> | |
| <div id="loading">載入模型與運算中,請稍候...</div> | |
| <img id="preview" alt="預覽圖"> | |
| <div id="result"></div> | |
| </div> | |
| <script> | |
| // ⚠️ 請依據你第一步印出來的類別順序,修改這個陣列 | |
| const LABELS = ["Glioma", "Meningioma", "No Tumor", "Pituitary"]; | |
| let session = null; | |
| // 初始化載入模型 | |
| async function initModel() { | |
| try { | |
| // 直接讀取跟 index.html 放在同一個目錄下的 model.onnx | |
| session = await ort.InferenceSession.create('./model.onnx'); | |
| console.log("模型載入成功!"); | |
| } catch (e) { | |
| alert("模型載入失敗,請確認 model.onnx 已上傳至正確路徑。"); | |
| console.error(e); | |
| } | |
| } | |
| initModel(); | |
| document.getElementById('imageLoader').addEventListener('change', handleImage, false); | |
| function handleImage(e) { | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| document.getElementById('preview').src = event.target.result; | |
| document.getElementById('preview').style.display = 'inline-block'; | |
| runInference(img); | |
| } | |
| img.src = event.target.result; | |
| } | |
| reader.readAsDataURL(e.target.files[0]); | |
| } | |
| // 圖像預處理與推理 (等同於 Python 中的 transforms + forward) | |
| async function runInference(img) { | |
| if (!session) { alert("模型尚未準備就緒,請稍候"); return; } | |
| document.getElementById('loading').style.display = 'block'; | |
| document.getElementById('result').style.display = 'none'; | |
| // 1. 將圖片繪製到 Canvas 並縮放到 224x224 | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 224; | |
| canvas.height = 224; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(img, 0, 0, 224, 224); | |
| const imgData = ctx.getImageData(0, 0, 224, 224).data; | |
| // 2. 實作 ImageNet 標準化 (Mean & Std) 並轉換成 (1, 3, 224, 224) 格式 | |
| const mean = [0.485, 0.456, 0.406]; | |
| const std = [0.229, 0.224, 0.225]; | |
| const float32Data = new Float32Array(3 * 224 * 224); | |
| for (let i = 0; i < 224 * 224; i++) { | |
| const r = imgData[i * 4] / 255.0; | |
| const g = imgData[i * 4 + 1] / 255.0; | |
| const b = imgData[i * 4 + 2] / 255.0; | |
| // 轉成 FastAI 預期之排序:R通道、G通道、B通道分開排列 | |
| float32Data[i] = (r - mean[0]) / std[0]; // R | |
| float32Data[i + 224 * 224] = (g - mean[1]) / std[1]; // G | |
| float32Data[i + 2 * 224 * 224] = (b - mean[2]) / std[2]; // B | |
| } | |
| // 3. 建立 ONNX Tensor | |
| const inputTensor = new ort.Tensor('float32', float32Data, [1, 3, 224, 224]); | |
| // 4. 執行推理 | |
| const feeds = {}; | |
| feeds[session.inputNames[0]] = inputTensor; | |
| const outputMap = await session.run(feeds); | |
| const rawOutput = outputMap[session.outputNames[0]].data; | |
| // 5. 計算 Softmax 機率值 | |
| const maxLogit = Math.max(...rawOutput); | |
| const exps = Array.from(rawOutput).map(x => Math.exp(x - maxLogit)); | |
| const sumExps = exps.reduce((a, b) => a + b, 0); | |
| const probs = exps.map(x => x / sumExps); | |
| // 6. 渲染結果到畫面上 | |
| let htmlResult = "<h3>分析結果:</h3>"; | |
| let resultsList = LABELS.map((label, idx) => ({ label, prob: probs[idx] })); | |
| resultsList.sort((a, b) => b.prob - a.prob); // 排序 | |
| resultsList.forEach(item => { | |
| const percentage = (item.prob * 100).toFixed(2); | |
| htmlResult += ` | |
| <div style="margin-bottom: 10px;"> | |
| <strong>${item.label}</strong>: ${percentage}% | |
| <div class="bar-container"><div class="bar" style="width: ${percentage}%"></div></div> | |
| </div> | |
| `; | |
| }); | |
| document.getElementById('loading').style.display = 'none'; | |
| document.getElementById('result').innerHTML = htmlResult; | |
| document.getElementById('result').style.display = 'block'; | |
| } | |
| </script> | |
| </body> | |
| </html> |