NV9523 commited on
Commit
fdbe73c
·
verified ·
1 Parent(s): 3562f46

Tối ưu lại code này cho đơn giản để có biểu đồ khi đếm được số lượng và có thể chọn camera trong hệ thống trình duyệt và khi vẽ gửi về server thì sẽ hiển thị trên frontend

Browse files
Files changed (3) hide show
  1. README.md +7 -4
  2. index.html +697 -19
  3. style.css +169 -17
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Yolo Vision Tracker
3
- emoji: 🚀
4
  colorFrom: blue
5
- colorTo: gray
 
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: YOLO Vision Tracker 👁️‍🗨️
 
3
  colorFrom: blue
4
+ colorTo: purple
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://huggingface.co/deepsite).
index.html CHANGED
@@ -1,19 +1,697 @@
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="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>YOLO Detection - Browser Camera</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ .container {
10
+ background: white;
11
+ border-radius: 16px;
12
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
13
+ overflow: hidden;
14
+ max-width: 1200px;
15
+ width: 100%;
16
+ }
17
+ .header {
18
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
19
+ color: white;
20
+ padding: 24px;
21
+ text-align: center;
22
+ }
23
+ h1 {
24
+ font-size: 28px;
25
+ font-weight: 700;
26
+ margin-bottom: 8px;
27
+ }
28
+ .status {
29
+ font-size: 14px;
30
+ opacity: 0.9;
31
+ }
32
+ .content {
33
+ padding: 24px;
34
+ }
35
+ .video-container {
36
+ position: relative;
37
+ background: #000;
38
+ border-radius: 12px;
39
+ overflow: hidden;
40
+ margin-bottom: 24px;
41
+ }
42
+ #outputCanvas {
43
+ width: 100%;
44
+ height: auto;
45
+ display: block;
46
+ cursor: crosshair;
47
+ }
48
+ #outputCanvas.drawing {
49
+ cursor: crosshair;
50
+ }
51
+ #videoInput {
52
+ display: none;
53
+ }
54
+ .controls {
55
+ text-align: center;
56
+ margin-bottom: 24px;
57
+ display: flex;
58
+ gap: 12px;
59
+ justify-content: center;
60
+ flex-wrap: wrap;
61
+ }
62
+ button {
63
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
64
+ color: white;
65
+ border: none;
66
+ padding: 12px 32px;
67
+ border-radius: 8px;
68
+ font-size: 16px;
69
+ font-weight: 600;
70
+ cursor: pointer;
71
+ transition: transform 0.2s;
72
+ }
73
+ button:hover {
74
+ transform: translateY(-2px);
75
+ }
76
+ button:disabled {
77
+ opacity: 0.5;
78
+ cursor: not-allowed;
79
+ }
80
+ button.draw-mode {
81
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
82
+ }
83
+ button.grid-mode {
84
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
85
+ }
86
+ .hint {
87
+ text-align: center;
88
+ color: #666;
89
+ font-size: 14px;
90
+ margin-bottom: 16px;
91
+ padding: 8px;
92
+ background: #f0f0f0;
93
+ border-radius: 8px;
94
+ }
95
+ .hint.active {
96
+ background: #fff3cd;
97
+ color: #856404;
98
+ }
99
+ .stats {
100
+ display: grid;
101
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
102
+ gap: 16px;
103
+ }
104
+ .stat-card {
105
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
106
+ color: white;
107
+ padding: 20px;
108
+ border-radius: 12px;
109
+ text-align: center;
110
+ }
111
+ .stat-label {
112
+ font-size: 14px;
113
+ opacity: 0.9;
114
+ margin-bottom: 8px;
115
+ }
116
+ .stat-value {
117
+ font-size: 32px;
118
+ font-weight: 700;
119
+ }
120
+ .footer {
121
+ text-align: center;
122
+ padding: 16px;
123
+ color: #666;
124
+ font-size: 14px;
125
+ }
126
+ @media (max-width: 768px) {
127
+ h1 { font-size: 24px; }
128
+ .stat-value { font-size: 28px; }
129
+ }
130
+ </style>
131
+ </head>
132
+ <body>
133
+ <div class="container">
134
+ <div class="header">
135
+ <h1>📹 YOLO Detection - Browser Camera</h1>
136
+ <div class="status">Real-time Object Detection from Your Camera</div>
137
+ </div>
138
+
139
+ <div class="content">
140
+ <div class="controls">
141
+ <select id="cameraSelect" class="camera-select">
142
+ <option value="">-- Select Camera --</option>
143
+ </select>
144
+ <button id="startBtn">🎥 Start Camera</button>
145
+ <button id="stopBtn" disabled>⏹️ Stop Camera</button>
146
+ <button id="drawLineBtn" disabled>✏️ Draw Line</button>
147
+ <button id="toggleGridBtn" disabled>📐 Show Grid</button>
148
+ <button id="resetCountBtn">🔄 Reset Count</button>
149
+ </div>
150
+ <div class="hint" id="hintText">
151
+ Click "Start Camera" để bắt đầu. Sau đó nhấn "Draw Line" để vẽ vạch đếm.
152
+ </div>
153
+
154
+ <div class="video-container">
155
+ <video id="videoInput" autoplay playsinline></video>
156
+ <canvas id="outputCanvas"></canvas>
157
+ </div>
158
+ <div class="stats">
159
+ <div class="stat-card">
160
+ <div class="stat-label">Total Objects</div>
161
+ <div class="stat-value" id="totalCount">0</div>
162
+ </div>
163
+ <div class="stat-card">
164
+ <div class="stat-label">FPS</div>
165
+ <div class="stat-value" id="fpsValue">0</div>
166
+ </div>
167
+ <div class="stat-card">
168
+ <div class="stat-label">Status</div>
169
+ <div class="stat-value" style="font-size: 20px;" id="statusText">⚪ Ready</div>
170
+ </div>
171
+ </div>
172
+
173
+ <div class="chart-container">
174
+ <canvas id="countChart"></canvas>
175
+ </div>
176
+ </div>
177
+
178
+ <div class="footer">
179
+ Powered by FastAPI + YOLO | Browser Camera Detection
180
+ </div>
181
+ </div>
182
+
183
+ <script>
184
+ const video = document.getElementById('videoInput');
185
+ const canvas = document.getElementById('outputCanvas');
186
+ const ctx = canvas.getContext('2d');
187
+ const startBtn = document.getElementById('startBtn');
188
+ const stopBtn = document.getElementById('stopBtn');
189
+ const drawLineBtn = document.getElementById('drawLineBtn');
190
+ const toggleGridBtn = document.getElementById('toggleGridBtn');
191
+ const resetCountBtn = document.getElementById('resetCountBtn');
192
+ const statusText = document.getElementById('statusText');
193
+ const hintText = document.getElementById('hintText');
194
+
195
+ let ws = null;
196
+ let stream = null;
197
+ let animationId = null;
198
+ let isDrawingMode = false;
199
+ let showGrid = false;
200
+ let linePoints = [];
201
+ let tempLine = null;
202
+ let currentLine = null; // Lưu line đã vẽ
203
+
204
+ // Vẽ tất cả overlays (grid + lines)
205
+ function drawOverlays() {
206
+ drawGrid();
207
+ drawCountLine();
208
+ drawTempLine();
209
+ }
210
+
211
+ // Vẽ line đã vẽ (COUNT LINE)
212
+ function drawCountLine() {
213
+ if (!currentLine) return;
214
+
215
+ const w = canvas.width;
216
+ const h = canvas.height;
217
+ const x1 = currentLine[0] * w;
218
+ const y1 = currentLine[1] * h;
219
+ const x2 = currentLine[2] * w;
220
+ const y2 = currentLine[3] * h;
221
+
222
+ // Vẽ line chính màu xanh dương đậm
223
+ ctx.strokeStyle = '#00FF00'; // Màu xanh lá neon
224
+ ctx.lineWidth = 5;
225
+ ctx.setLineDash([]);
226
+ ctx.shadowColor = '#00FF00';
227
+ ctx.shadowBlur = 10;
228
+ ctx.beginPath();
229
+ ctx.moveTo(x1, y1);
230
+ ctx.lineTo(x2, y2);
231
+ ctx.stroke();
232
+ ctx.shadowBlur = 0;
233
+
234
+ // Vẽ điểm endpoint
235
+ ctx.fillStyle = '#00FF00';
236
+ ctx.strokeStyle = '#FFFFFF';
237
+ ctx.lineWidth = 2;
238
+ ctx.beginPath();
239
+ ctx.arc(x1, y1, 10, 0, 2 * Math.PI);
240
+ ctx.fill();
241
+ ctx.stroke();
242
+ ctx.beginPath();
243
+ ctx.arc(x2, y2, 10, 0, 2 * Math.PI);
244
+ ctx.fill();
245
+ ctx.stroke();
246
+
247
+ // Label với background
248
+ ctx.font = 'bold 18px Arial';
249
+ const labelText = '⚡ COUNT LINE';
250
+ const labelX = (x1 + x2) / 2;
251
+ const labelY = (y1 + y2) / 2 - 15;
252
+ const textWidth = ctx.measureText(labelText).width;
253
+
254
+ // Background
255
+ ctx.fillStyle = 'rgba(0, 255, 0, 0.8)';
256
+ ctx.fillRect(labelX - textWidth/2 - 10, labelY - 25, textWidth + 20, 35);
257
+
258
+ // Text
259
+ ctx.fillStyle = '#000000';
260
+ ctx.fillText(labelText, labelX - textWidth/2, labelY);
261
+ }
262
+
263
+ // Vẽ line tạm (preview khi đang vẽ)
264
+ function drawTempLine() {
265
+ if (!isDrawingMode || !tempLine) return;
266
+
267
+ ctx.strokeStyle = '#FF0000';
268
+ ctx.lineWidth = 3;
269
+ ctx.setLineDash([10, 5]);
270
+ ctx.beginPath();
271
+ ctx.moveTo(tempLine.x1, tempLine.y1);
272
+ ctx.lineTo(tempLine.x2, tempLine.y2);
273
+ ctx.stroke();
274
+ ctx.setLineDash([]);
275
+
276
+ // Vẽ điểm
277
+ ctx.fillStyle = '#FF0000';
278
+ ctx.strokeStyle = '#FFFFFF';
279
+ ctx.lineWidth = 2;
280
+ ctx.beginPath();
281
+ ctx.arc(tempLine.x1, tempLine.y1, 8, 0, 2 * Math.PI);
282
+ ctx.fill();
283
+ ctx.stroke();
284
+
285
+ if (tempLine.x2 !== undefined) {
286
+ ctx.beginPath();
287
+ ctx.arc(tempLine.x2, tempLine.y2, 8, 0, 2 * Math.PI);
288
+ ctx.fill();
289
+ ctx.stroke();
290
+ }
291
+
292
+ // Hiển thị tọa độ
293
+ ctx.font = 'bold 14px Arial';
294
+ const coord1 = `(${Math.round(tempLine.x1)}, ${Math.round(tempLine.y1)})`;
295
+
296
+ // Background cho text
297
+ const textWidth1 = ctx.measureText(coord1).width;
298
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
299
+ ctx.fillRect(tempLine.x1 + 5, tempLine.y1 - 25, textWidth1 + 10, 20);
300
+
301
+ ctx.fillStyle = '#FFFFFF';
302
+ ctx.fillText(coord1, tempLine.x1 + 10, tempLine.y1 - 10);
303
+
304
+ if (tempLine.x2 !== undefined) {
305
+ const coord2 = `(${Math.round(tempLine.x2)}, ${Math.round(tempLine.y2)})`;
306
+ const textWidth2 = ctx.measureText(coord2).width;
307
+
308
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
309
+ ctx.fillRect(tempLine.x2 + 5, tempLine.y2 - 25, textWidth2 + 10, 20);
310
+
311
+ ctx.fillStyle = '#FFFFFF';
312
+ ctx.fillText(coord2, tempLine.x2 + 10, tempLine.y2 - 10);
313
+ }
314
+ }
315
+
316
+ // Vẽ grid lines
317
+ function drawGrid() {
318
+ if (!showGrid) return;
319
+
320
+ const w = canvas.width;
321
+ const h = canvas.height;
322
+
323
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
324
+ ctx.lineWidth = 1;
325
+ ctx.setLineDash([5, 5]);
326
+
327
+ // Vẽ 9 ô (3x3 grid)
328
+ for (let i = 1; i <= 2; i++) {
329
+ // Vertical lines
330
+ ctx.beginPath();
331
+ ctx.moveTo(w * i / 3, 0);
332
+ ctx.lineTo(w * i / 3, h);
333
+ ctx.stroke();
334
+
335
+ // Horizontal lines
336
+ ctx.beginPath();
337
+ ctx.moveTo(0, h * i / 3);
338
+ ctx.lineTo(w, h * i / 3);
339
+ ctx.stroke();
340
+ }
341
+
342
+ // Center cross (highlight)
343
+ ctx.strokeStyle = 'rgba(0, 255, 0, 0.4)';
344
+ ctx.lineWidth = 2;
345
+
346
+ // Center vertical
347
+ ctx.beginPath();
348
+ ctx.moveTo(w / 2, 0);
349
+ ctx.lineTo(w / 2, h);
350
+ ctx.stroke();
351
+
352
+ // Center horizontal
353
+ ctx.beginPath();
354
+ ctx.moveTo(0, h / 2);
355
+ ctx.lineTo(w, h / 2);
356
+ ctx.stroke();
357
+
358
+ // Reset
359
+ ctx.setLineDash([]);
360
+ }
361
+ // Chart setup
362
+ const countChart = new Chart(
363
+ document.getElementById('countChart'),
364
+ {
365
+ type: 'line',
366
+ data: {
367
+ labels: [],
368
+ datasets: [{
369
+ label: 'Objects Count',
370
+ data: [],
371
+ borderColor: '#667eea',
372
+ backgroundColor: 'rgba(102, 126, 234, 0.1)',
373
+ tension: 0.1,
374
+ fill: true
375
+ }]
376
+ },
377
+ options: {
378
+ responsive: true,
379
+ scales: {
380
+ y: {
381
+ beginAtZero: true
382
+ }
383
+ }
384
+ }
385
+ }
386
+ );
387
+
388
+ // WebSocket connection
389
+ function connectWebSocket() {
390
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
391
+ ws = new WebSocket(`${protocol}//${window.location.host}/video_feed`);
392
+
393
+ ws.onopen = () => {
394
+ console.log('WebSocket connected');
395
+ statusText.innerHTML = '🟢 Active';
396
+ };
397
+
398
+ ws.onmessage = (event) => {
399
+ const msg = JSON.parse(event.data);
400
+ if (msg.type === 'frame') {
401
+ const img = new Image();
402
+ img.onload = () => {
403
+ canvas.width = img.width;
404
+ canvas.height = img.height;
405
+
406
+ // Vẽ frame từ server
407
+ ctx.drawImage(img, 0, 0);
408
+
409
+ // Vẽ overlays (grid + lines) lên trên frame
410
+ drawOverlays();
411
+ };
412
+ img.src = msg.data;
413
+ } else if (msg.type === 'count_update') {
414
+ updateChart(msg.count);
415
+ }
416
+ };
417
+ ws.onerror = (error) => {
418
+ console.error('WebSocket error:', error);
419
+ statusText.innerHTML = '🔴 Error';
420
+ };
421
+
422
+ ws.onclose = () => {
423
+ console.log('WebSocket closed');
424
+ statusText.innerHTML = '⚪ Ready';
425
+ };
426
+ }
427
+ // Update chart with new count
428
+ function updateChart(count) {
429
+ const now = new Date();
430
+ const timeLabel = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`;
431
+
432
+ countChart.data.labels.push(timeLabel);
433
+ countChart.data.datasets[0].data.push(count);
434
+
435
+ // Keep only last 20 data points
436
+ if (countChart.data.labels.length > 20) {
437
+ countChart.data.labels.shift();
438
+ countChart.data.datasets[0].data.shift();
439
+ }
440
+
441
+ countChart.update();
442
+ }
443
+
444
+ // Get available cameras
445
+ async function getCameras() {
446
+ try {
447
+ const devices = await navigator.mediaDevices.enumerateDevices();
448
+ const videoDevices = devices.filter(device => device.kind === 'videoinput');
449
+
450
+ const cameraSelect = document.getElementById('cameraSelect');
451
+ cameraSelect.innerHTML = '<option value="">-- Select Camera --</option>';
452
+
453
+ videoDevices.forEach(device => {
454
+ const option = document.createElement('option');
455
+ option.value = device.deviceId;
456
+ option.text = device.label || `Camera ${cameraSelect.length}`;
457
+ cameraSelect.appendChild(option);
458
+ });
459
+ } catch (err) {
460
+ console.error('Error enumerating devices:', err);
461
+ }
462
+ }
463
+
464
+ // Start camera
465
+ async function startCamera() {
466
+ const cameraSelect = document.getElementById('cameraSelect');
467
+ const deviceId = cameraSelect.value;
468
+
469
+ if (!deviceId) {
470
+ alert('Please select a camera first');
471
+ return;
472
+ }
473
+
474
+ try {
475
+ stream = await navigator.mediaDevices.getUserMedia({
476
+ video: {
477
+ deviceId: { exact: deviceId },
478
+ width: { ideal: 1280 },
479
+ height: { ideal: 720 }
480
+ }
481
+ });
482
+ video.srcObject = stream;
483
+
484
+ await video.play();
485
+ canvas.width = video.videoWidth;
486
+ canvas.height = video.videoHeight;
487
+
488
+ connectWebSocket();
489
+ sendFrames();
490
+
491
+ startBtn.disabled = true;
492
+ stopBtn.disabled = false;
493
+ drawLineBtn.disabled = false;
494
+ toggleGridBtn.disabled = false;
495
+ hintText.textContent = 'Nhấn "Show Grid" để hiển thị lưới, sau đó "Draw Line" để vẽ vạch đếm.';
496
+ } catch (err) {
497
+ console.error('Camera error:', err);
498
+ alert('Cannot access camera. Please check permissions.');
499
+ }
500
+ }
501
+
502
+ // Send frames to server
503
+ function sendFrames() {
504
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
505
+ animationId = requestAnimationFrame(sendFrames);
506
+ return;
507
+ }
508
+
509
+ // Không gửi frame khi đang vẽ line để tránh lag
510
+ if (!isDrawingMode) {
511
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
512
+ const frameData = canvas.toDataURL('image/jpeg', 0.8);
513
+
514
+ ws.send(JSON.stringify({
515
+ type: 'frame',
516
+ data: frameData
517
+ }));
518
+ }
519
+
520
+ animationId = requestAnimationFrame(sendFrames);
521
+ }
522
+
523
+ // Stop camera
524
+ function stopCamera() {
525
+ if (stream) {
526
+ stream.getTracks().forEach(track => track.stop());
527
+ stream = null;
528
+ }
529
+ if (ws) {
530
+ ws.close();
531
+ ws = null;
532
+ }
533
+ if (animationId) {
534
+ cancelAnimationFrame(animationId);
535
+ animationId = null;
536
+ }
537
+
538
+ startBtn.disabled = false;
539
+ stopBtn.disabled = true;
540
+ drawLineBtn.disabled = true;
541
+ toggleGridBtn.disabled = true;
542
+ statusText.innerHTML = '⚪ Ready';
543
+ hintText.textContent = 'Click "Start Camera" để bắt đầu.';
544
+ showGrid = false;
545
+ currentLine = null; // Clear line khi stop
546
+ }
547
+
548
+ // Toggle grid
549
+ function toggleGrid() {
550
+ showGrid = !showGrid;
551
+
552
+ if (showGrid) {
553
+ toggleGridBtn.classList.add('grid-mode');
554
+ toggleGridBtn.textContent = '📐 Hide Grid';
555
+ hintText.textContent = '✅ Lưới đã bật! Dùng làm tham chiếu để vẽ line chính xác.';
556
+ } else {
557
+ toggleGridBtn.classList.remove('grid-mode');
558
+ toggleGridBtn.textContent = '📐 Show Grid';
559
+ hintText.textContent = 'Nhấn "Draw Line" để vẽ vạch đếm vật thể.';
560
+ }
561
+ }
562
+
563
+ // Draw line mode
564
+ function toggleDrawLine() {
565
+ isDrawingMode = !isDrawingMode;
566
+
567
+ if (isDrawingMode) {
568
+ canvas.classList.add('drawing');
569
+ drawLineBtn.classList.add('draw-mode');
570
+ drawLineBtn.textContent = '❌ Cancel Draw';
571
+ hintText.textContent = '📍 Click 2 điểm trên video để vẽ vạch đếm. Dùng lưới làm tham chiếu.';
572
+ hintText.classList.add('active');
573
+ linePoints = [];
574
+ tempLine = null;
575
+ } else {
576
+ canvas.classList.remove('drawing');
577
+ drawLineBtn.classList.remove('draw-mode');
578
+ drawLineBtn.textContent = '✏️ Draw Line';
579
+ hintText.textContent = 'Nhấn "Draw Line" để vẽ vạch đếm vật thể.';
580
+ hintText.classList.remove('active');
581
+ linePoints = [];
582
+ tempLine = null;
583
+ }
584
+ }
585
+
586
+ // Canvas click để vẽ line
587
+ canvas.addEventListener('click', (e) => {
588
+ if (!isDrawingMode) return;
589
+
590
+ const rect = canvas.getBoundingClientRect();
591
+ const scaleX = canvas.width / rect.width;
592
+ const scaleY = canvas.height / rect.height;
593
+
594
+ const x = (e.clientX - rect.left) * scaleX;
595
+ const y = (e.clientY - rect.top) * scaleY;
596
+
597
+ linePoints.push({ x, y });
598
+
599
+ if (linePoints.length === 1) {
600
+ hintText.textContent = '📍 Click điểm thứ 2 để hoàn thành vạch đếm.';
601
+ tempLine = { x1: x, y1: y };
602
+ }
603
+
604
+ if (linePoints.length === 2) {
605
+ // Chuyển đổi sang tỷ lệ % (0-1)
606
+ const line = [
607
+ linePoints[0].x / canvas.width,
608
+ linePoints[0].y / canvas.height,
609
+ linePoints[1].x / canvas.width,
610
+ linePoints[1].y / canvas.height
611
+ ];
612
+
613
+ // Lưu line để hiển thị
614
+ currentLine = line;
615
+
616
+ // Gửi line mới đến server
617
+ if (ws && ws.readyState === WebSocket.OPEN) {
618
+ ws.send(JSON.stringify({
619
+ type: 'set_line',
620
+ line: line
621
+ }));
622
+ }
623
+
624
+ console.log('Line created:', line);
625
+
626
+ // Tắt draw mode
627
+ isDrawingMode = false;
628
+ canvas.classList.remove('drawing');
629
+ drawLineBtn.classList.remove('draw-mode');
630
+ drawLineBtn.textContent = '✏️ Draw Line';
631
+ hintText.textContent = '✅ Vạch đếm đã được tạo! Vật thể sẽ được đếm khi chạm vạch.';
632
+ hintText.classList.remove('active');
633
+ linePoints = [];
634
+ tempLine = null;
635
+ }
636
+ });
637
+
638
+ // Canvas mousemove để preview line
639
+ canvas.addEventListener('mousemove', (e) => {
640
+ if (!isDrawingMode || linePoints.length !== 1) return;
641
+
642
+ const rect = canvas.getBoundingClientRect();
643
+ const scaleX = canvas.width / rect.width;
644
+ const scaleY = canvas.height / rect.height;
645
+
646
+ const x = (e.clientX - rect.left) * scaleX;
647
+ const y = (e.clientY - rect.top) * scaleY;
648
+
649
+ tempLine = {
650
+ x1: linePoints[0].x,
651
+ y1: linePoints[0].y,
652
+ x2: x,
653
+ y2: y
654
+ };
655
+ });
656
+
657
+ // Reset count
658
+ async function resetCount() {
659
+ if (confirm('Bạn có chắc muốn reset số lượng đếm về 0?')) {
660
+ try {
661
+ await fetch('/count', { method: 'DELETE' });
662
+ updateStats();
663
+ alert('✅ Đã reset count về 0!');
664
+ } catch (error) {
665
+ console.error('Error resetting count:', error);
666
+ }
667
+ }
668
+ }
669
+ // Update stats
670
+ async function updateStats() {
671
+ try {
672
+ const response = await fetch('/count');
673
+ const data = await response.json();
674
+
675
+ document.getElementById('totalCount').textContent = data.total;
676
+ document.getElementById('fpsValue').textContent = data.fps;
677
+ updateChart(data.total);
678
+ } catch (error) {
679
+ console.error('Error fetching stats:', error);
680
+ }
681
+ }
682
+ // Event listeners
683
+ startBtn.addEventListener('click', startCamera);
684
+ stopBtn.addEventListener('click', stopCamera);
685
+ drawLineBtn.addEventListener('click', toggleDrawLine);
686
+ toggleGridBtn.addEventListener('click', toggleGrid);
687
+ resetCountBtn.addEventListener('click', resetCount);
688
+
689
+ // Initialize
690
+ getCameras();
691
+
692
+ // Update stats every second
693
+ setInterval(updateStats, 1000);
694
+ </script>
695
+ <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
696
+ </body>
697
+ </html>
style.css CHANGED
@@ -1,28 +1,180 @@
 
 
 
 
 
 
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
  h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
 
 
 
 
 
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
  body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ padding: 20px;
15
+ }
16
+
17
+ .container {
18
+ background: white;
19
+ border-radius: 16px;
20
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
21
+ overflow: hidden;
22
+ max-width: 1200px;
23
+ width: 100%;
24
+ }
25
+
26
+ .header {
27
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
28
+ color: white;
29
+ padding: 24px;
30
+ text-align: center;
31
  }
32
 
33
  h1 {
34
+ font-size: 28px;
35
+ font-weight: 700;
36
+ margin-bottom: 8px;
37
+ }
38
+
39
+ .status {
40
+ font-size: 14px;
41
+ opacity: 0.9;
42
+ }
43
+
44
+ .content {
45
+ padding: 24px;
46
+ }
47
+
48
+ .video-container {
49
+ position: relative;
50
+ background: #000;
51
+ border-radius: 12px;
52
+ overflow: hidden;
53
+ margin-bottom: 24px;
54
+ }
55
+
56
+ #outputCanvas {
57
+ width: 100%;
58
+ height: auto;
59
+ display: block;
60
+ cursor: crosshair;
61
+ }
62
+
63
+ #outputCanvas.drawing {
64
+ cursor: crosshair;
65
+ }
66
+
67
+ #videoInput {
68
+ display: none;
69
+ }
70
+
71
+ .controls {
72
+ text-align: center;
73
+ margin-bottom: 24px;
74
+ display: flex;
75
+ gap: 12px;
76
+ justify-content: center;
77
+ flex-wrap: wrap;
78
+ }
79
+
80
+ .camera-select {
81
+ padding: 12px;
82
+ border-radius: 8px;
83
+ border: 1px solid #ddd;
84
+ font-size: 16px;
85
+ min-width: 200px;
86
  }
87
 
88
+ button {
89
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
90
+ color: white;
91
+ border: none;
92
+ padding: 12px 32px;
93
+ border-radius: 8px;
94
+ font-size: 16px;
95
+ font-weight: 600;
96
+ cursor: pointer;
97
+ transition: transform 0.2s;
98
  }
99
 
100
+ button:hover {
101
+ transform: translateY(-2px);
 
 
 
 
102
  }
103
 
104
+ button:disabled {
105
+ opacity: 0.5;
106
+ cursor: not-allowed;
107
  }
108
+
109
+ button.draw-mode {
110
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
111
+ }
112
+
113
+ button.grid-mode {
114
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
115
+ }
116
+
117
+ .hint {
118
+ text-align: center;
119
+ color: #666;
120
+ font-size: 14px;
121
+ margin-bottom: 16px;
122
+ padding: 8px;
123
+ background: #f0f0f0;
124
+ border-radius: 8px;
125
+ }
126
+
127
+ .hint.active {
128
+ background: #fff3cd;
129
+ color: #856404;
130
+ }
131
+
132
+ .stats {
133
+ display: grid;
134
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
135
+ gap: 16px;
136
+ margin-bottom: 24px;
137
+ }
138
+
139
+ .stat-card {
140
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
141
+ color: white;
142
+ padding: 20px;
143
+ border-radius: 12px;
144
+ text-align: center;
145
+ }
146
+
147
+ .stat-label {
148
+ font-size: 14px;
149
+ opacity: 0.9;
150
+ margin-bottom: 8px;
151
+ }
152
+
153
+ .stat-value {
154
+ font-size: 32px;
155
+ font-weight: 700;
156
+ }
157
+
158
+ .chart-container {
159
+ background: white;
160
+ padding: 20px;
161
+ border-radius: 12px;
162
+ margin-top: 24px;
163
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
164
+ }
165
+
166
+ .footer {
167
+ text-align: center;
168
+ padding: 16px;
169
+ color: #666;
170
+ font-size: 14px;
171
+ }
172
+
173
+ @media (max-width: 768px) {
174
+ h1 { font-size: 24px; }
175
+ .stat-value { font-size: 28px; }
176
+ .controls {
177
+ flex-direction: column;
178
+ align-items: center;
179
+ }
180
+ }