Skip to main content

dicom_toolkit_codec/jpeg_ls/
context.rs

1//! JPEG-LS context modeling (statistical accumulators for adaptive Golomb-Rice coding).
2//!
3//! Port of CharLS `context.h` and `ctxtrmod.h`.
4
5/// Context for regular (non-run) mode.
6///
7/// Each context stores running statistics used to compute the Golomb parameter
8/// and prediction correction (bias C).
9#[derive(Debug, Clone)]
10pub struct JlsContext {
11    /// Accumulated |error| (magnitude).
12    pub a: i32,
13    /// Accumulated error (signed, used for bias).
14    pub b: i32,
15    /// Prediction correction (bias).
16    pub c: i8,
17    /// Count (number of samples coded in this context).
18    pub n: i16,
19}
20
21/// Lookup table for bias adjustment. Index = `B/N + 128` clamped to [0, 255].
22/// The table maps the bias ratio to a correction increment of -1, 0, or +1.
23static TABLE_C: [i8; 256] = {
24    let mut table = [0i8; 256];
25    // B/N < -1  → C += 1 (indices 0..127)
26    let mut i = 0;
27    while i < 127 {
28        table[i] = 1;
29        i += 1;
30    }
31    // B/N == -1 → C += 0 (index 127)
32    table[127] = 0;
33    // B/N == 0  → C += 0 (index 128)
34    table[128] = 0;
35    // B/N > 0   → C -= 1 (indices 129..255)
36    let mut i = 129;
37    while i < 256 {
38        table[i] = -1;
39        i += 1;
40    }
41    table
42};
43
44impl JlsContext {
45    /// Create a new context with initial values per the JPEG-LS standard.
46    pub fn new(a_init: i32) -> Self {
47        Self {
48            a: a_init,
49            b: 0,
50            c: 0,
51            n: 1,
52        }
53    }
54
55    /// Compute the Golomb coding parameter k for this context.
56    #[inline]
57    pub fn get_golomb(&self) -> i32 {
58        let mut k = 0;
59        let mut ntest = self.n as i32;
60        while ntest < self.a {
61            k += 1;
62            ntest <<= 1;
63        }
64        k
65    }
66
67    /// Get the error correction value for Golomb parameter k.
68    #[inline]
69    pub fn get_error_correction(&self, k: i32) -> i32 {
70        if k != 0 {
71            0
72        } else if 2 * self.b <= -(self.n as i32) {
73            1
74        } else {
75            0
76        }
77    }
78
79    /// Update context statistics after coding an error value.
80    /// `err_val` is the mapped (quantized, mod-ranged) error value.
81    #[inline]
82    pub fn update_variables(&mut self, err_val: i32, near: i32, reset_value: i32) {
83        self.a += (if err_val < 0 { -err_val } else { err_val }) - (self.near_correction(near));
84        self.b += err_val * (2 * near + 1);
85        self.adjust_bias(reset_value);
86    }
87
88    /// Compute near-lossless correction: A must be adjusted to reflect quantized errors.
89    #[inline]
90    fn near_correction(&self, _near: i32) -> i32 {
91        // In the original CharLS, this is handled differently;
92        // the error is already quantized before being passed here.
93        0
94    }
95
96    /// Adjust bias (C) and halve counters when N reaches RESET.
97    fn adjust_bias(&mut self, reset_value: i32) {
98        let n = self.n as i32;
99
100        if self.b + n <= 0 {
101            self.b += n;
102            if self.b <= -n {
103                self.b = -n + 1;
104            }
105            // Look up bias correction from table.
106            let idx = ((self.b.wrapping_div(n.max(1)) + 128) as usize).min(255);
107            self.c = self.c.wrapping_add(TABLE_C[idx]);
108            self.c = self.c.max(-128);
109        } else if self.b > 0 {
110            self.b -= n;
111            if self.b > 0 {
112                self.b = 0;
113            }
114            let idx = ((self.b.wrapping_div(n.max(1)) + 128) as usize).min(255);
115            self.c = self.c.wrapping_add(TABLE_C[idx]);
116        }
117
118        self.n += 1;
119        if self.n as i32 == reset_value {
120            self.a >>= 1;
121            self.b >>= 1;
122            self.n >>= 1;
123            // Ensure N doesn't drop to 0.
124            if self.n == 0 {
125                self.n = 1;
126            }
127        }
128    }
129}
130
131// ── Run mode context ──────────────────────────────────────────────────────────
132
133/// Context for run-interruption mode.
134///
135/// When the codec detects a run of identical pixels and the run ends,
136/// it uses a separate set of statistics for coding the interruption sample.
137#[derive(Debug, Clone)]
138pub struct RunModeContext {
139    /// Accumulated |error|.
140    pub a: i32,
141    /// Sample count.
142    pub n: i32,
143    /// Count of negative errors (used for map).
144    pub nn: i32,
145    /// Run interruption type (0 or 1).
146    pub ri_type: i32,
147    /// Reset threshold.
148    pub reset: i32,
149}
150
151impl RunModeContext {
152    pub fn new(a_init: i32, reset: i32) -> Self {
153        Self {
154            a: a_init,
155            n: 1,
156            nn: 0,
157            ri_type: 0,
158            reset,
159        }
160    }
161
162    /// Compute Golomb parameter for run interruption.
163    #[inline]
164    pub fn get_golomb(&self) -> i32 {
165        let mut k = 0;
166        let mut ntest = self.n;
167        while ntest < self.a {
168            k += 1;
169            ntest <<= 1;
170        }
171        k
172    }
173
174    /// Compute the map value (whether to add 1 to the mapped error).
175    #[inline]
176    pub fn compute_map(&self, err_val: i32, k: i32) -> i32 {
177        if (k == 0 && err_val > 0 && 2 * self.nn < self.n)
178            || (err_val < 0 && (2 * self.nn >= self.n || k != 0))
179        {
180            1
181        } else {
182            0
183        }
184    }
185
186    /// Compute the mapped error value for Golomb-Rice coding.
187    pub fn compute_map_negative_e(&self, k: i32) -> bool {
188        k != 0 || 2 * self.nn >= self.n
189    }
190
191    /// Update run-mode statistics after coding an error.
192    pub fn update_variables(&mut self, err_val: i32, e_mapped: i32) {
193        if err_val < 0 {
194            self.nn += 1;
195        }
196        self.a += (e_mapped + 1 - self.ri_type) >> 1;
197        self.n += 1;
198
199        if self.n == self.reset {
200            self.a >>= 1;
201            self.n >>= 1;
202            self.nn >>= 1;
203            if self.n == 0 {
204                self.n = 1;
205            }
206        }
207    }
208}
209
210// ── Tests ─────────────────────────────────────────────────────────────────────
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn initial_golomb_parameter() {
218        // Initial A = 4, N = 1 → k should satisfy N << k >= A → 1 << 2 = 4 >= 4, so k=2
219        let ctx = JlsContext::new(4);
220        assert_eq!(ctx.get_golomb(), 2);
221    }
222
223    #[test]
224    fn golomb_k_zero() {
225        // A <= N → k = 0
226        let ctx = JlsContext {
227            a: 1,
228            b: 0,
229            c: 0,
230            n: 2,
231        };
232        assert_eq!(ctx.get_golomb(), 0);
233    }
234
235    #[test]
236    fn error_correction_at_k_zero() {
237        // k=0, 2*B <= -N → correction = 1
238        let ctx = JlsContext {
239            a: 1,
240            b: -5,
241            c: 0,
242            n: 2,
243        };
244        assert_eq!(ctx.get_error_correction(0), 1);
245
246        // k=0, 2*B > -N → correction = 0
247        let ctx2 = JlsContext {
248            a: 1,
249            b: 0,
250            c: 0,
251            n: 2,
252        };
253        assert_eq!(ctx2.get_error_correction(0), 0);
254
255        // k != 0 → correction = 0
256        assert_eq!(ctx.get_error_correction(1), 0);
257    }
258
259    #[test]
260    fn update_variables_accumulates() {
261        let mut ctx = JlsContext::new(4);
262        ctx.update_variables(3, 0, 64);
263        assert_eq!(ctx.a, 7); // 4 + |3|
264        ctx.update_variables(-2, 0, 64);
265        assert_eq!(ctx.a, 9); // 7 + |-2|
266    }
267
268    #[test]
269    fn counter_halving_at_reset() {
270        let mut ctx = JlsContext::new(100);
271        ctx.n = 63; // just before reset
272        ctx.a = 200;
273        ctx.b = 10;
274        ctx.update_variables(5, 0, 64);
275        // N was 63, incremented to 64, hits reset → halve
276        assert!(ctx.n <= 32);
277        assert!(ctx.a <= 103); // (200+5)/2 ≈ 102
278    }
279
280    #[test]
281    fn run_mode_context_golomb() {
282        let ctx = RunModeContext::new(4, 64);
283        assert_eq!(ctx.get_golomb(), 2);
284    }
285
286    #[test]
287    fn run_mode_update() {
288        let mut ctx = RunModeContext::new(1, 64);
289        ctx.update_variables(-1, 2);
290        assert_eq!(ctx.nn, 1);
291        assert!(ctx.n > 1);
292    }
293}