Repository navigation
Quality pass: faster DCT, consistent numbers, standard SSIM, accessibility, two inspector features - #5
Merged
Conversation
The direct formula evaluated two std::cos calls per term, 4096 terms per block. Precompute the orthonormal basis once and apply the transform as two 1D passes (1024 multiply-adds per block). 768x512 4:2:0 in Chromium: process_image (DCT) 118 -> 33 ms, rd_encode (DCT) 99 -> 14 ms. Output matches the direct definition to 1e-9 (new TransformTest.MatchesDirectDefinition), plus an orientation test so a transposed implementation can't slip through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 390 px wide: - Viewer: the canvas header (Comparison/Original/Processed + Zoom) was a fixed 3-column grid with 90 px-min buttons and scrolled the page 115 px sideways. Below 600 px the toggle gets its own full-width row. - Inspectors: the sidebar was clamped to 200 px with its own scrollbar while an empty pane filled the screen; the 56 px topbar made the artifact mode chips wrap over the sidebar; Zoom was absolutely positioned over the view toggle. Below 900 px the page now scrolls as one column, the topbar grows, chips get a scrollable row, and Zoom flows inline. - Selecting a block from the navigator or a suggestion scrolls the pipeline into view on narrow screens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Coefficient histogram compared 8x8 Haar blocks, which the codec no longer uses, in linear 16-wide bins (nearly everything in bar one). It now comes from ImageCodec::coefficientStats: the codec's own DCT / Haar / CDF 9/7 coefficients of the luma channel, log2 bins, plus the share that quantizes to zero at the current quality (follows the slider). get_coeff_histogram is replaced by get_coeff_stats. - Block inspector codes DC as the difference from the previous block's quantized DC, as JPEG does (non-interleaved raster scan; subsampled chroma steps back in its own grid). "Est. Bits" is relabelled "JPEG Bits (est.)" and its tooltip explains why it differs from the viewer's whole-image entropy estimate. - Artifact heatmap legend shows real pixel levels for the current map and gain instead of the 0-255 map scale, matching the tooltip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Inspector 8x8 grids are labelled groups with one tab stop each (roving tabindex). Arrow keys / Home / End move between cells without triggering block navigation; focusing a cell does what hovering does (cross-grid highlight, tooltip or basis popover); Enter/Space does what a click does; the first Escape closes the popover, the next exits. - Every cell has an aria-label (position, value, meaning, action). - 18 stateful toggle buttons (view toggles, chips, presets, blink / heatmap / grid, zoom, suggested blocks) expose aria-pressed. - The image viewer's label names what is shown (original, compressed with transform and quality, or the artifact map and its meaning). - The inspector's shortcut panel documents the grid keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SSIM used a flat 8x8 window sampled every 4 pixels, so its numbers didn't match other tools (and multi-channel input indexed the buffer as if it had one channel). It now follows Wang et al. (2004): 11x11 Gaussian window, sigma 1.5, K1 = 0.01, K2 = 0.03, every pixel, mean over the region where the window fits; clamped borders for tiny images; mean over channels for multi-channel input. Verified against an independent NumPy implementation of the same definition (the ssim_index.m / scikit-image formulation) to 1e-9 on three image pairs. The blur pairs symmetric taps and only clamps near the edges: full encode with metrics is 56 ms on 768x512 (the RD sweep, which skips SSIM, is unaffected at 14 ms). Typical values move slightly down, e.g. Kodak #3 luma at Q50: 0.9435 -> 0.9359. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer, block inspector and artifact inspector each had their own quality slider + presets with different commit rules (two committed on release, the inspector re-encoded on every input event). They now share QualityControl.svelte, which keeps the existing element ids: - The committed value (appState.quality) is written on release, by presets, or by keyboard; the slider follows external changes. - While dragging, an optional onPreview runs at most once per frame. The viewer and artifact inspector use the new preview_image binding, which re-encodes and redraws without PSNR/SSIM (computed once on release); the block inspector commits live, now frame-throttled. - Unit tests mount the component (vitest resolves Svelte's browser build under jsdom). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A "what if?" bar in the decoding pipeline: - Keep first N (1-64) coefficients in zig-zag order, and/or switch on "Edit coefficients" and click (or Enter on) a coefficient in the decoder-side Quantized / Dequantized grids to drop or restore it. - Dropped cells are struck through; reconstruction, error, MSE / peak error, zero count, JPEG bit estimate, zig-zag and entropy panels all follow the edit, with a "Dropping k of n non-zero" summary and Reset. - Per-block toggles clear when you move to another block; keep-first-N persists, so arrowing through blocks shows each one's low-frequency approximation. inspection.ts now copies a block out of WASM once (inspectBlock) and renders it with edits applied (renderInspectedBlock, untracked so the block-change effect doesn't subscribe to edit state). Edited blocks are re-synthesised by reconstructBlock in JS; a real-WASM browser test checks it matches the codec's reconstruction when nothing is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In DCT mode the artifact map outlines the 8x8 block under the cursor
(the tooltip says which), and a click (outside zoom mode) opens it in
the Block Inspector. Back / Escape there returns to the artifact
inspector on the same map ("Back to Artifacts") instead of the viewer.
Wavelets have no blocks, so there the hint says to switch to DCT and
clicks do nothing.
- state: leaveCurrentMode() / openBlockInspector() replace the
duplicated Escape / Back logic in App and InspectorMode, with an
optional return target.
- ImageViewer: optional blockOutline prop (pointer cursor while set).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR mostly improves existing behaviour, plus two small features. Each item is its own commit.
Improvements
perf(core)): a separable, table-driven 8×8 DCT/IDCT. On a 768×512 image at 4:2:0,process_image(DCT) drops from 118 to 33 ms andrd_encodefrom 99 to 14 ms. Output matches the direct definition to 1e-9.fix(ui)): at 390 px wide the viewer scrolled 115 px sideways, and both inspectors were cramped into short scrolling panels with overlapping controls. Narrow screens now use a single scrolling column.fix):get_coeff_histogramis replaced byget_coeff_stats.feat(a11y)):aria-label.aria-pressed.fix(core)): 11×11 Gaussian window with σ = 1.5 (Wang et al. 2004), checked against an independent NumPy implementation to 1e-9. Values move down slightly; for example, Kodak Wavelet fixes (CDF 9/7, fair bit estimates), DCT edge fixes, and viewer UX #3 luma at Q50 goes from 0.9435 to 0.9359.feat(ui)):QualityControl.sveltereplaces three separate slider implementations. While dragging, the image updates live through the newpreview_imagebinding, which skips PSNR/SSIM; the metrics are computed on release.Features
feat(inspector)): keep the first N coefficients in zig-zag order, or turn on "Edit coefficients" and toggle individual ones. Reconstruction, error, statistics, bits, zig-zag and entropy panels all update. The JS reconstruction is verified against the codec's own in a real-WASM browser test.feat(artifacts)): in DCT mode, clicking a spot on the artifact map opens that 8×8 block in the block inspector. Back or Escape returns to the same map.Behaviour changes
Test plan
make test: 62/62. New: DCT vs. direct definition, orientation, coefficient stats, and SSIM vs. the reference.npm run check: 0 errors, 0 warnings.npm run buildsucceeds.npm run test: 221. New: grid keyboard navigation,QualityControl, coefficient-edit state,reconstructBlock, DC prediction and scan order,getCoeffStats, mode navigation.npm run test:browser: 46, including real-WASM tests for coefficient stats and JS-vs-codec reconstruction.Manual check in Chromium via Playwright:
No console errors.
Known limitation
Opening a block from the artifact map is mouse-only.
🤖 Generated with Claude Code