summaryrefslogtreecommitdiff
path: root/tools/cnn_v2_test/index.html
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-13 20:32:18 +0100
committerskal <pascal.massimino@gmail.com>2026-02-13 20:32:18 +0100
commit23d286e2fda7fe8380f92b13b6840363cf2bfb5a (patch)
treee8bf62d8bfdd8c40089081c227590aa52e7c8834 /tools/cnn_v2_test/index.html
parenta370fcc440d99f728dfc4832e94c758e0b92bc63 (diff)
CNN v2 web tool: Enhance UI visibility and layer preview interaction
Improve drop zone visibility with larger borders, bold blue text, and brighter hover states for better user guidance. Replace hover-based zoom with click-to-preview: clicking any of the 4 small channel views displays it large below. Active channel highlighted with white border for clear visual feedback. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'tools/cnn_v2_test/index.html')
-rw-r--r--tools/cnn_v2_test/index.html178
1 files changed, 62 insertions, 116 deletions
diff --git a/tools/cnn_v2_test/index.html b/tools/cnn_v2_test/index.html
index cab20ea..6d3f223 100644
--- a/tools/cnn_v2_test/index.html
+++ b/tools/cnn_v2_test/index.html
@@ -68,14 +68,16 @@
input[type="range"] { width: 120px; }
input[type="number"] { width: 60px; background: #1a1a1a; color: #e0e0e0; border: 1px solid #404040; padding: 4px; }
.drop-zone {
- border: 2px dashed #404040;
- padding: 16px;
+ border: 3px dashed #606060;
+ padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
- font-size: 12px;
- background: #1a1a1a;
- border-radius: 4px;
+ font-size: 13px;
+ font-weight: bold;
+ background: #252525;
+ border-radius: 6px;
+ color: #4a9eff;
}
button {
background: #1a1a1a;
@@ -91,7 +93,7 @@
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:hover { border-color: #4a9eff; background: #2a3545; }
.drop-zone.active { border-color: #4a9eff; background: #1a2a3a; }
.drop-zone.error { border-color: #ff4a4a; background: #3a1a1a; }
.content {
@@ -240,18 +242,25 @@
flex-direction: column;
overflow: hidden;
}
- .layer-zoom {
+ .layer-preview {
background: #1a1a1a;
border: 1px solid #404040;
display: flex;
flex-direction: column;
overflow: hidden;
+ margin-top: 8px;
}
- .layer-zoom canvas {
+ .layer-preview canvas {
width: 100%;
height: 100%;
image-rendering: pixelated;
}
+ .layer-view.active {
+ border: 2px solid #ffffff;
+ }
+ .layer-view canvas {
+ cursor: pointer;
+ }
.layer-view-label {
background: #2a2a2a;
padding: 4px;
@@ -413,7 +422,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let rgba = textureSampleLevel(input_tex, input_sampler, uv, f32(mip_level));
let d = textureLoad(depth_tex, coord, 0).r;
let uv_x = f32(coord.x) / f32(dims.x);
- let uv_y = 1.0 - (f32(coord.y) / f32(dims.y));
+ let uv_y = f32(coord.y) / f32(dims.y);
let sin20_y = sin(20.0 * uv_y);
let packed = vec4<u32>(
@@ -662,6 +671,7 @@ class CNNTester {
this.fps = 30;
this.isProcessing = false;
this.mipLevel = 0;
+ this.selectedChannel = 0;
this.init();
}
@@ -1360,7 +1370,7 @@ 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>';
+ html += '<div class="layer-preview"><div class="layer-view-label" id="previewLabel">Ch0</div><canvas id="previewCanvas"></canvas></div>';
panel.innerHTML = html;
this.log(`Layer visualization ready: ${this.layerOutputs.length} layers`);
@@ -1395,8 +1405,10 @@ class CNNTester {
<div class="layer-view-label" id="channelLabel${c}">Ch ${c}</div>
<canvas id="layerCanvas${c}"></canvas>
`;
+ div.onclick = () => this.selectChannel(c);
grid.appendChild(div);
}
+ this.selectedChannel = 0;
}
async visualizeLayer(layerIdx, channelOffset = 0) {
@@ -1527,42 +1539,37 @@ class CNNTester {
await this.device.queue.onSubmittedWorkDone();
this.log(`Rendered 4 channels for ${layerName}`);
- // Set up mouse tracking for zoom view
- this.setupZoomTracking(layerTex, channelOffset);
+ // Update active channel highlighting and preview
+ this.updateChannelSelection();
+ await this.renderChannelPreview();
}
- setupZoomTracking(layerTex, channelOffset) {
- const zoomCanvas = document.getElementById('zoomCanvas');
- if (!zoomCanvas) return;
-
- 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;
-
- // Add mousemove handlers to all layer canvases
- for (let c = 0; c < 4; c++) {
- const canvas = document.getElementById(`layerCanvas${c}`);
- if (!canvas) continue;
+ selectChannel(channelIdx) {
+ this.selectedChannel = channelIdx;
+ this.updateChannelSelection();
+ this.renderChannelPreview();
+ }
- 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);
- };
+ updateChannelSelection() {
+ const grid = document.getElementById('layerGrid');
+ if (!grid) return;
- canvas.onmousemove = updateZoom;
- canvas.onmouseenter = updateZoom;
- }
+ const views = grid.querySelectorAll('.layer-view');
+ views.forEach((view, idx) => {
+ view.classList.toggle('active', idx === this.selectedChannel);
+ });
}
- async renderZoom(layerTex, channelOffset, centerX, centerY, zoomSize) {
- const zoomCanvas = document.getElementById('zoomCanvas');
- if (!zoomCanvas || !this.device) return;
+ async renderChannelPreview() {
+ const previewCanvas = document.getElementById('previewCanvas');
+ const previewLabel = document.getElementById('previewLabel');
+ if (!previewCanvas || !this.device) return;
- const ctx = zoomCanvas.getContext('webgpu');
+ const { width, height } = this.getDimensions();
+ previewCanvas.width = width;
+ previewCanvas.height = height;
+
+ const ctx = previewCanvas.getContext('webgpu');
if (!ctx) return;
try {
@@ -1571,91 +1578,30 @@ class CNNTester {
return;
}
- const halfSize = Math.floor(zoomSize / 2);
- 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 = `
- @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);
+ // Update label
+ const channelLabel = document.getElementById(`channelLabel${this.selectedChannel}`);
+ if (channelLabel && previewLabel) {
+ previewLabel.textContent = channelLabel.textContent;
+ }
- let val = clamp(channels[channelIdx] * scale, 0.0, 1.0);
- return vec4<f32>(val, val, val, 1.0);
- }
- `;
+ // Render selected channel
+ const layerIdx = this.currentLayerIdx;
+ const channelOffset = this.currentChannelOffset;
+ const layerTex = this.layerOutputs[layerIdx];
+ if (!layerTex) return;
- 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 = layerIdx === 0 ? 1.0 : 0.5;
+ const actualChannel = channelOffset + this.selectedChannel;
- const vizScale = channelOffset === 0 ? 1.0 : 0.5;
const paramsBuffer = this.device.createBuffer({
- size: 16,
+ size: 8,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
- const paramsData = new Float32Array([centerX, centerY, channelOffset, vizScale]);
+ const paramsData = new Float32Array([actualChannel, vizScale]);
this.device.queue.writeBuffer(paramsBuffer, 0, paramsData);
const bindGroup = this.device.createBindGroup({
- layout: this.zoomPipeline.getBindGroupLayout(0),
+ layout: this.layerVizPipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: layerTex.createView() },
{ binding: 1, resource: { buffer: paramsBuffer } }
@@ -1671,7 +1617,7 @@ class CNNTester {
}]
});
- renderPass.setPipeline(this.zoomPipeline);
+ renderPass.setPipeline(this.layerVizPipeline);
renderPass.setBindGroup(0, bindGroup);
renderPass.draw(6);
renderPass.end();