Skip to content

fix(qfactor): correct 1/√2 offset scaling and LL-band visual weight#437

Merged
osamu620 merged 2 commits into
mainfrom
fix/qfactor-offset-ll-weight
Jun 18, 2026
Merged

fix(qfactor): correct 1/√2 offset scaling and LL-band visual weight#437
osamu620 merged 2 commits into
mainfrom
fix/qfactor-offset-ll-weight

Conversation

@osamu620

Copy link
Copy Markdown
Owner

Summary

Two related correctness fixes to the Qfactor quantization path, validated against the WG1 guideline N100430 (Controlling JPEG 2000 image quality using a single parameter). Applied symmetrically to QCD_marker, QCC_marker, and the estimate_qfactor inverse predictor.

1. 1/√2 offset scaling

The guideline applies the constant offset as part of Δ_Q, before the √G_Y scaling:

Δ_Q   = 2^P0 · α_Q · M_Q + 1/√2
Δ_ref = Δ_Q · √G_Y

The code added eps0 (the normalized 1/√2) outside the √G_Y multiply, so the offset term was too small by a factor of √G_Y ≈ 1.73. The deviation is largest at high Qfactor, where the offset dominates as M_Q → 0 (the best-quality regime).

-      double delta_Q    = alpha_Q * M_Q;
-      double delta_ref  = delta_Q * G_c_sqrt[0] + eps0;
+      double delta_Q    = alpha_Q * M_Q + eps0;
+      double delta_ref  = delta_Q * G_c_sqrt[0];

2. LL-band visual weight at dwt_levels < 5

The guideline states the LL sub-band is assigned visual weight 1.0 in all cases. The old (i >= W_b.size()) guard only catches the LL band when dwt_levels == 5 (LL index == 15 == table size). For fewer levels the LL band picked up a stray detail-band weight — wrong for chroma (QCC) at <5 levels. Now the last band (always LL) is forced to 1.0, while the i >= size clause still covers extra low-freq bands when dwt_levels > 5.

3. estimate_qfactor kept in lock-step

The tool inverts the same formula, so it carried both bugs. Fixed identically so it stays exact against streams from the corrected encoder.

Verification

  • Emitted QCC step sizes match the guideline formula bit-for-bit (independent Python reference), across all 16 subbands of both chroma components.
  • Confirmed empirically that anchoring Δ_ref to √G_Y (not the per-component √G_c) is correct: swapping to G_c_sqrt[Cqcc] collapses all components' LL bands to the identical luminance value, dropping the per-component color weighting.
  • estimate_qfactor recovers the true Q with 0.0000 residual across Q ∈ {10,30,50,65,80,90,97,100} (all piecewise branches) and dwt_levels ∈ {2,3,4} (exercises the LL-weight fix). Before the fix it estimated Q=79 for a true-Q=80 stream.
  • Builds clean; ctest -R "^enc_" and the Part 18 round-trip chain pass.

🤖 Generated with Claude Code

osamu620 and others added 2 commits June 17, 2026 19:46
Per the WG1 guideline (N100430), the Qfactor step-size formula applies
the constant 1/sqrt(2) offset as part of Delta_Q, before the sqrt(G_Y)
scaling:
  Delta_Q   = 2^P0 * alpha_Q * M_Q + 1/sqrt(2)
  Delta_ref = Delta_Q * sqrt(G_Y)
The code added eps0 (the normalized 1/sqrt(2)) outside the sqrt(G_Y)
multiply, making the offset term too small by a factor of sqrt(G_Y)
(~1.73). The deviation is largest at high Qfactor, where the offset
dominates as M_Q -> 0 (the best-quality regime).

Separately, the LL sub-band visual weight was only forced to 1.0 when
dwt_levels == 5 (LL index == weight-table size). For fewer levels the
LL band picked up a stray detail-band weight - wrong for chroma (QCC)
at <5 levels. Force the last band (always LL) to 1.0.

Both fixes are applied symmetrically to QCD_marker and QCC_marker, and
to the estimate_qfactor inverse predictor so it stays exact against
corrected streams (verified: Q recovered with 0.0000 residual across
Q in {10..100} and dwt_levels in {2,3,4}).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The enclosing else already peels off qfactor==0xFF (handled by the
preceding if), so the `(qfactor != 0xFF) ? ... : basestep/...` ternary
was always true and its else arm unreachable. Simplify to the direct
expression, matching QCC_marker which never carried the redundant check.
No change to emitted step sizes.

Closes #436

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR corrects two guideline-defined details in the Qfactor-based quantization step-size computation (encoder-side QCD_marker/QCC_marker) and keeps the estimate_qfactor inverse predictor mathematically consistent with the corrected encoder behavior.

Changes:

  • Applies the 1/√2 offset inside Δ_Q (before √G_Y scaling) so the offset scales correctly with G_c_sqrt[0].
  • Forces the LL band’s visual weight to 1.0 for all dwt_levels by explicitly treating the last band as LL (while still defaulting to 1.0 for bands beyond the 5-level table when dwt_levels > 5).
  • Removes an effectively-dead qfactor == 0xFF ternary inside the qfactor-only branch (since 0xFF is handled by the earlier branch).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
source/core/codestream/j2kmarkers.cpp Fixes Qfactor quantization math for offset placement and LL-band weighting in emitted QCD/QCC marker step sizes.
source/apps/estimate_qfactor/main.cpp Mirrors the same formula fixes so estimated Qfactor remains exact against streams produced by the corrected encoder.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@osamu620 osamu620 merged commit b74a380 into main Jun 18, 2026
10 checks passed
@osamu620 osamu620 deleted the fix/qfactor-offset-ll-weight branch June 18, 2026 07:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants