phasm_core/stego/armor/
resample.rs1use crate::stego::armor::template::AffineTransform;
12
13pub fn resample_bilinear(
19 pixels: &[f64],
20 src_w: usize,
21 src_h: usize,
22 transform: &AffineTransform,
23 dst_w: usize,
24 dst_h: usize,
25) -> Vec<f64> {
26 let mut result = vec![128.0f64; dst_w * dst_h];
27
28 let (sin_t, cos_t) = crate::det_math::det_sincos(transform.rotation_rad);
30 let inv_scale = if transform.scale.abs() > 1e-12 { 1.0 / transform.scale } else { 1.0 };
31
32 let src_cx = src_w as f64 / 2.0;
34 let src_cy = src_h as f64 / 2.0;
35 let dst_cx = dst_w as f64 / 2.0;
36 let dst_cy = dst_h as f64 / 2.0;
37
38 for dy in 0..dst_h {
39 for dx in 0..dst_w {
40 let x = dx as f64 - dst_cx;
42 let y = dy as f64 - dst_cy;
43
44 let xr = x * cos_t + y * sin_t;
46 let yr = -x * sin_t + y * cos_t;
47
48 let xs = xr * inv_scale;
50 let ys = yr * inv_scale;
51
52 let sx = xs + src_cx;
54 let sy = ys + src_cy;
55
56 result[dy * dst_w + dx] = bilinear_sample(pixels, src_w, src_h, sx, sy);
58 }
59 }
60
61 result
62}
63
64fn bilinear_sample(pixels: &[f64], w: usize, h: usize, x: f64, y: f64) -> f64 {
68 let x0 = x.floor() as i64;
69 let y0 = y.floor() as i64;
70 let x1 = x0 + 1;
71 let y1 = y0 + 1;
72
73 let fx = x - x0 as f64;
74 let fy = y - y0 as f64;
75
76 let get = |px: i64, py: i64| -> f64 {
77 if px >= 0 && px < w as i64 && py >= 0 && py < h as i64 {
78 pixels[py as usize * w + px as usize]
79 } else {
80 128.0
81 }
82 };
83
84 let v00 = get(x0, y0);
85 let v10 = get(x1, y0);
86 let v01 = get(x0, y1);
87 let v11 = get(x1, y1);
88
89 v00 * (1.0 - fx) * (1.0 - fy)
90 + v10 * fx * (1.0 - fy)
91 + v01 * (1.0 - fx) * fy
92 + v11 * fx * fy
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn identity_transform_preserves_image() {
101 let w = 16;
102 let h = 16;
103 let pixels: Vec<f64> = (0..w * h).map(|i| (i as f64) * 1.5 + 10.0).collect();
104
105 let identity = AffineTransform {
106 rotation_rad: 0.0,
107 scale: 1.0,
108 };
109
110 let result = resample_bilinear(&pixels, w, h, &identity, w, h);
111
112 for y in 1..h - 1 {
114 for x in 1..w - 1 {
115 let idx = y * w + x;
116 assert!(
117 (pixels[idx] - result[idx]).abs() < 0.01,
118 "Mismatch at ({x},{y}): expected {}, got {}",
119 pixels[idx],
120 result[idx]
121 );
122 }
123 }
124 }
125
126 #[test]
127 fn rotation_180_roundtrip() {
128 let w = 16;
129 let h = 16;
130 let pixels: Vec<f64> = (0..w * h).map(|i| (i % 7) as f64 * 30.0 + 50.0).collect();
131
132 let rot180 = AffineTransform {
134 rotation_rad: std::f64::consts::PI,
135 scale: 1.0,
136 };
137
138 let rotated = resample_bilinear(&pixels, w, h, &rot180, w, h);
139 let roundtrip = resample_bilinear(&rotated, w, h, &rot180, w, h);
140
141 for y in 2..h - 2 {
143 for x in 2..w - 2 {
144 let idx = y * w + x;
145 assert!(
146 (pixels[idx] - roundtrip[idx]).abs() < 1.0,
147 "Mismatch at ({x},{y}): expected {}, got {}",
148 pixels[idx],
149 roundtrip[idx]
150 );
151 }
152 }
153 }
154
155 #[test]
156 fn scale_2x_then_half() {
157 let w = 16;
158 let h = 16;
159 let pixels: Vec<f64> = (0..w * h).map(|i| 128.0 + (i as f64 * 0.5).sin() * 40.0).collect();
160
161 let scale2 = AffineTransform {
162 rotation_rad: 0.0,
163 scale: 2.0,
164 };
165 let scale_half = AffineTransform {
166 rotation_rad: 0.0,
167 scale: 0.5,
168 };
169
170 let scaled_up = resample_bilinear(&pixels, w, h, &scale2, w, h);
171 let roundtrip = resample_bilinear(&scaled_up, w, h, &scale_half, w, h);
172
173 for y in 4..h - 4 {
175 for x in 4..w - 4 {
176 let idx = y * w + x;
177 assert!(
178 (pixels[idx] - roundtrip[idx]).abs() < 5.0,
179 "Large mismatch at ({x},{y}): expected {}, got {}",
180 pixels[idx],
181 roundtrip[idx]
182 );
183 }
184 }
185 }
186}