From d052b73f3729dacfec8b889f7aaecfa44d9fc320 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Sat, 10 Oct 2026 00:12:59 +0530 Subject: [PATCH 1/9] perf(core): separable table-driven 8x8 DCT/IDCT 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 --- core/src/transform.cpp | 79 +++++++++++++++++++++------------- core/tests/transform_test.cpp | 44 +++++++++++++++++++ web/public/codec.wasm | Bin 83560 -> 86272 bytes 3 files changed, 94 insertions(+), 29 deletions(-) diff --git a/core/src/transform.cpp b/core/src/transform.cpp index da79d48..1f0b645 100644 --- a/core/src/transform.cpp +++ b/core/src/transform.cpp @@ -20,52 +20,73 @@ const double PI = 3.14159265358979323846; - /* -* Helper function to compute the normalization factor C(u) used in DCT and IDCT. +* Orthonormal 8-point DCT-II basis: B[u][x] = 1/2 · C(u) · cos((2x+1)uπ/16), +* with C(0) = 1/√2 and C(u) = 1 otherwise. Computed once on first use. +* +* The 2D DCT is separable, F = B · f · Bᵀ, and the IDCT is f = Bᵀ · F · B. +* Applying it as two 1D passes costs 2·8³ = 1024 multiply-adds per block, +* instead of 8⁴ = 4096 terms with two cos() calls each for the direct formula +* F(u,v) = 1/4 C(u) C(v) Σx Σy f(x,y) cos((2x+1)uπ/16) cos((2y+1)vπ/16). */ -static double C(int u) { - return (u == 0) ? (1.0 / std::sqrt(2.0)) : 1.0; +struct DctBasis { + double m[8][8]; + DctBasis() { + for (int u = 0; u < 8; ++u) { + const double c = (u == 0) ? (1.0 / std::sqrt(2.0)) : 1.0; + for (int x = 0; x < 8; ++x) + m[u][x] = 0.5 * c * std::cos(((2 * x + 1) * u * PI) / 16.0); + } + } +}; + +static const double (&dctBasis())[8][8] { + static const DctBasis basis; // thread-safe one-time initialisation + return basis.m; } /* -* Performs the Discrete Cosine Transform (DCT) on an 8x8 block. -* The input block should be of type CV_64F and the output will also be CV_64F. -* 2D DCT is computed using the formula: -* F(u, v) = 1/4 * C(u) * C(v) * sum_{x=0}^{7} sum_{y=0}^{7} f(x, y) * cos((2x+1)uπ/16) * cos((2y+1)vπ/16) +* Performs the 2D Discrete Cosine Transform (DCT-II) on an 8x8 block. +* src is indexed [x][y] (row, column); dst is indexed [u][v]. */ void dct8x8(const double src[8][8], double dst[8][8]) { - for (int u = 0; u < 8; ++u) { + const double (&B)[8][8] = dctBasis(); + double tmp[8][8]; // tmp = B · src (transform along the first index) + + for (int u = 0; u < 8; ++u) + for (int y = 0; y < 8; ++y) { + double sum = 0.0; + for (int x = 0; x < 8; ++x) sum += B[u][x] * src[x][y]; + tmp[u][y] = sum; + } + + for (int u = 0; u < 8; ++u) for (int v = 0; v < 8; ++v) { double sum = 0.0; - for (int x = 0; x < 8; ++x) { - for (int y = 0; y < 8; ++y) { - sum += src[x][y] * - std::cos(((2 * x + 1) * u * PI) / 16.0) * - std::cos(((2 * y + 1) * v * PI) / 16.0); - } - } - dst[u][v] = 0.25 * C(u) * C(v) * sum; + for (int y = 0; y < 8; ++y) sum += tmp[u][y] * B[v][y]; + dst[u][v] = sum; } - } } /* * Performs the Inverse Discrete Cosine Transform (IDCT) on an 8x8 block. -* The input block should be of type CV_64F and the output will also be CV_64F. +* src is indexed [u][v]; dst is indexed [x][y]. */ void idct8x8(const double src[8][8], double dst[8][8]) { - for (int x = 0; x < 8; ++x) { + const double (&B)[8][8] = dctBasis(); + double tmp[8][8]; // tmp = Bᵀ · src + + for (int x = 0; x < 8; ++x) + for (int v = 0; v < 8; ++v) { + double sum = 0.0; + for (int u = 0; u < 8; ++u) sum += B[u][x] * src[u][v]; + tmp[x][v] = sum; + } + + for (int x = 0; x < 8; ++x) for (int y = 0; y < 8; ++y) { double sum = 0.0; - for (int u = 0; u < 8; ++u) { - for (int v = 0; v < 8; ++v) { - sum += C(u) * C(v) * src[u][v] * - std::cos(((2 * x + 1) * u * PI) / 16.0) * - std::cos(((2 * y + 1) * v * PI) / 16.0); - } - } - dst[x][y] = 0.25 * sum; + for (int v = 0; v < 8; ++v) sum += tmp[x][v] * B[v][y]; + dst[x][y] = sum; } - } } diff --git a/core/tests/transform_test.cpp b/core/tests/transform_test.cpp index 54f06b3..efc65c9 100644 --- a/core/tests/transform_test.cpp +++ b/core/tests/transform_test.cpp @@ -56,3 +56,47 @@ TEST(TransformTest, RoundTrip) { } } } + +// Reference: the direct (non-separable) DCT-II definition the fast version +// must match exactly (up to floating-point rounding). +static void referenceDct(const double src[8][8], double dst[8][8]) { + const double pi = 3.14159265358979323846; + for (int u = 0; u < 8; ++u) + for (int v = 0; v < 8; ++v) { + double sum = 0.0; + for (int x = 0; x < 8; ++x) + for (int y = 0; y < 8; ++y) + sum += src[x][y] * std::cos((2 * x + 1) * u * pi / 16.0) + * std::cos((2 * y + 1) * v * pi / 16.0); + const double cu = u == 0 ? 1.0 / std::sqrt(2.0) : 1.0; + const double cv = v == 0 ? 1.0 / std::sqrt(2.0) : 1.0; + dst[u][v] = 0.25 * cu * cv * sum; + } +} + +TEST(TransformTest, MatchesDirectDefinition) { + double src[8][8], fast[8][8], ref[8][8]; + unsigned state = 7u; + for (int i = 0; i < 8; ++i) + for (int j = 0; j < 8; ++j) { + state = state * 1103515245u + 12345u; + src[i][j] = static_cast((state >> 16) % 256) - 128.0; + } + dct8x8(src, fast); + referenceDct(src, ref); + for (int u = 0; u < 8; ++u) + for (int v = 0; v < 8; ++v) + ASSERT_NEAR(fast[u][v], ref[u][v], 1e-9) << u << "," << v; +} + +TEST(TransformTest, AsymmetricBlockKeepsOrientation) { + // A horizontal ramp (varies along the column index y) must put its + // energy in row u = 0, i.e. dst[0][v], not in column dst[u][0]. + double src[8][8], dst[8][8]; + for (int x = 0; x < 8; ++x) + for (int y = 0; y < 8; ++y) + src[x][y] = y * 10.0; + dct8x8(src, dst); + EXPECT_GT(std::abs(dst[0][1]), 1.0); + EXPECT_NEAR(dst[1][0], 0.0, 1e-9); +} diff --git a/web/public/codec.wasm b/web/public/codec.wasm index c96723162b8f11c8f430d754042730beb185eb08..c2c7eedb7ca76aa6daa7273dc5ffa214b6a06a5b 100755 GIT binary patch delta 10024 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 z%0AZhXVq&hbeL<-o}bHphdXIJ@M z+ji>6sd<4>k)lz!uWRevqA34u_n>Cy4>Z`^OqVD(feZajN8dZB;qm*?Ly%lhXr_FC4#u>%` zhj%LcvL8B|VFYf|s$B&GDAK`jN!10x009}MU{F_cl0nIK)r|v!R8qlUNLLplJKc9f2hCSf4JXPN^uj{O5wI~rDrr&N{2O8N;R)aDS}ihJvUJ)1jdz;L{aHE zQaXD*7?#8_*?}M+Ot*8~wV_po>0srFt=YaYMqeX1vJmb( zcC$BvWz9WGd3;T^2m7j%DX*`f-9Vk+Av{~e=o`pMm9E?n2pm8)QUFL*b+|DTfe;{% zDFR6X9V-{%s2g6%78xgMq@Q_rP7OxK_b>R4vQkJUcGFuK(^tqp`tHe$-Eo2L3>_Ez zS*IVJQ8alM#Jux58{(k+Bo3k@qyFosAKgkLKt7>PJR2=uf+qU-+M=Z`Ng(Nf;9Ic#9Lq z#zZ0NS~8~khM_9o`RhM7Zy8n%{S;!D$;VoZd54TO)%f6W1h1g!Y{8%Zn>sNvn&0x9 zBPGj_JP_K5N^SE(Q4b8o>f?(9#(KD23WSxoRi(GC;?US5@A&*~5{QyQcY^cz=+(WF^R4tg$oVt&akY3H<2y5X zb^q;x&ojKK+X7;2=o;VBlBtY!+hoa4P>%PkF@S^bQoWkf#vj>4?F~)YZM_>(x7z-f$?=o62ZFk z1`FMf*nG|mgiJC4wSByqlw3V#(HW=vu+1MVo~tYbPj^X#Z=O`q65;nvsu+y$dnZ-Y zl}xEWVcq~ZMib6NL_bMrGzTUDHtFi4Q#9W{B?G;J6*>A>j4!4v>i~J z-fala7V7Kw#o!c*K`)_NwO_nZX~=LOl{h2frM}hvfoW`8K}04=f^=fdl=YJMbe!{)v$YmZHa( zU?&?HW0V9SRs`65bLva5#Y3-WeNe9>0QEzYFe~g>iHFG_Fd@TJCpy_0t5eh F_J8a7Dz*Rs From 2f9a6a20bbb3cb00baacc58dd21fc9ebdf9cc1d0 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Sat, 10 Oct 2026 00:15:16 +0530 Subject: [PATCH 2/9] fix(ui): phone layouts for viewer and both inspectors 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 --- web/src/ArtifactInspectorMode.svelte | 27 +++++++++++++++++++++++++ web/src/InspectorMode.svelte | 10 +++++++++ web/src/styles/inspector-responsive.css | 23 ++++++++++++++++----- web/src/styles/viewer.css | 23 +++++++++++++++++++++ 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/web/src/ArtifactInspectorMode.svelte b/web/src/ArtifactInspectorMode.svelte index 13db7d0..6ad27ed 100644 --- a/web/src/ArtifactInspectorMode.svelte +++ b/web/src/ArtifactInspectorMode.svelte @@ -696,6 +696,33 @@ } /* ===== View Toggle ===== */ + @media (max-width: 900px) { + /* Chips get their own scrollable row under the title. */ + .artifact-chips { + margin-left: 0; + width: 100%; + flex-wrap: nowrap; + overflow-x: auto; + } + + .artifact-chip { + flex-shrink: 0; + } + + .artifact-content { + overflow: visible; + min-height: 60vh; + } + + .view-toggle-container { + flex-wrap: wrap; + } + + .art-zoom-controls { + position: static; + } + } + .view-toggle-container { display: flex; align-items: center; diff --git a/web/src/InspectorMode.svelte b/web/src/InspectorMode.svelte index 5686268..77b3c47 100644 --- a/web/src/InspectorMode.svelte +++ b/web/src/InspectorMode.svelte @@ -180,12 +180,21 @@ }; } + // In the stacked phone layout the pipeline sits below the sidebar, so a + // click on the navigator would otherwise change content off-screen. + function revealPipelineOnNarrowScreens(): void { + if (!window.matchMedia('(max-width: 900px)').matches) return; + requestAnimationFrame(() => + document.getElementById('inspectorContent')?.scrollIntoView({ behavior: 'smooth', block: 'start' })); + } + function onThumbnailClick(e: MouseEvent) { if (!appState.originalImageData) return; const { bx, by, valid } = getBlockFromEvent(e); if (valid) { appState.inspectedBlock = { x: bx, y: by }; document.querySelectorAll('.suggested-block-btn').forEach(b => b.classList.remove('active')); + revealPipelineOnNarrowScreens(); } } @@ -219,6 +228,7 @@ function onSuggestedBlockSelect(bx: number, by: number) { appState.inspectedBlock = { x: bx, y: by }; highlightThumbnailBlock(bx, by); + revealPipelineOnNarrowScreens(); } // ===== Back button ===== diff --git a/web/src/styles/inspector-responsive.css b/web/src/styles/inspector-responsive.css index 3708ec5..50d31b4 100644 --- a/web/src/styles/inspector-responsive.css +++ b/web/src/styles/inspector-responsive.css @@ -1,22 +1,38 @@ /* ===== Inspector Responsive Overrides ===== */ @media (max-width: 900px) { + /* Stacked layout: the whole page scrolls instead of two cramped panes. */ + .inspector-fullpage { + overflow-y: auto; + } + + .inspector-topbar { + height: auto; + min-height: 56px; + padding-top: 8px; + padding-bottom: 8px; + row-gap: 8px; + } + .inspector-layout { flex-direction: column; + flex: none; + overflow: visible; } .inspector-sidebar { width: 100%; min-width: 100%; max-width: 100%; + height: auto; border-right: none; border-bottom: 1px solid var(--border); - max-height: 260px; - overflow-y: auto; + overflow: visible; } .inspector-main { padding: 16px; + overflow: visible; } .pipeline-row { @@ -105,7 +121,4 @@ display: none; } - .inspector-sidebar { - max-height: 200px; - } } diff --git a/web/src/styles/viewer.css b/web/src/styles/viewer.css index b679e4f..880a82b 100644 --- a/web/src/styles/viewer.css +++ b/web/src/styles/viewer.css @@ -335,6 +335,29 @@ canvas { box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); } +/* Phones: dimensions + zoom share the first row, the view toggle spans the second. */ +@media (max-width: 600px) { + .viewer-canvas-header { + grid-template-columns: 1fr auto; + row-gap: 8px; + } + + .canvas-header-center { + grid-column: 1 / -1; + grid-row: 2; + } + + .view-mode-toggle { + width: 100%; + } + + .view-mode-toggle button { + flex: 1; + min-width: 0; + padding: 5px 8px; + } +} + .viewer-zoom-controls { display: flex; align-items: center; From f0d07e2d4506fd4fc7dff9b7385b9dc6afd6c49e Mon Sep 17 00:00:00 2001 From: Abhinav Date: Sat, 10 Oct 2026 00:19:01 +0530 Subject: [PATCH 3/9] fix: consistent bit/coefficient numbers across the app - 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 --- Makefile | 2 +- core/inc/ImageCodec.h | 16 ++ core/src/ImageCodec.cpp | 56 ++++++ core/tests/test_imagecodec.cpp | 37 ++++ web/cpp/codec_web.cpp | 71 ++----- web/public/codec.js | 2 +- web/public/codec.wasm | Bin 86272 -> 85561 bytes web/src/ArtifactInspectorMode.svelte | 21 +- web/src/InspectorMode.svelte | 4 +- .../components/viewer/CoeffHistogram.svelte | 190 +++++++++--------- web/src/lib/dct-utils.ts | 28 ++- web/src/lib/grid-renderer.ts | 3 +- web/src/lib/inspection.ts | 40 +++- web/src/lib/wasm-bridge.ts | 32 ++- web/src/styles/viewer.css | 18 -- web/src/types/wasm.d.ts | 2 +- web/tests/browser/codec.browser.test.js | 13 +- web/tests/setup/unit.setup.js | 1 + web/tests/unit/dct-utils.test.js | 21 ++ web/tests/unit/inspection.test.js | 42 +++- web/tests/unit/wasm-bridge.test.js | 31 +++ 21 files changed, 428 insertions(+), 202 deletions(-) diff --git a/Makefile b/Makefile index 978e993..20b27d9 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ WEB_EXPORTS = _init_session _process_image _rd_encode _get_rd_bit_estimate \ _get_view_ptr _set_view_tint _set_artifact_gain \ _get_psnr_y _get_psnr_cr _get_psnr_cb \ _get_ssim_y _get_ssim_cr _get_ssim_cb \ - _get_last_bit_estimate _inspect_block_data _get_coeff_histogram \ + _get_last_bit_estimate _inspect_block_data _get_coeff_stats \ _init_me_session _run_motion_estimation _get_mv_count \ _get_me_residual_ptr _get_search_steps _get_search_step_count \ _malloc _free diff --git a/core/inc/ImageCodec.h b/core/inc/ImageCodec.h index 0e93f87..947e050 100644 --- a/core/inc/ImageCodec.h +++ b/core/inc/ImageCodec.h @@ -65,6 +65,22 @@ class ImageCodec { bool isWavelet() const { return m_transformType != TransformType::DCT; } + /* + * Distribution of a channel's AC / detail coefficients (DC and LL excluded) + * under this codec's transform, before quantization. The channel is treated + * as luma (DCT uses the luma quantization table). + * histogram[0] — fraction with |c| < 1 + * histogram[b] — fraction with 2^(b-1) <= |c| < 2^b; the last bin is open-ended + * zeroFraction — fraction that quantize to zero at this codec's quality + */ + struct CoefficientStats { + static constexpr int NUM_BINS = 12; + double histogram[NUM_BINS] = {}; + double zeroFraction = 0.0; + }; + + CoefficientStats coefficientStats(const Image& channel) const; + /* * Downsamples a chroma channel by box-averaging (2×1 for 4:2:2, 2×2 for 4:2:0). */ diff --git a/core/src/ImageCodec.cpp b/core/src/ImageCodec.cpp index ea0f33b..de9da69 100644 --- a/core/src/ImageCodec.cpp +++ b/core/src/ImageCodec.cpp @@ -237,6 +237,62 @@ Image ImageCodec::processChannelDWT(const Image& channel, bool isChroma) return result; } +// Log2 magnitude bin: 0 for |c| < 1, b for 2^(b-1) <= |c| < 2^b, capped. +static int magnitudeBin(double c) { + const double mag = std::abs(c); + if (mag < 1.0) return 0; + const int bin = 1 + static_cast(std::floor(std::log2(mag))); + return std::min(bin, ImageCodec::CoefficientStats::NUM_BINS - 1); +} + +ImageCodec::CoefficientStats ImageCodec::coefficientStats(const Image& channel) const +{ + CoefficientStats stats; + long long total = 0; + long long zeroed = 0; + + auto record = [&](double coeff, double step) { + stats.histogram[magnitudeBin(coeff)] += 1.0; + const bool zero = m_enableQuantization ? std::round(coeff / step) == 0.0 : coeff == 0.0; + if (zero) ++zeroed; + ++total; + }; + + if (isWavelet()) { + std::vector buf; + int W = 0, H = 0, levels = 0; + transformChannelDWT(channel, buf, W, H, levels); + const double baseStep = DWT_BASE_STEP * qualityScale(m_quality); + for (int y = 0; y < H; ++y) + for (int x = 0; x < W; ++x) { + if (dwtSubbandIndex(x, y, W, H, levels) == 0) continue; // LL + record(buf[(size_t)y * W + x], dwtQuantStep(x, y, W, H, levels, baseStep)); + } + } else { + // Same block extraction (edge replication, level shift) as processChannel. + const int width = channel.width(); + const int height = channel.height(); + const double* data = channel.data(); + for (int y = 0; y < height; y += 8) + for (int x = 0; x < width; x += 8) { + double block[8][8], coeffs[8][8]; + for (int i = 0; i < 8; ++i) + for (int j = 0; j < 8; ++j) + block[i][j] = data[std::min(y + i, height - 1) * width + std::min(x + j, width - 1)] - 128.0; + dct8x8(block, coeffs); + for (int i = 0; i < 8; ++i) + for (int j = 0; j < 8; ++j) + if (i != 0 || j != 0) record(coeffs[i][j], m_lumaQuantTable[i][j]); + } + } + + if (total > 0) { + for (double& h : stats.histogram) h /= static_cast(total); + stats.zeroFraction = static_cast(zeroed) / static_cast(total); + } + return stats; +} + Image ImageCodec::subbandMap(const Image& channel) const { if (!isWavelet()) return Image(); diff --git a/core/tests/test_imagecodec.cpp b/core/tests/test_imagecodec.cpp index 23996c8..fc91c22 100644 --- a/core/tests/test_imagecodec.cpp +++ b/core/tests/test_imagecodec.cpp @@ -440,3 +440,40 @@ TEST(ImageCodecTest, InspectBlockPadsPartialEdgeBlocksByReplication) { EXPECT_DOUBLE_EQ(debug.original[i][j], channel.at(x, y, 0)) << i << "," << j; } } + +TEST(ImageCodecTest, CoefficientStatsAreNormalisedAndTrackQuality) { + Image noise = createNoiseImage(64, 48); + Image luma(64, 48, 1); + for (int y = 0; y < 48; ++y) + for (int x = 0; x < 64; ++x) + luma.at(x, y, 0) = noise.at(x, y, 0); + + for (auto t : ALL_TRANSFORMS) { + ImageCodec low(10.0, true, ImageCodec::ChromaSubsampling::CS_444, t); + ImageCodec high(90.0, true, ImageCodec::ChromaSubsampling::CS_444, t); + const auto lo = low.coefficientStats(luma); + const auto hi = high.coefficientStats(luma); + + double sum = 0.0; + for (double h : lo.histogram) sum += h; + EXPECT_NEAR(sum, 1.0, 1e-9) << "transform " << static_cast(t); + + // Same transform → same histogram; coarser quantization zeroes more. + for (int b = 0; b < ImageCodec::CoefficientStats::NUM_BINS; ++b) + EXPECT_DOUBLE_EQ(lo.histogram[b], hi.histogram[b]); + EXPECT_GT(lo.zeroFraction, hi.zeroFraction); + } +} + +TEST(ImageCodecTest, CoefficientStatsWithoutQuantizationCountOnlyExactZeros) { + Image flat(16, 16, 1); + for (size_t i = 0; i < flat.size(); ++i) flat.data()[i] = 77.0; + ImageCodec codec(50.0, false); + // A flat block has all-zero AC coefficients (up to rounding). + EXPECT_GT(codec.coefficientStats(flat).histogram[0], 0.99); + + Image noise(16, 16, 1); + unsigned st = 3u; + for (size_t i = 0; i < noise.size(); ++i) { st = st * 1103515245u + 12345u; noise.data()[i] = (st >> 16) % 256; } + EXPECT_LT(codec.coefficientStats(noise).zeroFraction, 0.05); +} diff --git a/web/cpp/codec_web.cpp b/web/cpp/codec_web.cpp index 8673083..5253ed1 100644 --- a/web/cpp/codec_web.cpp +++ b/web/cpp/codec_web.cpp @@ -271,65 +271,28 @@ double get_last_bit_estimate() { return g_session.initialized ? g_session.lastBitEstimate : 0.0; } -// Returns a malloc'd array of 2*num_bins doubles: [dct_bins | dwt_bins]. -// Each series is normalized by the total AC coefficient count. -// Scans all 8×8 blocks of the Y channel, applies both DCT and DWT per block, -// and bins the absolute magnitudes of AC coefficients (skipping DC [0][0]). -// Caller must free the returned pointer. +// Coefficient statistics of the original luma for every transform at the +// given quality, computed with the codec's own transform and quantizer. +// Returns a malloc'd array of 3 × (NUM_BINS + 1) doubles, one record per +// transform mode (0 = DCT, 1 = Haar, 2 = CDF 9/7): +// [histogram[0..NUM_BINS-1], zeroFraction] +// See ImageCodec::CoefficientStats for the bin layout. Caller must free. EMSCRIPTEN_KEEPALIVE -double* get_coeff_histogram(int num_bins, double max_val) { - if (!g_session.initialized || num_bins <= 0 || max_val <= 0.0) return nullptr; - - const Image& ycrcb = g_session.originalYCrCb; - const int w = ycrcb.width(); - const int h = ycrcb.height(); - const int blocksX = w / 8; - const int blocksY = h / 8; - if (blocksX == 0 || blocksY == 0) return nullptr; +double* get_coeff_stats(int quality) { + if (!g_session.initialized) return nullptr; - double* out = (double*)calloc(2 * num_bins, sizeof(double)); + constexpr int BINS = ImageCodec::CoefficientStats::NUM_BINS; + constexpr int RECORD = BINS + 1; + double* out = (double*)malloc(3 * RECORD * sizeof(double)); if (!out) return nullptr; - double* dctBins = out; - double* dwtBins = out + num_bins; - long long totalCount = 0; - - double src[8][8], dctCoeffs[8][8], dwtCoeffs[8][8]; - const double* ycrcbData = ycrcb.data(); - - for (int by = 0; by < blocksY; by++) { - for (int bx = 0; bx < blocksX; bx++) { - // Extract 8×8 Y block with level shift matching processChannel - for (int row = 0; row < 8; row++) { - for (int col = 0; col < 8; col++) { - src[row][col] = ycrcbData[((by * 8 + row) * w + (bx * 8 + col)) * 3] - 128.0; - } - } - - dct8x8(src, dctCoeffs); - dwt8x8(src, dwtCoeffs); - - // Bin AC coefficients — skip [0][0] (DC / LL) - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 8; j++) { - if (i == 0 && j == 0) continue; - - int dctBin = std::min(num_bins - 1, (int)(std::abs(dctCoeffs[i][j]) / max_val * num_bins)); - dctBins[dctBin]++; - - int dwtBin = std::min(num_bins - 1, (int)(std::abs(dwtCoeffs[i][j]) / max_val * num_bins)); - dwtBins[dwtBin]++; - - totalCount++; - } - } - } - } - - if (totalCount > 0) { - for (int i = 0; i < 2 * num_bins; i++) out[i] /= totalCount; + const Image luma = extractChannel(g_session.originalYCrCb, 0); + for (int mode = 0; mode < 3; ++mode) { + ImageCodec codec(quality, true, ImageCodec::ChromaSubsampling::CS_444, map_transform_mode(mode)); + const auto stats = codec.coefficientStats(luma); + std::copy(stats.histogram, stats.histogram + BINS, out + mode * RECORD); + out[mode * RECORD + BINS] = stats.zeroFraction; } - return out; } diff --git a/web/public/codec.js b/web/public/codec.js index 85a8c67..509fa13 100644 --- a/web/public/codec.js +++ b/web/public/codec.js @@ -1 +1 @@ -var Module=typeof Module!="undefined"?Module:{};var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=globalThis.document?.currentScript?.src;if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);if(typeof module!="undefined"){module["exports"]=Module}quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["e"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("codec.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;var _init_session,_process_image,_rd_encode,_get_rd_bit_estimate,_get_view_ptr,_malloc,_set_view_tint,_set_artifact_gain,_get_psnr_y,_get_psnr_cr,_get_psnr_cb,_get_ssim_y,_get_ssim_cr,_get_ssim_cb,_get_last_bit_estimate,_get_coeff_histogram,_inspect_block_data,_init_me_session,_run_motion_estimation,_get_mv_count,_get_me_residual_ptr,_get_search_steps,_get_search_step_count,_free,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_init_session=Module["_init_session"]=wasmExports["f"];_process_image=Module["_process_image"]=wasmExports["g"];_rd_encode=Module["_rd_encode"]=wasmExports["h"];_get_rd_bit_estimate=Module["_get_rd_bit_estimate"]=wasmExports["i"];_get_view_ptr=Module["_get_view_ptr"]=wasmExports["j"];_malloc=Module["_malloc"]=wasmExports["k"];_set_view_tint=Module["_set_view_tint"]=wasmExports["l"];_set_artifact_gain=Module["_set_artifact_gain"]=wasmExports["m"];_get_psnr_y=Module["_get_psnr_y"]=wasmExports["n"];_get_psnr_cr=Module["_get_psnr_cr"]=wasmExports["o"];_get_psnr_cb=Module["_get_psnr_cb"]=wasmExports["p"];_get_ssim_y=Module["_get_ssim_y"]=wasmExports["q"];_get_ssim_cr=Module["_get_ssim_cr"]=wasmExports["r"];_get_ssim_cb=Module["_get_ssim_cb"]=wasmExports["s"];_get_last_bit_estimate=Module["_get_last_bit_estimate"]=wasmExports["t"];_get_coeff_histogram=Module["_get_coeff_histogram"]=wasmExports["u"];_inspect_block_data=Module["_inspect_block_data"]=wasmExports["v"];_init_me_session=Module["_init_me_session"]=wasmExports["w"];_run_motion_estimation=Module["_run_motion_estimation"]=wasmExports["x"];_get_mv_count=Module["_get_mv_count"]=wasmExports["y"];_get_me_residual_ptr=Module["_get_me_residual_ptr"]=wasmExports["z"];_get_search_steps=Module["_get_search_steps"]=wasmExports["A"];_get_search_step_count=Module["_get_search_step_count"]=wasmExports["B"];_free=Module["_free"]=wasmExports["C"];__emscripten_stack_restore=wasmExports["D"];__emscripten_stack_alloc=wasmExports["E"];_emscripten_stack_get_current=wasmExports["F"];memory=wasmMemory=wasmExports["d"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:___cxa_throw,b:__abort_js,c:_emscripten_resize_heap};function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;createWasm();run(); +var Module=typeof Module!="undefined"?Module:{};var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=globalThis.document?.currentScript?.src;if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);if(typeof module!="undefined"){module["exports"]=Module}quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["e"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("codec.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;var _init_session,_process_image,_rd_encode,_get_rd_bit_estimate,_get_view_ptr,_malloc,_set_view_tint,_set_artifact_gain,_get_psnr_y,_get_psnr_cr,_get_psnr_cb,_get_ssim_y,_get_ssim_cr,_get_ssim_cb,_get_last_bit_estimate,_get_coeff_stats,_inspect_block_data,_init_me_session,_run_motion_estimation,_get_mv_count,_get_me_residual_ptr,_get_search_steps,_get_search_step_count,_free,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_init_session=Module["_init_session"]=wasmExports["f"];_process_image=Module["_process_image"]=wasmExports["g"];_rd_encode=Module["_rd_encode"]=wasmExports["h"];_get_rd_bit_estimate=Module["_get_rd_bit_estimate"]=wasmExports["i"];_get_view_ptr=Module["_get_view_ptr"]=wasmExports["j"];_malloc=Module["_malloc"]=wasmExports["k"];_set_view_tint=Module["_set_view_tint"]=wasmExports["l"];_set_artifact_gain=Module["_set_artifact_gain"]=wasmExports["m"];_get_psnr_y=Module["_get_psnr_y"]=wasmExports["n"];_get_psnr_cr=Module["_get_psnr_cr"]=wasmExports["o"];_get_psnr_cb=Module["_get_psnr_cb"]=wasmExports["p"];_get_ssim_y=Module["_get_ssim_y"]=wasmExports["q"];_get_ssim_cr=Module["_get_ssim_cr"]=wasmExports["r"];_get_ssim_cb=Module["_get_ssim_cb"]=wasmExports["s"];_get_last_bit_estimate=Module["_get_last_bit_estimate"]=wasmExports["t"];_get_coeff_stats=Module["_get_coeff_stats"]=wasmExports["u"];_inspect_block_data=Module["_inspect_block_data"]=wasmExports["v"];_init_me_session=Module["_init_me_session"]=wasmExports["w"];_run_motion_estimation=Module["_run_motion_estimation"]=wasmExports["x"];_get_mv_count=Module["_get_mv_count"]=wasmExports["y"];_get_me_residual_ptr=Module["_get_me_residual_ptr"]=wasmExports["z"];_get_search_steps=Module["_get_search_steps"]=wasmExports["A"];_get_search_step_count=Module["_get_search_step_count"]=wasmExports["B"];_free=Module["_free"]=wasmExports["C"];__emscripten_stack_restore=wasmExports["D"];__emscripten_stack_alloc=wasmExports["E"];_emscripten_stack_get_current=wasmExports["F"];memory=wasmMemory=wasmExports["d"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:___cxa_throw,b:__abort_js,c:_emscripten_resize_heap};function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;createWasm();run(); diff --git a/web/public/codec.wasm b/web/public/codec.wasm index c2c7eedb7ca76aa6daa7273dc5ffa214b6a06a5b..a825face25ec3c8e4afc4eb343875529c20794be 100755 GIT binary patch delta 8715 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 { const mode = appState.currentViewMode; - const gain = mode === ViewMode.EdgeDistortion ? EDGE_DISTORTION_GAIN - : mode === ViewMode.BlockingMap ? BLOCKING_GAIN - : gainValue; + const gain = currentGain; const label = mode === ViewMode.EdgeDistortion ? 'Edge strength Δ' : mode === ViewMode.BlockingMap ? 'Boundary step' : '|Error|'; const value = tooltip.val / gain; // A saturated pixel only tells us the true value is at least 255 / gain. - const text = `${tooltip.val >= 255 ? '≥ ' : ''}${value < 10 ? value.toFixed(1) : value.toFixed(0)}`; + const text = `${tooltip.val >= 255 ? '≥ ' : ''}${formatLevels(value)}`; return { label, text, saturated: tooltip.val >= 255 }; }); @@ -474,12 +481,12 @@ {#if showHeatmap}
-
Error Magnitude
+
{tooltipReading.label} (pixel levels)
0 - 127 - 255 + {formatLevels(127.5 / currentGain)} + ≥ {formatLevels(255 / currentGain)}
{/if} diff --git a/web/src/InspectorMode.svelte b/web/src/InspectorMode.svelte index 77b3c47..21867c6 100644 --- a/web/src/InspectorMode.svelte +++ b/web/src/InspectorMode.svelte @@ -588,8 +588,8 @@
—
-
Est. Bits
-
Estimated Bits
Calculated via simplified Huffman simulation on Zig-zag scanned runs and symbol magnitudes.
+
JPEG Bits (est.)
+
Estimated Bits (JPEG-style)
A simplified JPEG Huffman simulation for this block: zig-zag runs and magnitudes, with DC coded as the difference from the previous block. The viewer's file size uses a whole-image entropy estimate instead, so the two don't add up exactly.
—
diff --git a/web/src/lib/components/viewer/CoeffHistogram.svelte b/web/src/lib/components/viewer/CoeffHistogram.svelte index 48d144e..15549f0 100644 --- a/web/src/lib/components/viewer/CoeffHistogram.svelte +++ b/web/src/lib/components/viewer/CoeffHistogram.svelte @@ -1,37 +1,60 @@ @@ -39,116 +62,97 @@
-

Coefficient Histogram Beta

-

AC coefficient magnitude distribution — DWT concentrates more energy near zero, explaining its advantage at low bitrates.

+

Coefficient Sparsity

+

Where each transform puts the luma channel's detail energy. Coefficients that quantize to zero are nearly free to code.

- {#if histLoading} + {#if loading} Computing… - {:else if histData} - {appState.imgWidth / 8 | 0}×{appState.imgHeight / 8 | 0} blocks {/if} - {#if appState.originalImageData && !histLoading} - + {#if appState.originalImageData && !loading && !stats} + {/if}
{#if !appState.originalImageData} -
Load an image to generate a coefficient histogram.
- {:else if histLoading} -
Computing transform coefficients for all blocks…
- {:else if !histData} -
Click Generate to compare DCT vs DWT coefficient sparsity.
+
Load an image to compare coefficient sparsity.
+ {:else if loading} +
Transforming the image with DCT, Haar and CDF 9/7…
+ {:else if !stats} +
Click Generate to compare how sparse each transform makes the image.
{:else} + {@const series = TRANSFORMS.map(t => ({ info: t, data: stats![t.value] }))} {@const svgW = 700} {@const svgH = 240} {@const padL = 52} - {@const padR = 24} - {@const padT = 18} + {@const padR = 16} + {@const padT = 30} {@const padB = 48} {@const chartW = svgW - padL - padR} {@const chartH = svgH - padT - padB} - {@const maxY = Math.max(...histData.dct, ...histData.dwt) * 1.15} - {@const groupW = chartW / HIST_BINS} - {@const barW = (groupW - 2) / 2} - {@const binWidth = HIST_MAX / HIST_BINS} - {@const mapBarY = (v: number) => padT + chartH - (v / maxY) * chartH} + {@const maxY = Math.max(...series.flatMap(s => s.data.histogram)) * 1.1 || 1} + {@const groupW = chartW / COEFF_BINS} + {@const barW = (groupW - 6) / series.length}
- - - - - - - + {#each [0.25, 0.5, 0.75, 1.0] as frac} {@const gy = padT + chartH * (1 - frac)} - {(maxY * frac * 100).toFixed(1)}% + {(maxY * frac * 100).toFixed(0)}% {/each} - - - {#each histData.dct as val, i} - {@const x = padL + i * groupW + 1} + {#each series as s, k} + {#each s.data.histogram as val, b} {@const barH = Math.max(0, (val / maxY) * chartH)} - + + {s.info.label}: {(val * 100).toFixed(1)}% of coefficients with |c| in bin {binLabel(b)} + {/each} - - - {#each histData.dwt as val, i} - {@const x = padL + i * groupW + 1 + barW} - {@const barH = Math.max(0, (val / maxY) * chartH)} - {/each} - - {#each {length: Math.floor(HIST_BINS / 8) + 1} as _, k} - {@const binIdx = k * 8} - {#if binIdx <= HIST_BINS} - - {binIdx === HIST_BINS ? `${Math.round(HIST_MAX)}+` : Math.round(binIdx * binWidth)} - - {/if} + {#each { length: COEFF_BINS } as _, b} + {binLabel(b)} {/each} - - AC Coefficient Magnitude - Fraction + |AC coefficient| (doubling-width bins) + Share - - - DCT - - DWT (Haar) + {#each series as s, k} + + {s.info.label} + {/each}
- - {@const dctNZ = (histData.dct[0] * 100)} - {@const dwtNZ = (histData.dwt[0] * 100)} - {@const gain = dwtNZ - dctNZ}
-
- DCT near-zero (<{Math.round(binWidth)}) - {dctNZ.toFixed(1)}% -
-
-
- DWT near-zero (<{Math.round(binWidth)}) - {dwtNZ.toFixed(1)}% -
-
-
- DWT sparsity gain - 0} class:stat-poor={gain < 0}> - {gain >= 0 ? '+' : ''}{gain.toFixed(1)} pp - + {#each series as s, k} + {#if k > 0}
{/if} +
+ {s.info.label} zeroed at Q{statsQuality} + {(s.data.zeroFraction * 100).toFixed(1)}%
+ {/each}
+

+ Current transform: {transformInfo(appState.transformType).label}. + DC / LL coefficients are excluded. The zeroed share follows the quality slider. +

{/if}
+ + diff --git a/web/src/lib/dct-utils.ts b/web/src/lib/dct-utils.ts index e21db25..d21d0e6 100644 --- a/web/src/lib/dct-utils.ts +++ b/web/src/lib/dct-utils.ts @@ -42,19 +42,29 @@ export interface EntropySymbol { baseBits: number; magBits: number; totalBits: number; + /** DC only: the block's own quantized DC and the predictor it is coded against. */ + dcValue?: number; + dcPredictor?: number; } -export function getEntropySymbols(quantized: Float64Array | number[]): EntropySymbol[] { +/** + * JPEG-style symbol stream for one quantized 8×8 block (zig-zag order). + * As in JPEG, the DC coefficient is coded as the difference from the previous + * block's quantized DC (`prevDc`, 0 for the first block of a scan). + */ +export function getEntropySymbols(quantized: Float64Array | number[], prevDc = 0): EntropySymbol[] { const symbols: EntropySymbol[] = []; - // DC coefficient - const dc = Math.round(quantized[0]); - if (dc !== 0) { - const size = Math.floor(Math.log2(Math.abs(dc))) + 1; + // DC coefficient, coded differentially + const dcValue = Math.round(quantized[0]); + const dcPredictor = Math.round(prevDc); + const diff = dcValue - dcPredictor; + if (diff !== 0) { + const size = Math.floor(Math.log2(Math.abs(diff))) + 1; // DC symbols use 2-9 bits for base category in JPEG - symbols.push({ type: 'DC', zIndex: 0, size, amplitude: dc, baseBits: 3, magBits: size, totalBits: 3 + size }); + symbols.push({ type: 'DC', zIndex: 0, size, amplitude: diff, baseBits: 3, magBits: size, totalBits: 3 + size, dcValue, dcPredictor }); } else { - symbols.push({ type: 'DC', zIndex: 0, size: 0, amplitude: 0, baseBits: 2, magBits: 0, totalBits: 2 }); + symbols.push({ type: 'DC', zIndex: 0, size: 0, amplitude: 0, baseBits: 2, magBits: 0, totalBits: 2, dcValue, dcPredictor }); } // AC coefficients (Zig-zag scan) @@ -90,8 +100,8 @@ export function getEntropySymbols(quantized: Float64Array | number[]): EntropySy return symbols; } -export function estimateBlockBits(quantized: Float64Array | number[]): number { - const symbols = getEntropySymbols(quantized); +export function estimateBlockBits(quantized: Float64Array | number[], prevDc = 0): number { + const symbols = getEntropySymbols(quantized, prevDc); return symbols.reduce((acc, sym) => acc + sym.totalBits, 0); } diff --git a/web/src/lib/grid-renderer.ts b/web/src/lib/grid-renderer.ts index 4663684..f7cb4fe 100644 --- a/web/src/lib/grid-renderer.ts +++ b/web/src/lib/grid-renderer.ts @@ -923,7 +923,8 @@ export function renderEntropySummary(symbols: any[]): void { const valCellHtml = `
${s.amplitude}
`; valHtml = `
${valCellHtml}${runHtml ? 'and' : ''}${runHtml}
`; } else if (s.type === 'DC') { - valHtml = `
${s.amplitude}
`; + const why = `DC ${s.dcValue} minus the previous block's DC ${s.dcPredictor}`; + valHtml = `
Δ${s.amplitude}
`; } else if (s.type === 'ZRL') { const dots = Array.from({ length: 5 }, () => '').join(''); valHtml = `
16 Zeros
${dots}+
`; diff --git a/web/src/lib/inspection.ts b/web/src/lib/inspection.ts index df245ca..18418aa 100644 --- a/web/src/lib/inspection.ts +++ b/web/src/lib/inspection.ts @@ -4,6 +4,31 @@ import { hideBasisPopover, setCachedGridData } from './basis-popover.js'; import { renderGrid, renderLossMeter, renderZigzagArray, renderEntropySummary, stopReconstructionAnimation } from './grid-renderer.js'; import { estimateBlockBits, getEntropySymbols } from './dct-utils.js'; +// Index of the quantized grid within BlockDebugData (original, coefficients, +// quantTable, quantized, reconstructed — 64 doubles each). +const QUANTIZED_GRID = 3; + +/** + * The block coded just before (blockX, blockY) in a non-interleaved raster + * scan of this channel, in the original-image block coordinates that + * inspectBlockData expects; null for the first block. Subsampled chroma has + * its own, coarser block grid, so the step back happens in that grid. + */ +export function previousBlockInScan(blockX: number, blockY: number, channelIndex: number): { x: number; y: number } | null { + const chroma = channelIndex !== 0; + const scaleX = chroma && appState.currentCsMode !== 444 ? 2 : 1; + const scaleY = chroma && appState.currentCsMode === 420 ? 2 : 1; + const blocksPerRow = Math.ceil(Math.ceil(appState.imgWidth / scaleX) / 8); + + const cx = Math.floor(blockX / scaleX); + const cy = Math.floor(blockY / scaleY); + let px: number, py: number; + if (cx > 0) { px = cx - 1; py = cy; } + else if (cy > 0) { px = blocksPerRow - 1; py = cy - 1; } + else return null; + return { x: px * scaleX, y: py * scaleY }; +} + export function inspectBlock(blockX: number, blockY: number): void { hideBasisPopover(); stopReconstructionAnimation(); @@ -22,6 +47,15 @@ export function inspectBlock(blockX: number, blockY: number): void { if (qTableType) qTableType.innerText = tableLabel; if (qTableType2) qTableType2.innerText = tableLabel; + // Read the previous block's quantized DC first: inspectBlockData returns a + // pointer to one shared buffer that the next call overwrites. + const prev = previousBlockInScan(blockX, blockY, channelIndex); + let prevDc = 0; + if (prev) { + const prevPtr = inspectBlockData(prev.x, prev.y, channelIndex, appState.quality); + if (prevPtr) prevDc = new Float64Array(Module.HEAPU8.buffer, prevPtr + QUANTIZED_GRID * 64 * 8, 1)[0]; + } + const ptr = inspectBlockData(blockX, blockY, channelIndex, appState.quality); if (!ptr) { console.error('Failed to inspect block: Ptr is null'); @@ -37,7 +71,7 @@ export function inspectBlock(blockX: number, blockY: number): void { const originalData = readGrid(0); const coeffData = readGrid(1); const qtData = readGrid(2); - const quantData = readGrid(3); + const quantData = readGrid(QUANTIZED_GRID); const reconData = readGrid(4); const errorData = new Float64Array(64); @@ -99,7 +133,7 @@ export function inspectBlock(blockX: number, blockY: number): void { setStatEl('statZeros', `${zeroCount}/64`, null, null); setStatEl('statCompression', `${zeroPercent}%`, compClass, compClass); - const estBits = estimateBlockBits(quantData); + const estBits = estimateBlockBits(quantData, prevDc); const bpp = estBits / 64; // Good bpp is strictly less than 1.5, moderate is < 3.0, otherwise poor. @@ -120,7 +154,7 @@ export function inspectBlock(blockX: number, blockY: number): void { renderLossMeter(mse, peakError); renderZigzagArray(quantData); - const entropySymbols = getEntropySymbols(quantData); + const entropySymbols = getEntropySymbols(quantData, prevDc); renderEntropySummary(entropySymbols); } diff --git a/web/src/lib/wasm-bridge.ts b/web/src/lib/wasm-bridge.ts index b175490..2b45003 100644 --- a/web/src/lib/wasm-bridge.ts +++ b/web/src/lib/wasm-bridge.ts @@ -91,21 +91,33 @@ export function inspectBlockData(blockX: number, blockY: number, channelIndex: n return Module._inspect_block_data(blockX, blockY, channelIndex, quality, appState.currentCsMode, t); } -// Returns normalized DCT and DWT AC coefficient histograms for the Y channel. -// num_bins bins from [0, max_val); the last bin is an overflow bucket. +// Must match ImageCodec::CoefficientStats::NUM_BINS. +export const COEFF_BINS = 12; + +export interface CoeffStats { + /** Fraction of AC/detail coefficients per log2 magnitude bin (see binLabel). */ + histogram: number[]; + /** Fraction of AC/detail coefficients that quantize to zero at the given quality. */ + zeroFraction: number; +} + +// Luma coefficient statistics for every transform at `quality`, computed with +// the codec's own transforms and quantizers. Keyed by transform value. // Returns null if WASM is not ready or no image is loaded. -export function getCoeffHistogram(numBins: number, maxVal: number): { dct: number[]; dwt: number[] } | null { - const ptr = Module._get_coeff_histogram(numBins, maxVal); +export function getCoeffStats(quality: number): Record | null { + const ptr = Module._get_coeff_stats(quality); if (!ptr) return null; try { const view = new DataView(Module.HEAPU8.buffer); - const dct: number[] = []; - const dwt: number[] = []; - for (let i = 0; i < numBins; i++) { - dct.push(view.getFloat64(ptr + i * 8, true)); - dwt.push(view.getFloat64(ptr + (numBins + i) * 8, true)); + const record = COEFF_BINS + 1; + const result: Record = {}; + for (let mode = 0; mode < 3; mode++) { + const base = ptr + mode * record * 8; + const histogram: number[] = []; + for (let b = 0; b < COEFF_BINS; b++) histogram.push(view.getFloat64(base + b * 8, true)); + result[mode] = { histogram, zeroFraction: view.getFloat64(base + COEFF_BINS * 8, true) }; } - return { dct, dwt }; + return result; } finally { Module._free(ptr); } diff --git a/web/src/styles/viewer.css b/web/src/styles/viewer.css index 880a82b..230c69b 100644 --- a/web/src/styles/viewer.css +++ b/web/src/styles/viewer.css @@ -638,22 +638,4 @@ canvas { .inspector-quickstart-btn:hover { background: var(--primary-hover); transform: translateY(-1px); -} - -.hist-bar-dct { - fill: var(--primary); - opacity: 0.82; -} - -.hist-bar-dwt { - fill: #f59e0b; - opacity: 0.82; -} - -.hist-dct-value { - color: var(--primary); -} - -.hist-dwt-value { - color: #f59e0b; } \ No newline at end of file diff --git a/web/src/types/wasm.d.ts b/web/src/types/wasm.d.ts index db32183..572ddd9 100644 --- a/web/src/types/wasm.d.ts +++ b/web/src/types/wasm.d.ts @@ -18,7 +18,7 @@ export interface WasmModule { _set_view_tint(enabled: number): void; _set_artifact_gain(gain: number): void; _inspect_block_data(blockX: number, blockY: number, channelIndex: number, quality: number, csMode: number, transformMode: number): number; - _get_coeff_histogram(numBins: number, maxVal: number): number; + _get_coeff_stats(quality: number): number; _get_psnr_y(): number; _get_psnr_cr(): number; _get_psnr_cb(): number; diff --git a/web/tests/browser/codec.browser.test.js b/web/tests/browser/codec.browser.test.js index 013ec0a..97d686c 100644 --- a/web/tests/browser/codec.browser.test.js +++ b/web/tests/browser/codec.browser.test.js @@ -2,7 +2,7 @@ // not the Module mock used by the other browser tests. import { describe, it, expect, beforeAll } from 'vitest'; import { appState, ViewMode } from '../../src/lib/state.svelte.js'; -import { initSession, processImage, rdEncode, getStats, getLastBitEstimate, getProcessedRgba } from '../../src/lib/wasm-bridge.js'; +import { initSession, processImage, rdEncode, getStats, getLastBitEstimate, getProcessedRgba, getCoeffStats } from '../../src/lib/wasm-bridge.js'; import { Transform, TRANSFORMS } from '../../src/lib/transforms.js'; const WIDTH = 384; @@ -123,4 +123,15 @@ describe('codec with the real WASM module', () => { for (let i = 0; i < edges.length; i += 4) if (edges[i] > 0) nonZero++; expect(nonZero).toBeGreaterThan(WIDTH * HEIGHT * 0.05); }); + + it('getCoeffStats gives normalised histograms whose zeroed share falls as quality rises', () => { + const low = getCoeffStats(10); + const high = getCoeffStats(90); + for (const t of TRANSFORMS) { + const sum = low[t.value].histogram.reduce((a, b) => a + b, 0); + expect(sum).toBeCloseTo(1, 6); + expect(low[t.value].histogram).toEqual(high[t.value].histogram); + expect(low[t.value].zeroFraction).toBeGreaterThan(high[t.value].zeroFraction); + } + }); }); diff --git a/web/tests/setup/unit.setup.js b/web/tests/setup/unit.setup.js index 2de6e03..f9e93e8 100644 --- a/web/tests/setup/unit.setup.js +++ b/web/tests/setup/unit.setup.js @@ -55,6 +55,7 @@ globalThis.Module = { _init_session: vi.fn(), _process_image: vi.fn(), _rd_encode: vi.fn(() => 33.25), + _get_coeff_stats: vi.fn(() => 0), _get_rd_bit_estimate: vi.fn(() => 12345), _get_view_ptr: vi.fn(() => 256), _set_view_tint: vi.fn(), diff --git a/web/tests/unit/dct-utils.test.js b/web/tests/unit/dct-utils.test.js index 0f182c5..6b68394 100644 --- a/web/tests/unit/dct-utils.test.js +++ b/web/tests/unit/dct-utils.test.js @@ -170,3 +170,24 @@ describe('dct-utils', () => { }); }); }); + +describe('differential DC coding', () => { + const block = (dc) => { const b = new Float64Array(64); b[0] = dc; return b; }; + + it('codes DC as the difference from the previous block', () => { + const [dcSym] = getEntropySymbols(block(20), 18); + expect(dcSym.type).toBe('DC'); + expect(dcSym.amplitude).toBe(2); + expect(dcSym.dcValue).toBe(20); + expect(dcSym.dcPredictor).toBe(18); + expect(dcSym.size).toBe(2); // |2| needs 2 magnitude bits + }); + + it('makes a DC equal to its predictor cheap', () => { + expect(estimateBlockBits(block(37), 37)).toBeLessThan(estimateBlockBits(block(37), 0)); + }); + + it('defaults to a zero predictor (first block of a scan)', () => { + expect(getEntropySymbols(block(5))[0].amplitude).toBe(5); + }); +}); diff --git a/web/tests/unit/inspection.test.js b/web/tests/unit/inspection.test.js index 6a7d998..ef67a2a 100644 --- a/web/tests/unit/inspection.test.js +++ b/web/tests/unit/inspection.test.js @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { inspectBlock } from '../../src/lib/inspection.js'; +import { inspectBlock, previousBlockInScan } from '../../src/lib/inspection.js'; import { appState, ViewMode } from '../../src/lib/state.svelte.js'; import { inspectBlockData, getProcessedRgba } from '../../src/lib/wasm-bridge.js'; @@ -140,3 +140,43 @@ describe('inspection.js - inspectBlock', () => { expect(mseSpan.innerText).toBe('0.00'); }); }); + +describe('previousBlockInScan', () => { + beforeEach(() => { + appState.imgWidth = 100; // 13 luma blocks per row; 50 px chroma → 7 blocks at 4:2:x + appState.imgHeight = 64; + appState.currentCsMode = 444; + }); + + it('steps left within a row and wraps to the end of the previous row', () => { + expect(previousBlockInScan(3, 2, 0)).toEqual({ x: 2, y: 2 }); + expect(previousBlockInScan(0, 2, 0)).toEqual({ x: 12, y: 1 }); + expect(previousBlockInScan(0, 0, 0)).toBeNull(); + }); + + it('uses the coarser chroma grid at 4:2:0', () => { + appState.currentCsMode = 420; + // Luma-grid block (5, 3) is chroma block (2, 1); previous is chroma (1, 1). + expect(previousBlockInScan(5, 3, 1)).toEqual({ x: 2, y: 2 }); + // Chroma block (0, 1) wraps to chroma (6, 0). + expect(previousBlockInScan(1, 2, 2)).toEqual({ x: 12, y: 0 }); + expect(previousBlockInScan(1, 1, 1)).toBeNull(); + }); + + it('keeps luma on its own grid regardless of subsampling', () => { + appState.currentCsMode = 420; + expect(previousBlockInScan(5, 3, 0)).toEqual({ x: 4, y: 3 }); + }); +}); + +describe('inspectBlock - DC prediction', () => { + it('reads the previous block before the current one', () => { + appState.currentViewMode = ViewMode.Y; + appState.imgWidth = 64; + appState.imgHeight = 64; + vi.mocked(inspectBlockData).mockClear(); + inspectBlock(2, 1); + const calls = vi.mocked(inspectBlockData).mock.calls.map(c => [c[0], c[1]]); + expect(calls).toEqual([[1, 1], [2, 1]]); + }); +}); diff --git a/web/tests/unit/wasm-bridge.test.js b/web/tests/unit/wasm-bridge.test.js index 1b85307..4c9536e 100644 --- a/web/tests/unit/wasm-bridge.test.js +++ b/web/tests/unit/wasm-bridge.test.js @@ -5,6 +5,8 @@ import { initSession, processImage, rdEncode, + getCoeffStats, + COEFF_BINS, getViewPtr, getStats, setViewTint, @@ -240,3 +242,32 @@ describe('rdEncode', () => { expect(globalThis.Module._process_image).not.toHaveBeenCalled(); }); }); + +// --------------------------------------------------------------------------- +// getCoeffStats +// --------------------------------------------------------------------------- +describe('getCoeffStats', () => { + it('returns null when no stats are available', () => { + globalThis.Module._get_coeff_stats.mockReturnValueOnce(0); + expect(getCoeffStats(50)).toBeNull(); + }); + + it('parses one histogram + zero fraction record per transform and frees the buffer', () => { + const ptr = 1024; + const view = new DataView(globalThis.Module.HEAPU8.buffer); + const record = COEFF_BINS + 1; + for (let mode = 0; mode < 3; mode++) { + for (let b = 0; b < COEFF_BINS; b++) view.setFloat64(ptr + (mode * record + b) * 8, mode * 100 + b, true); + view.setFloat64(ptr + (mode * record + COEFF_BINS) * 8, 0.1 * (mode + 1), true); + } + globalThis.Module._get_coeff_stats.mockReturnValueOnce(ptr); + + const stats = getCoeffStats(30); + + expect(globalThis.Module._get_coeff_stats).toHaveBeenCalledWith(30); + expect(stats[2].histogram[3]).toBe(203); + expect(stats[0].zeroFraction).toBeCloseTo(0.1); + expect(stats[2].zeroFraction).toBeCloseTo(0.3); + expect(globalThis.Module._free).toHaveBeenCalledWith(ptr); + }); +}); From d8e978df18654979d0e09342a0624559bc040801 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Sat, 10 Oct 2026 00:21:40 +0530 Subject: [PATCH 4/9] feat(a11y): keyboard and screen-reader access to the inspectors - 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 --- web/src/ArtifactInspectorMode.svelte | 17 ++- web/src/InspectorMode.svelte | 7 +- web/src/ViewerMode.svelte | 9 +- web/src/lib/basis-popover.ts | 4 +- web/src/lib/components/ImageViewer.svelte | 10 +- .../components/viewer/CodecControls.svelte | 2 +- .../components/viewer/ImageSourcePanel.svelte | 2 +- .../lib/components/viewer/ToolsPanel.svelte | 4 +- web/src/lib/grid-renderer.ts | 137 ++++++++++++++---- web/src/lib/tooltip.ts | 3 +- web/src/styles/inspector-grid.css | 7 + web/tests/unit/grid-renderer.test.js | 81 ++++++++++- 12 files changed, 231 insertions(+), 52 deletions(-) diff --git a/web/src/ArtifactInspectorMode.svelte b/web/src/ArtifactInspectorMode.svelte index 6eee414..844e99d 100644 --- a/web/src/ArtifactInspectorMode.svelte +++ b/web/src/ArtifactInspectorMode.svelte @@ -361,7 +361,7 @@
{#each artifactModes as mode} - - - - @@ -561,7 +561,7 @@ Zoom out {:else} - - -
@@ -612,7 +612,7 @@ Zoom out {:else} - {:else} - {/each} - - - + {#if appState.currentViewMode === ViewMode.Artifacts}