react2023 commited on
Commit
ec52b61
·
verified ·
1 Parent(s): 53d92a5

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +768 -19
index.html CHANGED
@@ -1,19 +1,768 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>夜视监控检测系统</title>
7
+ <!-- 引入 Google Fonts -->
8
+ <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap" rel="stylesheet">
9
+ <!-- 引入 FontAwesome 图标 -->
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
+ <!-- 引入 TensorFlow.js COCO-SSD 模型 -->
12
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd"></script>
14
+
15
+ <style>
16
+ :root {
17
+ --primary-green: #0f0;
18
+ --dim-green: #003300;
19
+ --bg-color: #050505;
20
+ --panel-bg: rgba(0, 20, 0, 0.85);
21
+ --alert-red: #ff3333;
22
+ --scan-line-color: rgba(0, 255, 0, 0.1);
23
+ }
24
+
25
+ * {
26
+ box-sizing: border-box;
27
+ margin: 0;
28
+ padding: 0;
29
+ }
30
+
31
+ body {
32
+ font-family: 'Share Tech Mono', monospace;
33
+ background-color: var(--bg-color);
34
+ color: var(--primary-green);
35
+ height: 100vh;
36
+ overflow: hidden;
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+
41
+ /* 顶部导航栏 */
42
+ header {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ align-items: center;
46
+ padding: 1rem 2rem;
47
+ background: var(--panel-bg);
48
+ border-bottom: 2px solid var(--primary-green);
49
+ z-index: 10;
50
+ }
51
+
52
+ .brand {
53
+ font-size: 1.5rem;
54
+ text-transform: uppercase;
55
+ letter-spacing: 2px;
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 10px;
59
+ }
60
+
61
+ .brand i {
62
+ animation: pulse 2s infinite;
63
+ }
64
+
65
+ .status-badge {
66
+ font-size: 0.9rem;
67
+ padding: 5px 10px;
68
+ border: 1px solid var(--primary-green);
69
+ border-radius: 4px;
70
+ background: rgba(0, 255, 0, 0.1);
71
+ }
72
+
73
+ .status-badge.offline {
74
+ border-color: var(--alert-red);
75
+ color: var(--alert-red);
76
+ background: rgba(255, 0, 0, 0.1);
77
+ }
78
+
79
+ /* 主布局 */
80
+ main {
81
+ flex: 1;
82
+ display: grid;
83
+ grid-template-columns: 1fr 350px;
84
+ gap: 1rem;
85
+ padding: 1rem;
86
+ height: calc(100vh - 70px);
87
+ }
88
+
89
+ @media (max-width: 900px) {
90
+ main {
91
+ grid-template-columns: 1fr;
92
+ grid-template-rows: 1fr auto;
93
+ }
94
+ }
95
+
96
+ /* 视频显示区域 */
97
+ .viewport-container {
98
+ position: relative;
99
+ background: #000;
100
+ border: 2px solid var(--dim-green);
101
+ border-radius: 8px;
102
+ overflow: hidden;
103
+ display: flex;
104
+ justify-content: center;
105
+ align-items: center;
106
+ }
107
+
108
+ /* 隐藏原始视频,我们将在Canvas上绘制 */
109
+ #webcam {
110
+ display: none;
111
+ }
112
+
113
+ #output-canvas {
114
+ width: 100%;
115
+ height: 100%;
116
+ object-fit: cover;
117
+ /* 夜视滤镜效果 */
118
+ filter: contrast(1.2) brightness(1.1) sepia(1) hue-rotate(50deg) saturate(3);
119
+ }
120
+
121
+ /* 扫描线叠加层 */
122
+ .scanlines {
123
+ position: absolute;
124
+ top: 0;
125
+ left: 0;
126
+ width: 100%;
127
+ height: 100%;
128
+ background: linear-gradient(
129
+ to bottom,
130
+ var(--scan-line-color) 50%,
131
+ rgba(0, 0, 0, 0) 50%
132
+ );
133
+ background-size: 100% 4px;
134
+ pointer-events: none;
135
+ z-index: 2;
136
+ }
137
+
138
+ .vignette {
139
+ position: absolute;
140
+ top: 0;
141
+ left: 0;
142
+ width: 100%;
143
+ height: 100%;
144
+ background: radial-gradient(circle, transparent 50%, black 100%);
145
+ pointer-events: none;
146
+ z-index: 3;
147
+ }
148
+
149
+ /* 覆盖层 UI 元素 (十字准星, 时间等) */
150
+ .overlay-ui {
151
+ position: absolute;
152
+ top: 0;
153
+ left: 0;
154
+ width: 100%;
155
+ height: 100%;
156
+ pointer-events: none;
157
+ z-index: 4;
158
+ padding: 20px;
159
+ }
160
+
161
+ .rec-indicator {
162
+ position: absolute;
163
+ top: 20px;
164
+ right: 20px;
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 8px;
168
+ color: var(--alert-red);
169
+ font-weight: bold;
170
+ }
171
+
172
+ .rec-dot {
173
+ width: 12px;
174
+ height: 12px;
175
+ background-color: var(--alert-red);
176
+ border-radius: 50%;
177
+ animation: blink 1s infinite;
178
+ }
179
+
180
+ .timestamp {
181
+ position: absolute;
182
+ bottom: 20px;
183
+ left: 20px;
184
+ font-size: 1.2rem;
185
+ }
186
+
187
+ .crosshair {
188
+ position: absolute;
189
+ top: 50%;
190
+ left: 50%;
191
+ transform: translate(-50%, -50%);
192
+ width: 60px;
193
+ height: 60px;
194
+ border: 2px solid var(--primary-green);
195
+ border-radius: 50%;
196
+ opacity: 0.6;
197
+ }
198
+
199
+ .crosshair::before, .crosshair::after {
200
+ content: '';
201
+ position: absolute;
202
+ background: var(--primary-green);
203
+ }
204
+
205
+ .crosshair::before {
206
+ top: 50%;
207
+ left: -10px;
208
+ width: 80px;
209
+ height: 1px;
210
+ }
211
+
212
+ .crosshair::after {
213
+ left: 50%;
214
+ top: -10px;
215
+ width: 1px;
216
+ height: 80px;
217
+ }
218
+
219
+ /* 控制面板 & 日志 */
220
+ aside {
221
+ background: var(--panel-bg);
222
+ border: 1px solid var(--primary-green);
223
+ border-radius: 8px;
224
+ display: flex;
225
+ flex-direction: column;
226
+ padding: 1rem;
227
+ gap: 1rem;
228
+ overflow-y: auto;
229
+ }
230
+
231
+ h2 {
232
+ font-size: 1.2rem;
233
+ border-bottom: 1px solid var(--dim-green);
234
+ padding-bottom: 0.5rem;
235
+ margin-bottom: 0.5rem;
236
+ }
237
+
238
+ .control-group {
239
+ display: flex;
240
+ flex-direction: column;
241
+ gap: 0.8rem;
242
+ }
243
+
244
+ button {
245
+ background: transparent;
246
+ border: 1px solid var(--primary-green);
247
+ color: var(--primary-green);
248
+ padding: 10px 15px;
249
+ font-family: inherit;
250
+ cursor: pointer;
251
+ text-transform: uppercase;
252
+ transition: all 0.3s;
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: center;
256
+ gap: 8px;
257
+ font-weight: bold;
258
+ }
259
+
260
+ button:hover {
261
+ background: var(--primary-green);
262
+ color: #000;
263
+ box-shadow: 0 0 15px var(--primary-green);
264
+ }
265
+
266
+ button:disabled {
267
+ opacity: 0.5;
268
+ cursor: not-allowed;
269
+ filter: grayscale(1);
270
+ }
271
+
272
+ button.stop {
273
+ border-color: var(--alert-red);
274
+ color: var(--alert-red);
275
+ }
276
+
277
+ button.stop:hover {
278
+ background: var(--alert-red);
279
+ color: #fff;
280
+ box-shadow: 0 0 15px var(--alert-red);
281
+ }
282
+
283
+ .log-container {
284
+ flex: 1;
285
+ background: #000;
286
+ border: 1px solid var(--dim-green);
287
+ padding: 10px;
288
+ overflow-y: auto;
289
+ font-size: 0.85rem;
290
+ max-height: 300px;
291
+ }
292
+
293
+ .log-entry {
294
+ margin-bottom: 5px;
295
+ display: flex;
296
+ justify-content: space-between;
297
+ }
298
+
299
+ .log-entry .time {
300
+ color: #888;
301
+ margin-right: 10px;
302
+ }
303
+
304
+ .log-entry.alert {
305
+ color: var(--alert-red);
306
+ }
307
+
308
+ /* 加载遮罩 */
309
+ #loader {
310
+ position: fixed;
311
+ top: 0;
312
+ left: 0;
313
+ width: 100%;
314
+ height: 100%;
315
+ background: rgba(0, 0, 0, 0.9);
316
+ display: flex;
317
+ flex-direction: column;
318
+ justify-content: center;
319
+ align-items: center;
320
+ z-index: 100;
321
+ transition: opacity 0.5s;
322
+ }
323
+
324
+ .spinner {
325
+ width: 50px;
326
+ height: 50px;
327
+ border: 5px solid var(--dim-green);
328
+ border-top: 5px solid var(--primary-green);
329
+ border-radius: 50%;
330
+ animation: spin 1s linear infinite;
331
+ margin-bottom: 20px;
332
+ }
333
+
334
+ /* 动画 */
335
+ @keyframes spin {
336
+ 0% { transform: rotate(0deg); }
337
+ 100% { transform: rotate(360deg); }
338
+ }
339
+
340
+ @keyframes pulse {
341
+ 0% { opacity: 1; }
342
+ 50% { opacity: 0.5; }
343
+ 100% { opacity: 1; }
344
+ }
345
+
346
+ @keyframes blink {
347
+ 0% { opacity: 1; }
348
+ 50% { opacity: 0; }
349
+ 100% { opacity: 1; }
350
+ }
351
+
352
+ /* 页脚 */
353
+ .footer-credit {
354
+ margin-top: auto;
355
+ text-align: center;
356
+ font-size: 0.8rem;
357
+ color: #666;
358
+ padding-top: 10px;
359
+ border-top: 1px solid var(--dim-green);
360
+ }
361
+
362
+ .footer-credit a {
363
+ color: var(--primary-green);
364
+ text-decoration: none;
365
+ }
366
+
367
+ .footer-credit a:hover {
368
+ text-decoration: underline;
369
+ }
370
+
371
+ /* 统计数据 */
372
+ .stats-grid {
373
+ display: grid;
374
+ grid-template-columns: 1fr 1fr;
375
+ gap: 10px;
376
+ margin-bottom: 10px;
377
+ }
378
+
379
+ .stat-box {
380
+ background: rgba(0, 50, 0, 0.3);
381
+ padding: 10px;
382
+ border-radius: 4px;
383
+ text-align: center;
384
+ }
385
+
386
+ .stat-value {
387
+ font-size: 1.5rem;
388
+ font-weight: bold;
389
+ }
390
+
391
+ .stat-label {
392
+ font-size: 0.7rem;
393
+ color: #aaa;
394
+ }
395
+
396
+ </style>
397
+ </head>
398
+ <body>
399
+
400
+ <!-- 加载界面 -->
401
+ <div id="loader">
402
+ <div class="spinner"></div>
403
+ <div id="loading-text">正在初始化神经接口...</div>
404
+ </div>
405
+
406
+ <header>
407
+ <div class="brand">
408
+ <i class="fa-solid fa-eye"></i>
409
+ <span>NIGHT VISION <span style="font-size:0.8em; color:#aaa;">AI PRO</span></span>
410
+ </div>
411
+ <div class="status-badge offline" id="system-status">
412
+ SYSTEM OFFLINE
413
+ </div>
414
+ </header>
415
+
416
+ <main>
417
+ <!-- 视频与AI检测区域 -->
418
+ <section class="viewport-container">
419
+ <video id="webcam" playsinline muted></video>
420
+ <canvas id="output-canvas"></canvas>
421
+
422
+ <!-- 视觉特效层 -->
423
+ <div class="scanlines"></div>
424
+ <div class="vignette"></div>
425
+
426
+ <!-- UI 叠加层 -->
427
+ <div class="overlay-ui">
428
+ <div class="rec-indicator" id="rec-indicator" style="display: none;">
429
+ <div class="rec-dot"></div> REC
430
+ </div>
431
+ <div class="timestamp" id="timestamp">00:00:00:00</div>
432
+ <div class="crosshair"></div>
433
+ </div>
434
+ </section>
435
+
436
+ <!-- 控制侧边栏 -->
437
+ <aside>
438
+ <h2><i class="fa-solid fa-sliders"></i> 控制面板</h2>
439
+
440
+ <div class="control-group">
441
+ <button id="btn-start" onclick="startSystem()">
442
+ <i class="fa-solid fa-power-off"></i> 启动系统
443
+ </button>
444
+ <button id="btn-stop" class="stop" onclick="stopSystem()" disabled>
445
+ <i class="fa-solid fa-stop"></i> 停止监控
446
+ </button>
447
+ </div>
448
+
449
+ <h2><i class="fa-solid fa-chart-line"></i> 检测统计</h2>
450
+ <div class="stats-grid">
451
+ <div class="stat-box">
452
+ <div class="stat-value" id="fps-counter">0</div>
453
+ <div class="stat-label">FPS</div>
454
+ </div>
455
+ <div class="stat-box">
456
+ <div class="stat-value" id="obj-counter">0</div>
457
+ <div class="stat-label">检测目标</div>
458
+ </div>
459
+ </div>
460
+
461
+ <h2><i class="fa-solid fa-list-ul"></i> 检测日志</h2>
462
+ <div class="log-container" id="log-container">
463
+ <div class="log-entry"><span class="time">--:--:--</span> 等待启动...</div>
464
+ </div>
465
+
466
+ <div class="footer-credit">
467
+ Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a>
468
+ </div>
469
+ </aside>
470
+ </main>
471
+
472
+ <script>
473
+ // DOM 元素引用
474
+ const video = document.getElementById('webcam');
475
+ const canvas = document.getElementById('output-canvas');
476
+ const ctx = canvas.getContext('2d');
477
+ const loader = document.getElementById('loader');
478
+ const loadingText = document.getElementById('loading-text');
479
+ const logContainer = document.getElementById('log-container');
480
+ const systemStatus = document.getElementById('system-status');
481
+ const recIndicator = document.getElementById('rec-indicator');
482
+ const timestampEl = document.getElementById('timestamp');
483
+ const btnStart = document.getElementById('btn-start');
484
+ const btnStop = document.getElementById('btn-stop');
485
+ const fpsCounter = document.getElementById('fps-counter');
486
+ const objCounter = document.getElementById('obj-counter');
487
+
488
+ // 状态变量
489
+ let model = undefined;
490
+ let isRunning = false;
491
+ let animationId = null;
492
+ let lastFrameTime = 0;
493
+ let frameCount = 0;
494
+ let lastFpsUpdate = 0;
495
+
496
+ // 初始化
497
+ async function init() {
498
+ try {
499
+ loadingText.innerText = "正在加载 TensorFlow.js 模���...";
500
+ // 加载 COCO-SSD 模型 (用于物体检测)
501
+ model = await cocoSsd.load();
502
+
503
+ loadingText.innerText = "模型加载完成,准备就绪。";
504
+
505
+ // 模拟一点延迟以展示加载动画
506
+ setTimeout(() => {
507
+ loader.style.opacity = '0';
508
+ setTimeout(() => {
509
+ loader.style.display = 'none';
510
+ }, 500);
511
+ }, 800);
512
+
513
+ addLog("系统初始化完成");
514
+ addLog("等待用户启动摄像头...");
515
+ } catch (error) {
516
+ loadingText.innerText = "错误: 无法加载模型。";
517
+ console.error(error);
518
+ addLog("严重错误: 模型加载失败", true);
519
+ }
520
+ }
521
+
522
+ // 启动系统
523
+ async function startSystem() {
524
+ if (isRunning) return;
525
+
526
+ try {
527
+ loadingText.style.display = 'block';
528
+ loadingText.innerText = "正在请求摄像头权限...";
529
+ loader.style.display = 'flex';
530
+ loader.style.opacity = '1';
531
+
532
+ // 获取摄像头流
533
+ const stream = await navigator.mediaDevices.getUserMedia({
534
+ audio: false,
535
+ video: {
536
+ facingMode: 'environment', // 优先使用后置摄像头
537
+ width: { ideal: 1280 },
538
+ height: { ideal: 720 }
539
+ }
540
+ });
541
+
542
+ video.srcObject = stream;
543
+
544
+ // 等待视频元数据加载完成
545
+ video.onloadedmetadata = () => {
546
+ video.play();
547
+
548
+ // 设置 Canvas 尺寸匹配视频
549
+ canvas.width = video.videoWidth;
550
+ canvas.height = video.videoHeight;
551
+
552
+ loader.style.opacity = '0';
553
+ setTimeout(() => loader.style.display = 'none', 500);
554
+
555
+ isRunning = true;
556
+ updateUIState(true);
557
+ addLog("摄像头已连接,开始监控");
558
+
559
+ // 开始预测循环
560
+ predictWebcam();
561
+ };
562
+
563
+ } catch (error) {
564
+ console.error(error);
565
+ loadingText.innerText = "无法访问摄像头";
566
+ addLog("错误: 摄像头访问被拒绝", true);
567
+ setTimeout(() => {
568
+ loader.style.opacity = '0';
569
+ setTimeout(() => loader.style.display = 'none', 500);
570
+ }, 2000);
571
+ }
572
+ }
573
+
574
+ // 停止系统
575
+ function stopSystem() {
576
+ if (!isRunning) return;
577
+
578
+ isRunning = false;
579
+
580
+ // 停止视频流
581
+ const stream = video.srcObject;
582
+ if (stream) {
583
+ const tracks = stream.getTracks();
584
+ tracks.forEach(track => track.stop());
585
+ video.srcObject = null;
586
+ }
587
+
588
+ // 停止动画循环
589
+ if (animationId) {
590
+ cancelAnimationFrame(animationId);
591
+ }
592
+
593
+ // 清空 Canvas
594
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
595
+
596
+ updateUIState(false);
597
+ addLog("监控已停止");
598
+ }
599
+
600
+ // 预测循环 (核心逻辑)
601
+ async function predictWebcam() {
602
+ if (!isRunning) return;
603
+
604
+ // 1. 在 Canvas 上绘制当前视频帧
605
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
606
+
607
+ // 2. 使用模型进行检测
608
+ // 仅当模型加载完毕时执行
609
+ if (model !== undefined) {
610
+ try {
611
+ const predictions = await model.detect(video);
612
+
613
+ // 3. 绘制检测框和标签
614
+ drawPredictions(predictions);
615
+
616
+ // 更新统计数据
617
+ updateStats(predictions);
618
+
619
+ } catch (error) {
620
+ console.warn("检测跳帧", error);
621
+ }
622
+ }
623
+
624
+ // 4. 更新时间戳
625
+ updateTimestamp();
626
+
627
+ // 5. 循环调用
628
+ animationId = requestAnimationFrame(predictWebcam);
629
+ }
630
+
631
+ // 绘制预测结果
632
+ function drawPredictions(predictions) {
633
+ ctx.font = '16px "Share Tech Mono"';
634
+ ctx.textBaseline = 'top';
635
+
636
+ let detectedObjects = new Set();
637
+
638
+ predictions.forEach(prediction => {
639
+ const [x, y, width, height] = prediction.bbox;
640
+ const label = prediction.class;
641
+ const score = Math.round(prediction.score * 100) + '%';
642
+
643
+ detectedObjects.add(label);
644
+
645
+ // 设置样式 (夜视风格:亮绿色)
646
+ ctx.strokeStyle = '#0f0';
647
+ ctx.lineWidth = 2;
648
+
649
+ // 绘制矩形框
650
+ ctx.strokeRect(x, y, width, height);
651
+
652
+ // 绘制背景标签
653
+ const text = `${label.toUpperCase()} ${score}`;
654
+ const textWidth = ctx.measureText(text).width;
655
+ const textHeight = 16;
656
+
657
+ ctx.fillStyle = 'rgba(0, 50, 0, 0.7)';
658
+ ctx.fillRect(x, y, textWidth + 10, textHeight + 4);
659
+
660
+ // 绘制文字
661
+ ctx.fillStyle = '#0f0';
662
+ ctx.fillText(text, x + 5, y + 2);
663
+
664
+ // 绘制角落装饰 (增加科技感)
665
+ drawCornerDecorations(ctx, x, y, width, height);
666
+ });
667
+
668
+ // 更新目标计数器显示
669
+ objCounter.innerText = detectedObjects.size;
670
+ }
671
+
672
+ // 绘制角落装饰
673
+ function drawCornerDecorations(ctx, x, y, w, h) {
674
+ const lineLen = 10;
675
+ ctx.lineWidth = 3;
676
+
677
+ // 左上
678
+ ctx.beginPath();
679
+ ctx.moveTo(x, y + lineLen);
680
+ ctx.lineTo(x, y);
681
+ ctx.lineTo(x + lineLen, y);
682
+ ctx.stroke();
683
+
684
+ // 右上
685
+ ctx.beginPath();
686
+ ctx.moveTo(x + w - lineLen, y);
687
+ ctx.lineTo(x + w, y);
688
+ ctx.lineTo(x + w, y + lineLen);
689
+ ctx.stroke();
690
+
691
+ // 左下
692
+ ctx.beginPath();
693
+ ctx.moveTo(x, y + h - lineLen);
694
+ ctx.lineTo(x, y + h);
695
+ ctx.lineTo(x + lineLen, y + h);
696
+ ctx.stroke();
697
+
698
+ // 右下
699
+ ctx.beginPath();
700
+ ctx.moveTo(x + w - lineLen, y + h);
701
+ ctx.lineTo(x + w, y + h);
702
+ ctx.lineTo(x + w, y + h - lineLen);
703
+ ctx.stroke();
704
+ }
705
+
706
+ // 更新统计数据 (FPS)
707
+ function updateStats(predictions) {
708
+ const now = performance.now();
709
+ frameCount++;
710
+
711
+ if (now - lastFpsUpdate >= 1000) {
712
+ fpsCounter.innerText = frameCount;
713
+ frameCount = 0;
714
+ lastFpsUpdate = now;
715
+ }
716
+
717
+ // 记录首次检测到的物体到日志 (防止日志刷屏,这里简单处理,只记录最近检测到的物体变化)
718
+ // 实际应用中可以做更复杂的去重逻辑
719
+ }
720
+
721
+ // 更新 UI 状态
722
+ function updateUIState(active) {
723
+ if (active) {
724
+ systemStatus.innerText = "SYSTEM ONLINE - MONITORING";
725
+ systemStatus.classList.remove('offline');
726
+ recIndicator.style.display = 'flex';
727
+ btnStart.disabled = true;
728
+ btnStop.disabled = false;
729
+ } else {
730
+ systemStatus.innerText = "SYSTEM OFFLINE";
731
+ systemStatus.classList.add('offline');
732
+ recIndicator.style.display = 'none';
733
+ btnStart.disabled = false;
734
+ btnStop.disabled = true;
735
+ fpsCounter.innerText = '0';
736
+ objCounter.innerText = '0';
737
+ }
738
+ }
739
+
740
+ // 更新时间戳
741
+ function updateTimestamp() {
742
+ const now = new Date();
743
+ const h = String(now.getHours()).padStart(2, '0');
744
+ const m = String(now.getMinutes()).padStart(2, '0');
745
+ const s = String(now.getSeconds()).padStart(2, '0');
746
+ const ms = String(Math.floor(now.getMilliseconds() / 10)).padStart(2, '0');
747
+ timestampEl.innerText = `${h}:${m}:${s}:${ms}`;
748
+ }
749
+
750
+ // 添加日志
751
+ function addLog(message, isAlert = false) {
752
+ const now = new Date();
753
+ const timeStr = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;
754
+
755
+ const div = document.createElement('div');
756
+ div.className = `log-entry ${isAlert ? 'alert' : ''}`;
757
+ div.innerHTML = `<span class="time">[${timeStr}]</span> <span>${message}</span>`;
758
+
759
+ logContainer.appendChild(div);
760
+ logContainer.scrollTop = logContainer.scrollHeight; // 自动滚动到底部
761
+ }
762
+
763
+ // 页面加载完成后初始化
764
+ window.addEventListener('DOMContentLoaded', init);
765
+
766
+ </script>
767
+ </body>
768
+ </html>