Spaces:
Running
Running
Upload 2 files
Browse files- Yolo.js +11 -0
- index.html +220 -8
Yolo.js
CHANGED
|
@@ -140,6 +140,17 @@ export async function loadModel(modelPath) {
|
|
| 140 |
executionProviders: [provider],
|
| 141 |
});
|
| 142 |
console.log(`[ONNX] Using execution provider: ${provider}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
return session;
|
| 144 |
} catch {
|
| 145 |
// provider khΓ΄ng khαΊ£ dα»₯ng, thα» tiαΊΏp
|
|
|
|
| 140 |
executionProviders: [provider],
|
| 141 |
});
|
| 142 |
console.log(`[ONNX] Using execution provider: ${provider}`);
|
| 143 |
+
console.log(`[ONNX] Model: ${modelPath}`);
|
| 144 |
+
|
| 145 |
+
// Ghi chΓΊ vα» FP16 performance
|
| 146 |
+
if (modelPath.includes('-fp16')) {
|
| 147 |
+
console.warn(
|
| 148 |
+
'[ONNX] FP16 models may be slower than FP32 on web browsers.\n' +
|
| 149 |
+
'Reason: WebGPU/WebGL FP16 support is limited, WASM converts FP16βFP32.\n' +
|
| 150 |
+
'Use FP32 for best performance, FP16 only for size reduction.'
|
| 151 |
+
);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
return session;
|
| 155 |
} catch {
|
| 156 |
// provider khΓ΄ng khαΊ£ dα»₯ng, thα» tiαΊΏp
|
index.html
CHANGED
|
@@ -171,7 +171,8 @@
|
|
| 171 |
opacity: 1;
|
| 172 |
}
|
| 173 |
|
| 174 |
-
#file-input
|
|
|
|
| 175 |
display: none;
|
| 176 |
}
|
| 177 |
|
|
@@ -199,6 +200,8 @@
|
|
| 199 |
/* Webcam */
|
| 200 |
#webcam-panel { display: none; flex-direction: column; align-items: center; gap: 10px; width: 100%; }
|
| 201 |
#webcam-panel.active { display: flex; }
|
|
|
|
|
|
|
| 202 |
#image-panel { display: flex; flex-direction: column; align-items: center; gap: 10px; }
|
| 203 |
#image-panel.hidden { display: none; }
|
| 204 |
|
|
@@ -210,6 +213,38 @@
|
|
| 210 |
display: none;
|
| 211 |
}
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
.webcam-controls {
|
| 214 |
display: flex;
|
| 215 |
gap: 10px;
|
|
@@ -429,6 +464,7 @@
|
|
| 429 |
<div class="source-tabs">
|
| 430 |
<button class="tab-btn active" id="tab-image">πΌ αΊ’nh</button>
|
| 431 |
<button class="tab-btn" id="tab-webcam">π· Webcam</button>
|
|
|
|
| 432 |
</div>
|
| 433 |
|
| 434 |
<!-- Image panel -->
|
|
@@ -451,6 +487,28 @@
|
|
| 451 |
<button class="btn-danger" id="webcam-stop-btn" disabled>β Dα»«ng</button>
|
| 452 |
</div>
|
| 453 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
</div>
|
| 455 |
|
| 456 |
<div class="canvas-area">
|
|
@@ -678,14 +736,17 @@
|
|
| 678 |
// ββ Source Tabs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 679 |
document.getElementById('tab-image').addEventListener('click', () => switchTab('image'));
|
| 680 |
document.getElementById('tab-webcam').addEventListener('click', () => switchTab('webcam'));
|
|
|
|
| 681 |
|
| 682 |
function switchTab(tab) {
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
document.getElementById('image-panel').classList.toggle('hidden', !
|
| 687 |
-
document.getElementById('webcam-panel').classList.toggle('active',
|
| 688 |
-
|
|
|
|
|
|
|
| 689 |
}
|
| 690 |
|
| 691 |
// ββ Webcam ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -784,6 +845,157 @@
|
|
| 784 |
}
|
| 785 |
}
|
| 786 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
// ββ Magnifier βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 788 |
(function initMagnifier() {
|
| 789 |
const magnifier = document.getElementById('magnifier');
|
|
@@ -813,7 +1025,7 @@
|
|
| 813 |
applyLensSize(lensSize);
|
| 814 |
});
|
| 815 |
|
| 816 |
-
['original-canvas', 'result-canvas', 'webcam-video'].forEach(id => {
|
| 817 |
const el = document.getElementById(id);
|
| 818 |
el.addEventListener('mouseenter', () => { magnifier.style.display = 'block'; el.style.cursor = 'crosshair'; });
|
| 819 |
el.addEventListener('mouseleave', () => { magnifier.style.display = 'none'; el.style.cursor = ''; });
|
|
|
|
| 171 |
opacity: 1;
|
| 172 |
}
|
| 173 |
|
| 174 |
+
#file-input,
|
| 175 |
+
#video-file-input {
|
| 176 |
display: none;
|
| 177 |
}
|
| 178 |
|
|
|
|
| 200 |
/* Webcam */
|
| 201 |
#webcam-panel { display: none; flex-direction: column; align-items: center; gap: 10px; width: 100%; }
|
| 202 |
#webcam-panel.active { display: flex; }
|
| 203 |
+
#video-panel { display: none; flex-direction: column; align-items: center; gap: 10px; width: 100%; }
|
| 204 |
+
#video-panel.active { display: flex; }
|
| 205 |
#image-panel { display: flex; flex-direction: column; align-items: center; gap: 10px; }
|
| 206 |
#image-panel.hidden { display: none; }
|
| 207 |
|
|
|
|
| 213 |
display: none;
|
| 214 |
}
|
| 215 |
|
| 216 |
+
#video-player {
|
| 217 |
+
max-width: 100%;
|
| 218 |
+
border-radius: 8px;
|
| 219 |
+
border: 1px solid #e0e0e0;
|
| 220 |
+
background: #111;
|
| 221 |
+
display: none;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.video-progress-wrap {
|
| 225 |
+
width: 100%;
|
| 226 |
+
max-width: 640px;
|
| 227 |
+
display: flex;
|
| 228 |
+
flex-direction: column;
|
| 229 |
+
gap: 4px;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
#video-progress {
|
| 233 |
+
width: 100%;
|
| 234 |
+
accent-color: #1565c0;
|
| 235 |
+
cursor: pointer;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
#video-progress:disabled { opacity: 0.4; cursor: default; }
|
| 239 |
+
|
| 240 |
+
.video-time {
|
| 241 |
+
display: flex;
|
| 242 |
+
justify-content: space-between;
|
| 243 |
+
font-size: 0.82rem;
|
| 244 |
+
color: #888;
|
| 245 |
+
padding: 0 2px;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
.webcam-controls {
|
| 249 |
display: flex;
|
| 250 |
gap: 10px;
|
|
|
|
| 464 |
<div class="source-tabs">
|
| 465 |
<button class="tab-btn active" id="tab-image">πΌ αΊ’nh</button>
|
| 466 |
<button class="tab-btn" id="tab-webcam">π· Webcam</button>
|
| 467 |
+
<button class="tab-btn" id="tab-video">π¬ Video</button>
|
| 468 |
</div>
|
| 469 |
|
| 470 |
<!-- Image panel -->
|
|
|
|
| 487 |
<button class="btn-danger" id="webcam-stop-btn" disabled>β Dα»«ng</button>
|
| 488 |
</div>
|
| 489 |
</div>
|
| 490 |
+
|
| 491 |
+
<!-- Video panel -->
|
| 492 |
+
<div id="video-panel">
|
| 493 |
+
<label class="file-label" for="video-file-input">
|
| 494 |
+
π Chα»n video (MP4, WebM, MOV)
|
| 495 |
+
</label>
|
| 496 |
+
<input type="file" id="video-file-input" accept="video/mp4,video/webm,video/quicktime" />
|
| 497 |
+
<video id="video-player" playsinline muted width="640" height="360"></video>
|
| 498 |
+
<div class="video-progress-wrap">
|
| 499 |
+
<input type="range" id="video-progress" min="0" max="100" step="0.1" value="0" disabled />
|
| 500 |
+
<div class="video-time">
|
| 501 |
+
<span id="video-current-time">0:00</span>
|
| 502 |
+
<span id="video-duration">0:00</span>
|
| 503 |
+
</div>
|
| 504 |
+
</div>
|
| 505 |
+
<div class="webcam-controls">
|
| 506 |
+
<button class="btn-secondary" id="video-play-btn" disabled>βΆ Play</button>
|
| 507 |
+
<button class="btn-secondary" id="video-detect-btn" disabled>π― BαΊ―t ΔαΊ§u nhαΊn diα»n</button>
|
| 508 |
+
<button class="btn-secondary" id="video-capture-btn" disabled>π Capture β Clipboard</button>
|
| 509 |
+
<button class="btn-danger" id="video-stop-btn" disabled>β Reset</button>
|
| 510 |
+
</div>
|
| 511 |
+
</div>
|
| 512 |
</div>
|
| 513 |
|
| 514 |
<div class="canvas-area">
|
|
|
|
| 736 |
// ββ Source Tabs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 737 |
document.getElementById('tab-image').addEventListener('click', () => switchTab('image'));
|
| 738 |
document.getElementById('tab-webcam').addEventListener('click', () => switchTab('webcam'));
|
| 739 |
+
document.getElementById('tab-video').addEventListener('click', () => switchTab('video'));
|
| 740 |
|
| 741 |
function switchTab(tab) {
|
| 742 |
+
['image', 'webcam', 'video'].forEach(t => {
|
| 743 |
+
document.getElementById(`tab-${t}`).classList.toggle('active', t === tab);
|
| 744 |
+
});
|
| 745 |
+
document.getElementById('image-panel').classList.toggle('hidden', tab !== 'image');
|
| 746 |
+
document.getElementById('webcam-panel').classList.toggle('active', tab === 'webcam');
|
| 747 |
+
document.getElementById('video-panel').classList.toggle('active', tab === 'video');
|
| 748 |
+
if (tab !== 'webcam') stopWebcam();
|
| 749 |
+
if (tab !== 'video') stopVideo();
|
| 750 |
}
|
| 751 |
|
| 752 |
// ββ Webcam ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 845 |
}
|
| 846 |
}
|
| 847 |
|
| 848 |
+
// ββ Video βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 849 |
+
let videoObjectUrl = null, videoDetecting = false, videoRafId = null;
|
| 850 |
+
let videoFpsCount = 0, videoFpsLastTime = 0, videoCurrentFps = 0;
|
| 851 |
+
|
| 852 |
+
const videoPlayer = document.getElementById('video-player');
|
| 853 |
+
const videoProgress = document.getElementById('video-progress');
|
| 854 |
+
const videoCurrentEl = document.getElementById('video-current-time');
|
| 855 |
+
const videoDurationEl = document.getElementById('video-duration');
|
| 856 |
+
const videoPlayBtn = document.getElementById('video-play-btn');
|
| 857 |
+
const videoDetectBtn = document.getElementById('video-detect-btn');
|
| 858 |
+
const videoCaptureBtn = document.getElementById('video-capture-btn');
|
| 859 |
+
const videoStopBtn = document.getElementById('video-stop-btn');
|
| 860 |
+
|
| 861 |
+
function formatTime(s) {
|
| 862 |
+
const m = Math.floor(s / 60);
|
| 863 |
+
return `${m}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
document.getElementById('video-file-input').addEventListener('change', function (e) {
|
| 867 |
+
const file = e.target.files[0];
|
| 868 |
+
if (!file) return;
|
| 869 |
+
stopVideo();
|
| 870 |
+
if (videoObjectUrl) URL.revokeObjectURL(videoObjectUrl);
|
| 871 |
+
videoObjectUrl = URL.createObjectURL(file);
|
| 872 |
+
videoPlayer.src = videoObjectUrl;
|
| 873 |
+
videoPlayer.style.display = 'block';
|
| 874 |
+
videoPlayer.load();
|
| 875 |
+
});
|
| 876 |
+
|
| 877 |
+
videoPlayer.addEventListener('loadedmetadata', () => {
|
| 878 |
+
videoProgress.max = videoPlayer.duration;
|
| 879 |
+
videoProgress.value = 0;
|
| 880 |
+
videoProgress.disabled = false;
|
| 881 |
+
videoDurationEl.textContent = formatTime(videoPlayer.duration);
|
| 882 |
+
videoCurrentEl.textContent = '0:00';
|
| 883 |
+
videoPlayBtn.disabled = false;
|
| 884 |
+
videoDetectBtn.disabled = false;
|
| 885 |
+
videoStopBtn.disabled = false;
|
| 886 |
+
setStatus('ready', 'Video ΔΓ£ tαΊ£i β nhαΊ₯n Play hoαΊ·c BαΊ―t ΔαΊ§u nhαΊn diα»n');
|
| 887 |
+
});
|
| 888 |
+
|
| 889 |
+
videoPlayer.addEventListener('timeupdate', () => {
|
| 890 |
+
if (!videoPlayer.seeking) {
|
| 891 |
+
videoProgress.value = videoPlayer.currentTime;
|
| 892 |
+
videoCurrentEl.textContent = formatTime(videoPlayer.currentTime);
|
| 893 |
+
}
|
| 894 |
+
});
|
| 895 |
+
|
| 896 |
+
videoPlayer.addEventListener('ended', () => {
|
| 897 |
+
videoPlayBtn.textContent = 'βΆ Play';
|
| 898 |
+
if (videoDetecting) stopVideoDetection();
|
| 899 |
+
});
|
| 900 |
+
|
| 901 |
+
videoProgress.addEventListener('input', () => {
|
| 902 |
+
videoPlayer.currentTime = parseFloat(videoProgress.value);
|
| 903 |
+
});
|
| 904 |
+
|
| 905 |
+
videoPlayBtn.addEventListener('click', () => {
|
| 906 |
+
if (videoPlayer.paused) {
|
| 907 |
+
videoPlayer.play();
|
| 908 |
+
videoPlayBtn.textContent = 'βΈ Pause';
|
| 909 |
+
} else {
|
| 910 |
+
videoPlayer.pause();
|
| 911 |
+
videoPlayBtn.textContent = 'βΆ Play';
|
| 912 |
+
}
|
| 913 |
+
});
|
| 914 |
+
|
| 915 |
+
videoDetectBtn.addEventListener('click', () => {
|
| 916 |
+
if (videoDetecting) {
|
| 917 |
+
stopVideoDetection();
|
| 918 |
+
} else {
|
| 919 |
+
if (!detector) { setStatus('error', 'ChΖ°a tαΊ£i model'); return; }
|
| 920 |
+
videoDetecting = true;
|
| 921 |
+
videoFpsCount = 0; videoFpsLastTime = performance.now();
|
| 922 |
+
videoDetectBtn.textContent = 'βΉ Dα»«ng nhαΊn diα»n';
|
| 923 |
+
videoCaptureBtn.disabled = false;
|
| 924 |
+
if (videoPlayer.paused) videoPlayer.play();
|
| 925 |
+
videoPlayBtn.textContent = 'βΈ Pause';
|
| 926 |
+
videoDetectLoop();
|
| 927 |
+
}
|
| 928 |
+
});
|
| 929 |
+
|
| 930 |
+
async function videoDetectLoop() {
|
| 931 |
+
if (!videoDetecting || videoPlayer.paused || videoPlayer.ended) {
|
| 932 |
+
if (videoPlayer.ended) stopVideoDetection();
|
| 933 |
+
return;
|
| 934 |
+
}
|
| 935 |
+
const t0 = performance.now();
|
| 936 |
+
const origCanvas = document.getElementById('original-canvas');
|
| 937 |
+
origCanvas.width = videoPlayer.videoWidth || 640;
|
| 938 |
+
origCanvas.height = videoPlayer.videoHeight || 360;
|
| 939 |
+
origCanvas.getContext('2d').drawImage(videoPlayer, 0, 0);
|
| 940 |
+
|
| 941 |
+
const pre = preprocessFromCanvas(origCanvas);
|
| 942 |
+
const detections = await runDetection(detector, currentModelEntry, pre);
|
| 943 |
+
const elapsed = performance.now() - t0;
|
| 944 |
+
|
| 945 |
+
const resultCanvas = document.getElementById('result-canvas');
|
| 946 |
+
resultCanvas.width = origCanvas.width; resultCanvas.height = origCanvas.height;
|
| 947 |
+
const ctx = resultCanvas.getContext('2d');
|
| 948 |
+
ctx.drawImage(origCanvas, 0, 0);
|
| 949 |
+
drawDetectionsOnCtx(ctx, detections, classes.length);
|
| 950 |
+
renderTable(detections);
|
| 951 |
+
|
| 952 |
+
videoFpsCount++;
|
| 953 |
+
const now = performance.now();
|
| 954 |
+
if (now - videoFpsLastTime >= 500) {
|
| 955 |
+
videoCurrentFps = videoFpsCount / ((now - videoFpsLastTime) / 1000);
|
| 956 |
+
videoFpsCount = 0; videoFpsLastTime = now;
|
| 957 |
+
}
|
| 958 |
+
showTiming(elapsed, videoCurrentFps);
|
| 959 |
+
|
| 960 |
+
videoRafId = requestAnimationFrame(videoDetectLoop);
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
function stopVideoDetection() {
|
| 964 |
+
videoDetecting = false;
|
| 965 |
+
if (videoRafId) cancelAnimationFrame(videoRafId);
|
| 966 |
+
videoDetectBtn.textContent = 'π― BαΊ―t ΔαΊ§u nhαΊn diα»n';
|
| 967 |
+
videoCaptureBtn.disabled = true;
|
| 968 |
+
}
|
| 969 |
+
|
| 970 |
+
function stopVideo() {
|
| 971 |
+
stopVideoDetection();
|
| 972 |
+
videoPlayer.pause();
|
| 973 |
+
videoPlayer.src = '';
|
| 974 |
+
videoPlayer.style.display = 'none';
|
| 975 |
+
videoProgress.value = 0;
|
| 976 |
+
videoProgress.disabled = true;
|
| 977 |
+
videoCurrentEl.textContent = '0:00';
|
| 978 |
+
videoDurationEl.textContent = '0:00';
|
| 979 |
+
videoPlayBtn.textContent = 'βΆ Play';
|
| 980 |
+
videoPlayBtn.disabled = true;
|
| 981 |
+
videoDetectBtn.disabled = true;
|
| 982 |
+
videoCaptureBtn.disabled = true;
|
| 983 |
+
videoStopBtn.disabled = true;
|
| 984 |
+
}
|
| 985 |
+
|
| 986 |
+
videoCaptureBtn.addEventListener('click', async () => {
|
| 987 |
+
const resultCanvas = document.getElementById('result-canvas');
|
| 988 |
+
try {
|
| 989 |
+
const blob = await new Promise(res => resultCanvas.toBlob(res, 'image/png'));
|
| 990 |
+
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
| 991 |
+
setStatus('ready', 'β
ΔΓ£ copy frame vΓ o clipboard');
|
| 992 |
+
} catch (err) {
|
| 993 |
+
setStatus('error', `KhΓ΄ng thα» copy: ${err.message}`);
|
| 994 |
+
}
|
| 995 |
+
});
|
| 996 |
+
|
| 997 |
+
videoStopBtn.addEventListener('click', stopVideo);
|
| 998 |
+
|
| 999 |
// ββ Magnifier βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1000 |
(function initMagnifier() {
|
| 1001 |
const magnifier = document.getElementById('magnifier');
|
|
|
|
| 1025 |
applyLensSize(lensSize);
|
| 1026 |
});
|
| 1027 |
|
| 1028 |
+
['original-canvas', 'result-canvas', 'webcam-video', 'video-player'].forEach(id => {
|
| 1029 |
const el = document.getElementById(id);
|
| 1030 |
el.addEventListener('mouseenter', () => { magnifier.style.display = 'block'; el.style.cursor = 'crosshair'; });
|
| 1031 |
el.addEventListener('mouseleave', () => { magnifier.style.display = 'none'; el.style.cursor = ''; });
|