diff options
Diffstat (limited to 'tools/cnn_v2_test')
| -rw-r--r-- | tools/cnn_v2_test/index.html | 188 |
1 files changed, 166 insertions, 22 deletions
diff --git a/tools/cnn_v2_test/index.html b/tools/cnn_v2_test/index.html index cc045f1..95ec788 100644 --- a/tools/cnn_v2_test/index.html +++ b/tools/cnn_v2_test/index.html @@ -14,9 +14,19 @@ - weights.layers[] array: Layer 0 = weights.layers[0], Layer 1 = weights.layers[1] Features: + - Input: PNG images or video files (MP4, WebM, etc.) + - Video playback: Play/Pause, frame-by-frame navigation (◄/► buttons) + - Video mode: Non-realtime processing (drops frames if CNN slower than playback) - Side panel: .bin metadata display, weight statistics per layer - Layer inspection: 4-channel grayscale split, intermediate layer visualization - View modes: CNN output, original, diff (×10) + - Optimization: Layer viz updates only on pause/seek during video playback + + WGSL Shader Reuse: + - CNN_SHADER (inference), STATIC_SHADER, LAYER_VIZ_SHADER are inline for single-file deployment + - Can extract to .wgsl files for: better IDE support, testing, cross-tool reuse + - Tradeoff: extraction needs fetch() or build step, breaks single-file portability + - C++ sync: manual (WGSL ≠ GLSL) but logic identical --> <head> <meta charset="UTF-8"> @@ -67,6 +77,20 @@ background: #1a1a1a; border-radius: 4px; } + button { + background: #1a1a1a; + border: 1px solid #404040; + color: #e0e0e0; + padding: 6px 12px; + font-size: 12px; + font-family: 'Courier New', monospace; + cursor: pointer; + transition: all 0.2s; + border-radius: 4px; + } + button:hover { border-color: #606060; background: #252525; } + button:disabled { opacity: 0.3; cursor: not-allowed; } + video { display: none; } .drop-zone:hover { border-color: #606060; background: #252525; } .drop-zone.active { border-color: #4a9eff; background: #1a2a3a; } .drop-zone.error { border-color: #ff4a4a; background: #3a1a1a; } @@ -97,7 +121,7 @@ background: #1a1a1a; } .main.drop-active::after { - content: 'Drop PNG image here'; + content: 'Drop PNG/video here'; position: absolute; inset: 24px; display: flex; @@ -267,6 +291,11 @@ <div class="header"> <h1>CNN v2 Testing Tool</h1> <div class="controls"> + <div class="control-group" id="videoControls"> + <button id="playPauseBtn" disabled>Play</button> + <button id="stepBackBtn" disabled>◄ Frame</button> + <button id="stepForwardBtn" disabled>Frame ►</button> + </div> <div class="control-group"> <label>Blend:</label> <input type="range" id="blend" min="0" max="1" step="0.01" value="1.0"> @@ -282,6 +311,7 @@ </div> </div> </div> + <video id="videoSource" muted></video> <div class="content"> <div class="left-sidebar"> <input type="file" id="weightsFile" accept=".bin" style="display: none;"> @@ -316,7 +346,7 @@ </div> <div class="footer"> <div class="footer-top"> - <span class="status" id="status">Drop PNG image anywhere to begin</span> + <span class="status" id="status">Drop PNG/video anywhere to begin</span> <span class="shortcuts">[SPACE] Original | [D] Diff (×10)</span> </div> <div class="console" id="console"></div> @@ -572,12 +602,16 @@ class CNNTester { this.status = document.getElementById('status'); this.console = document.getElementById('console'); this.image = null; + this.video = document.getElementById('videoSource'); this.weights = null; this.viewMode = 0; this.blendAmount = 1.0; this.depth = 1.0; this.currentLayerIdx = null; this.currentChannelOffset = null; + this.isVideo = false; + this.fps = 30; + this.isProcessing = false; this.init(); } @@ -690,8 +724,12 @@ class CNNTester { async loadImage(file) { const img = await createImageBitmap(file); this.image = img; + this.isVideo = false; this.canvas.width = img.width; this.canvas.height = img.height; + document.getElementById('playPauseBtn').disabled = true; + document.getElementById('stepBackBtn').disabled = true; + document.getElementById('stepForwardBtn').disabled = true; this.log(`Loaded image: ${file.name} (${img.width}×${img.height})`); if (this.weights) { this.setStatus(`Ready: ${img.width}×${img.height}`); @@ -702,6 +740,86 @@ class CNNTester { } } + // Video loading: wait for metadata, then first frame decode (readyState≥2) + async loadVideo(file) { + return new Promise((resolve, reject) => { + this.video.src = URL.createObjectURL(file); + + this.video.onloadedmetadata = () => { + const w = this.video.videoWidth; + const h = this.video.videoHeight; + if (w === 0 || h === 0) { + reject(new Error('Video has invalid dimensions')); + return; + } + + this.isVideo = true; + this.canvas.width = w; + this.canvas.height = h; + this.fps = 30; + this.log(`Loaded video: ${file.name} (${w}×${h}, ${this.video.duration.toFixed(1)}s)`); + + // Enable video controls + ['playPauseBtn', 'stepBackBtn', 'stepForwardBtn'].forEach(id => + document.getElementById(id).disabled = false + ); + + // Set up event handlers + this.video.onpause = () => { document.getElementById('playPauseBtn').textContent = 'Play'; }; + this.video.onplay = () => { document.getElementById('playPauseBtn').textContent = 'Pause'; this.playbackLoop(); }; + + // Wait for first frame to be decoded before displaying + const displayFirstFrame = () => { + this.video.onseeked = () => { if (!this.isProcessing) this.processVideoFrame(); }; + if (this.video.readyState >= 2) { // HAVE_CURRENT_DATA or better + if (this.weights) { + this.setStatus(`Ready: ${w}×${h}`); + this.processVideoFrame().then(() => resolve()); + } else { + this.setStatus(`Video loaded - drop .bin weights to process`); + this.displayOriginal(); + resolve(); + } + } else { + setTimeout(displayFirstFrame, 50); // Poll until frame ready + } + }; + + this.video.onseeked = displayFirstFrame; + this.video.currentTime = 0; + }; + + this.video.onerror = () => reject(new Error('Failed to load video')); + }); + } + + // Video playback loop (non-realtime, drops frames if CNN slow) + playbackLoop() { + if (this.video.paused || this.video.ended) return; + if (!this.isProcessing) this.processVideoFrame(); + requestAnimationFrame(() => this.playbackLoop()); + } + + // Process current video frame through CNN pipeline + async processVideoFrame() { + if (!this.weights || this.isProcessing) return; + this.isProcessing = true; + await this.run(); + this.isProcessing = false; + } + + // Video controls + togglePlayPause() { + this.video.paused ? this.video.play() : this.video.pause(); + } + + stepFrame(direction) { + if (!this.isVideo) return; + this.video.pause(); + this.video.currentTime = Math.max(0, Math.min(this.video.duration, + this.video.currentTime + direction / this.fps)); + } + async loadWeights(file) { const buffer = await file.arrayBuffer(); this.weights = this.parseWeights(buffer); @@ -776,9 +894,11 @@ class CNNTester { } displayOriginal() { - if (!this.image || !this.device) return; + const source = this.isVideo ? this.video : this.image; + if (!source || !this.device) return; - const { width, height } = this.image; + const width = this.isVideo ? this.video.videoWidth : this.image.width; + const height = this.isVideo ? this.video.videoHeight : this.image.height; this.context.configure({ device: this.device, format: this.format }); const inputTex = this.device.createTexture({ @@ -788,7 +908,7 @@ class CNNTester { }); this.device.queue.copyExternalImageToTexture( - { source: this.image }, + { source: source }, { texture: inputTex }, [width, height] ); @@ -843,9 +963,14 @@ class CNNTester { this.device.queue.submit([encoder.finish()]); } + // Run CNN inference pipeline on current source (image or video frame) async run() { const t0 = performance.now(); - const { width, height } = this.image; + const source = this.isVideo ? this.video : this.image; + if (!source) return; + const width = this.isVideo ? this.video.videoWidth : this.image.width; + const height = this.isVideo ? this.video.videoHeight : this.image.height; + this.log(`Running CNN pipeline (${this.weights.layers.length} layers)...`); this.context.configure({ device: this.device, format: this.format }); @@ -859,7 +984,7 @@ class CNNTester { }); this.device.queue.copyExternalImageToTexture( - { source: this.image }, + { source: source }, { texture: this.inputTexture }, [width, height] ); @@ -1067,6 +1192,9 @@ class CNNTester { } updateLayerVizPanel() { + // Optimization: Skip layer viz updates during video playback (~5-10ms saved per frame) + if (this.isVideo && !this.video.paused) return; + const panel = document.getElementById('layerViz'); if (!this.layerOutputs || this.layerOutputs.length === 0) { @@ -1153,7 +1281,8 @@ class CNNTester { const layerName = layerIdx === 0 ? `Static Features (${channelOffset}-${channelOffset + 3})` : `Layer ${layerIdx - 1}`; const layerTex = this.layerOutputs[layerIdx]; - const { width, height } = this.image; + const width = this.isVideo ? this.video.videoWidth : this.image.width; + const height = this.isVideo ? this.video.videoHeight : this.image.height; this.log(`Visualizing ${layerName} activations (${width}×${height})`); @@ -1265,7 +1394,8 @@ class CNNTester { const zoomCanvas = document.getElementById('zoomCanvas'); if (!zoomCanvas) return; - const { width, height } = this.image; + const width = this.isVideo ? this.video.videoWidth : this.image.width; + const height = this.isVideo ? this.video.videoHeight : this.image.height; const zoomSize = 32; // Show 32x32 area zoomCanvas.width = zoomSize; zoomCanvas.height = zoomSize; @@ -1301,7 +1431,8 @@ class CNNTester { } const halfSize = Math.floor(zoomSize / 2); - const { width, height } = this.image; + const width = this.isVideo ? this.video.videoWidth : this.image.width; + const height = this.isVideo ? this.video.videoHeight : this.image.height; // Create shader for zoomed view (samples 4 channels and displays as 2x2 grid) const zoomShader = ` @@ -1520,7 +1651,7 @@ function setupDropZone(id, callback) { }); } -// Whole window drop for PNG images +// Whole window drop for PNG images and videos const mainArea = document.getElementById('mainDrop'); ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => { mainArea.addEventListener(e, ev => { ev.preventDefault(); ev.stopPropagation(); }); @@ -1529,11 +1660,18 @@ const mainArea = document.getElementById('mainDrop'); ['dragleave', 'drop'].forEach(e => mainArea.addEventListener(e, () => mainArea.classList.remove('drop-active'))); mainArea.addEventListener('drop', e => { const file = e.dataTransfer.files[0]; - if (file && file.type.startsWith('image/')) { - tester.loadImage(file).catch(err => { - tester.setStatus(err.message, true); - tester.log(err.message, 'error'); - }); + if (file) { + if (file.type.startsWith('image/')) { + tester.loadImage(file).catch(err => { + tester.setStatus(err.message, true); + tester.log(err.message, 'error'); + }); + } else if (file.type.startsWith('video/')) { + tester.loadVideo(file).catch(err => { + tester.setStatus(err.message, true); + tester.log(err.message, 'error'); + }); + } } }); @@ -1554,7 +1692,7 @@ document.getElementById('weightsFile').addEventListener('change', e => { document.getElementById('blend').addEventListener('input', e => { tester.blendAmount = parseFloat(e.target.value); document.getElementById('blendValue').textContent = e.target.value; - if (tester.image && tester.weights) { + if ((tester.image || tester.isVideo) && tester.weights) { tester.log(`Blend changed to ${e.target.value}`); tester.run(); } @@ -1562,9 +1700,13 @@ document.getElementById('blend').addEventListener('input', e => { document.getElementById('depth').addEventListener('input', e => { tester.depth = parseFloat(e.target.value); - if (tester.image && tester.weights) tester.run(); + if ((tester.image || tester.isVideo) && tester.weights) tester.run(); }); +document.getElementById('playPauseBtn').addEventListener('click', () => tester.togglePlayPause()); +document.getElementById('stepBackBtn').addEventListener('click', () => tester.stepFrame(-1)); +document.getElementById('stepForwardBtn').addEventListener('click', () => tester.stepFrame(1)); + document.addEventListener('keydown', e => { if (e.code === 'Space') { e.preventDefault(); @@ -1575,10 +1717,11 @@ document.addEventListener('keydown', e => { } const modeName = ['CNN Output', 'Original', 'Diff (×10)'][tester.viewMode]; document.getElementById('viewMode').textContent = modeName; - if (tester.image && tester.weights) { + if ((tester.image || tester.isVideo) && tester.weights) { tester.log(`View mode: ${modeName}`); tester.updateDisplay(); - const { width, height } = tester.image; + const width = tester.isVideo ? tester.video.videoWidth : tester.image.width; + const height = tester.isVideo ? tester.video.videoHeight : tester.image.height; tester.setStatus(`${width}×${height} | ${modeName}`); } } else if (e.code === 'KeyD') { @@ -1590,10 +1733,11 @@ document.addEventListener('keydown', e => { } const modeName = ['CNN Output', 'Original', 'Diff (×10)'][tester.viewMode]; document.getElementById('viewMode').textContent = modeName; - if (tester.image && tester.weights) { + if ((tester.image || tester.isVideo) && tester.weights) { tester.log(`View mode: ${modeName}`); tester.updateDisplay(); - const { width, height } = tester.image; + const width = tester.isVideo ? tester.video.videoWidth : tester.image.width; + const height = tester.isVideo ? tester.video.videoHeight : tester.image.height; tester.setStatus(`${width}×${height} | ${modeName}`); } } |
