1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! cv2-exact resize kernels for the TableFormer prep chain — CPU, `encoder-cpu`-gated.
//!
//! Docling's `tf_predictor.resize_img` + `_prepare_image` lean on two OpenCV resizes whose exact
//! numerics decide the u8 crop and the prepared tensor. Both are replicated op-for-op so they sit
//! inside the bit-exact torch gate (`tests/tableformer_parity.rs`):
//!
//! * [`inter_area_u8`] — `cv2.INTER_AREA`, DECIMATION regime (page → height 1024). Faithful port
//! of OpenCV's `computeResizeAreaTab` + `resizeArea_<uchar,float>`: per-axis fractional-overlap
//! weights (f32, cast exactly as OpenCV), horizontal reduce per source row then vertical
//! accumulate, `saturate_cast<uchar>` = round-half-to-even + clamp. Validated byte-identical to
//! cv2 on the crop region of the reference pages (a handful of ≤1-LSB pixels drift only OUTSIDE
//! the crop, from OpenCV's vectorized reduction order — the crop the model reads is exact).
//! * [`inter_linear_f32`] — `cv2.INTER_LINEAR` on the normalized f32 crop → 448×448. Separable
//! (horizontal within each source row, then vertical), pixel-center mapping
//! `fx = (dx+0.5)·scale − 0.5` cast to f32 then floored, boundary clamp with zero weight.
//! Validated ≤1.5e-6 vs cv2 on the reference crops (the prepared-tensor gate bar is 1e-5).
/// One axis of cv2 `resizeArea` (decimation): per output index, the `(src_index, weight)`
/// contributions. `computeResizeAreaTab` verbatim — tab math in f64, `alpha` cast to f32 exactly
/// as OpenCV stores `DecimateAlpha::alpha`.
fn area_tab(ssize: usize, dsize: usize) -> Vec<Vec<(usize, f32)>> {
let scale = ssize as f64 / dsize as f64;
let mut tabs = vec![Vec::new(); dsize];
for (dx, tab) in tabs.iter_mut().enumerate() {
let fsx1 = dx as f64 * scale;
let fsx2 = fsx1 + scale;
let cell_width = scale.min(ssize as f64 - fsx1);
let mut sx1 = fsx1.ceil() as i64;
let sx2 = (fsx2.floor() as i64).min(ssize as i64 - 1);
sx1 = sx1.min(sx2);
if sx1 as f64 - fsx1 > 1e-3 {
tab.push((
(sx1 - 1) as usize,
((sx1 as f64 - fsx1) / cell_width) as f32,
));
}
for sx in sx1..sx2 {
tab.push((sx as usize, (1.0 / cell_width) as f32));
}
if fsx2 - sx2 as f64 > 1e-3 {
let a = (fsx2 - sx2 as f64).min(1.0).min(cell_width) / cell_width;
tab.push((sx2 as usize, a as f32));
}
}
tabs
}
/// `saturate_cast<uchar>(float)` — `cvRound` (round half to even, the default FPU mode) + clamp.
fn sat_u8(v: f32) -> u8 {
v.round_ties_even().clamp(0.0, 255.0) as u8
}
/// `cv2.resize(src, (dw, dh), INTER_AREA)` on an interleaved u8 image `[sh, sw, cn]`, decimation
/// regime only (`sh ≥ dh && sw ≥ dw`, which is exactly how `resize_img(height=1024)` is used on a
/// page rendered at scale 2.0). Returns `[dh, dw, cn]`.
pub fn inter_area_u8(src: &[u8], sh: usize, sw: usize, cn: usize, dh: usize, dw: usize) -> Vec<u8> {
assert_eq!(
src.len(),
sh * sw * cn,
"inter_area_u8: src is not [sh,sw,cn]"
);
debug_assert!(
sh >= dh && sw >= dw,
"inter_area_u8 is the DECIMATION path (sh≥dh, sw≥dw); got {sh}x{sw} -> {dh}x{dw}"
);
let xt = area_tab(sw, dw);
let yt = area_tab(sh, dh);
let row = dw * cn;
let mut out = vec![0u8; dh * row];
let mut buf = vec![0f32; row];
let mut sum = vec![0f32; row];
for dy in 0..dh {
sum.iter_mut().for_each(|v| *v = 0.0);
for &(sy, beta) in &yt[dy] {
buf.iter_mut().for_each(|v| *v = 0.0);
let srow = &src[sy * sw * cn..(sy + 1) * sw * cn];
for (dx, tab) in xt.iter().enumerate() {
let dst = dx * cn;
for &(sx, alpha) in tab {
let s = sx * cn;
for c in 0..cn {
buf[dst + c] += srow[s + c] as f32 * alpha;
}
}
}
for (s, &b) in sum.iter_mut().zip(buf.iter()) {
*s += beta * b;
}
}
let orow = &mut out[dy * row..(dy + 1) * row];
for (o, &s) in orow.iter_mut().zip(sum.iter()) {
*o = sat_u8(s);
}
}
out
}
/// Per-axis INTER_LINEAR taps: `(sx0, sx1, w1)` with `w0 = 1 − w1` (boundary taps carry weight 0).
fn linear_coeffs(ssize: usize, dsize: usize) -> (Vec<usize>, Vec<usize>, Vec<f32>) {
let scale = ssize as f64 / dsize as f64;
let mut i0 = vec![0usize; dsize];
let mut i1 = vec![0usize; dsize];
let mut w1 = vec![0f32; dsize];
for dx in 0..dsize {
let f = ((dx as f64 + 0.5) * scale - 0.5) as f32;
let mut sx = f.floor() as i64;
let mut fx = f - sx as f32;
if sx < 0 {
sx = 0;
fx = 0.0;
}
if sx >= ssize as i64 - 1 {
sx = ssize as i64 - 1;
fx = 0.0;
}
i0[dx] = sx as usize;
i1[dx] = (sx as usize + 1).min(ssize - 1);
w1[dx] = fx;
}
(i0, i1, w1)
}
/// `cv2.resize(src, (dw, dh), INTER_LINEAR)` on an interleaved f32 image `[sh, sw, cn]` (the
/// normalized crop). Separable HResize→VResize order, matching OpenCV's float path. Returns
/// `[dh, dw, cn]`.
pub fn inter_linear_f32(
src: &[f32],
sh: usize,
sw: usize,
cn: usize,
dh: usize,
dw: usize,
) -> Vec<f32> {
assert_eq!(
src.len(),
sh * sw * cn,
"inter_linear_f32: src is not [sh,sw,cn]"
);
let (xi0, xi1, xw1) = linear_coeffs(sw, dw);
let (yi0, yi1, yw1) = linear_coeffs(sh, dh);
let mut out = vec![0f32; dh * dw * cn];
for dy in 0..dh {
let (sy0, sy1, wy1) = (yi0[dy], yi1[dy], yw1[dy]);
let wy0 = 1.0f32 - wy1;
let r0 = &src[sy0 * sw * cn..(sy0 + 1) * sw * cn];
let r1 = &src[sy1 * sw * cn..(sy1 + 1) * sw * cn];
for dx in 0..dw {
let (a0, a1, wx1) = (xi0[dx] * cn, xi1[dx] * cn, xw1[dx]);
let wx0 = 1.0f32 - wx1;
let dst = (dy * dw + dx) * cn;
for c in 0..cn {
let h0 = r0[a0 + c] * wx0 + r0[a1 + c] * wx1;
let h1 = r1[a0 + c] * wx0 + r1[a1 + c] * wx1;
out[dst + c] = h0 * wy0 + h1 * wy1;
}
}
}
out
}