summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tools/cnn_v2_test/README.md94
-rw-r--r--tools/cnn_v2_test/index.html403
2 files changed, 322 insertions, 175 deletions
diff --git a/tools/cnn_v2_test/README.md b/tools/cnn_v2_test/README.md
index 2a8e08d..b9439e5 100644
--- a/tools/cnn_v2_test/README.md
+++ b/tools/cnn_v2_test/README.md
@@ -6,12 +6,14 @@ WebGPU-based browser tool for testing trained CNN v2 weights.
## Features
-- Drag-drop PNG images and `.bin` weights
+- Drag-drop PNG images and `.bin` weights (or click to browse)
- Real-time CNN inference with WebGPU compute shaders
- View modes: CNN output, original input, difference (×10)
- Adjustable blend amount and depth
- Data-driven pipeline (supports variable layer count)
- GPU timing display
+- **Left Panel:** Weights info + kernel visualization (1px/weight, all layers)
+- **Right Panel:** Layer activation viewer with 4-channel split + 4× zoom
---
@@ -41,28 +43,36 @@ python3 -m http.server 8000
### 2. Load Data
-1. **Drop PNG image** anywhere in window (shows preview immediately)
-2. **Drop `.bin` weights** into header drop zone
+1. **Drop `.bin` weights** into left sidebar zone (or click to browse)
+2. **Drop PNG image** anywhere in center canvas area
3. CNN runs automatically when both loaded
-### 3. Controls
+### 3. Layout
-**Sliders:**
-- **Blend:** Mix between original (0.0) and CNN output (1.0)
-- **Depth:** Uniform depth value for all pixels (0.0–1.0)
+**Left Sidebar:**
+- Weights drop zone (click or drag-drop `.bin` files)
+- Weights info panel (layer specs, ranges, file size)
+- Weights visualization (click Layer 0/1/2 buttons)
+ - 1 pixel per weight, all input channels horizontally
+ - Output channels (Out 0-3) stacked vertically
+
+**Center Canvas:**
+- Main output view (CNN result, original, or diff)
+- Keyboard: `SPACE` = original, `D` = diff (×10)
-**Keyboard:**
-- `SPACE` - Toggle original input view
-- `D` - Toggle difference view (×10 amplification)
+**Right Sidebar:**
+- Layer selection buttons (Static 0-3/4-7, Layer 0/1/2)
+- 4 small activation views (Ch0/1/2/3) in a row
+- Large zoom view below (4× magnification, follows mouse)
-**Status Bar:**
-- Shows GPU timing (ms), image dimensions, and current view mode
-- Red text indicates errors
+**Header Controls:**
+- **Blend:** Mix between original (0.0) and CNN output (1.0)
+- **Depth:** Uniform depth value for all pixels (0.0–1.0)
+- **View:** Current display mode
-**Console Log:**
-- Timestamped event log at bottom
-- Tracks file loads, pipeline execution, errors
-- Auto-scrolls to latest messages
+**Footer:**
+- Status: GPU timing (ms), image dimensions, view mode
+- Console: Timestamped event log (file loads, errors)
---
@@ -167,24 +177,32 @@ Open browser console (F12) for detailed errors. Common issues:
---
-## TODO
+## Implemented Features
+
+**✓ Weights Metadata Panel:**
+- Layer descriptions (kernel size, channels, weight count)
+- Weight statistics (min/max per layer)
+- File size and layer count
+
+**✓ Weights Visualization:**
+- Per-layer kernel heatmaps (1px/weight)
+- All input channels displayed horizontally
+- Output channels stacked vertically
+- Normalized grayscale display
-**Side Panel (Right):**
-- Display .bin content metadata:
- - Layer descriptions (kernel size, channels, weight count)
- - Weight statistics (min/max/mean per layer)
- - Weight heatmap visualization
- - Binary format validation status
- - Memory usage breakdown
+**✓ Layer Activation Viewer:**
+- Static features (8D split into 0-3 and 4-7 views)
+- All CNN layer outputs (Layer 0/1/2...)
+- 4-channel split view (grayscale per channel)
+- Mouse-driven 4× zoom view
+
+## TODO
-**Layer Inspection Views:**
-- Split R/G/B/A plane visualization
-- Intermediate layer output display:
- - View static features (8D packed as heatmaps)
- - View layer 0 output (before activation)
- - View layer 1 output
- - Toggle between channels
-- Activation heatmaps (where neurons fire)
+**Future Enhancements:**
+- Weight distribution histograms per layer
+- Activation statistics (min/max/mean overlay)
+- Side-by-side diff mode (browser vs C++ output)
+- Export rendered layers as PNG
---
@@ -213,13 +231,15 @@ Planned enhancements:
## Size
-- HTML structure: ~1 KB
-- CSS styling: ~1 KB
-- JavaScript logic: ~5 KB
+- HTML structure: ~2 KB
+- CSS styling: ~2 KB
+- JavaScript logic: ~10 KB (includes zoom + weights viz)
- Static shader: ~1 KB
- CNN shader: ~3 KB
- Display shader: ~1 KB
-- **Total: ~12 KB** (single file, no dependencies)
+- Layer viz shader: ~2 KB
+- Zoom shader: ~1 KB
+- **Total: ~22 KB** (single file, no dependencies)
---
diff --git a/tools/cnn_v2_test/index.html b/tools/cnn_v2_test/index.html
index 91f7942..cc045f1 100644
--- a/tools/cnn_v2_test/index.html
+++ b/tools/cnn_v2_test/index.html
@@ -37,8 +37,12 @@
background: #2a2a2a;
padding: 16px;
border-bottom: 1px solid #404040;
+ display: flex;
+ align-items: center;
+ gap: 24px;
+ flex-wrap: wrap;
}
- h1 { font-size: 18px; margin-bottom: 12px; }
+ h1 { font-size: 18px; }
.controls {
display: flex;
gap: 16px;
@@ -60,7 +64,8 @@
cursor: pointer;
transition: all 0.2s;
font-size: 12px;
- margin-top: 12px;
+ background: #1a1a1a;
+ border-radius: 4px;
}
.drop-zone:hover { border-color: #606060; background: #252525; }
.drop-zone.active { border-color: #4a9eff; background: #1a2a3a; }
@@ -69,6 +74,17 @@
flex: 1;
display: flex;
overflow: hidden;
+ gap: 1px;
+ background: #404040;
+ }
+ .left-sidebar {
+ width: 300px;
+ background: #2a2a2a;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 16px;
}
.main {
flex: 1;
@@ -78,6 +94,7 @@
padding: 24px;
overflow: auto;
position: relative;
+ background: #1a1a1a;
}
.main.drop-active::after {
content: 'Drop PNG image here';
@@ -94,9 +111,8 @@
z-index: 10;
}
.sidebar {
- width: 350px;
+ width: 400px;
background: #2a2a2a;
- border-left: 1px solid #404040;
overflow-y: auto;
display: flex;
flex-direction: column;
@@ -175,8 +191,9 @@
}
.layer-grid {
display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 4px;
+ margin-bottom: 12px;
}
.layer-view {
aspect-ratio: 1;
@@ -186,6 +203,18 @@
flex-direction: column;
overflow: hidden;
}
+ .layer-zoom {
+ background: #1a1a1a;
+ border: 1px solid #404040;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+ .layer-zoom canvas {
+ width: 100%;
+ height: 100%;
+ image-rendering: pixelated;
+ }
.layer-view-label {
background: #2a2a2a;
padding: 4px;
@@ -252,9 +281,27 @@
<span id="viewMode">CNN Output</span>
</div>
</div>
- <div class="drop-zone" id="weightsDrop">Drop .bin Weights</div>
</div>
<div class="content">
+ <div class="left-sidebar">
+ <input type="file" id="weightsFile" accept=".bin" style="display: none;">
+ <div class="drop-zone" id="weightsDrop" onclick="document.getElementById('weightsFile').click()">
+ Drop .bin Weights or Click to Browse
+ </div>
+ <div class="panel" id="weightsInfoPanel">
+ <div class="panel-header">Weights Info</div>
+ <div class="panel-content" id="weightsInfo">
+ <p style="color: #808080; text-align: center;">No weights loaded</p>
+ </div>
+ </div>
+ <div class="panel" id="weightsVizPanel" style="display: none;">
+ <div class="panel-header">Weights Visualization</div>
+ <div class="panel-content" id="weightsViz">
+ <div class="layer-buttons" id="weightsLayerButtons"></div>
+ <canvas id="weightsCanvas" style="width: 100%; image-rendering: pixelated; border: 1px solid #404040;"></canvas>
+ </div>
+ </div>
+ </div>
<div class="main" id="mainDrop">
<canvas id="canvas"></canvas>
</div>
@@ -265,14 +312,6 @@
<p style="color: #808080; text-align: center;">Load image + weights</p>
</div>
</div>
- <div class="panel" id="weightsInfoPanel">
- <div class="panel-header" style="cursor: pointer; user-select: none;" onclick="tester.toggleWeightsInfo()">
- Weights Info <span id="weightsInfoToggle">▼</span>
- </div>
- <div class="panel-content" id="weightsInfo">
- <p style="color: #808080; text-align: center;">No weights loaded</p>
- </div>
- </div>
</div>
</div>
<div class="footer">
@@ -537,6 +576,8 @@ class CNNTester {
this.viewMode = 0;
this.blendAmount = 1.0;
this.depth = 1.0;
+ this.currentLayerIdx = null;
+ this.currentChannelOffset = null;
this.init();
}
@@ -718,6 +759,20 @@ class CNNTester {
`;
panel.innerHTML = html;
+
+ // Show weights visualization panel and create layer buttons
+ const weightsVizPanel = document.getElementById('weightsVizPanel');
+ weightsVizPanel.style.display = 'block';
+
+ const weightsLayerButtons = document.getElementById('weightsLayerButtons');
+ let buttonsHtml = '';
+ for (let i = 0; i < layers.length; i++) {
+ buttonsHtml += `<button onclick="tester.visualizeWeights(${i})" id="weightsBtn${i}">Layer ${i}</button>`;
+ }
+ weightsLayerButtons.innerHTML = buttonsHtml;
+
+ // Auto-select first layer
+ this.visualizeWeights(0);
}
displayOriginal() {
@@ -1019,20 +1074,10 @@ class CNNTester {
return;
}
- let html = `
- <div style="display: flex; gap: 8px; margin-bottom: 12px; border-bottom: 1px solid #404040; padding-bottom: 8px;">
- <button onclick="tester.setVizMode('activations')" id="vizModeActivations" style="flex:1; padding: 6px; background: #4a9eff; border: 1px solid #4a9eff; color: #1a1a1a; cursor: pointer; font-family: 'Courier New'; font-size: 10px;">Activations</button>
- <button onclick="tester.setVizMode('weights')" id="vizModeWeights" style="flex:1; padding: 6px; background: #1a1a1a; border: 1px solid #404040; color: #e0e0e0; cursor: pointer; font-family: 'Courier New'; font-size: 10px;">Weights</button>
- </div>
- `;
-
- html += `<div style="font-size: 9px; color: #808080; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #404040;">Static features (8D, split 0-3/4-7) + ${this.weights.layers.length} CNN layers (Layer 0-${this.weights.layers.length - 1}). All CNN layers: 12D→4D.</div>`;
-
- html += '<div class="layer-buttons">';
+ let html = '<div class="layer-buttons">';
// Static features: two buttons for 8D (0-3 and 4-7)
- const staticDisabled = (this.vizMode === 'weights') ? ' disabled' : '';
- html += `<button onclick="tester.visualizeLayer(0, 0)" id="layerBtn0_0"${staticDisabled}>Static 0-3</button>`;
- html += `<button onclick="tester.visualizeLayer(0, 4)" id="layerBtn0_4"${staticDisabled}>Static 4-7</button>`;
+ html += `<button onclick="tester.visualizeLayer(0, 0)" id="layerBtn0_0">Static 0-3</button>`;
+ html += `<button onclick="tester.visualizeLayer(0, 4)" id="layerBtn0_4">Static 4-7</button>`;
// CNN layers: Layer 0, Layer 1, Layer 2, ...
for (let i = 1; i < this.layerOutputs.length; i++) {
@@ -1044,50 +1089,20 @@ class CNNTester {
html += '</div>';
html += '<div class="layer-grid" id="layerGrid"></div>';
+ html += '<div class="layer-zoom"><div class="layer-view-label">Zoom x4</div><canvas id="zoomCanvas"></canvas></div>';
panel.innerHTML = html;
this.log(`Layer visualization ready: ${this.layerOutputs.length} layers`);
- this.vizMode = 'activations';
// Create initial canvases
this.recreateCanvases();
- // Auto-select static features (channels 0-3)
- this.visualizeLayer(0, 0);
- }
-
- setVizMode(mode) {
- this.vizMode = mode;
- document.getElementById('vizModeActivations').style.background = mode === 'activations' ? '#4a9eff' : '#1a1a1a';
- document.getElementById('vizModeActivations').style.borderColor = mode === 'activations' ? '#4a9eff' : '#404040';
- document.getElementById('vizModeActivations').style.color = mode === 'activations' ? '#1a1a1a' : '#e0e0e0';
- document.getElementById('vizModeWeights').style.background = mode === 'weights' ? '#4a9eff' : '#1a1a1a';
- document.getElementById('vizModeWeights').style.borderColor = mode === 'weights' ? '#4a9eff' : '#404040';
- document.getElementById('vizModeWeights').style.color = mode === 'weights' ? '#1a1a1a' : '#e0e0e0';
-
- // Update Static button states
- const staticBtn0 = document.getElementById('layerBtn0_0');
- const staticBtn4 = document.getElementById('layerBtn0_4');
- if (staticBtn0) staticBtn0.disabled = (mode === 'weights');
- if (staticBtn4) staticBtn4.disabled = (mode === 'weights');
-
- // Clear and recreate canvas grid
- this.recreateCanvases();
-
- // Re-visualize current layer (switch to Layer 0 if Static was active in weights mode)
- const activeBtn = document.querySelector('.layer-buttons button.active');
- if (activeBtn) {
- const isStatic = activeBtn.id.startsWith('layerBtn0_');
- if (isStatic && mode === 'weights') {
- this.visualizeLayer(1); // Switch to Layer 0 (first CNN layer)
- } else if (!isStatic) {
- const layerIdx = parseInt(activeBtn.id.replace('layerBtn', ''));
- this.visualizeLayer(layerIdx);
- } else {
- const channelOffset = parseInt(activeBtn.id.split('_')[1]);
- this.visualizeLayer(0, channelOffset);
- }
+ // Restore previous selection or default to static features (channels 0-3)
+ if (this.currentLayerIdx !== null) {
+ this.visualizeLayer(this.currentLayerIdx, this.currentChannelOffset || 0);
+ } else {
+ this.visualizeLayer(0, 0);
}
}
@@ -1120,6 +1135,10 @@ class CNNTester {
return;
}
+ // Store current selection
+ this.currentLayerIdx = layerIdx;
+ this.currentChannelOffset = channelOffset;
+
// Update button states
document.querySelectorAll('.layer-buttons button').forEach(btn => btn.classList.remove('active'));
if (layerIdx === 0) {
@@ -1133,30 +1152,6 @@ class CNNTester {
}
const layerName = layerIdx === 0 ? `Static Features (${channelOffset}-${channelOffset + 3})` : `Layer ${layerIdx - 1}`;
-
- // Check mode
- if (this.vizMode === 'weights' && layerIdx > 0) {
- // Map visualization index to weights.layers[] array index:
- // layerIdx=1 → Layer 0 → weights.layers[0]
- // layerIdx=2 → Layer 1 → weights.layers[1], etc.
- this.visualizeWeights(layerIdx - 1);
- return;
- }
-
- if (this.vizMode === 'weights' && layerIdx === 0) {
- this.log('Static features have no weights', 'error');
- // Clear canvases
- for (let c = 0; c < 4; c++) {
- const canvas = document.getElementById(`layerCanvas${c}`);
- if (canvas) {
- const ctx = canvas.getContext('2d');
- ctx.fillStyle = '#1a1a1a';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- }
- }
- return;
- }
-
const layerTex = this.layerOutputs[layerIdx];
const { width, height } = this.image;
@@ -1261,64 +1256,191 @@ class CNNTester {
// Wait for all renders to complete
await this.device.queue.onSubmittedWorkDone();
this.log(`Rendered 4 channels for ${layerName}`);
+
+ // Set up mouse tracking for zoom view
+ this.setupZoomTracking(layerTex, channelOffset);
+ }
+
+ setupZoomTracking(layerTex, channelOffset) {
+ const zoomCanvas = document.getElementById('zoomCanvas');
+ if (!zoomCanvas) return;
+
+ const { width, height } = this.image;
+ const zoomSize = 32; // Show 32x32 area
+ zoomCanvas.width = zoomSize;
+ zoomCanvas.height = zoomSize;
+
+ // Add mousemove handlers to all layer canvases
+ for (let c = 0; c < 4; c++) {
+ const canvas = document.getElementById(`layerCanvas${c}`);
+ if (!canvas) continue;
+
+ const updateZoom = (e) => {
+ const rect = canvas.getBoundingClientRect();
+ const x = Math.floor((e.clientX - rect.left) / rect.width * width);
+ const y = Math.floor((e.clientY - rect.top) / rect.height * height);
+ this.renderZoom(layerTex, channelOffset, x, y, zoomSize);
+ };
+
+ canvas.onmousemove = updateZoom;
+ canvas.onmouseenter = updateZoom;
+ }
+ }
+
+ async renderZoom(layerTex, channelOffset, centerX, centerY, zoomSize) {
+ const zoomCanvas = document.getElementById('zoomCanvas');
+ if (!zoomCanvas || !this.device) return;
+
+ const ctx = zoomCanvas.getContext('webgpu');
+ if (!ctx) return;
+
+ try {
+ ctx.configure({ device: this.device, format: this.format });
+ } catch (e) {
+ return;
+ }
+
+ const halfSize = Math.floor(zoomSize / 2);
+ const { width, height } = this.image;
+
+ // Create shader for zoomed view (samples 4 channels and displays as 2x2 grid)
+ const zoomShader = `
+ @group(0) @binding(0) var layer_tex: texture_2d<u32>;
+ @group(0) @binding(1) var<uniform> params: vec4<f32>; // centerX, centerY, channelOffset, scale
+
+ @vertex
+ fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, 1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0)
+ );
+ return vec4<f32>(pos[idx], 0.0, 1.0);
+ }
+
+ @fragment
+ fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
+ let dims = textureDimensions(layer_tex);
+ let centerX = i32(params.x);
+ let centerY = i32(params.y);
+ let channelOffset = u32(params.z);
+ let scale = params.w;
+
+ // Map output pixel to source pixel
+ let halfSize = 16;
+ let localX = i32(pos.x) - halfSize;
+ let localY = i32(pos.y) - halfSize;
+ let srcX = clamp(centerX + localX, 0, i32(dims.x) - 1);
+ let srcY = clamp(centerY + localY, 0, i32(dims.y) - 1);
+
+ let coord = vec2<i32>(srcX, srcY);
+ let packed = textureLoad(layer_tex, coord, 0);
+ let v0 = unpack2x16float(packed.x);
+ let v1 = unpack2x16float(packed.y);
+ let v2 = unpack2x16float(packed.z);
+ let v3 = unpack2x16float(packed.w);
+
+ var channels: array<f32, 8>;
+ channels[0] = v0.x;
+ channels[1] = v0.y;
+ channels[2] = v1.x;
+ channels[3] = v1.y;
+ channels[4] = v2.x;
+ channels[5] = v2.y;
+ channels[6] = v3.x;
+ channels[7] = v3.y;
+
+ // Determine which quadrant (channel) to show
+ let quadX = i32(pos.x) / 16;
+ let quadY = i32(pos.y) / 16;
+ let channelIdx = min(channelOffset + u32(quadY * 2 + quadX), 7u);
+
+ let val = clamp(channels[channelIdx] * scale, 0.0, 1.0);
+ return vec4<f32>(val, val, val, 1.0);
+ }
+ `;
+
+ if (!this.zoomPipeline) {
+ this.zoomPipeline = this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({ code: zoomShader }),
+ entryPoint: 'vs_main'
+ },
+ fragment: {
+ module: this.device.createShaderModule({ code: zoomShader }),
+ entryPoint: 'fs_main',
+ targets: [{ format: this.format }]
+ }
+ });
+ }
+
+ const vizScale = channelOffset === 0 ? 1.0 : 0.5;
+ const paramsBuffer = this.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
+ });
+ const paramsData = new Float32Array([centerX, centerY, channelOffset, vizScale]);
+ this.device.queue.writeBuffer(paramsBuffer, 0, paramsData);
+
+ const bindGroup = this.device.createBindGroup({
+ layout: this.zoomPipeline.getBindGroupLayout(0),
+ entries: [
+ { binding: 0, resource: layerTex.createView() },
+ { binding: 1, resource: { buffer: paramsBuffer } }
+ ]
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [{
+ view: ctx.getCurrentTexture().createView(),
+ loadOp: 'clear',
+ storeOp: 'store'
+ }]
+ });
+
+ renderPass.setPipeline(this.zoomPipeline);
+ renderPass.setBindGroup(0, bindGroup);
+ renderPass.draw(6);
+ renderPass.end();
+
+ this.device.queue.submit([encoder.finish()]);
}
visualizeWeights(cnnLayerIdx) {
- // cnnLayerIdx is index into weights.layers[] array (0-indexed)
- // Display as "Layer N" matching codebase convention
- // Example: weights.layers[0] = Layer 0, weights.layers[1] = Layer 1
const layer = this.weights.layers[cnnLayerIdx];
if (!layer) {
this.log(`Layer ${cnnLayerIdx} not found`, 'error');
return;
}
- const { kernelSize, inChannels, outChannels, weightOffset, min, max } = layer;
- this.log(`Visualizing Layer ${cnnLayerIdx} weights: ${inChannels}→${outChannels}, ${kernelSize}×${kernelSize}, offset=${weightOffset}`);
- this.log(`Weight range: [${min.toFixed(3)}, ${max.toFixed(3)}]`);
-
- // Update channel labels to show output channels
- const channelLabels = [`Out 0`, `Out 1`, `Out 2`, `Out 3`];
- for (let c = 0; c < 4; c++) {
- const label = document.getElementById(`channelLabel${c}`);
- if (label) label.textContent = c < outChannels ? channelLabels[c] : '-';
- }
+ // Update button states
+ document.querySelectorAll('#weightsLayerButtons button').forEach(btn => btn.classList.remove('active'));
+ const btn = document.getElementById(`weightsBtn${cnnLayerIdx}`);
+ if (btn) btn.classList.add('active');
- // Render each output channel's weights
- for (let outCh = 0; outCh < 4; outCh++) {
- const canvas = document.getElementById(`layerCanvas${outCh}`);
- if (!canvas) {
- this.log(`Canvas ${outCh} not found`, 'error');
- continue;
- }
+ const { kernelSize, inChannels, outChannels, weightOffset, min, max } = layer;
+ this.log(`Visualizing Layer ${cnnLayerIdx} weights: ${inChannels}→${outChannels}, ${kernelSize}×${kernelSize}`);
- // Get 2D context (canvases recreated by recreateCanvases())
- const ctx = canvas.getContext('2d', { willReadFrequently: false });
- if (!ctx) {
- this.log(`Failed to get 2D context for canvas ${outCh}`, 'error');
- continue;
- }
+ const canvas = document.getElementById('weightsCanvas');
+ const ctx = canvas.getContext('2d', { willReadFrequently: false });
- const cellSize = 20; // Pixels per weight
- const padding = 2;
- const gridWidth = inChannels * (kernelSize * cellSize + padding);
- const gridHeight = kernelSize * cellSize;
+ // 1 pixel per weight, show all input channels horizontally
+ const width = inChannels * kernelSize;
+ const height = outChannels * kernelSize;
- canvas.width = gridWidth;
- canvas.height = gridHeight;
- this.log(`Canvas ${outCh}: ${gridWidth}×${gridHeight}`);
+ canvas.width = width;
+ canvas.height = height;
- ctx.fillStyle = '#1a1a1a';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = '#1a1a1a';
+ ctx.fillRect(0, 0, width, height);
- if (outCh >= outChannels) {
- this.log(`Channel ${outCh} >= outChannels ${outChannels}, skipping`);
- continue;
- }
+ // Stack output channels vertically
+ for (let outCh = 0; outCh < outChannels; outCh++) {
+ const yOffset = outCh * kernelSize;
- // Draw kernels for each input channel
for (let inCh = 0; inCh < inChannels; inCh++) {
- const xOffset = inCh * (kernelSize * cellSize + padding);
+ const xOffset = inCh * kernelSize;
for (let ky = 0; ky < kernelSize; ky++) {
for (let kx = 0; kx < kernelSize; kx++) {
@@ -1329,23 +1451,17 @@ class CNNTester {
spatialIdx;
const weight = this.getWeightValue(wIdx);
- const normalized = (weight - min) / (max - min); // [0, 1]
+ const normalized = (weight - min) / (max - min);
const intensity = Math.floor(normalized * 255);
ctx.fillStyle = `rgb(${intensity}, ${intensity}, ${intensity})`;
- ctx.fillRect(
- xOffset + kx * cellSize,
- ky * cellSize,
- cellSize - 1,
- cellSize - 1
- );
+ ctx.fillRect(xOffset + kx, yOffset + ky, 1, 1);
}
}
}
- this.log(`Rendered output channel ${outCh}`);
}
- this.log(`Weight visualization complete`);
+ this.log(`Rendered ${outChannels} output channels (${width}×${height}px)`);
}
getWeightValue(idx) {
@@ -1424,6 +1540,17 @@ mainArea.addEventListener('drop', e => {
// Weights drop zone
setupDropZone('weightsDrop', f => tester.loadWeights(f));
+// Weights file input
+document.getElementById('weightsFile').addEventListener('change', e => {
+ const file = e.target.files[0];
+ if (file) {
+ tester.loadWeights(file).catch(err => {
+ tester.setStatus(err.message, true);
+ tester.log(err.message, 'error');
+ });
+ }
+});
+
document.getElementById('blend').addEventListener('input', e => {
tester.blendAmount = parseFloat(e.target.value);
document.getElementById('blendValue').textContent = e.target.value;