wai-rs: jpegai_bitstream — tools-header (TON) post-filter chain parser #18

Merged
dcharlot merged 1 commit from feat/jpegai-ton into main 2026-07-17 01:24:22 -04:00
2 changed files with 289 additions and 1 deletions

View file

@ -11,6 +11,12 @@
//! bit-granular cursor. Reads past the end return the bits available (zero-extended),
//! matching the reference's bounds check.
/// Bits needed to carry `max_value`, per the reference `HeaderCoder::calc_bits_count`:
/// `ceil(log2(max_value + 1))`. `max_value = 0` needs no bits.
pub fn bits_for_max(max_value: u64) -> u32 {
if max_value == 0 { 0 } else { u64::BITS - max_value.leading_zeros() }
}
/// Marker identifiers (big-endian 2-byte prefixes).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Marker {
@ -90,6 +96,16 @@ impl<'a> BitReader<'a> {
v
}
/// Read one symbol whose width is **derived from its maximum value**, per the
/// reference `HeaderCoder::calc_bits_count`: `bits = ceil(log2(max_value + 1))`.
///
/// This is the header's universal width rule — no field has a fixed width. Some are
/// runtime-dependent: a `max_symbol` decoded earlier in a segment sets the width of
/// the weights that follow it (e.g. 65535 ⇒ 16 bits, then 99 ⇒ 7 bits).
pub fn read_max(&mut self, max_value: u64) -> u64 {
self.read(bits_for_max(max_value))
}
/// Unsigned exp-Golomb, k=0: count leading zeros `n`, then read `n` bits;
/// value = `2^n − 1 + suffix`. Used for marker-segment payload lengths.
pub fn read_expgolomb_k0(&mut self) -> u64 {
@ -196,6 +212,205 @@ pub struct PictureHeader {
/// Branches not exercised by the 4:4:4 single-tile reference stream (region
/// sub-splits, `independent_beta_uv`'s second β, the signalled colour matrix,
/// multi-threading counts) follow the reference syntax and are decoded but untested.
// ---- tools header (TON) ----
/// One `decode_filters` group of the EFE **linear** filter header.
#[derive(Clone, Debug, Default)]
pub struct EflFilters {
/// `best_cand_idx − 1`; `-1` means the plane's filter is absent.
pub cand_u: i32,
pub cand_v: i32,
pub fl_u: u32,
pub fl_v: u32,
pub min_symbol: u32,
pub max_symbol: u32,
pub weights_u: Vec<u32>,
pub weights_u2: Vec<u32>,
pub weights_v: Vec<u32>,
pub weights_v2: Vec<u32>,
}
/// EFE linear filter header: the two plane means plus two filter groups.
#[derive(Clone, Debug, Default)]
pub struct EflHeader {
/// `B1[i] / 100` = the per-plane mean consumed by `jpegai_pipeline::efl_chroma`.
pub b1: [u32; 2],
pub filters_best2: EflFilters,
pub filters_best: EflFilters,
}
/// eICCI per-tile model selection.
#[derive(Clone, Debug, Default)]
pub struct IcciTile {
/// Per-plane use flags (Y, U, V).
pub use_yuv: [bool; 3],
pub use_short_list: bool,
/// Signalled model indices (into the active list) for the Y and the joint-UV paths.
pub model_idx_y: u32,
pub model_idx_uv: u32,
}
/// EFE nonlinear filter header (the luma-aided chroma filter's parameters).
#[derive(Clone, Debug, Default)]
pub struct EfnHeader {
pub min_symbol: u32,
pub max_symbol: u32,
pub mask1_enabled: bool,
pub mask2_enabled: bool,
pub nonlinear_u_enabled: bool,
pub nonlinear_v_enabled: bool,
pub tile_width: u32,
pub tile_height: u32,
/// Per-tile luma range — the `luma_min_q` / `luma_max_q` of `efn_filter`.
pub luma_min: Vec<u32>,
pub luma_max: Vec<u32>,
/// Per-plane filter weights (`A1`), already offset by `min_symbol` — the
/// `weights_q15` of `efn_filter`.
pub weights_u: Vec<u32>,
pub weights_v: Vec<u32>,
}
/// The decoded tools header (TON substream).
#[derive(Clone, Debug, Default)]
pub struct ToolsHeader {
pub efl_enabled: bool,
pub efl: Option<EflHeader>,
pub icci_enabled: bool,
pub icci_tile_manager_enabled: bool,
pub icci_tiles: Vec<IcciTile>,
pub efn_enabled: bool,
pub efn: Option<EfnHeader>,
pub lef_enabled: bool,
/// LEF's reference channel index.
pub lef_ch_idx: u32,
/// Bit position just past the last field (before byte-alignment padding).
pub end_bit: usize,
}
/// `cands[i][0]` — the spatial-tiling candidate filter counts of the EFE linear filter.
const EFL_CAND_FILTER_COUNT: [u32; 6] = [1, 2, 2, 3, 3, 4];
/// One `decode_filters` group. `scale_2x2` selects the 4:4:4 path, where a single
/// plane's coefficients are decoded and replicated rather than coded four times.
fn parse_efl_filters(br: &mut BitReader, scale_2x2: bool) -> EflFilters {
let mut f = EflFilters::default();
f.cand_u = br.read_max(15) as i32 - 1;
f.cand_v = br.read_max(15) as i32 - 1;
if f.cand_u > -1 { f.fl_u = br.read_max(9) as u32; }
if f.cand_v > -1 { f.fl_v = br.read_max(9) as u32; }
f.min_symbol = br.read_max((1 << 16) - 1) as u32;
f.max_symbol = br.read_max((1 << 16) - 1) as u32;
let mut group = |cand: i32, fl: u32, br: &mut BitReader| -> (Vec<u32>, Vec<u32>) {
if cand <= -1 { return (Vec::new(), Vec::new()); }
let taps = (fl * fl) as usize;
// shape[0]==4 is coded as a single plane under 4:4:4 and replicated
let n_main = if scale_2x2 { taps } else { EFL_CAND_FILTER_COUNT[cand as usize] as usize * taps };
let main = (0..n_main).map(|_| br.read_max(f.max_symbol as u64) as u32).collect();
let second = (0..taps).map(|_| br.read_max(f.max_symbol as u64) as u32).collect();
(main, second)
};
let (u, u2) = group(f.cand_u, f.fl_u, br);
f.weights_u = u;
f.weights_u2 = u2;
let (v, v2) = group(f.cand_v, f.fl_v, br);
f.weights_v = v;
f.weights_v2 = v2;
f
}
/// Parse the **post-filter chain** of the tools header (TON), in filters-factory order:
/// EFE-linear → ICCI → eICCI → EFE-nonlinear → LEF. Each tool is gated by its own enable
/// flag (the recursive header walk), and every field's width is derived from its
/// `max_symbol_value` — including the runtime-dependent ones, where a just-decoded
/// `max_symbol` sets the width of the weights that follow.
///
/// `start_bit` is the bit position of the **first filter's enable flag**, not the TON
/// payload start: `_stream_header_part` defaults to `"tool_header"`, so tools other than
/// the post-filters also emit into TON ahead of the filter chain (in the reference
/// stream two such bits precede EFE-linear's flag). Locating that prefix generally means
/// walking the whole tool tree; this function parses the chain from a given offset.
///
/// `models_count` is the size of the eICCI model list the stream selects from (it sets
/// the width of the signalled model indices); `num_tiles` is the eICCI tile count.
pub fn parse_tools_header(data: &[u8], start_bit: usize, scale_2x2: bool, models_count: u32, num_tiles: usize) -> ToolsHeader {
let mut br = BitReader::at(data, start_bit);
let mut t = ToolsHeader::default();
t.efl_enabled = br.read_max(1) == 1;
if t.efl_enabled {
let mut h = EflHeader::default();
h.b1[0] = br.read_max((1 << 15) - 1) as u32;
h.b1[1] = br.read_max((1 << 15) - 1) as u32;
h.filters_best2 = parse_efl_filters(&mut br, scale_2x2);
h.filters_best = parse_efl_filters(&mut br, scale_2x2);
t.efl = Some(h);
}
t.icci_enabled = br.read_max(1) == 1;
if t.icci_enabled {
t.icci_tile_manager_enabled = br.read_max(1) == 1;
for _ in 0..num_tiles.max(1) {
let mut tile = IcciTile::default();
for u in tile.use_yuv.iter_mut() { *u = br.read_max(1) == 1; }
if tile.use_yuv.iter().any(|&b| b) {
tile.use_short_list = br.read_max(1) == 1;
if tile.use_yuv[0] {
tile.model_idx_y = br.read_max(models_count as u64) as u32;
}
if tile.use_yuv[1] || tile.use_yuv[2] {
tile.model_idx_uv = br.read_max(models_count as u64) as u32;
}
}
t.icci_tiles.push(tile);
}
}
t.efn_enabled = br.read_max(1) == 1;
if t.efn_enabled {
let mut e = EfnHeader::default();
e.min_symbol = br.read_max((1 << 16) - 1) as u32;
e.max_symbol = br.read_max((1 << 16) - 1) as u32;
e.mask1_enabled = br.read_max(1) == 1;
e.mask2_enabled = br.read_max(1) == 1;
if e.mask1_enabled || e.mask2_enabled {
let _b_size = br.read_max((1 << 10) - 1);
let h = br.read_max((1 << 10) - 1) as usize;
let w = br.read_max((1 << 10) - 1) as usize;
// W5 masks: only the enabled one(s) carry h×w symbols (max 2 ⇒ 2 bits)
for enabled in [e.mask1_enabled, e.mask2_enabled] {
let n = if enabled { h * w } else { 0 };
for _ in 0..n { br.read_max(2); }
}
}
e.nonlinear_u_enabled = br.read_max(1) == 1;
e.nonlinear_v_enabled = br.read_max(1) == 1;
if e.nonlinear_u_enabled || e.nonlinear_v_enabled {
e.tile_width = br.read_max((1 << 16) - 1) as u32;
e.tile_height = br.read_max((1 << 16) - 1) as u32;
let n = br.read_max((1 << 16) - 1) as usize; // numTiles
e.luma_min = (0..n).map(|_| br.read_max((1 << 16) - 1) as u32).collect();
e.luma_max = (0..n).map(|_| br.read_max((1 << 16) - 1) as u32).collect();
// candNum + A1 are coded per ENABLED plane (U first, then V)
for (enabled, dst) in [(e.nonlinear_u_enabled, 0), (e.nonlinear_v_enabled, 1)] {
if !enabled { continue; }
let cand_num = br.read_max((1 << 16) - 1) as usize;
let w: Vec<u32> = (0..cand_num)
.map(|_| br.read_max(e.max_symbol as u64) as u32 + e.min_symbol)
.collect();
if dst == 0 { e.weights_u = w; } else { e.weights_v = w; }
}
}
t.efn = Some(e);
}
t.lef_enabled = br.read_max(1) == 1;
if t.lef_enabled {
t.lef_ch_idx = br.read_max(255) as u32;
}
t.end_bit = br.bit_pos();
t
}
pub fn parse_picture_header(data: &[u8], start_bit: usize) -> PictureHeader {
let mut br = BitReader::at(data, start_bit);
let mut h = PictureHeader::default();

View file

@ -9,7 +9,7 @@
//! env: WAI_JPEGAI_CODESTREAM = path to test256.bin
#![cfg(feature = "neural_int")]
use wai::jpegai_bitstream::{find_segment, parse_container, parse_picture_header, BitReader, Marker};
use wai::jpegai_bitstream::{find_segment, parse_container, parse_picture_header, parse_tools_header, BitReader, Marker};
fn codestream() -> Option<Vec<u8>> {
match std::env::var("WAI_JPEGAI_CODESTREAM") {
@ -126,3 +126,76 @@ fn full_picture_header_matches_reference() {
assert_eq!(h.end_bit, 437, "all PIH fields consumed");
assert!(h.end_bit <= (pih.start + pih.len) * 8, "within the PIH payload");
}
#[test]
fn tools_header_matches_reference() {
let Some(data) = codestream() else { return };
let segs = parse_container(&data);
let ton = find_segment(&segs, Marker::Ton).expect("TON present");
let pih = parse_picture_header(&data, find_segment(&segs, Marker::Pih).unwrap().start_bit());
// 4:4:4 ⇒ scale_ver = scale_hor = 3 − c/s = 2 ⇒ the single-plane filter path
let scale_2x2 = (3 - pih.c_ver / pih.s_ver == 2) && (3 - pih.c_hor / pih.s_hor == 2);
assert!(scale_2x2, "test256 is 4:4:4");
// The post-filter chain starts 2 bits into TON: `_stream_header_part` defaults to
// "tool_header", so other tools emit into TON ahead of the filters. models_count
// sets the signalled-index width (2 bits here).
let filters_bit = ton.start_bit() + 2;
let t = parse_tools_header(&data, filters_bit, scale_2x2, 3, 1);
eprintln!("{t:#?}");
// EFE linear: the two plane means feeding efl_chroma
assert!(t.efl_enabled);
let efl = t.efl.as_ref().unwrap();
assert_eq!(efl.b1, [12561, 13091], "B1/100 = 125.61 / 130.91 = the EFL plane means");
let f1 = &efl.filters_best2;
assert_eq!((f1.cand_u, f1.cand_v), (0, 0));
assert_eq!((f1.fl_u, f1.fl_v), (1, 1));
assert_eq!((f1.min_symbol, f1.max_symbol), (0, 65535));
assert_eq!((&f1.weights_u[..], &f1.weights_u2[..]), (&[32677u32][..], &[32772u32][..]));
assert_eq!((&f1.weights_v[..], &f1.weights_v2[..]), (&[32758u32][..], &[32776u32][..]));
let f2 = &efl.filters_best;
// the second group's max_symbol (99) narrows the weights to 7 bits
assert_eq!((f2.min_symbol, f2.max_symbol), (32677, 99));
assert_eq!((&f2.weights_u[..], &f2.weights_u2[..]), (&[0u32][..], &[95u32][..]));
assert_eq!((&f2.weights_v[..], &f2.weights_v2[..]), (&[81u32][..], &[99u32][..]));
// eICCI: the per-tile model selection (the last input the eICCI net needed)
assert!(t.icci_enabled);
assert!(!t.icci_tile_manager_enabled);
assert_eq!(t.icci_tiles.len(), 1);
let tile = &t.icci_tiles[0];
assert_eq!(tile.use_yuv, [true, true, true]);
assert!(tile.use_short_list);
assert_eq!((tile.model_idx_y, tile.model_idx_uv), (0, 1));
// EFE nonlinear: exactly the inputs efn_filter consumes
assert!(t.efn_enabled);
let efn = t.efn.as_ref().unwrap();
assert_eq!((efn.min_symbol, efn.max_symbol), (0, 65535));
assert!(!efn.mask1_enabled && !efn.mask2_enabled);
assert!(!efn.nonlinear_u_enabled && efn.nonlinear_v_enabled, "only V is filtered");
assert_eq!((efn.tile_width, efn.tile_height), (256, 256));
assert_eq!((&efn.luma_min[..], &efn.luma_max[..]), (&[497u32][..], &[25320u32][..]),
"== efn_fixture luma_min_q / luma_max_q");
assert!(efn.weights_u.is_empty(), "U disabled ⇒ no candNum/A1 for U");
assert_eq!(efn.weights_v, vec![32966, 32443, 33031, 32361, 33068, 33006, 31968, 33003],
"== efn_fixture weights_q15");
assert!(t.lef_enabled);
assert_eq!(t.lef_ch_idx, 0);
// the whole tools header consumed exactly, up to the pad closing the 63-byte TON
assert_eq!(t.end_bit, 973, "all TON fields consumed");
assert!(t.end_bit <= (ton.start + ton.len) * 8);
}
#[test]
fn header_widths_follow_calc_bits_count() {
use wai::jpegai_bitstream::bits_for_max;
// bits = ceil(log2(max+1)) — the reference HeaderCoder::calc_bits_count
for (max, want) in [(0u64, 0u32), (1, 1), (2, 2), (3, 2), (4, 3), (9, 4), (15, 4), (99, 7), (255, 8), (65535, 16)] {
assert_eq!(bits_for_max(max), want, "bits_for_max({max})");
}
}