diff options
Diffstat (limited to 'tools/cnn_v2_test/index.html')
| -rw-r--r-- | tools/cnn_v2_test/index.html | 421 |
1 files changed, 274 insertions, 147 deletions
diff --git a/tools/cnn_v2_test/index.html b/tools/cnn_v2_test/index.html index cab20ea..1dd2e78 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; @@ -314,6 +323,7 @@ <input type="range" id="depth" min="0" max="1" step="0.01" value="1.0"> <span id="depthValue">1.0</span> </div> + <button id="savePngBtn">Save PNG</button> </div> </div> <video id="videoSource" muted loop></video> @@ -378,7 +388,8 @@ // ============================================================================ // Default pre-trained weights (base64-encoded binary format) -const DEFAULT_WEIGHTS_B64 = 'Q05OMgEAAAADAAAAEAUAAAMAAAAMAAAABAAAAAAAAACwAQAAAwAAAAwAAAAEAAAAsAEAALABAAADAAAADAAAAAQAAABgAwAAsAEAAFQ3KjroN2yxSjQHsFi5HyjQtSo62zsJOq00bTXTqxC2zCnntyo44zoWOuwpajKup861uis/tj4kqyl6GkOp6CjTLbukERc2K0M4ijm8NpysMjQysgW4kLAHuJo5hTvGOtI0IDTio3C0yTDLtpM2ajq1ObctXjNlHEu2vqolt/ut2KMzJxWfSKf1p4et1C1eGfgt0atJMCihIKTyLZekxxRkKBas7ygWKpqoFS8LqHkruBhVqBwsELComn0yviiZMkUuVamVMKww2i/nMN0p/C1YKGmgBi8XKVilMCrApWGtaiJpI5OtJalcrZSkR5ceKvWrwSfxroSulyRcpXymsxTUIEcmXSx+rdap4KxIrCisTivhF1+tGi6VraYry6mdLNun26kKLHYod6o9r3AsyR7TpMytuykIpbStQ6ZoIZSsJqw/rM8o5K0eIbOgYCkXqAQpHysapWOsKCZcKPwtz6OPrRAuAapTq9AnBib9rPetVK4Lr6WstayeHqEo+Sk0LBskyqc6FueiOKn/JTknFqvapjcsJiy6K0+scqHVoyWnhKZ1rtWmkiiJq18sqaU3rpUyxS3pt2E4vDEIt7gj6ipZtSU5+zWjtKQ8uDaLqEguii/Qtck1SyRMtOs4UTA6ss6zSq8TtfSeFSwlJaOspisuowSuhCbGK3QxQaslt+83HTT4tAWoNKiTtSs4XTZos1g86zcZrLopxDBMtJY09iletg46dC13qy+vdax4tjIl8ynULASrUyG2rGqsGRklq5qpPasootsi3So3LJimlqpVJC8x6imJLmSoCyzPLkOrDCQtokovPaqMKmcsYKwWpS0gcSnzL2YseqXkJqorki1RMawtGC+MMEE0MiyfsCAzGTNEsgAz9qkJtBguArUutnqugieqtBk1FJ1Yszc1RK8vtqgpNidgtfYzpy5CsBwoHKopJGaZdKxwJFaqyJp/q4kzJSwrs78xeTMXs6AziYror8YwTrN8tcukZy0htKM1nC2qsOc00rDYtBwtXa3GtN00qqo5sf8m7aoBLnGqBaZIrbMekSa7LEam56g6rPaojJ1VIfoqbiRjIwkutSxCKGsr559+L4aw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'; +// Version 2: 4 layers (3×3, 5×5, 3×3, 3×3), 2496 f16 weights, mip_level=2 +const DEFAULT_WEIGHTS_B64 = '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'; // Reusable fullscreen quad vertex shader (2 triangles covering NDC) const FULLSCREEN_QUAD_VS = ` @@ -398,7 +409,7 @@ fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> { // Static features: 7D parametric features (RGBD + UV + sin(10*uv_x) + bias) const STATIC_SHADER = ` @group(0) @binding(0) var input_tex: texture_2d<f32>; -@group(0) @binding(1) var input_sampler: sampler; +@group(0) @binding(1) var point_sampler: sampler; @group(0) @binding(2) var depth_tex: texture_2d<f32>; @group(0) @binding(3) var output_tex: texture_storage_2d<rgba32uint, write>; @group(0) @binding(4) var<uniform> mip_level: u32; @@ -409,18 +420,25 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) { let dims = textureDimensions(input_tex); if (coord.x >= i32(dims.x) || coord.y >= i32(dims.y)) { return; } + // Use normalized UV coords with point sampler (no filtering) let uv = (vec2<f32>(coord) + 0.5) / vec2<f32>(dims); - let rgba = textureSampleLevel(input_tex, input_sampler, uv, f32(mip_level)); - let d = textureLoad(depth_tex, coord, 0).r; + let rgba = textureSampleLevel(input_tex, point_sampler, uv, f32(mip_level)); + + let p0 = rgba.r; + let p1 = rgba.g; + let p2 = rgba.b; + let p3 = 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 bias = 1.0; let packed = vec4<u32>( - pack2x16float(vec2<f32>(rgba.r, rgba.g)), - pack2x16float(vec2<f32>(rgba.b, d)), + pack2x16float(vec2<f32>(p0, p1)), + pack2x16float(vec2<f32>(p2, p3)), pack2x16float(vec2<f32>(uv_x, uv_y)), - pack2x16float(vec2<f32>(sin20_y, 1.0)) + pack2x16float(vec2<f32>(sin20_y, bias)) ); textureStore(output_tex, coord, packed); }`; @@ -662,6 +680,7 @@ class CNNTester { this.fps = 30; this.isProcessing = false; this.mipLevel = 0; + this.selectedChannel = 0; this.init(); } @@ -780,7 +799,7 @@ class CNNTester { } this.log(` Weight buffer: ${weights.length} u32 (${nonZero} non-zero)`); - return { layers, weights, mipLevel, fileSize: buffer.byteLength }; + return { version, layers, weights, mipLevel, fileSize: buffer.byteLength }; } unpackF16(packed) { @@ -914,11 +933,12 @@ class CNNTester { updateWeightsPanel() { const panel = document.getElementById('weightsInfo'); - const { layers, mipLevel, fileSize } = this.weights; + const { version, layers, mipLevel, fileSize } = this.weights; let html = ` <div style="margin-bottom: 12px;"> <div><strong>File Size:</strong> ${(fileSize / 1024).toFixed(2)} KB</div> + <div><strong>Version:</strong> ${version}</div> <div><strong>CNN Layers:</strong> ${layers.length}</div> <div><strong>Mip Level:</strong> ${mipLevel} (p0-p3 features)</div> <div style="font-size: 9px; color: #808080; margin-top: 4px;">Static features (input) + ${layers.length} conv layers</div> @@ -1118,19 +1138,6 @@ class CNNTester { // Generate mipmaps this.generateMipmaps(this.inputTexture, width, height); - const depthTex = this.device.createTexture({ - size: [width, height], - format: 'r32float', - usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST - }); - const depthData = new Float32Array(width * height).fill(this.depth); - this.device.queue.writeTexture( - { texture: depthTex }, - depthData, - { bytesPerRow: width * 4 }, - [width, height] - ); - const staticTex = this.device.createTexture({ size: [width, height], format: 'rgba32uint', @@ -1196,20 +1203,49 @@ class CNNTester { }); this.device.queue.writeBuffer(mipLevelBuffer, 0, new Uint32Array([this.mipLevel])); - if (!this.linearSampler) { - this.linearSampler = this.device.createSampler({ - magFilter: 'linear', - minFilter: 'linear', - mipmapFilter: 'linear' + if (!this.pointSampler) { + this.pointSampler = this.device.createSampler({ + magFilter: 'nearest', + minFilter: 'nearest', + mipmapFilter: 'nearest' }); } + // Extract depth from alpha channel (or 1.0 if no alpha) + const depthTex = this.device.createTexture({ + size: [width, height, 1], + format: 'r32float', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST + }); + + // Read image data to extract alpha channel + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = width; + tempCanvas.height = height; + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.drawImage(source, 0, 0, width, height); + const imageData = tempCtx.getImageData(0, 0, width, height); + const pixels = imageData.data; + + // Extract alpha channel (RGBA format: every 4th byte) + const depthData = new Float32Array(width * height); + for (let i = 0; i < width * height; i++) { + depthData[i] = pixels[i * 4 + 3] / 255.0; // Alpha channel [0, 255] → [0, 1] + } + + this.device.queue.writeTexture( + { texture: depthTex }, + depthData, + { bytesPerRow: width * 4 }, + [width, height, 1] + ); + const staticBG = this.device.createBindGroup({ layout: staticPipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: this.inputTexture.createView() }, - { binding: 1, resource: this.linearSampler }, - { binding: 2, resource: depthTex.createView() }, + { binding: 1, resource: this.pointSampler }, + { binding: 2, resource: depthTex.createView() }, // Depth from alpha (matches training) { binding: 3, resource: staticTex.createView() }, { binding: 4, resource: { buffer: mipLevelBuffer } } ] @@ -1237,7 +1273,9 @@ class CNNTester { const isOutput = i === this.weights.layers.length - 1; // Calculate absolute weight offset in f16 units (add header offset) - const headerOffsetU32 = 4 + this.weights.layers.length * 5; // Header + layer info in u32 + // Version 1: 4 u32 header, Version 2: 5 u32 header + const headerSizeU32 = (this.weights.version === 1) ? 4 : 5; + const headerOffsetU32 = headerSizeU32 + this.weights.layers.length * 5; // Header + layer info in u32 const absoluteWeightOffset = headerOffsetU32 * 2 + layer.weightOffset; // Convert to f16 units const paramsData = new Uint32Array(7); @@ -1357,10 +1395,11 @@ class CNNTester { const label = `Layer ${i - 1}`; html += `<button onclick="tester.visualizeLayer(${i})" id="layerBtn${i}">${label}</button>`; } + html += `<button onclick="tester.saveCompositedLayer()" style="margin-left: 20px; background: #28a745;">Save Composited</button>`; 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 +1434,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) { @@ -1486,7 +1527,7 @@ class CNNTester { continue; } - const vizScale = layerIdx === 0 ? 1.0 : 0.5; // Static: 1.0, CNN layers: 0.5 (4 channels [0,1]) + const vizScale = 1.0; // Always 1.0, shader clamps to [0,1] const paramsBuffer = this.device.createBuffer({ size: 8, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST @@ -1527,42 +1568,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 { width, height } = this.getDimensions(); + previewCanvas.width = width; + previewCanvas.height = height; - const ctx = zoomCanvas.getContext('webgpu'); + const ctx = previewCanvas.getContext('webgpu'); if (!ctx) return; try { @@ -1571,91 +1607,31 @@ 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 }] - } - }); - } + // Always 1.0, shader clamps to [0,1] - show exact layer values + const vizScale = 1.0; + 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 +1647,7 @@ class CNNTester { }] }); - renderPass.setPipeline(this.zoomPipeline); + renderPass.setPipeline(this.layerVizPipeline); renderPass.setBindGroup(0, bindGroup); renderPass.draw(6); renderPass.end(); @@ -1770,6 +1746,156 @@ class CNNTester { this.device.queue.submit([encoder.finish()]); } + + async savePNG() { + if (!this.image && !this.isVideo) { + this.log('No image loaded', 'error'); + return; + } + + if (!this.resultTexture) { + this.log('No result to save', 'error'); + return; + } + + try { + const { width, height } = this.getDimensions(); + + // GPU readback from result texture + const bytesPerRow = width * 16; // 4×u32 per pixel + const paddedBytesPerRow = Math.ceil(bytesPerRow / 256) * 256; + const bufferSize = paddedBytesPerRow * height; + + const stagingBuffer = this.device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ + }); + + const encoder = this.device.createCommandEncoder(); + encoder.copyTextureToBuffer( + { texture: this.resultTexture }, + { buffer: stagingBuffer, bytesPerRow: paddedBytesPerRow, rowsPerImage: height }, + { width, height, depthOrArrayLayers: 1 } + ); + this.device.queue.submit([encoder.finish()]); + + await stagingBuffer.mapAsync(GPUMapMode.READ); + const mapped = new Uint8Array(stagingBuffer.getMappedRange()); + + // Unpack f16 to RGBA8 + const pixels = new Uint8Array(width * height * 4); + for (let y = 0; y < height; y++) { + const rowOffset = y * paddedBytesPerRow; + for (let x = 0; x < width; x++) { + const pixelOffset = rowOffset + x * 16; + const data = new Uint32Array(mapped.buffer, mapped.byteOffset + pixelOffset, 4); + + // Unpack f16 (first 4 channels only) + const unpack = (u32, idx) => { + const h = (idx === 0) ? (u32 & 0xFFFF) : ((u32 >> 16) & 0xFFFF); + const sign = (h >> 15) & 1; + const exp = (h >> 10) & 0x1F; + const frac = h & 0x3FF; + if (exp === 0) return 0; + if (exp === 31) return sign ? 0 : 255; + const e = exp - 15; + const val = (1 + frac / 1024) * Math.pow(2, e); + return Math.max(0, Math.min(255, Math.round(val * 255))); + }; + + const outIdx = (y * width + x) * 4; + pixels[outIdx + 0] = unpack(data[0], 0); // R + pixels[outIdx + 1] = unpack(data[0], 1); // G + pixels[outIdx + 2] = unpack(data[1], 0); // B + pixels[outIdx + 3] = 255; // A + } + } + + stagingBuffer.unmap(); + stagingBuffer.destroy(); + + // Create blob from pixels + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + const imageData = new ImageData(new Uint8ClampedArray(pixels), width, height); + ctx.putImageData(imageData, 0, 0); + + const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png')); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + const mode = ['cnn', 'original', 'diff'][this.viewMode]; + a.href = url; + a.download = `output_${width}x${height}_${mode}.png`; + a.click(); + URL.revokeObjectURL(url); + + this.log(`Saved PNG: ${a.download}`); + this.setStatus(`Saved: ${a.download}`); + } catch (err) { + this.log(`Failed to save PNG: ${err.message}`, 'error'); + this.setStatus(`Save failed: ${err.message}`, true); + } + } + + async saveCompositedLayer() { + if (!this.currentLayerIdx) { + this.log('No layer selected for compositing', 'error'); + return; + } + + try { + const canvases = []; + for (let i = 0; i < 4; i++) { + const canvas = document.getElementById(`layerCanvas${i}`); + if (!canvas) { + this.log(`Canvas layerCanvas${i} not found`, 'error'); + return; + } + canvases.push(canvas); + } + + const width = canvases[0].width; + const height = canvases[0].height; + const compositedWidth = width * 4; + + // Create composited canvas + const compositedCanvas = document.createElement('canvas'); + compositedCanvas.width = compositedWidth; + compositedCanvas.height = height; + const ctx = compositedCanvas.getContext('2d'); + + // Composite horizontally + for (let i = 0; i < 4; i++) { + ctx.drawImage(canvases[i], i * width, 0); + } + + // Convert to grayscale + const imageData = ctx.getImageData(0, 0, compositedWidth, height); + const pixels = imageData.data; + for (let i = 0; i < pixels.length; i += 4) { + const gray = 0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2]; + pixels[i] = pixels[i + 1] = pixels[i + 2] = gray; + } + ctx.putImageData(imageData, 0, 0); + + // Save as PNG + const blob = await new Promise(resolve => compositedCanvas.toBlob(resolve, 'image/png')); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `composited_layer${this.currentLayerIdx - 1}_${compositedWidth}x${height}.png`; + a.click(); + URL.revokeObjectURL(url); + + this.log(`Saved composited layer: ${a.download}`); + this.setStatus(`Saved: ${a.download}`); + } catch (err) { + this.log(`Failed to save composited layer: ${err.message}`, 'error'); + this.setStatus(`Compositing failed: ${err.message}`, true); + } + } } const tester = new CNNTester(); @@ -1869,6 +1995,7 @@ document.getElementById('mipLevel').addEventListener('change', e => { document.getElementById('playPauseBtn').addEventListener('click', () => tester.togglePlayPause()); document.getElementById('stepBackBtn').addEventListener('click', () => tester.stepFrame(-1)); document.getElementById('stepForwardBtn').addEventListener('click', () => tester.stepFrame(1)); +document.getElementById('savePngBtn').addEventListener('click', () => tester.savePNG()); document.addEventListener('keydown', e => { if (e.code === 'Space') { |
