From a76ebe05341b3afdbd73e5cf8b0950fb627b04f6 Mon Sep 17 00:00:00 2001 From: Osamu Watanabe Date: Mon, 6 Jul 2026 01:35:05 +0900 Subject: [PATCH 1/2] fix(encoder): force the MCT off when component sub-sampling differs on either axis The Cycc guard only disabled the color transform when components differed in BOTH XRsiz and YRsiz, so 4:2:2-shaped input (horizontal-only mismatch) kept the ICT enabled across differently-sized component buffers and corrupted the heap ("double free or corruption" on a plain 3-component PGX encode with the default Cycc=yes). Any single-axis mismatch already rules out the built-in MCT, so the guard now ORs the per-axis checks. Applied to both the buffered and streaming entry points; regression tests (enc_qf_422_legacy) land with the sub-sampled-weighting commit. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01M9cKpzqghsLxJUu7kkyRAw --- source/core/interface/encoder.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/source/core/interface/encoder.cpp b/source/core/interface/encoder.cpp index b5a907c7..76897743 100644 --- a/source/core/interface/encoder.cpp +++ b/source/core/interface/encoder.cpp @@ -616,8 +616,11 @@ size_t openhtj2k_encoder_impl::invoke_internal() { } // check component size - if (siz->Csiz == 3 && cod->use_color_trafo == 1 && (XRsiz[0] != XRsiz[1] || XRsiz[1] != XRsiz[2]) - && (YRsiz[0] != YRsiz[1] || YRsiz[1] != YRsiz[2])) { + // The MCT requires identically-sized components: a mismatch on EITHER axis + // (e.g. 4:2:2, which differs only horizontally) must force it off, or the + // color transform runs across differently-sized buffers and corrupts memory. + if (siz->Csiz == 3 && cod->use_color_trafo == 1 + && (XRsiz[0] != XRsiz[1] || XRsiz[1] != XRsiz[2] || YRsiz[0] != YRsiz[1] || YRsiz[1] != YRsiz[2])) { cod->use_color_trafo = 0; printf("WARNING: Cycc is set to 'no' because size of each component is not identical.\n"); } @@ -805,8 +808,11 @@ size_t openhtj2k_encoder_impl::invoke_line_based_stream( YRsiz.push_back(siz->YRsiz[c]); } - if (siz->Csiz == 3 && cod->use_color_trafo == 1 && (XRsiz[0] != XRsiz[1] || XRsiz[1] != XRsiz[2]) - && (YRsiz[0] != YRsiz[1] || YRsiz[1] != YRsiz[2])) { + // The MCT requires identically-sized components: a mismatch on EITHER axis + // (e.g. 4:2:2, which differs only horizontally) must force it off, or the + // color transform runs across differently-sized buffers and corrupts memory. + if (siz->Csiz == 3 && cod->use_color_trafo == 1 + && (XRsiz[0] != XRsiz[1] || XRsiz[1] != XRsiz[2] || YRsiz[0] != YRsiz[1] || YRsiz[1] != YRsiz[2])) { cod->use_color_trafo = 0; printf("WARNING: Cycc is set to 'no' because size of each component is not identical.\n"); } From 20047cb89c4a216276745e23440ccbdaa8d3de8d Mon Sep 17 00:00:00 2001 From: Osamu Watanabe Date: Mon, 6 Jul 2026 01:37:38 +0900 Subject: [PATCH 2/2] feat(qfactor): sub-sampling-aware analytic chroma weighting via component-type hints The analytic (Qcsf=mannos/daly) chroma path already derived 4:2:0/4:2:2 weights from one chroma CSF via a per-axis (sx, sy) frequency shift, but it was unreachable for genuinely sub-sampled input: sub-sampling forces the MCT off, and the no-MCT branch routed every component to the luminance CSF because the codestream cannot label channels. This makes the mapping usable end to end: - Qctype=Y,Cb,Cr (encoder) / --ctype (estimate_qfactor): per-component role hints for no-MCT input; hinted Cb/Cr take the chroma CSF with the component's SIZ XRsiz/YRsiz folded into the per-axis frequency mapping. Unhinted components stay generic (luminance CSF), now also with the (sx, sy) shift. New API: set_component_types(). - (sx, sy) now come from the per-component SIZ fields threaded through QCC_marker, with the 444/420/422 chroma_format mapping as fallback; luma_visual_weights() gains the same per-axis form (LH from the vertical axis, HL from the horizontal), bit-identical at (1,1). - Qchromacsf=luma / --chroma-csf luma: A/B switch that keeps the sub-sampling mapping but evaluates the luminance CSF for Cb/Cr, separating the CSF shape from the frequency mapping. New API: set_chroma_csf_reuse_luma(). - visual_weight_check (tests/tools): dumps the derived weight table for any (ctype, sx, sy, ppd, zoom, levels) and asserts the mapping invariants: (1,1) bit-identity with the pre-generalization output, the exact one-level shift at (2,2) (the "chroma one level down" rule falls out of the mapping; no manual offset), 4:2:2 HL > LH anisotropy (impossible for a scalar table), and the generic default role. - Tests: qf_vw_invariants plus 4:2:0 and 4:2:2 encode<->estimate round-trips (residual 0), a negative control proving the ctype hints actually reach the emitted QCC bytes, and a legacy 4:2:2 encode that regression-guards the preceding MCT guard fix. Tiny synthetic 4:2:2 PGX fixtures added (QCD/QCC bytes never depend on sample data). Defaults are unchanged: legacy remains byte-identical (existing golden test), and all analytic 4:4:4 outputs were verified byte-identical against pre-change encodes across Qfactor x model. Encoder-only; no bitstream syntax or decoder change. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01M9cKpzqghsLxJUu7kkyRAw --- CMakeLists.txt | 5 + conformance_data/references/synth422_cb.pgx | Bin 0 -> 2064 bytes conformance_data/references/synth422_cr.pgx | Bin 0 -> 2064 bytes conformance_data/references/synth422_y.pgx | Bin 0 -> 4112 bytes docs/cli_encoder.md | 11 + docs/qfactor.md | 51 ++- source/apps/encoder/enc_utils.hpp | 56 ++- source/apps/encoder/main_enc.cpp | 13 +- source/apps/estimate_qfactor/main.cpp | 192 ++++++----- source/core/codestream/j2kmarkers.cpp | 66 ++-- source/core/codestream/j2kmarkers.hpp | 22 +- source/core/codestream/visual_weighting.hpp | 126 +++++-- source/core/interface/encoder.cpp | 34 +- source/core/interface/encoder.hpp | 45 ++- tests/encoder_test.cmake | 47 +++ .../tools/visual_weight_check/CMakeLists.txt | 8 + tests/tools/visual_weight_check/main.cpp | 325 ++++++++++++++++++ 17 files changed, 819 insertions(+), 182 deletions(-) create mode 100644 conformance_data/references/synth422_cb.pgx create mode 100644 conformance_data/references/synth422_cr.pgx create mode 100644 conformance_data/references/synth422_y.pgx create mode 100644 tests/tools/visual_weight_check/CMakeLists.txt create mode 100644 tests/tools/visual_weight_check/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a9117891..bf7acbae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -306,6 +306,11 @@ add_executable(codestream_bounds_check) add_subdirectory(tests/tools/codestream_bounds_check) target_link_libraries(codestream_bounds_check PUBLIC open_htj2k) +# visual_weight_check: dump + invariant self-check for the analytic per-subband +# visual (CSF) weighting (header-only, no library dependency) +add_executable(visual_weight_check) +add_subdirectory(tests/tools/visual_weight_check) + # region_decode_bench: M1 region-decode latency/throughput benchmark (OSD eval) add_executable(region_decode_bench) add_subdirectory(tests/tools/region_decode_bench) diff --git a/conformance_data/references/synth422_cb.pgx b/conformance_data/references/synth422_cb.pgx new file mode 100644 index 0000000000000000000000000000000000000000..c57b4f31ac4f184da73e1ded1ab5586e3ea0b2a7 GIT binary patch literal 2064 zcmai!hgXha7>8vhBdg4Kvz1v%D6?S{Ss59b$tYWB*dvM*GBcxPw#dxP%t&U0%*@P| z&i%djd%o}a-se5{KX9Gjxz71r&JKc;qhKmn3Fc;krG;LJQf13m)URext8RVc#?4Gx zwKccwXx+7kU9Z0V1`HlLVpKW3N>!>G)~?r}VUy;jt=n02=w#Ear+sh7{sV^$a~(Z) zLglJ8YSu9l8Z~XvvW;1LtIl1z+dA}davtO|eB_vM6DLnq1iOu&Wou_EA@3&x6o zV|FoH5h71+M4sGQZZg#XLo*ORWVK^At9Q!bIG SJR(mzM4ohsJn0j8GAssdlod$; literal 0 HcmV?d00001 diff --git a/conformance_data/references/synth422_cr.pgx b/conformance_data/references/synth422_cr.pgx new file mode 100644 index 0000000000000000000000000000000000000000..c57b4f31ac4f184da73e1ded1ab5586e3ea0b2a7 GIT binary patch literal 2064 zcmai!hgXha7>8vhBdg4Kvz1v%D6?S{Ss59b$tYWB*dvM*GBcxPw#dxP%t&U0%*@P| z&i%djd%o}a-se5{KX9Gjxz71r&JKc;qhKmn3Fc;krG;LJQf13m)URext8RVc#?4Gx zwKccwXx+7kU9Z0V1`HlLVpKW3N>!>G)~?r}VUy;jt=n02=w#Ear+sh7{sV^$a~(Z) zLglJ8YSu9l8Z~XvvW;1LtIl1z+dA}davtO|eB_vM6DLnq1iOu&Wou_EA@3&x6o zV|FoH5h71+M4sGQZZg#XLo*ORWVK^At9Q!bIG SJR(mzM4ohsJn0j8GAssdlod$; literal 0 HcmV?d00001 diff --git a/conformance_data/references/synth422_y.pgx b/conformance_data/references/synth422_y.pgx new file mode 100644 index 0000000000000000000000000000000000000000..d24c67a430a1eff9e4c2e8926900774dcc6232f5 GIT binary patch literal 4112 zcmbuC_g9Z$7{()tFGXapuRXFQ*;_`Dz4zXGk5KmBd+)us2oWM=W@bi+5Fz56_j-SL z-}9X3-1qZ6=lKIZ=Q;O%-Pgz0%j)B8b+dX}J>364i=Dkg%G7DnWyqW@XYPCjixewa z#-n_tsx@lYZP2iBvzBezck0@scfWx{hL0LMVN$@fnRDhZTDoHOx{X`5@7jCd@UfGD zjuxl1=`&`@o-0rOLT<%NxtFU@xmwLS^}HK3Y2K=BhtA!4_US)p=!ntd{3cJCK5OoR z#miQ%S-)xPj@|nX9yxyM%(-A^7uQT#b6E2hC|tBe>9U>`t5mP$Ro}lXU|`}9I6Z+bm7vKYd6A`!9%WI zzj^y!gfe*8tvmN0K8fW2IKb~dc=Yu7t0?}D1N`x`7q8#GkKz9~z+b+3_uM3bNs>8QC^IeuPfVITMJi_(%8U!av38Uh z7lLE$q{}G-$J$AkQwEN_kS?bT9C;y56#_W&LYyiDa0Hz=RS4h+I#V3d!4Y((IHZFk zp-pi}2S-BdInD-0LhCus2FGd9bDRy1(-No(j?)sX3XYo?stS&q7>*oA93oZdH!%V^ zjyPl=^GM`4;*fpJqg26h%wtr+(L&->!O=nz*gtM;AvV^hARFsbh>i6rOttsvYp2@# z^tDs%efrw53#$&U)u(87Vb#I4`V`A9tU9<>pW^9F+XSxFr$luhu|7*k^v6Eg$DD%A-K?BJ~+%A8q5cWng0U~S15`A literal 0 HcmV?d00001 diff --git a/docs/cli_encoder.md b/docs/cli_encoder.md index d9512f68..c5226e2c 100644 --- a/docs/cli_encoder.md +++ b/docs/cli_encoder.md @@ -86,6 +86,17 @@ JPH file format. - `Qzoom=Float` *(experimental)* - Display magnification; `> 1` is zoom-in, which flattens the weighting toward flat MSE-optimal quantization. Default **1.0**. +- `Qctype=T,T,T` *(experimental)* + - Per-component role hints (`Y|Cb|Cr|generic`) for the analytic models + when the input is pre-decorrelated YCbCr **without** an MCT — the + sub-sampled 4:2:0 / 4:2:2 configuration, where the codestream cannot + label channels itself. `Qctype=Y,Cb,Cr` selects the chroma CSF (with + the component's sub-sampling frequency shift) for components 1 and 2. + Default **generic** (luminance CSF). +- `Qchromacsf=chroma|luma` *(experimental)* + - CSF shape used for `Cb`/`Cr` components: the low-pass chroma CSF + (default) or the luminance CSF (`luma`), keeping the sub-sampling + frequency mapping — an A/B switch separating shape from mapping. - See [`qfactor.md`](qfactor.md) for the analytic visual-weighting model. ### JPH and component layout diff --git a/docs/qfactor.md b/docs/qfactor.md index eabf2bd9..a76a1e37 100644 --- a/docs/qfactor.md +++ b/docs/qfactor.md @@ -139,8 +139,36 @@ parameter pair per opponent channel calibrated at `Qppd=72`: Chroma subsampling is **not** a separate model: 4:2:0 / 4:2:2 are the same CSF sampled at frequencies shifted by the horizontal / vertical subsampling factors -`(sx, sy)`. A 4:2:2 channel therefore gets different `LH` (vertical detail) and -`HL` (horizontal detail) weights for free. +`(sx, sy)` — taken per component from the SIZ `XRsiz`/`YRsiz` fields. A +component sub-sampled by `s` on an axis sits on a grid `s×` coarser, so that +axis's angular frequency divides by `s`. Two consequences fall out of the +mapping with no special-casing: + +- **Isotropic 4:2:0 is exactly a one-level shift.** Halving both axes halves + every subband's radial frequency, which is identical to moving one + decomposition level coarser — the classic "weight chroma like luma one level + down" rule. Do not add a manual level offset on top; the `(sx, sy)` scaling + already subsumes it (this equivalence is asserted bit-for-bit by + `visual_weight_check check`). +- **4:2:2 is anisotropic.** Only the horizontal axis is halved, so a 4:2:2 + channel gets different `LH` (vertical detail) and `HL` (horizontal detail) + weights — something no scalar per-subband table can represent. + +**Component roles need a hint when chroma is sub-sampled.** Sub-sampled +components rule out the built-in MCT (it needs identically-sized components), +and without an MCT the codestream does not label channels — the encoder cannot +know component 1 is `Cb`. Pass `Qctype=Y,Cb,Cr` to select the chroma CSF; +unhinted components default to `generic` (luminance CSF, still with the +`(sx, sy)` frequency shift). `Qchromacsf=luma` is an A/B switch that keeps the +mapping but swaps the chroma shape for the luminance CSF. + +The `visual_weight_check` test tool (built with the test suite) dumps the +derived table for any configuration, e.g. for a 4:2:2 `Cb` channel: + +```bash +visual_weight_check dump --ctype Cb --sx 2 --sy 1 # analytic +visual_weight_check dump --ctype Cb --legacy-format 422 # legacy table row +``` ### Colour transform and gains @@ -155,16 +183,24 @@ reconstructed RGB. The model in force is resolved per encode: Legacy mode always assumes `ict` (reproducing historical behaviour); analytic modes honour the MCT actually applied, so an undecorrelated RGB encode gets unit -gains and the **luminance** CSF on every channel (never the chroma roll-off). +gains and the **luminance** CSF on every channel. The one exception is a +`Qctype` hint: a no-MCT component labelled `Cb`/`Cr` (pre-decorrelated, +typically sub-sampled YCbCr input) takes the chroma CSF while keeping unit +colour gain — there is no inverse transform amplifying its error, but its +content is still chrominance. ## CLI reference -All three are encoder options and require `Qfactor`: +All of these are encoder options and require `Qfactor`: - `Qcsf=legacy|mannos|daly` — visual-weighting model. Default **legacy** (bit-identical). `mannos` / `daly` are experimental. - `Qppd=Float` — reference pixels-per-degree at zoom 1.0. Default **72**. - `Qzoom=Float` — display magnification; `> 1` is zoom-in. Default **1.0**. +- `Qctype=T,T,T` — per-component role hints (`Y|Cb|Cr|generic`) for analytic + models on no-MCT (e.g. sub-sampled YCbCr) input. Default **generic**. +- `Qchromacsf=chroma|luma` — CSF shape for `Cb`/`Cr` components. Default + **chroma** (the low-pass chroma CSF). When an analytic model is selected the encoder prints an `EXPERIMENTAL:` status line; selecting one without `Qfactor` prints a warning and has no effect. @@ -177,6 +213,10 @@ encoder.set_output_buffer(out); // EXPERIMENTAL: model 0 = legacy (default), 1 = Mannos–Sakrison, 2 = Daly. // ref_ppd / zoom ≤ 0 keep their defaults. Call before invoke_*. encoder.set_visual_weighting(/*model=*/1, /*ref_ppd=*/72.0, /*zoom=*/2.0); +// EXPERIMENTAL: component roles for no-MCT input (0 generic, 1 Y, 2 Cb, 3 Cr) +// and the chroma-CSF A/B switch — the Qctype= / Qchromacsf= equivalents. +encoder.set_component_types(1, 2, 3); +encoder.set_chroma_csf_reuse_luma(false); encoder.invoke_line_based(); ``` @@ -198,6 +238,9 @@ estimate_qfactor out.j2c # Analytic encode — pass the same model / viewing condition used at encode time estimate_qfactor out.j2c --csf mannos --zoom 2 +# Sub-sampled YCbCr analytic encode — pass the same role hints too +estimate_qfactor out420.j2c --csf mannos --ctype Y,Cb,Cr + # Scriptable check mode (CI): assert the recovered Q and a residual ceiling estimate_qfactor out.j2c --csf mannos --expect-q 90 --max-residual 0.01 ``` diff --git a/source/apps/encoder/enc_utils.hpp b/source/apps/encoder/enc_utils.hpp index f5731ca0..42ddbb90 100644 --- a/source/apps/encoder/enc_utils.hpp +++ b/source/apps/encoder/enc_utils.hpp @@ -89,6 +89,13 @@ void print_help(char *cmd) { "Qppd=Float (EXPERIMENTAL) reference pixels-per-degree at zoom 1.0 (default 72).\n" "Qzoom=Float (EXPERIMENTAL) display magnification, >1 = zoom-in (default 1.0);\n" " zoom-in flattens the weighting toward flat MSE-optimal quantization.\n"); + printf( + "Qctype=T,T,T (EXPERIMENTAL) per-component role hints (Y|Cb|Cr|generic) for the\n" + " analytic models when input is pre-decorrelated YCbCr (no MCT), e.g. sub-sampled\n" + " 4:2:0/4:2:2: the codestream cannot label channels, so Qctype=Y,Cb,Cr selects the\n" + " chroma CSF for components 1 and 2. Default generic (luminance CSF).\n" + "Qchromacsf=chroma|luma (EXPERIMENTAL) CSF shape for Cb/Cr: low-pass chroma CSF\n" + " (default) or reuse the luminance CSF (A/B-tests shape vs sub-sampling mapping).\n"); printf( "-jph_color_space\n" " Color space of input components: Valid entry is one of RGB, YCC.\n If inputs are represented in " @@ -154,9 +161,11 @@ class j2k_argset { bool qderived; uint8_t qfactor; // EXPERIMENTAL analytic visual (CSF) weighting for the Qfactor path. - uint8_t csf_model; // 0 = legacy table (default), 1 = Mannos-Sakrison, 2 = Daly - double csf_ppd; // reference pixels-per-degree at zoom 1.0 - double csf_zoom; // display magnification (> 1 = zoom-in) + uint8_t csf_model; // 0 = legacy table (default), 1 = Mannos-Sakrison, 2 = Daly + double csf_ppd; // reference pixels-per-degree at zoom 1.0 + double csf_zoom; // display magnification (> 1 = zoom-in) + uint8_t csf_ctype[3]; // per-component role hints: 0 generic, 1 Y, 2 Cb, 3 Cr + bool csf_chroma_reuse_luma; // A/B: luminance CSF shape for Cb/Cr components static void get_coordinate(const std::string ¶m_name, std::string &arg, element_siz_local &dims) { size_t pos0, pos1; @@ -411,6 +420,8 @@ class j2k_argset { csf_model(0), csf_ppd(72.0), csf_zoom(1.0), + csf_ctype{0, 0, 0}, + csf_chroma_reuse_luma(false), ifnames{}, num_iteration(1), num_threads(0), @@ -427,7 +438,7 @@ class j2k_argset { is_i_found = true; } else if ((tmp.front() == '-' || tmp.front() == 'C' || tmp.front() == 'S' || tmp.front() == 'Q') && is_i_found) { - if (tmp[1] == ':') continue; // for Windows + if (tmp[1] == ':') continue; // for Windows fname_stop = i; is_i_found = false; } @@ -596,6 +607,41 @@ class j2k_argset { csf_ppd = get_numerical_param(c, param, arg, 1.0, 100000.0); } else if (param == "zoom") { csf_zoom = get_numerical_param(c, param, arg, 0.01, 1000.0); + } else if (param == "ctype") { + // EXPERIMENTAL: comma-separated per-component role hints for analytic + // weighting of sub-sampled (no-MCT) input, e.g. Qctype=Y,Cb,Cr. + std::string val = arg.substr(arg.find_first_of('=') + 1); + size_t idx = 0, pos = 0; + while (idx < 3 && pos <= val.size()) { + size_t comma = val.find(',', pos); + std::string token = val.substr(pos, (comma == std::string::npos) ? comma : comma - pos); + if (token == "Y" || token == "y") { + csf_ctype[idx] = 1; + } else if (token == "Cb" || token == "cb" || token == "CB") { + csf_ctype[idx] = 2; + } else if (token == "Cr" || token == "cr" || token == "CR") { + csf_ctype[idx] = 3; + } else if (token == "generic" || token == "-") { + csf_ctype[idx] = 0; + } else { + printf("ERROR: unknown Qctype entry '%s' (use Y|Cb|Cr|generic)\n", token.c_str()); + exit(EXIT_FAILURE); + } + ++idx; + if (comma == std::string::npos) break; + pos = comma + 1; + } + } else if (param == "chromacsf") { + // EXPERIMENTAL: chroma CSF shape selection (A/B vs the frequency mapping). + std::string val = arg.substr(arg.find_first_of('=') + 1); + if (val == "chroma") { + csf_chroma_reuse_luma = false; + } else if (val == "luma") { + csf_chroma_reuse_luma = true; + } else { + printf("ERROR: unknown Qchromacsf value '%s' (use chroma|luma)\n", val.c_str()); + exit(EXIT_FAILURE); + } } else { printf("ERROR: unknown parameter Q%s\n", param.c_str()); exit(EXIT_FAILURE); @@ -633,4 +679,6 @@ class j2k_argset { OPENHTJ2K_NODISCARD uint8_t get_csf_model() const { return csf_model; } OPENHTJ2K_NODISCARD double get_csf_ppd() const { return csf_ppd; } OPENHTJ2K_NODISCARD double get_csf_zoom() const { return csf_zoom; } + OPENHTJ2K_NODISCARD uint8_t get_csf_ctype(size_t c) const { return (c < 3) ? csf_ctype[c] : 0; } + OPENHTJ2K_NODISCARD bool get_csf_chroma_reuse_luma() const { return csf_chroma_reuse_luma; } }; diff --git a/source/apps/encoder/main_enc.cpp b/source/apps/encoder/main_enc.cpp index b8499afa..e7399913 100644 --- a/source/apps/encoder/main_enc.cpp +++ b/source/apps/encoder/main_enc.cpp @@ -61,7 +61,6 @@ // Stream readers (PNM / PGX / TIFF) live in the openhtj2k_imgio static lib; // see source/apps/imgio/imgio.hpp. - int main(int argc, char *argv[]) { j2k_argset args(argc, argv); // parsed command line std::vector fnames = args.ifnames; @@ -98,8 +97,8 @@ int main(int argc, char *argv[]) { std::string fext = out_filename.substr(pos, 4); if (fext == ".jph" || fext == ".JPH") { isJPH = true; - } else if (fext.compare(".j2c") && fext.compare(".j2k") && fext.compare(".jphc") && fext.compare(".J2C") - && fext.compare(".J2K") && fext.compare(".JPHC")) { + } else if (fext.compare(".j2c") && fext.compare(".j2k") && fext.compare(".jphc") + && fext.compare(".J2C") && fext.compare(".J2K") && fext.compare(".JPHC")) { printf("ERROR: invalid extension for output file\n"); exit(EXIT_FAILURE); } @@ -119,8 +118,8 @@ int main(int argc, char *argv[]) { if (!reader) return EXIT_FAILURE; element_siz_local image_origin_s = args.get_origin(); element_siz_local image_size_s(reader->get_width(), reader->get_height()); - stat_width = reader->get_width(); - stat_height = reader->get_height(); + stat_width = reader->get_width(); + stat_height = reader->get_height(); uint16_t nc_s = reader->get_num_components(); uint8_t bd_s = reader->get_bitdepth(0); @@ -181,6 +180,8 @@ int main(int argc, char *argv[]) { if (!toFile) encoder.set_output_buffer(outbuf); // EXPERIMENTAL: analytic visual weighting (Qcsf/Qppd/Qzoom); legacy default is a no-op. encoder.set_visual_weighting(args.get_csf_model(), args.get_csf_ppd(), args.get_csf_zoom()); + encoder.set_component_types(args.get_csf_ctype(0), args.get_csf_ctype(1), args.get_csf_ctype(2)); + encoder.set_chroma_csf_reuse_luma(args.get_csf_chroma_reuse_luma()); try { total_size = encoder.invoke_line_based_stream( [&reader](uint32_t y, int32_t **rows, uint16_t nc) { reader->get_row(y, rows, nc); }); @@ -267,6 +268,8 @@ int main(int argc, char *argv[]) { if (!toFile) encoder.set_output_buffer(outbuf); // EXPERIMENTAL: analytic visual weighting (Qcsf/Qppd/Qzoom); legacy default is a no-op. encoder.set_visual_weighting(args.get_csf_model(), args.get_csf_ppd(), args.get_csf_zoom()); + encoder.set_component_types(args.get_csf_ctype(0), args.get_csf_ctype(1), args.get_csf_ctype(2)); + encoder.set_chroma_csf_reuse_luma(args.get_csf_chroma_reuse_luma()); try { total_size = encoder.invoke_line_based(); } catch (std::exception &exc) { diff --git a/source/apps/estimate_qfactor/main.cpp b/source/apps/estimate_qfactor/main.cpp index 60f5ef94..c08183a5 100644 --- a/source/apps/estimate_qfactor/main.cpp +++ b/source/apps/estimate_qfactor/main.cpp @@ -33,29 +33,29 @@ constexpr uint8_t YCC420 = 1; constexpr uint8_t YCC422 = 2; struct Band { - uint8_t epsilon; // 0..31 - uint16_t mantissa; // 0..2047 + uint8_t epsilon; // 0..31 + uint16_t mantissa; // 0..2047 }; struct QuantMarker { - uint16_t component_index; // 0xFFFF for QCD, otherwise QCC's Cqcc - uint8_t Sq; // raw Sqcd/Sqcc - uint8_t style; // Sq & 0x1F (0=lossless, 1=derived, 2=expounded) - uint8_t guardbits; // Sq >> 5 - std::vector bands; // signaling order: LL_N, HL_N, LH_N, HH_N, HL_{N-1}, ... - std::vector raw; // verbatim segment bytes (marker..end), for --dump-quant + uint16_t component_index; // 0xFFFF for QCD, otherwise QCC's Cqcc + uint8_t Sq; // raw Sqcd/Sqcc + uint8_t style; // Sq & 0x1F (0=lossless, 1=derived, 2=expounded) + uint8_t guardbits; // Sq >> 5 + std::vector bands; // signaling order: LL_N, HL_N, LH_N, HH_N, HL_{N-1}, ... + std::vector raw; // verbatim segment bytes (marker..end), for --dump-quant }; struct Header { // SIZ uint16_t Csiz = 0; - std::vector Ssiz; // bit-depth byte (top bit = signed) + std::vector Ssiz; // bit-depth byte (top bit = signed) std::vector XRsiz; std::vector YRsiz; // COD - uint8_t dwt_levels = 5; - uint8_t transformation = 0; // 0 = 9/7 (irreversible), 1 = 5/3 (reversible) - bool use_color_trafo = false; // SGcod MCT byte != 0 + uint8_t dwt_levels = 5; + uint8_t transformation = 0; // 0 = 9/7 (irreversible), 1 = 5/3 (reversible) + bool use_color_trafo = false; // SGcod MCT byte != 0 // Quantization std::vector qmarkers; }; @@ -76,11 +76,11 @@ bool parse_main_header(const std::vector& buf, Header& out) { fprintf(stderr, "ERROR: input does not start with SOC marker (0xFF4F)\n"); return false; } - size_t p = 2; + size_t p = 2; bool got_siz = false, got_cod = false, got_qcd = false; while (p + 4 <= buf.size()) { uint16_t marker = rd_u16(&buf[p]); - if (marker == SOT) break; // end of main header + if (marker == SOT) break; // end of main header if ((marker & 0xFF00) != 0xFF00) { fprintf(stderr, "ERROR: invalid marker 0x%04X at offset %zu\n", marker, p); return false; @@ -93,7 +93,7 @@ bool parse_main_header(const std::vector& buf, Header& out) { return false; } const uint8_t* body = &buf[p + 2]; - size_t body_len = static_cast(Lmar) - 2; + size_t body_len = static_cast(Lmar) - 2; switch (marker) { case SIZ: { @@ -116,7 +116,7 @@ bool parse_main_header(const std::vector& buf, Header& out) { // SGcod (4 bytes): progression, layers (2), MCT (1) out.use_color_trafo = body[1 + 3] != 0; // SPcod: NLevels, cblkW, cblkH, style, transformation - out.dwt_levels = body[1 + 4 + 0]; + out.dwt_levels = body[1 + 4 + 0]; out.transformation = body[1 + 4 + 4]; (void)Scod; got_cod = true; @@ -133,29 +133,29 @@ bool parse_main_header(const std::vector& buf, Header& out) { if (marker == QCC) { if (out.Csiz < 257) { qm.component_index = body[0]; - off = 1; + off = 1; } else { qm.component_index = rd_u16(&body[0]); - off = 2; + off = 2; } } else { qm.component_index = 0xFFFF; } if (off + 1 > body_len) return false; - qm.Sq = body[off++]; - qm.style = static_cast(qm.Sq & 0x1F); + qm.Sq = body[off++]; + qm.style = static_cast(qm.Sq & 0x1F); qm.guardbits = static_cast(qm.Sq >> 5); - size_t band_count = 0; + size_t band_count = 0; size_t bytes_per_band = 0; - if (qm.style == 0) { // no-quant (lossless): 1 byte per band, ε in upper 5 bits - band_count = (body_len - off); + if (qm.style == 0) { // no-quant (lossless): 1 byte per band, ε in upper 5 bits + band_count = (body_len - off); bytes_per_band = 1; - } else if (qm.style == 1) { // scalar-derived: one (ε, μ) for LL only - band_count = 1; + } else if (qm.style == 1) { // scalar-derived: one (ε, μ) for LL only + band_count = 1; bytes_per_band = 2; - } else if (qm.style == 2) { // scalar-expounded: 2 bytes per band - band_count = (body_len - off) / 2; + } else if (qm.style == 2) { // scalar-expounded: 2 bytes per band + band_count = (body_len - off) / 2; bytes_per_band = 2; } else { fprintf(stderr, "ERROR: unknown quant style %u\n", qm.style); @@ -167,8 +167,7 @@ bool parse_main_header(const std::vector& buf, Header& out) { qm.bands.push_back({static_cast(body[off] >> 3), 0}); } else { uint16_t v = rd_u16(&body[off]); - qm.bands.push_back({static_cast(v >> 11), - static_cast(v & 0x7FF)}); + qm.bands.push_back({static_cast(v >> 11), static_cast(v & 0x7FF)}); } off += bytes_per_band; } @@ -191,25 +190,28 @@ bool parse_main_header(const std::vector& buf, Header& out) { // Forward step-size predictor — mirrors QCD_marker / QCC_marker exactly. // Returns predicted (epsilon, mantissa) per band in QCD signaling order // (LL_N, HL_N, LH_N, HH_N, HL_{N-1}, ..., HL_1, LH_1, HH_1). -std::vector predict_bands(uint8_t qfactor, uint8_t dwt_levels, uint8_t RI, - uint8_t Cqcc, uint8_t chroma_format, - const open_htj2k::visual_weighting_params& vp, bool mct_on) { +std::vector predict_bands(uint8_t qfactor, uint8_t dwt_levels, uint8_t RI, uint8_t Cqcc, + uint8_t chroma_format, const open_htj2k::visual_weighting_params& vp, + bool mct_on, uint8_t sub_x = 0, uint8_t sub_y = 0) { const std::vector D97SL = {-0.091271763114250, -0.057543526228500, 0.591271763114250, 1.115087052457000, 0.5912717631142500, -0.05754352622850, -0.091271763114250}; - const std::vector D97SH = {0.053497514821622, 0.033728236885750, - -0.156446533057980, -0.533728236885750, - 1.205898036472720, -0.533728236885750, - -0.156446533057980, 0.033728236885750, - 0.053497514821622}; + const std::vector D97SH = {0.053497514821622, 0.033728236885750, -0.156446533057980, + -0.533728236885750, 1.205898036472720, -0.533728236885750, + -0.156446533057980, 0.033728236885750, 0.053497514821622}; // Visual weights from the shared encoder header (single source of truth), so the // inversion tracks whatever model produced the file. Component 0 is luma (QCD); - // components 1/2 are chroma (QCC) under a luma/chroma transform. + // components 1/2 are chroma (QCC) under a luma/chroma transform, otherwise their + // role comes from vp.ctype_hint (--ctype) with the component's SIZ sub-sampling + // factors folded into the analytic frequency mapping -- mirroring QCC_marker. const open_htj2k::color_transform ct = open_htj2k::resolve_color_transform(vp, mct_on); + const open_htj2k::component_type ctype = + (Cqcc < 3) ? vp.ctype_hint[Cqcc] : open_htj2k::component_type::generic; const std::vector weights = - (Cqcc == 0) ? open_htj2k::luma_visual_weights(dwt_levels, vp) - : open_htj2k::chroma_visual_weights(dwt_levels, vp, Cqcc, chroma_format, ct); + (Cqcc == 0) + ? open_htj2k::luma_visual_weights(dwt_levels, vp) + : open_htj2k::chroma_visual_weights(dwt_levels, vp, Cqcc, chroma_format, ct, ctype, sub_x, sub_y); // Build wmse in the encoder's accumulation order: HH_1, LH_1, HL_1, ..., LL_N. const size_t num_bands = static_cast(3 * dwt_levels + 1); @@ -221,13 +223,13 @@ std::vector predict_bands(uint8_t qfactor, uint8_t dwt_levels, uint8_t RI, wmse.push_back(1.0); } else { for (uint8_t lvl = 0; lvl < dwt_levels; ++lvl) { - gain_low = 0; + gain_low = 0; gain_high = 0; for (double e : outL) gain_low += e * e; for (double e : outH) gain_high += e * e; - wmse.push_back(gain_high * gain_high); // HH - wmse.push_back(gain_low * gain_high); // LH - wmse.push_back(gain_high * gain_low); // HL + wmse.push_back(gain_high * gain_high); // HH + wmse.push_back(gain_low * gain_high); // LH + wmse.push_back(gain_high * gain_low); // HL auto upsample = [](const std::vector& v) { std::vector r; r.reserve(v.size() * 2); @@ -248,28 +250,27 @@ std::vector predict_bands(uint8_t qfactor, uint8_t dwt_levels, uint8_t RI, outL = tmpL; outH = tmpH; } - wmse.push_back(gain_low * gain_low); // LL_N + wmse.push_back(gain_low * gain_low); // LL_N } // Q-dependent scalars from the shared encoder header (same q_to_delta() the // encoder uses), so the inversion matches the encoder's step exactly. const open_htj2k::q_scaling qs = open_htj2k::q_to_delta(qfactor, RI); - const double qpower = qs.qfactor_power; - const double delta_ref = qs.delta_Q * open_htj2k::color_gain(ct, 0); - const double G_c = open_htj2k::color_gain(ct, Cqcc); + const double qpower = qs.qfactor_power; + const double delta_ref = qs.delta_Q * open_htj2k::color_gain(ct, 0); + const double G_c = open_htj2k::color_gain(ct, Cqcc); std::vector out(num_bands); for (size_t i = 0; i < num_bands; ++i) { // LL band (last entry, always weight 1.0) and any extra low-freq bands beyond the 5-level table - double w_b = (i == num_bands - 1 || i >= weights.size()) ? 1.0 : std::pow(weights[i], qpower); - double fval = delta_ref / (std::sqrt(wmse[i]) * w_b * G_c); + double w_b = (i == num_bands - 1 || i >= weights.size()) ? 1.0 : std::pow(weights[i], qpower); + double fval = delta_ref / (std::sqrt(wmse[i]) * w_b * G_c); int32_t exponent = 0; while (fval < 1.0) { fval *= 2.0; exponent++; } - int32_t mantissa = - static_cast(std::floor((fval - 1.0) * static_cast(1 << 11) + 0.5)); + int32_t mantissa = static_cast(std::floor((fval - 1.0) * static_cast(1 << 11) + 0.5)); if (mantissa >= (1 << 11)) { mantissa = 0; exponent--; @@ -293,16 +294,17 @@ double step_value(const Band& b) { struct ScoreResult { uint8_t best_q; - double best_residual; // sum of (log2 step_obs - log2 step_pred)^2 over bands + double best_residual; // sum of (log2 step_obs - log2 step_pred)^2 over bands double median_per_band; }; -ScoreResult find_best_q(const std::vector& observed, uint8_t dwt_levels, uint8_t RI, - uint8_t Cqcc, uint8_t chroma_format, - const open_htj2k::visual_weighting_params& vp, bool mct_on) { +ScoreResult find_best_q(const std::vector& observed, uint8_t dwt_levels, uint8_t RI, uint8_t Cqcc, + uint8_t chroma_format, const open_htj2k::visual_weighting_params& vp, bool mct_on, + uint8_t sub_x = 0, uint8_t sub_y = 0) { ScoreResult best{0, std::numeric_limits::infinity(), 0.0}; for (int q = 1; q <= 100; ++q) { - auto pred = predict_bands(static_cast(q), dwt_levels, RI, Cqcc, chroma_format, vp, mct_on); + auto pred = predict_bands(static_cast(q), dwt_levels, RI, Cqcc, chroma_format, vp, mct_on, + sub_x, sub_y); if (pred.size() != observed.size()) continue; double sumsq = 0; for (size_t i = 0; i < pred.size(); ++i) { @@ -311,7 +313,7 @@ ScoreResult find_best_q(const std::vector& observed, uint8_t dwt_levels, u } if (sumsq < best.best_residual) { best.best_residual = sumsq; - best.best_q = static_cast(q); + best.best_q = static_cast(q); } } best.median_per_band = @@ -324,21 +326,24 @@ void print_band_table(const std::vector& obs, const std::vector& pre printf(" band observed (eps, mu) predicted (eps, mu) step_obs/step_pred\n"); for (size_t i = 0; i < n; ++i) { double ratio = step_value(obs[i]) / step_value(pred[i]); - printf(" %3zu (%2u, %4u) (%2u, %4u) %.4f\n", - i, obs[i].epsilon, obs[i].mantissa, pred[i].epsilon, pred[i].mantissa, ratio); + printf(" %3zu (%2u, %4u) (%2u, %4u) %.4f\n", i, obs[i].epsilon, + obs[i].mantissa, pred[i].epsilon, pred[i].mantissa, ratio); } } -} // namespace +} // namespace int main(int argc, char** argv) { if (argc < 2) { fprintf(stderr, "Usage: %s [--verbose] [--csf legacy|mannos|daly] [--ppd F] [--zoom F]\n" + " [--ctype Y,Cb,Cr] [--chroma-csf chroma|luma]\n" " Estimates the OpenHTJ2K Qfactor [0..100] used at encode time by inverting the\n" " QCD/QCC step-size formula. The visual-weighting model is NOT signaled in the\n" " codestream, so for an analytic (EXPERIMENTAL) encode pass the same --csf/--ppd/\n" " --zoom used at encode time; a low residual confirms the assumption was right.\n" + " --ctype / --chroma-csf mirror the encoder's Qctype= / Qchromacsf= hints for\n" + " no-MCT (e.g. sub-sampled YCbCr) streams; pass the values used at encode time.\n" " --expect-q N / --max-residual F: exit non-zero if violated (for scripts/CI).\n" " --dump-quant FILE: write the verbatim QCD/QCC marker bytes to FILE and exit.\n" " These bytes are a function of Qfactor + bit-depth + the visual-weighting\n" @@ -348,9 +353,9 @@ int main(int argc, char** argv) { argv[0]); return 1; } - bool verbose = false; - int expect_q = -1; // >= 0 enables an exit-code check on the recovered Q - double max_residual = -1.0; // >= 0 enables an exit-code check on the per-band residual + bool verbose = false; + int expect_q = -1; // >= 0 enables an exit-code check on the recovered Q + double max_residual = -1.0; // >= 0 enables an exit-code check on the per-band residual const char* dump_quant = nullptr; // != null: dump verbatim QCD/QCC bytes and exit open_htj2k::visual_weighting_params vp; // default: legacy table (bit-identical inversion) for (int i = 2; i < argc; ++i) { @@ -380,6 +385,40 @@ int main(int argc, char** argv) { fprintf(stderr, "ERROR: --zoom must be > 0\n"); return 1; } + } else if (std::strcmp(argv[i], "--ctype") == 0 && i + 1 < argc) { + // Comma-separated per-component role hints, mirroring the encoder's Qctype=. + const char* v = argv[++i]; + size_t idx = 0; + while (idx < 3 && *v != '\0') { + const char* comma = std::strchr(v, ','); + const size_t len = (comma != nullptr) ? static_cast(comma - v) : std::strlen(v); + if ((len == 1 && (v[0] == 'Y' || v[0] == 'y'))) { + vp.ctype_hint[idx] = open_htj2k::component_type::Y; + } else if (len == 2 && (v[0] == 'C' || v[0] == 'c') && (v[1] == 'b' || v[1] == 'B')) { + vp.ctype_hint[idx] = open_htj2k::component_type::Cb; + } else if (len == 2 && (v[0] == 'C' || v[0] == 'c') && (v[1] == 'r' || v[1] == 'R')) { + vp.ctype_hint[idx] = open_htj2k::component_type::Cr; + } else if ((len == 7 && std::strncmp(v, "generic", 7) == 0) || (len == 1 && v[0] == '-')) { + vp.ctype_hint[idx] = open_htj2k::component_type::generic; + } else { + fprintf(stderr, "ERROR: unknown --ctype entry '%.*s' (use Y|Cb|Cr|generic)\n", + static_cast(len), v); + return 1; + } + ++idx; + if (comma == nullptr) break; + v = comma + 1; + } + } else if (std::strcmp(argv[i], "--chroma-csf") == 0 && i + 1 < argc) { + const char* m = argv[++i]; + if (std::strcmp(m, "chroma") == 0) { + vp.chroma_reuse_luma_csf = false; + } else if (std::strcmp(m, "luma") == 0) { + vp.chroma_reuse_luma_csf = true; + } else { + fprintf(stderr, "ERROR: unknown --chroma-csf '%s' (use chroma|luma)\n", m); + return 1; + } } else if (std::strcmp(argv[i], "--expect-q") == 0 && i + 1 < argc) { expect_q = std::atoi(argv[++i]); if (expect_q < 0 || expect_q > 100) { @@ -407,8 +446,7 @@ int main(int argc, char** argv) { fprintf(stderr, "ERROR: cannot open '%s'\n", argv[1]); return 1; } - std::vector buf((std::istreambuf_iterator(in)), - std::istreambuf_iterator()); + std::vector buf((std::istreambuf_iterator(in)), std::istreambuf_iterator()); Header h; if (!parse_main_header(buf, h)) return 1; @@ -445,8 +483,7 @@ int main(int argc, char** argv) { } uint8_t chroma_format = infer_chroma_format(h); - const char* cf_name = (chroma_format == YCC444) ? "4:4:4" - : (chroma_format == YCC420) ? "4:2:0" : "4:2:2"; + const char* cf_name = (chroma_format == YCC444) ? "4:4:4" : (chroma_format == YCC420) ? "4:2:0" : "4:2:2"; printf("File: %s\n", argv[1]); printf("Components: %u (chroma format %s)\n", h.Csiz, cf_name); @@ -499,7 +536,9 @@ int main(int argc, char** argv) { printf("\nGuard bits: %u\n", qcd->guardbits); printf("Qstyle: %u (%s)\n", qcd->style, - qcd->style == 0 ? "no-quant" : qcd->style == 2 ? "scalar-expounded" : "scalar-derived"); + qcd->style == 0 ? "no-quant" + : qcd->style == 2 ? "scalar-expounded" + : "scalar-derived"); // Score per component. std::vector> per_component; @@ -512,18 +551,17 @@ int main(int argc, char** argv) { } } uint8_t RI = static_cast((h.Ssiz[c] & 0x7F) + 1); - auto score = find_best_q(qm->bands, h.dwt_levels, RI, - static_cast(c), chroma_format, vp, h.use_color_trafo); + auto score = find_best_q(qm->bands, h.dwt_levels, RI, static_cast(c), chroma_format, vp, + h.use_color_trafo, h.XRsiz[c], h.YRsiz[c]); per_component.emplace_back(c, score); - printf("\nComponent %u (RI=%u, %s)\n", c, RI, - qm->component_index == 0xFFFF ? "QCD" : "QCC"); + printf("\nComponent %u (RI=%u, %s)\n", c, RI, qm->component_index == 0xFFFF ? "QCD" : "QCC"); printf(" best Q : %u\n", score.best_q); printf(" residual : %.4f (sum log2 step^2)\n", score.best_residual); - printf(" per-band : %.4f log2 (~ factor %.4fx)\n", - score.median_per_band, std::pow(2.0, score.median_per_band)); + printf(" per-band : %.4f log2 (~ factor %.4fx)\n", score.median_per_band, + std::pow(2.0, score.median_per_band)); if (verbose) { - auto pred = predict_bands(score.best_q, h.dwt_levels, RI, - static_cast(c), chroma_format, vp, h.use_color_trafo); + auto pred = predict_bands(score.best_q, h.dwt_levels, RI, static_cast(c), chroma_format, vp, + h.use_color_trafo, h.XRsiz[c], h.YRsiz[c]); print_band_table(qm->bands, pred); } } diff --git a/source/core/codestream/j2kmarkers.cpp b/source/core/codestream/j2kmarkers.cpp index d5109eb5..bb8dc901 100644 --- a/source/core/codestream/j2kmarkers.cpp +++ b/source/core/codestream/j2kmarkers.cpp @@ -333,8 +333,8 @@ PRF_marker::PRF_marker(j2c_src_memory &in) : j2k_marker_io_base(_PRF), PRFnum(0) // Lprf = 2 + 2N; N 16-bit Pprf words follow. PRFnum is informational only // (no codec processing) — see Rec. ITU-T T.800 | ISO/IEC 15444-1, A.5.3: // PRFnum = 4095 + sum_{i=1..N} Pprf_i * 2^(16*(i-1)). - const size_t n = static_cast(Lmar - 2U) / 2U; - uint64_t prfnum = 4095; + const size_t n = static_cast(Lmar - 2U) / 2U; + uint64_t prfnum = 4095; for (size_t i = 0; i < n; ++i) { const uint16_t w = get_word(); Pprf.push_back(w); @@ -369,8 +369,7 @@ COD_marker::COD_marker(j2c_src_memory &in) // precinct loop — NL = 255 never terminates for the uint8_t counter and grows // precinct_size without bound — and indexes SPcod[5 + r] out of range. if (SPcod[0] > 32) { - printf("ERROR: COD number of decomposition levels %u exceeds 32.\n", - static_cast(SPcod[0])); + printf("ERROR: COD number of decomposition levels %u exceeds 32.\n", static_cast(SPcod[0])); throw std::exception(); } is_set = true; @@ -649,7 +648,7 @@ DFS_marker::DFS_marker(j2c_src_memory &in) : j2k_marker_io_base(_DFS) { uint8_t dfs_lev = static_cast(Ids - r + 1); // coarsest first qcd_offset[r] = flat; dwt_type t = Ddfs[dfs_lev - 1]; - flat = static_cast(flat + ((t == DWT_BIDIR) ? 3 : (t == DWT_NO) ? 0 : 1)); + flat = static_cast(flat + ((t == DWT_BIDIR) ? 3 : (t == DWT_NO) ? 0 : 1)); } is_set = true; } @@ -703,7 +702,7 @@ ATK_marker::ATK_marker(j2c_src_memory &in) : j2k_marker_io_base(_ATK), Katk(1.0f } else { steps.resize(Natk); for (uint8_t k = 0; k < Natk; ++k) { - steps[k].mk = get_byte(); + steps[k].mk = get_byte(); uint32_t bits = get_dword(); memcpy(&steps[k].Aatk, &bits, sizeof(float)); } @@ -855,9 +854,8 @@ static std::vector build_quant_steps(uint8_t number_of_guardbits, uint // the colour-component gain. The float step is packed into (epsilon, mu) per // Eq. E-3, with clamping epsilon in [0,31], mu in [0,2047]. for (size_t i = 0; i < epsilon.size(); ++i) { - double w_b = (!have_qfactor || i == epsilon.size() - 1 || i >= W_b.size()) - ? 1.0 - : pow(W_b[i], qfactor_power); + double w_b = + (!have_qfactor || i == epsilon.size() - 1 || i >= W_b.size()) ? 1.0 : pow(W_b[i], qfactor_power); double fval = delta_ref / (sqrt(wmse_or_BIBO[i]) * w_b * G_c); pack_quant_step(fval, epsilon[epsilon.size() - i - 1], mu[epsilon.size() - i - 1]); } @@ -952,8 +950,8 @@ QCD_marker::QCD_marker(uint8_t number_of_guardbits, uint8_t dwt_levels, uint8_t G_c = open_htj2k::color_gain(ct, 0); } - SPqcd = build_quant_steps(number_of_guardbits, dwt_levels, is_reversible, is_derived, RI, use_ycc, W_b, - have_qfactor, delta_ref, G_c, qfactor_power); + SPqcd = build_quant_steps(number_of_guardbits, dwt_levels, is_reversible, is_derived, RI, use_ycc, W_b, + have_qfactor, delta_ref, G_c, qfactor_power); is_set = true; } @@ -1025,7 +1023,7 @@ uint8_t QCD_marker::get_MAGB() { QCC_marker::QCC_marker(uint16_t Csiz, uint16_t c, uint8_t number_of_guardbits, uint8_t dwt_levels, uint8_t transformation, bool is_derived, uint8_t RI, uint8_t use_ycc, uint8_t qfactor, uint8_t chroma_format, - const open_htj2k::visual_weighting_params &vp) + const open_htj2k::visual_weighting_params &vp, uint8_t sub_x, uint8_t sub_y) : j2k_marker_io_base(_QCC), max_components(Csiz), Cqcc(c), Sqcc(0), is_reversible(transformation == 1) { if (is_derived && qfactor != 0xFF) { is_derived = false; @@ -1059,14 +1057,18 @@ QCC_marker::QCC_marker(uint16_t Csiz, uint16_t c, uint8_t number_of_guardbits, u // Square-root-domain chroma visual weights for this component. Default // (legacy_table) returns the historical 4:4:4/4:2:0/4:2:2 QCC table verbatim // (bit-identical); an analytic CSF model in `vp` instead follows the viewing - // distance / zoom, deriving 4:2:0 & 4:2:2 from one chroma CSF via subsampling. + // distance / zoom, deriving 4:2:0 & 4:2:2 from one chroma CSF via the + // component's SIZ sub-sampling factors (sub_x, sub_y). Without an MCT the + // channel role comes from vp.ctype_hint (default generic = luminance CSF). // delta_ref / qfactor_power follow the Q->step mapping (q_to_delta(), shared with // QCD and estimate_qfactor; HTJ2K white paper); G_c is this component's synthesis // gain. The basis-gain derivation and (epsilon, mu) packing are shared with QCD // through build_quant_steps(). QCC is only emitted on the Qfactor path, so the // lossy perceptual weights always apply (have_qfactor is true). + const open_htj2k::component_type ctype = + (Cqcc < 3) ? vp.ctype_hint[Cqcc] : open_htj2k::component_type::generic; const std::vector W_b = - open_htj2k::chroma_visual_weights(dwt_levels, vp, Cqcc, chroma_format, ct); + open_htj2k::chroma_visual_weights(dwt_levels, vp, Cqcc, chroma_format, ct, ctype, sub_x, sub_y); double qfactor_power = 0.0, delta_ref = 0.0, G_c = 1.0; if (!is_reversible) { const open_htj2k::q_scaling qs = open_htj2k::q_to_delta(qfactor, RI); @@ -1075,8 +1077,8 @@ QCC_marker::QCC_marker(uint16_t Csiz, uint16_t c, uint8_t number_of_guardbits, u G_c = open_htj2k::color_gain(ct, Cqcc); // component synthesis gain } - SPqcc = build_quant_steps(number_of_guardbits, dwt_levels, is_reversible, is_derived, RI, use_ycc, W_b, - /*have_qfactor=*/true, delta_ref, G_c, qfactor_power); + SPqcc = build_quant_steps(number_of_guardbits, dwt_levels, is_reversible, is_derived, RI, use_ycc, W_b, + /*have_qfactor=*/true, delta_ref, G_c, qfactor_power); is_set = true; } @@ -1343,24 +1345,28 @@ TLM_marker::TLM_marker(uint8_t ztlm, const std::vector &tile_indices, // If tile indices are provided, use ST=2 (16-bit Ttlm). uint8_t ST = tile_indices.empty() ? 0 : 2; uint8_t SP = 1; - Stlm = static_cast((ST << 4) | (SP << 6)); - is_set = true; + Stlm = static_cast((ST << 4) | (SP << 6)); + is_set = true; } void TLM_marker::write(j2c_dst_memory &buf) const { - uint8_t ST = (Stlm >> 4) & 0x03; - uint8_t SP = ((Stlm >> 4) & 0x0C) >> 2; + uint8_t ST = (Stlm >> 4) & 0x03; + uint8_t SP = ((Stlm >> 4) & 0x0C) >> 2; size_t entry_size = static_cast((ST == 0 ? 0 : (ST == 1 ? 1 : 2)) + (SP == 0 ? 2 : 4)); - uint16_t Ltlm = static_cast(4 + Ptlm.size() * entry_size); + uint16_t Ltlm = static_cast(4 + Ptlm.size() * entry_size); buf.put_word(_TLM); buf.put_word(Ltlm); buf.put_byte(Ztlm); buf.put_byte(Stlm); for (size_t i = 0; i < Ptlm.size(); ++i) { - if (ST == 1) buf.put_byte(static_cast(Ttlm[i])); - else if (ST == 2) buf.put_word(Ttlm[i]); - if (SP == 0) buf.put_word(static_cast(Ptlm[i])); - else buf.put_dword(Ptlm[i]); + if (ST == 1) + buf.put_byte(static_cast(Ttlm[i])); + else if (ST == 2) + buf.put_word(Ttlm[i]); + if (SP == 0) + buf.put_word(static_cast(Ptlm[i])); + else + buf.put_dword(Ptlm[i]); } } @@ -1592,10 +1598,12 @@ j2k_main_header::j2k_main_header(SIZ_marker *siz, COD_marker *cod, QCD_marker *q throw std::exception(); } for (uint16_t c = 1; c < siz->get_num_components(); ++c) { - QCC.push_back(MAKE_UNIQUE(siz->get_num_components(), c, qcd->get_number_of_guardbits(), - cod->get_dwt_levels(), cod->get_transformation(), false, - siz->get_bitdepth(c), cod->use_color_trafo(), qfactor, - SIZ->get_chroma_format(), vw)); + element_siz sub; + siz->get_subsampling_factor(sub, c); + QCC.push_back(MAKE_UNIQUE( + siz->get_num_components(), c, qcd->get_number_of_guardbits(), cod->get_dwt_levels(), + cod->get_transformation(), false, siz->get_bitdepth(c), cod->use_color_trafo(), qfactor, + SIZ->get_chroma_format(), vw, static_cast(sub.x), static_cast(sub.y))); } } diff --git a/source/core/codestream/j2kmarkers.hpp b/source/core/codestream/j2kmarkers.hpp index 20def156..e201b4bd 100644 --- a/source/core/codestream/j2kmarkers.hpp +++ b/source/core/codestream/j2kmarkers.hpp @@ -197,7 +197,7 @@ class COC_marker : public j2k_marker_io_base { COC_marker(j2c_src_memory &in, uint16_t Csiz); uint16_t get_component_index() const; bool is_maximum_precincts() const; - bool is_dfs_defined() const; // bit 7 of SPcoc[0] set → DFS active for this component + bool is_dfs_defined() const; // bit 7 of SPcoc[0] set → DFS active for this component uint8_t get_dfs_index() const; // bits[3:0] of SPcoc[0] when DFS active uint8_t get_dwt_levels(); void get_codeblock_size(element_siz &out); @@ -230,8 +230,8 @@ enum dwt_type : uint8_t { DWT_NO = 0, DWT_BIDIR = 1, DWT_HORZ = 2, DWT_VERT = 3 class DFS_marker : public j2k_marker_io_base { private: - uint16_t Sdfs; // DFS descriptor word; bits[3:0] = DFS index (1-15) - uint8_t Ids; // number of DWT levels described + uint16_t Sdfs; // DFS descriptor word; bits[3:0] = DFS index (1-15) + uint8_t Ids; // number of DWT levels described std::vector Ddfs; // per-level DWT type (index 0 = finest, i.e. Ddfs[0]=type of level 1) public: @@ -248,7 +248,7 @@ class DFS_marker : public j2k_marker_io_base { explicit DFS_marker(j2c_src_memory &in); uint8_t get_index() const; uint8_t get_num_levels() const; - dwt_type get_dwt_type(uint8_t level) const; // level 1..Ids (1=finest); DWT_BIDIR if out of range + dwt_type get_dwt_type(uint8_t level) const; // level 1..Ids (1=finest); DWT_BIDIR if out of range uint8_t get_num_bands(uint8_t r, uint8_t NL) const; // num subbands for resolution r (0=LL) // Returns count of consecutive DWT_BIDIR levels from the finest level onward. // This is the maximum -reduce value that produces a valid 2D reduced image for @@ -262,8 +262,8 @@ class DFS_marker : public j2k_marker_io_base { * Defines an arbitrary lifting kernel (irreversible only for our implementation). *******************************************************************************/ struct atk_step { - uint8_t mk; // step offset parameter - float Aatk; // lifting coefficient (irreversible) + uint8_t mk; // step offset parameter + float Aatk; // lifting coefficient (irreversible) }; class ATK_marker : public j2k_marker_io_base { @@ -319,9 +319,13 @@ class QCC_marker : public j2k_marker_io_base { bool is_reversible; public: + // (sub_x, sub_y) are this component's SIZ sub-sampling factors (XRsiz, YRsiz); + // analytic CSF models fold them into the per-axis visual-frequency mapping. + // 0 keeps the historical chroma_format-derived factors. QCC_marker(uint16_t Csiz, uint16_t c, uint8_t number_of_guardbits, uint8_t dwt_levels, uint8_t transformation, bool is_derived, uint8_t RI, uint8_t use_ycc, uint8_t qfactor, - uint8_t chroma_format, const open_htj2k::visual_weighting_params &vp = {}); + uint8_t chroma_format, const open_htj2k::visual_weighting_params &vp = {}, uint8_t sub_x = 0, + uint8_t sub_y = 0); QCC_marker(j2c_src_memory &in, uint16_t Csiz); int write(j2c_dst_memory &dst); uint16_t get_component_index() const; @@ -372,8 +376,8 @@ class TLM_marker : public j2k_marker_io_base { void write(j2c_dst_memory &buf) const; uint8_t index() const { return Ztlm; } - size_t num_entries() const { return Ptlm.size(); } - bool has_tile_indices() const { return !Ttlm.empty() && ((Stlm >> 4) & 0x03) != 0; } + size_t num_entries() const { return Ptlm.size(); } + bool has_tile_indices() const { return !Ttlm.empty() && ((Stlm >> 4) & 0x03) != 0; } const std::vector &tile_indices() const { return Ttlm; } const std::vector &tile_part_lengths() const { return Ptlm; } }; diff --git a/source/core/codestream/visual_weighting.hpp b/source/core/codestream/visual_weighting.hpp index 8533b5b8..81a6a230 100644 --- a/source/core/codestream/visual_weighting.hpp +++ b/source/core/codestream/visual_weighting.hpp @@ -58,6 +58,19 @@ enum class csf_model { daly // analytic Daly CSF (light-adaptation form) }; +// Perceptual role of a codestream component. With the built-in ICT the roles +// are implied by component order (Y, Cb, Cr). Without an MCT -- the only +// configuration in which chroma sub-sampling is possible (T.800 J.13) -- the +// codestream does not label channels, so the role must be hinted by the caller +// (CLI `Qctype=`). `generic` takes the luminance CSF but still applies the +// component's (sx, sy) sub-sampling frequency shift. +enum class component_type : uint8_t { + generic = 0, // unlabelled: luminance CSF + sub-sampling mapping + Y = 1, + Cb = 2, + Cr = 3 +}; + // Color decorrelation actually in force, which drives BOTH the per-component // synthesis gain and whether a QCC component is treated as chroma (opponent) or // luma (e.g. undecorrelated RGB). Quantization error in component c is amplified @@ -84,6 +97,16 @@ struct visual_weighting_params { // horizontal/vertical (LH/HL) center. Geometric value is sqrt(2); the legacy // table behaves closer to ~1.25 (no oblique-effect penalty). double hh_factor = 1.4142135623730951; + // A/B switch: evaluate the luminance CSF (instead of the low-pass chroma CSF) + // for Cb/Cr components, keeping the (sx, sy) sub-sampling frequency mapping. + // Separates the effect of the CSF *shape* from the frequency *mapping*. + bool chroma_reuse_luma_csf = false; + // Per-component role hints, used only when no MCT labels the channels (the + // sub-sampled-chroma configuration). Default `generic` keeps the historical + // luminance-CSF treatment; the CLI `Qctype=Y,Cb,Cr` maps to {Y, Cb, Cr}. + // Qfactor is restricted to 1- or 3-component images, so 3 entries suffice. + component_type ctype_hint[3] = {component_type::generic, component_type::generic, + component_type::generic}; }; // --- color synthesis gain -------------------------------------------------- @@ -124,11 +147,11 @@ struct q_scaling { inline q_scaling q_to_delta(uint8_t qfactor, uint8_t RI) { const uint8_t t0 = 65, t1 = 97; const double alpha_T0 = 0.04, alpha_T1 = 0.10; - const double M_T0 = 2.0 * (1.0 - t0 / 100.0); - const double M_T1 = 2.0 * (1.0 - t1 / 100.0); - const double M_Q = (qfactor < 50) ? 50.0 / qfactor : 2.0 * (1.0 - qfactor / 100.0); - double alpha_Q = alpha_T0; - double qfactor_power = 1.0; + const double M_T0 = 2.0 * (1.0 - t0 / 100.0); + const double M_T1 = 2.0 * (1.0 - t1 / 100.0); + const double M_Q = (qfactor < 50) ? 50.0 / qfactor : 2.0 * (1.0 - qfactor / 100.0); + double alpha_Q = alpha_T0; + double qfactor_power = 1.0; if (qfactor >= t1) { qfactor_power = 0.0; alpha_Q = alpha_T1; @@ -168,9 +191,7 @@ inline double csf_value(double f, csf_model m) { // would make the step `delta/(basis*w*G)` divide by zero / propagate NaN and // emit a corrupt QCD/QCC. Replaces only non-finite or non-positive values, so it // is a no-op for every realistic weight (bit-identical normal output). -inline double finite_weight(double w) { - return (std::isfinite(w) && w > 0.0) ? w : 1e-300; -} +inline double finite_weight(double w) { return (std::isfinite(w) && w > 0.0) ? w : 1e-300; } // Peak (frequency, amplitude) of a CSF, found once by a coarse scan. Used to // normalize the weight to <= 1 and to clamp the band-pass low side to flat. @@ -209,8 +230,15 @@ inline double csf_weight(double f, csf_model m, const csf_peak_t &pk) { // is 3 * dwt_levels for analytic models; the LL band is handled by the caller // (weight 1.0). In legacy mode the historical 15-entry table is returned // verbatim, so the caller's existing out-of-range guard still applies. -inline std::vector luma_visual_weights(uint8_t dwt_levels, - const visual_weighting_params &vp) { +// +// (sub_x, sub_y) are the component's SIZ sub-sampling factors (XRsiz, YRsiz). +// A sub-sampled grid is coarser, so each axis's angular frequency divides by +// its factor; per-axis evaluation (LH from the vertical axis, HL from the +// horizontal) lets anisotropic sub-sampling (4:2:2) split LH and HL. At +// (1, 1) -- every luma/QCD call site -- the output is bit-identical to the +// historical single-radial form by construction. +inline std::vector luma_visual_weights(uint8_t dwt_levels, const visual_weighting_params &vp, + int sub_x = 1, int sub_y = 1) { if (vp.model == csf_model::legacy_table) { // Zeng et al., Table 2, Y column (= sqrt of the MSE-domain weights). return {0.0901, 0.2758, 0.2758, 0.7018, 0.8378, 0.8378, 1.0000, 1.0000, @@ -218,19 +246,28 @@ inline std::vector luma_visual_weights(uint8_t dwt_levels, } const csf_peak_t pk = csf_peak(vp.model); - const double zoom = (vp.zoom > 0.0) ? vp.zoom : 1.0; - const double ppd = vp.ref_ppd / zoom; // zoom-in lowers effective ppd - const double f_N = ppd / 2.0; // Nyquist in cycles/degree + const double zoom = (vp.zoom > 0.0) ? vp.zoom : 1.0; + const double ppd = vp.ref_ppd / zoom; // zoom-in lowers effective ppd + const double f_N = ppd / 2.0; // full-resolution Nyquist (cycles/degree) + const double sx = (sub_x > 0) ? sub_x : 1; + const double sy = (sub_y > 0) ? sub_y : 1; + const double f_Nx = f_N / sx; // per-axis Nyquist on this component's grid + const double f_Ny = f_N / sy; std::vector w; w.reserve(static_cast(3) * dwt_levels); for (uint8_t lvl = 1; lvl <= dwt_levels; ++lvl) { - // Geometric-mean radial center of octave band [f_N/2^lvl, f_N/2^(lvl-1)]. - const double f_r = f_N * std::pow(2.0, -static_cast(lvl)) * std::sqrt(2.0); - const double f_hh = f_r * vp.hh_factor; + // Geometric-mean per-axis center of octave band [f_N/2^lvl, f_N/2^(lvl-1)]. + const double dx = f_Nx * std::pow(2.0, -static_cast(lvl)) * std::sqrt(2.0); + const double dy = f_Ny * std::pow(2.0, -static_cast(lvl)) * std::sqrt(2.0); + // Isotropic grids keep the exact historical HH expression (dx * hh_factor); + // the general form is the per-axis diagonal scaled so hh_factor = sqrt(2) + // is the geometric (no oblique-penalty) value, as in the chroma path. + const double f_hh = + (dx == dy) ? dx * vp.hh_factor : (vp.hh_factor / std::sqrt(2.0)) * std::sqrt(dx * dx + dy * dy); w.push_back(csf_weight(f_hh, vp.model, pk)); // HH - w.push_back(csf_weight(f_r, vp.model, pk)); // LH - w.push_back(csf_weight(f_r, vp.model, pk)); // HL + w.push_back(csf_weight(dy, vp.model, pk)); // LH (vertical detail) + w.push_back(csf_weight(dx, vp.model, pk)); // HL (horizontal detail) } return w; } @@ -275,9 +312,15 @@ inline std::vector legacy_chroma_row(int comp_index, int chroma_format) 0.8254, 0.8254, 0.8254, 0.9424, 0.9424, 0.9424, 1.0000}; const double *r; switch (chroma_format) { - case 1: r = (comp_index == 1) ? Cb420 : Cr420; break; // 4:2:0 - case 2: r = (comp_index == 1) ? Cb422 : Cr422; break; // 4:2:2 - default: r = (comp_index == 1) ? Cb444 : Cr444; break; // 4:4:4 + case 1: + r = (comp_index == 1) ? Cb420 : Cr420; + break; // 4:2:0 + case 2: + r = (comp_index == 1) ? Cb422 : Cr422; + break; // 4:2:2 + default: + r = (comp_index == 1) ? Cb444 : Cr444; + break; // 4:4:4 } return std::vector(r, r + 15); } @@ -287,22 +330,29 @@ inline std::vector legacy_chroma_row(int comp_index, int chroma_format) // historical row verbatim (bit-identical). Analytic models fold chroma // subsampling into the effective horizontal/vertical ppd, so LH (vertical // detail) and HL (horizontal detail) diverge under 4:2:2 as they should. -inline std::vector chroma_visual_weights(uint8_t dwt_levels, - const visual_weighting_params &vp, int comp_index, - int chroma_format, - color_transform ct = color_transform::ict) { +// +// Component role: with the ICT in force the roles are positional (comp 1 = Cb, +// comp 2 = Cr) as always. Without an MCT the codestream does not label channels +// (and only then can chroma be sub-sampled, T.800 J.13), so `ctype_hint` +// decides: Cb/Cr take the chroma CSF, Y/generic take the luminance CSF -- in +// every case with this component's (sub_x, sub_y) frequency mapping applied. +// (sub_x, sub_y) are the SIZ XRsiz/YRsiz factors; pass 0 to derive them from +// `chroma_format` (444/420/422) for callers without SIZ access. +inline std::vector chroma_visual_weights(uint8_t dwt_levels, const visual_weighting_params &vp, + int comp_index, int chroma_format, + color_transform ct = color_transform::ict, + component_type ctype = component_type::generic, + int sub_x = 0, int sub_y = 0) { if (vp.model == csf_model::legacy_table) { return legacy_chroma_row(comp_index, chroma_format); } - // Without a luma/chroma decorrelating transform this component carries - // luminance (e.g. a raw RGB channel), so it takes the luminance CSF, never - // the chroma roll-off. Such components are not subsampled (full resolution). - if (ct == color_transform::none) { - return luma_visual_weights(dwt_levels, vp); - } + // Resolve (sx, sy): SIZ factors when supplied, else the chroma_format legacy mapping. double sx = 1.0, sy = 1.0; // horizontal/vertical chroma subsampling factors - if (chroma_format == 1) { // 4:2:0 + if (sub_x > 0 && sub_y > 0) { + sx = sub_x; + sy = sub_y; + } else if (chroma_format == 1) { // 4:2:0 sx = 2.0; sy = 2.0; } else if (chroma_format == 2) { // 4:2:2 @@ -310,7 +360,17 @@ inline std::vector chroma_visual_weights(uint8_t dwt_levels, sy = 1.0; } - const chroma_csf_params cp = chroma_params_for(comp_index); + // Resolve the perceptual role of this component. + const component_type role = + (ct == color_transform::ict) ? ((comp_index == 1) ? component_type::Cb : component_type::Cr) : ctype; + // Luminance-CSF cases: an unlabelled / luma component (e.g. a raw RGB channel + // or unhinted YCbCr), or the reuse-luma A/B switch. The (sx, sy) sub-sampling + // mapping still applies; at (1, 1) this is the historical no-MCT behavior. + if (role == component_type::generic || role == component_type::Y || vp.chroma_reuse_luma_csf) { + return luma_visual_weights(dwt_levels, vp, static_cast(sx), static_cast(sy)); + } + + const chroma_csf_params cp = chroma_params_for(role == component_type::Cb ? 1 : 2); const double zoom = (vp.zoom > 0.0) ? vp.zoom : 1.0; const double f_N = (vp.ref_ppd / zoom) / 2.0; // luma Nyquist (cpd) const double f_Nx = f_N / sx; // chroma horizontal Nyquist diff --git a/source/core/interface/encoder.cpp b/source/core/interface/encoder.cpp index 76897743..569495b4 100644 --- a/source/core/interface/encoder.cpp +++ b/source/core/interface/encoder.cpp @@ -547,6 +547,8 @@ class openhtj2k_encoder_impl { qcd_params &, uint8_t, bool, uint8_t); void set_output_buffer(std::vector &); void set_visual_weighting(uint8_t model, double ref_ppd, double zoom); + void set_component_types(uint8_t c0, uint8_t c1, uint8_t c2); + void set_chroma_csf_reuse_luma(bool reuse_luma); ~openhtj2k_encoder_impl(); size_t invoke_line_based(); size_t invoke_line_based_stream(std::function src_fn); @@ -568,9 +570,15 @@ void openhtj2k_encoder_impl::set_output_buffer(std::vector &output_buf) void openhtj2k_encoder_impl::set_visual_weighting(uint8_t model, double ref_ppd, double zoom) { // model: 0 = legacy table (default, bit-identical), 1 = Mannos-Sakrison, 2 = Daly. switch (model) { - case 1: vw.model = csf_model::mannos_sakrison; break; - case 2: vw.model = csf_model::daly; break; - default: vw.model = csf_model::legacy_table; break; + case 1: + vw.model = csf_model::mannos_sakrison; + break; + case 2: + vw.model = csf_model::daly; + break; + default: + vw.model = csf_model::legacy_table; + break; } if (ref_ppd > 0.0) vw.ref_ppd = ref_ppd; if (zoom > 0.0) vw.zoom = zoom; @@ -586,6 +594,18 @@ void openhtj2k_encoder_impl::set_visual_weighting(uint8_t model, double ref_ppd, } } +void openhtj2k_encoder_impl::set_component_types(uint8_t c0, uint8_t c1, uint8_t c2) { + const uint8_t t[3] = {c0, c1, c2}; + for (int i = 0; i < 3; ++i) { + // 0 = generic, 1 = Y, 2 = Cb, 3 = Cr; anything else falls back to generic. + vw.ctype_hint[i] = (t[i] <= 3) ? static_cast(t[i]) : component_type::generic; + } +} + +void openhtj2k_encoder_impl::set_chroma_csf_reuse_luma(bool reuse_luma) { + vw.chroma_reuse_luma_csf = reuse_luma; +} + openhtj2k_encoder_impl::~openhtj2k_encoder_impl() = default; size_t openhtj2k_encoder_impl::invoke_internal() { @@ -992,6 +1012,14 @@ void openhtj2k_encoder::set_visual_weighting(uint8_t model, double ref_ppd, doub this->impl->set_visual_weighting(model, ref_ppd, zoom); } +void openhtj2k_encoder::set_component_types(uint8_t c0, uint8_t c1, uint8_t c2) { + this->impl->set_component_types(c0, c1, c2); +} + +void openhtj2k_encoder::set_chroma_csf_reuse_luma(bool reuse_luma) { + this->impl->set_chroma_csf_reuse_luma(reuse_luma); +} + size_t openhtj2k_encoder::invoke_line_based() { return this->impl->invoke_line_based(); } size_t openhtj2k_encoder::invoke_line_based_stream( diff --git a/source/core/interface/encoder.hpp b/source/core/interface/encoder.hpp index 5a3226ae..4ef87710 100644 --- a/source/core/interface/encoder.hpp +++ b/source/core/interface/encoder.hpp @@ -66,11 +66,11 @@ class image { std::vector is_signed; public: -OPENHTJ2K_EXPORT explicit image(const std::vector &filenames); -OPENHTJ2K_EXPORT int read_pnmpgx(const std::string &filename, uint16_t nc); - #if defined(OPENHTJ2K_TIFF_SUPPORT) -OPENHTJ2K_EXPORT int read_tiff(const std::string &filename); - #endif + OPENHTJ2K_EXPORT explicit image(const std::vector &filenames); + OPENHTJ2K_EXPORT int read_pnmpgx(const std::string &filename, uint16_t nc); +#if defined(OPENHTJ2K_TIFF_SUPPORT) + OPENHTJ2K_EXPORT int read_tiff(const std::string &filename); +#endif OPENHTJ2K_NODISCARD uint32_t get_width() const { return this->width; } OPENHTJ2K_NODISCARD uint32_t get_height() const { return this->height; } @@ -149,18 +149,27 @@ class openhtj2k_encoder { std::unique_ptr impl; public: -OPENHTJ2K_EXPORT openhtj2k_encoder(const char *, const std::vector &input_buf, siz_params &siz, cod_params &cod, - qcd_params &qcd, uint8_t qfactor, bool isJPH, uint8_t color_space, - uint32_t num_threads); -OPENHTJ2K_EXPORT void set_output_buffer(std::vector &output_buf); -// EXPERIMENTAL: enable analytic visual (CSF) weighting for the Qfactor path. -// model: 0 = legacy table (default, output unchanged), 1 = Mannos-Sakrison, 2 = Daly. -// ref_ppd: reference pixels-per-degree at zoom 1.0 (<= 0 keeps the default). -// zoom: display magnification, > 1 = zoom-in (<= 0 keeps the default). -// Call before invoke_*; only affects encodes that also use Qfactor. -OPENHTJ2K_EXPORT void set_visual_weighting(uint8_t model, double ref_ppd, double zoom); -OPENHTJ2K_EXPORT size_t invoke_line_based(); -OPENHTJ2K_EXPORT size_t invoke_line_based_stream(std::function src_fn); -OPENHTJ2K_EXPORT ~openhtj2k_encoder(); + OPENHTJ2K_EXPORT openhtj2k_encoder(const char *, const std::vector &input_buf, siz_params &siz, + cod_params &cod, qcd_params &qcd, uint8_t qfactor, bool isJPH, + uint8_t color_space, uint32_t num_threads); + OPENHTJ2K_EXPORT void set_output_buffer(std::vector &output_buf); + // EXPERIMENTAL: enable analytic visual (CSF) weighting for the Qfactor path. + // model: 0 = legacy table (default, output unchanged), 1 = Mannos-Sakrison, 2 = Daly. + // ref_ppd: reference pixels-per-degree at zoom 1.0 (<= 0 keeps the default). + // zoom: display magnification, > 1 = zoom-in (<= 0 keeps the default). + // Call before invoke_*; only affects encodes that also use Qfactor. + OPENHTJ2K_EXPORT void set_visual_weighting(uint8_t model, double ref_ppd, double zoom); + // EXPERIMENTAL: per-component role hints for analytic weighting when no MCT is in + // force (the sub-sampled YCbCr configuration, where the codestream cannot label + // channels itself). c0..c2: 0 = generic (luminance CSF; default), 1 = Y, 2 = Cb, + // 3 = Cr. Ignored by the legacy model and whenever the built-in ICT is applied. + OPENHTJ2K_EXPORT void set_component_types(uint8_t c0, uint8_t c1, uint8_t c2); + // EXPERIMENTAL: A/B switch -- evaluate the luminance CSF for Cb/Cr components + // instead of the low-pass chroma CSF, keeping the sub-sampling frequency mapping. + OPENHTJ2K_EXPORT void set_chroma_csf_reuse_luma(bool reuse_luma); + OPENHTJ2K_EXPORT size_t invoke_line_based(); + OPENHTJ2K_EXPORT size_t + invoke_line_based_stream(std::function src_fn); + OPENHTJ2K_EXPORT ~openhtj2k_encoder(); }; } // namespace open_htj2k diff --git a/tests/encoder_test.cmake b/tests/encoder_test.cmake index 1cbb99fe..c745ac88 100644 --- a/tests/encoder_test.cmake +++ b/tests/encoder_test.cmake @@ -152,6 +152,48 @@ set_tests_properties(qfest_daly PROPERTIES DEPENDS enc_qf_daly) add_test(NAME qfest_mismatch COMMAND estimate_qfactor kodim23_qfmannos.j2c --max-residual 0.01) set_tests_properties(qfest_mismatch PROPERTIES DEPENDS enc_qf_mannos PASS_REGULAR_EXPRESSION "CHECK FAIL") +# --- Sub-sampling-aware analytic weighting (per-axis frequency mapping) -------- +# visual_weight_check pins the mapping invariants directly on the shared header: +# (1,1) bit-identity with the pre-generalization output, the exact one-level +# shift at (2,2), the 4:2:2 HL>LH anisotropy, and the generic default role. +add_test(NAME qf_vw_invariants COMMAND visual_weight_check check) + +# End-to-end 4:2:0: a 3-component input with half-size components 1/2 (any PGX +# trio with those shapes will do -- QCD/QCC bytes never depend on sample data). +# Sub-sampled input forces the MCT off, so the codestream cannot label channels; +# Qctype=Y,Cb,Cr selects the chroma CSF and the estimator must be told the same +# hints to invert exactly. The residual-0 round-trip proves encoder and +# estimator agree on the XRsiz/YRsiz-driven frequency mapping. +set(QF420_INPUT "${ENCODER_REF_DIR}/c1p0_05-0.pgx,${ENCODER_REF_DIR}/c1p0_05-2.pgx,${ENCODER_REF_DIR}/c1p0_05-3.pgx") +add_test(NAME enc_qf_420_ctype COMMAND open_htj2k_enc -i ${QF420_INPUT} -o kodim_qf420ct.j2c Qfactor=88 Qcsf=mannos Qctype=Y,Cb,Cr) +add_test(NAME qfest_420_ctype COMMAND estimate_qfactor kodim_qf420ct.j2c --csf mannos --ctype Y,Cb,Cr --expect-q 88 --max-residual 0.01) +set_tests_properties(qfest_420_ctype PROPERTIES DEPENDS enc_qf_420_ctype) + +# Negative control: without the ctype hints the estimator assumes generic +# (luminance-CSF) chroma and must NOT match -- proof the chroma CSF actually +# reached the emitted QCC bytes (the path was dead code before Qctype existed). +add_test(NAME qfest_420_ctype_required COMMAND estimate_qfactor kodim_qf420ct.j2c --csf mannos --max-residual 0.01) +set_tests_properties(qfest_420_ctype_required PROPERTIES DEPENDS enc_qf_420_ctype PASS_REGULAR_EXPRESSION "CHECK FAIL") + +# Reuse-luma A/B switch round-trip (luminance CSF shape + sub-sampling mapping). +add_test(NAME enc_qf_420_reuseluma COMMAND open_htj2k_enc -i ${QF420_INPUT} -o kodim_qf420rl.j2c Qfactor=88 Qcsf=mannos Qctype=Y,Cb,Cr Qchromacsf=luma) +add_test(NAME qfest_420_reuseluma COMMAND estimate_qfactor kodim_qf420rl.j2c --csf mannos --ctype Y,Cb,Cr --chroma-csf luma --expect-q 88 --max-residual 0.01) +set_tests_properties(qfest_420_reuseluma PROPERTIES DEPENDS enc_qf_420_reuseluma) + +# End-to-end 4:2:2 on tiny synthetic fixtures (Y 64x64, Cb/Cr 32x64). Also a +# regression guard for the Cycc guard fix: 4:2:2 differs in size only +# horizontally, which used to leave the MCT enabled across differently-sized +# component buffers and corrupt the heap. The legacy encode exercises exactly +# that path (default Cycc=yes + Qfactor); the analytic pair round-trips the +# anisotropic (2,1) mapping through the emitted QCC bytes. +set(QF422_INPUT "${ENCODER_REF_DIR}/synth422_y.pgx,${ENCODER_REF_DIR}/synth422_cb.pgx,${ENCODER_REF_DIR}/synth422_cr.pgx") +add_test(NAME enc_qf_422_legacy COMMAND open_htj2k_enc -i ${QF422_INPUT} -o synth_qf422lg.j2c Qfactor=85) +add_test(NAME qfest_422_legacy COMMAND estimate_qfactor synth_qf422lg.j2c --expect-q 85 --max-residual 0.01) +set_tests_properties(qfest_422_legacy PROPERTIES DEPENDS enc_qf_422_legacy) +add_test(NAME enc_qf_422_ctype COMMAND open_htj2k_enc -i ${QF422_INPUT} -o synth_qf422ct.j2c Qfactor=85 Qcsf=mannos Qctype=Y,Cb,Cr) +add_test(NAME qfest_422_ctype COMMAND estimate_qfactor synth_qf422ct.j2c --csf mannos --ctype Y,Cb,Cr --expect-q 85 --max-residual 0.01) +set_tests_properties(qfest_422_ctype PROPERTIES DEPENDS enc_qf_422_ctype) + # Require the decoder-conformance cleanup fixture so these encoder tests are # guaranteed to run AFTER cleanup_artifacts, never concurrently with it. # Without this, ctest -j was free to schedule cleanup_artifacts (which globs @@ -180,4 +222,9 @@ set_tests_properties( enc_qf_mannos_zoom qfest_mannos_zoom enc_qf_daly qfest_daly qfest_mismatch + qf_vw_invariants + enc_qf_420_ctype qfest_420_ctype qfest_420_ctype_required + enc_qf_420_reuseluma qfest_420_reuseluma + enc_qf_422_legacy qfest_422_legacy + enc_qf_422_ctype qfest_422_ctype PROPERTIES FIXTURES_REQUIRED test_artifacts) diff --git a/tests/tools/visual_weight_check/CMakeLists.txt b/tests/tools/visual_weight_check/CMakeLists.txt new file mode 100644 index 00000000..e5575cc1 --- /dev/null +++ b/tests/tools/visual_weight_check/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_policy(SET CMP0076 NEW) +target_sources(visual_weight_check + PRIVATE + main.cpp +) +target_include_directories(visual_weight_check PRIVATE + ${CMAKE_SOURCE_DIR}/source/core/codestream +) diff --git a/tests/tools/visual_weight_check/main.cpp b/tests/tools/visual_weight_check/main.cpp new file mode 100644 index 00000000..bd396131 --- /dev/null +++ b/tests/tools/visual_weight_check/main.cpp @@ -0,0 +1,325 @@ +// Copyright (c) 2019 - 2026, Osamu Watanabe +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// visual_weight_check: dump and self-check harness for the analytic per-subband +// visual (CSF) weighting in source/core/codestream/visual_weighting.hpp. +// +// visual_weight_check check +// Verifies the sub-sampling-aware frequency-mapping invariants and exits +// non-zero on the first violation (used by ctest): +// 1. (1,1) golden pin -- analytic luma and Cb/Cr 4:4:4 weights match +// the values produced before the per-axis (sx, sy) generalization +// to 1e-6 (regression guard for the "4:4:4 output never changes" +// contract; tolerance only absorbs cross-toolchain libm/FMA ULPs). +// 2. Isotropic shift -- a (2, 2) sub-sampled component's weights at +// level l equal the (1, 1) weights at level l+1, bit-for-bit, for the +// chroma CSF, the reuse-luma CSF, and the generic/luma path. This is +// the classic "chroma is weighted like luma one level down" rule, +// falling out of the frequency mapping with no manual level offset. +// 3. 4:2:2 anisotropy -- with (sx, sy) = (2, 1), HL (horizontal detail, +// the sub-sampled axis) always outweighs LH. No scalar per-subband +// table can represent this; it is the signature that the per-axis +// mapping is live. +// 4. Default role -- without a ctype hint an unlabelled (no-MCT) +// component takes the luminance CSF, the historical behavior. +// +// visual_weight_check dump [--csf mannos|daly] [--ppd F] [--zoom F] +// [--levels N] [--ctype Y|Cb|Cr|generic] +// [--sx N] [--sy N] [--reuse-luma] +// [--legacy-format 444|420|422] +// Prints the derived per-level [HH, LH, HL] weight table for one component +// configuration (one command per T.802 Annex B comparison). + +#include +#include +#include +#include "visual_weighting.hpp" + +namespace { + +using namespace open_htj2k; + +int failures = 0; + +void expect(bool cond, const char *what) { + if (!cond) { + printf("FAIL: %s\n", what); + ++failures; + } +} + +// Weight vectors for one role/sub-sampling configuration. Cb/Cr go through +// chroma_visual_weights with a no-MCT ctype hint (the sub-sampled-chroma +// configuration); Y/generic go through the luminance path. +std::vector weights_for(uint8_t levels, const visual_weighting_params &vp, component_type role, + int sx, int sy) { + if (role == component_type::Cb || role == component_type::Cr) { + const int comp_index = (role == component_type::Cb) ? 1 : 2; + return chroma_visual_weights(levels, vp, comp_index, 0, color_transform::none, role, sx, sy); + } + return luma_visual_weights(levels, vp, sx, sy); +} + +// --- check 1: (1,1) golden pin --------------------------------------------- +// Captured from the pre-generalization implementation (mannos_sakrison model, +// ref_ppd = 72, zoom = 1, 5 levels). 17 significant digits round-trip doubles +// exactly, so == is a bit-identity comparison. +void check_golden_pin() { + const double golden_luma[15] = {0.096817675716936266, + 0.30684673263740037, + 0.30684673263740037, + 0.60532817339313194, + 0.864324646011562, + 0.864324646011562, + 0.99043615641172789, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0}; + const double golden_cb[15] = {0.034960121305188455, 0.081551235507758238, 0.081551235507758238, + 0.15359475448367257, 0.24653448385473559, 0.24653448385473559, + 0.35113522529678459, 0.4573791611974069, 0.4573791611974069, + 0.55729121401399506, 0.64597480708719213, 0.64597480708719213, + 0.72135733443385142, 0.78339023685855358, 0.78339023685855358}; + const double golden_cr[15] = {0.071702417473406138, 0.16019272426776643, 0.16019272426776643, + 0.28006527167126605, 0.41292177620501364, 0.41292177620501364, + 0.54080888621381895, 0.65234354609406364, 0.65234354609406364, + 0.74313662078190756, 0.813575745821562, 0.813575745821562, + 0.86642359141464398, 0.90515943597529802, 0.90515943597529802}; + + visual_weighting_params vp; + vp.model = csf_model::mannos_sakrison; + + // NOT a bitwise compare: the reference values were captured on one + // toolchain, and the analytic weights go through libm exp/pow and + // compiler-contracted (FMA) float expressions, which legitimately differ by + // ULPs across compilers (clang vs gcc vs MSVC) and platforms. 1e-6 is far + // below any real formula change but far above toolchain noise. Same-binary + // bit-identity of the (sx, sy) generalization at (1, 1) holds by + // construction (identical expressions); cross-run byte-identity of the + // emitted markers is guarded by the qfest_* round-trip tests. + auto near_golden = [](double a, double b) { return std::fabs(a - b) <= 1e-6; }; + + const std::vector luma = luma_visual_weights(5, vp); + const std::vector cb = chroma_visual_weights(5, vp, 1, 0, color_transform::ict); + const std::vector cr = chroma_visual_weights(5, vp, 2, 0, color_transform::ict); + expect(luma.size() == 15 && cb.size() == 15 && cr.size() == 15, "golden pin: 15 weights per table"); + for (size_t i = 0; i < 15 && failures == 0; ++i) { + expect(near_golden(luma[i], golden_luma[i]), "golden pin: analytic luma weight drifted at (1,1)"); + expect(near_golden(cb[i], golden_cb[i]), "golden pin: analytic Cb 4:4:4 weight drifted"); + expect(near_golden(cr[i], golden_cr[i]), "golden pin: analytic Cr 4:4:4 weight drifted"); + } +} + +// --- check 2: isotropic one-level shift ------------------------------------- +// (2,2) sub-sampling divides every subband's angular frequency by 2, which is +// identically a one-decomposition-level shift, so the weight tables must match +// bit-for-bit -- proof that the mapping subsumes the manual level offset. +void check_isotropic_shift() { + const csf_model models[2] = {csf_model::mannos_sakrison, csf_model::daly}; + const component_type roles[3] = {component_type::Cb, component_type::Cr, component_type::generic}; + const bool reuse_luma_variants[2] = {false, true}; + for (int m = 0; m < 2; ++m) { + for (int r = 0; r < 3; ++r) { + for (int v = 0; v < 2; ++v) { + visual_weighting_params vp; + vp.model = models[m]; + vp.chroma_reuse_luma_csf = reuse_luma_variants[v]; + const std::vector w22 = weights_for(5, vp, roles[r], 2, 2); + const std::vector w11 = weights_for(6, vp, roles[r], 1, 1); + expect(w22.size() == 15 && w11.size() == 18, "isotropic shift: table sizes"); + for (size_t i = 0; i < w22.size(); ++i) { + expect(w22[i] == w11[i + 3], + "isotropic shift: (2,2) level l != (1,1) level l+1 (must be bit-identical)"); + if (failures) return; + } + } + } + } +} + +// --- check 3: 4:2:2 anisotropy ---------------------------------------------- +// Horizontal-only sub-sampling halves only the horizontal frequency, so HL +// (horizontal detail) must outweigh LH. The chroma CSF is strictly decreasing +// (strict >); the luminance CSF is flat below its peak, so the reuse-luma and +// generic variants assert >= everywhere and strict > at the finest level. +void check_422_anisotropy() { + const csf_model models[2] = {csf_model::mannos_sakrison, csf_model::daly}; + for (int m = 0; m < 2; ++m) { + visual_weighting_params vp; + vp.model = models[m]; + for (int r = 1; r <= 3; ++r) { // 1 = Y-like generic handled below; use Cb, Cr, generic + const component_type role = (r == 1) ? component_type::Cb + : (r == 2) ? component_type::Cr + : component_type::generic; + const bool strict_all = (role == component_type::Cb || role == component_type::Cr); + const std::vector w = weights_for(5, vp, role, 2, 1); + for (size_t lvl = 0; lvl < 5; ++lvl) { + const double w_lh = w[3 * lvl + 1]; + const double w_hl = w[3 * lvl + 2]; + if (strict_all) { + expect(w_hl > w_lh, "4:2:2 anisotropy: chroma-CSF w_HL not strictly > w_LH"); + } else { + expect(w_hl >= w_lh, "4:2:2 anisotropy: luminance-CSF w_HL < w_LH"); + if (lvl == 0) expect(w_hl > w_lh, "4:2:2 anisotropy: no split at the finest level"); + } + } + } + } +} + +// --- check 4: default role -------------------------------------------------- +// Unhinted no-MCT components keep the historical luminance-CSF treatment. +void check_default_role() { + visual_weighting_params vp; + vp.model = csf_model::mannos_sakrison; + const std::vector unhinted = chroma_visual_weights(5, vp, 1, 0, color_transform::none); + const std::vector luma = luma_visual_weights(5, vp); + expect(unhinted == luma, "default role: unhinted no-MCT component must take the luminance weights"); +} + +int run_checks() { + check_golden_pin(); + check_isotropic_shift(); + check_422_anisotropy(); + check_default_role(); + if (failures == 0) { + printf("visual_weight_check: all invariants hold\n"); + return EXIT_SUCCESS; + } + printf("visual_weight_check: %d failure(s)\n", failures); + return EXIT_FAILURE; +} + +int run_dump(int argc, char **argv) { + visual_weighting_params vp; + vp.model = csf_model::mannos_sakrison; + component_type role = component_type::Y; + int sx = 1, sy = 1; + int levels = 5; + int legacy_format = -1; + for (int i = 2; i < argc; ++i) { + if (std::strcmp(argv[i], "--csf") == 0 && i + 1 < argc) { + const char *m = argv[++i]; + if (std::strcmp(m, "mannos") == 0) { + vp.model = csf_model::mannos_sakrison; + } else if (std::strcmp(m, "daly") == 0) { + vp.model = csf_model::daly; + } else { + fprintf(stderr, "ERROR: unknown --csf '%s' (use mannos|daly)\n", m); + return EXIT_FAILURE; + } + } else if (std::strcmp(argv[i], "--ppd") == 0 && i + 1 < argc) { + vp.ref_ppd = std::atof(argv[++i]); + } else if (std::strcmp(argv[i], "--zoom") == 0 && i + 1 < argc) { + vp.zoom = std::atof(argv[++i]); + } else if (std::strcmp(argv[i], "--levels") == 0 && i + 1 < argc) { + levels = std::atoi(argv[++i]); + } else if (std::strcmp(argv[i], "--sx") == 0 && i + 1 < argc) { + sx = std::atoi(argv[++i]); + } else if (std::strcmp(argv[i], "--sy") == 0 && i + 1 < argc) { + sy = std::atoi(argv[++i]); + } else if (std::strcmp(argv[i], "--ctype") == 0 && i + 1 < argc) { + const char *t = argv[++i]; + if (std::strcmp(t, "Y") == 0) { + role = component_type::Y; + } else if (std::strcmp(t, "Cb") == 0) { + role = component_type::Cb; + } else if (std::strcmp(t, "Cr") == 0) { + role = component_type::Cr; + } else if (std::strcmp(t, "generic") == 0) { + role = component_type::generic; + } else { + fprintf(stderr, "ERROR: unknown --ctype '%s' (use Y|Cb|Cr|generic)\n", t); + return EXIT_FAILURE; + } + } else if (std::strcmp(argv[i], "--reuse-luma") == 0) { + vp.chroma_reuse_luma_csf = true; + } else if (std::strcmp(argv[i], "--legacy-format") == 0 && i + 1 < argc) { + const char *f = argv[++i]; + legacy_format = (std::strcmp(f, "420") == 0) ? 1 : (std::strcmp(f, "422") == 0) ? 2 : 0; + } else { + fprintf(stderr, "ERROR: unrecognized argument '%s'\n", argv[i]); + return EXIT_FAILURE; + } + } + if (levels < 1 || levels > 32 || sx < 1 || sy < 1) { + fprintf(stderr, "ERROR: --levels must be 1..32 and --sx/--sy >= 1\n"); + return EXIT_FAILURE; + } + + std::vector w; + if (legacy_format >= 0) { + // Historical T.802-derived table row (15 entries = 5 levels), for + // side-by-side comparison with the analytic output. + const int comp_index = (role == component_type::Cr) ? 2 : 1; + w = legacy_chroma_row(comp_index, legacy_format); + levels = 5; + printf("# legacy table, comp=%s, format=%s\n", (comp_index == 1) ? "Cb" : "Cr", + (legacy_format == 1) ? "4:2:0" + : (legacy_format == 2) ? "4:2:2" + : "4:4:4"); + } else { + w = weights_for(static_cast(levels), vp, role, sx, sy); + printf("# csf=%s ppd=%g zoom=%g ctype=%s sx=%d sy=%d%s\n", + (vp.model == csf_model::daly) ? "daly" : "mannos", vp.ref_ppd, vp.zoom, + (role == component_type::Y) ? "Y" + : (role == component_type::Cb) ? "Cb" + : (role == component_type::Cr) ? "Cr" + : "generic", + sx, sy, vp.chroma_reuse_luma_csf ? " reuse-luma" : ""); + } + printf("# level HH LH HL (finest first; sqrt-domain weights)\n"); + for (size_t lvl = 0; lvl < static_cast(levels) && 3 * lvl + 2 < w.size(); ++lvl) { + printf("%7d %.10f %.10f %.10f\n", static_cast(lvl) + 1, w[3 * lvl], w[3 * lvl + 1], + w[3 * lvl + 2]); + } + return EXIT_SUCCESS; +} + +} // namespace + +int main(int argc, char **argv) { + if (argc >= 2 && std::strcmp(argv[1], "check") == 0) { + return run_checks(); + } + if (argc >= 2 && std::strcmp(argv[1], "dump") == 0) { + return run_dump(argc, argv); + } + fprintf(stderr, + "Usage: %s check\n" + " %s dump [--csf mannos|daly] [--ppd F] [--zoom F] [--levels N]\n" + " [--ctype Y|Cb|Cr|generic] [--sx N] [--sy N] [--reuse-luma]\n" + " [--legacy-format 444|420|422]\n", + argv[0], argv[0]); + return EXIT_FAILURE; +}