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>