1use chess_corners_core::{CornerDescriptor, ImageView};
14use serde::{Deserialize, Serialize};
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27#[non_exhaustive]
28pub enum UpscaleConfig {
29 #[default]
31 Disabled,
32 Fixed(u32),
34}
35
36impl UpscaleConfig {
37 pub fn disabled() -> Self {
39 Self::Disabled
40 }
41
42 pub fn fixed(factor: u32) -> Self {
46 Self::Fixed(factor)
47 }
48
49 #[inline]
51 pub fn effective_factor(&self) -> u32 {
52 match *self {
53 Self::Disabled => 1,
54 Self::Fixed(k) => k,
55 }
56 }
57
58 pub fn validate(&self) -> Result<(), UpscaleError> {
60 match *self {
61 Self::Disabled => Ok(()),
62 Self::Fixed(2..=4) => Ok(()),
63 Self::Fixed(k) => Err(UpscaleError::InvalidFactor(k)),
64 }
65 }
66}
67
68#[derive(Debug, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum UpscaleError {
72 InvalidFactor(u32),
74 DimensionOverflow { src: (usize, usize), factor: u32 },
76 DimensionMismatch {
78 actual: usize,
80 expected: usize,
82 },
83}
84
85impl core::fmt::Display for UpscaleError {
86 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
87 match self {
88 Self::InvalidFactor(k) => {
89 write!(f, "upscale factor {k} not supported (expected 2, 3, or 4)")
90 }
91 Self::DimensionOverflow { src, factor } => write!(
92 f,
93 "upscaled dimensions overflow: {}x{} * {} exceeds usize",
94 src.0, src.1, factor
95 ),
96 Self::DimensionMismatch { actual, expected } => write!(
97 f,
98 "image buffer length mismatch: expected {expected} bytes (src_w*src_h), got {actual}"
99 ),
100 }
101 }
102}
103
104impl std::error::Error for UpscaleError {}
105
106#[derive(Debug, Default, Clone)]
112pub struct UpscaleBuffers {
113 buf: Vec<u8>,
114 w: usize,
115 h: usize,
116}
117
118impl UpscaleBuffers {
119 pub fn new() -> Self {
121 Self::default()
122 }
123
124 fn ensure(&mut self, w: usize, h: usize) {
125 self.w = w;
126 self.h = h;
127 let needed = w.saturating_mul(h);
128 if self.buf.len() < needed {
129 self.buf.resize(needed, 0);
130 }
131 }
132
133 pub fn width(&self) -> usize {
135 self.w
136 }
137
138 pub fn height(&self) -> usize {
140 self.h
141 }
142}
143
144pub fn upscale_bilinear_u8<'a>(
149 src: &[u8],
150 src_w: usize,
151 src_h: usize,
152 factor: u32,
153 buffers: &'a mut UpscaleBuffers,
154) -> Result<ImageView<'a>, UpscaleError> {
155 if !matches!(factor, 2..=4) {
156 return Err(UpscaleError::InvalidFactor(factor));
157 }
158 let k = factor as usize;
159 let dst_w = src_w
160 .checked_mul(k)
161 .ok_or(UpscaleError::DimensionOverflow {
162 src: (src_w, src_h),
163 factor,
164 })?;
165 let dst_h = src_h
166 .checked_mul(k)
167 .ok_or(UpscaleError::DimensionOverflow {
168 src: (src_w, src_h),
169 factor,
170 })?;
171
172 let expected = src_w * src_h;
173 if src.len() != expected {
174 return Err(UpscaleError::DimensionMismatch {
175 actual: src.len(),
176 expected,
177 });
178 }
179 buffers.ensure(dst_w, dst_h);
180
181 if src_w == 0 || src_h == 0 {
182 return Ok(ImageView::from_u8_slice(dst_w, dst_h, &buffers.buf[..dst_w * dst_h]).unwrap());
183 }
184
185 let inv_k = 1.0f32 / factor as f32;
186 let max_x = src_w as i32 - 1;
187 let max_y = src_h as i32 - 1;
188
189 let mut xw: Vec<(usize, usize, f32)> = Vec::with_capacity(dst_w);
193 for x_out in 0..dst_w {
194 let xf = (x_out as f32 + 0.5) * inv_k - 0.5;
195 let x0 = xf.floor() as i32;
196 let wx = xf - x0 as f32;
197 let x0c = x0.clamp(0, max_x) as usize;
198 let x1c = (x0 + 1).clamp(0, max_x) as usize;
199 xw.push((x0c, x1c, wx));
200 }
201
202 for y_out in 0..dst_h {
203 let yf = (y_out as f32 + 0.5) * inv_k - 0.5;
204 let y0 = yf.floor() as i32;
205 let wy = yf - y0 as f32;
206 let y0c = y0.clamp(0, max_y) as usize;
207 let y1c = (y0 + 1).clamp(0, max_y) as usize;
208 let row0 = y0c * src_w;
209 let row1 = y1c * src_w;
210 let dst_row = y_out * dst_w;
211
212 for (x_out, &(x0, x1, wx)) in xw.iter().enumerate().take(dst_w) {
213 let i00 = src[row0 + x0] as f32;
214 let i10 = src[row0 + x1] as f32;
215 let i01 = src[row1 + x0] as f32;
216 let i11 = src[row1 + x1] as f32;
217 let top = i00 + (i10 - i00) * wx;
218 let bot = i01 + (i11 - i01) * wx;
219 let v = top + (bot - top) * wy;
220 let rounded = v + 0.5;
222 buffers.buf[dst_row + x_out] = rounded.clamp(0.0, 255.0) as u8;
223 }
224 }
225
226 let slice = &buffers.buf[..dst_w * dst_h];
227 Ok(ImageView::from_u8_slice(dst_w, dst_h, slice).expect("dims match"))
228}
229
230pub fn rescale_descriptors_to_input(descriptors: &mut [CornerDescriptor], factor: u32) {
246 if factor <= 1 {
247 return;
248 }
249 let inv = 1.0f32 / factor as f32;
250 let shift = 0.5 * (1.0 - inv);
251 for d in descriptors.iter_mut() {
252 d.x = d.x * inv - shift;
253 d.y = d.y * inv - shift;
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn config_default_is_disabled() {
263 let cfg = UpscaleConfig::default();
264 assert_eq!(cfg, UpscaleConfig::Disabled);
265 assert_eq!(cfg.effective_factor(), 1);
266 assert!(cfg.validate().is_ok());
267 }
268
269 #[test]
270 fn config_rejects_invalid_factors() {
271 for bad in [0u32, 1, 5, 8] {
272 let cfg = UpscaleConfig::fixed(bad);
273 assert_eq!(cfg.validate(), Err(UpscaleError::InvalidFactor(bad)));
274 }
275 }
276
277 #[test]
278 fn config_accepts_valid_factors() {
279 for good in [2u32, 3, 4] {
280 let cfg = UpscaleConfig::fixed(good);
281 assert!(cfg.validate().is_ok());
282 assert_eq!(cfg.effective_factor(), good);
283 }
284 }
285
286 #[test]
287 fn disabled_round_trips_through_serde() {
288 let cfg = UpscaleConfig::Disabled;
289 let json = serde_json::to_string(&cfg).expect("serialize disabled");
290 assert!(json.contains("disabled"));
291 let decoded: UpscaleConfig = serde_json::from_str(&json).expect("deserialize disabled");
292 assert_eq!(decoded, cfg);
293 }
294
295 #[test]
296 fn fixed_round_trips_through_serde() {
297 let cfg = UpscaleConfig::Fixed(3);
298 let json = serde_json::to_string(&cfg).expect("serialize fixed");
299 assert!(json.contains("fixed"));
300 let decoded: UpscaleConfig = serde_json::from_str(&json).expect("deserialize fixed");
301 assert_eq!(decoded, cfg);
302 }
303
304 #[test]
305 fn upscale_factor_2_uniform_image_is_uniform() {
306 let src = vec![42u8; 8 * 6];
307 let mut buffers = UpscaleBuffers::new();
308 let view = upscale_bilinear_u8(&src, 8, 6, 2, &mut buffers).unwrap();
309 assert_eq!(view.width, 16);
310 assert_eq!(view.height, 12);
311 assert!(view.data.iter().all(|&v| v == 42));
312 }
313
314 #[test]
315 fn upscale_factor_2_of_1x1_fills_buffer() {
316 let src = [77u8];
317 let mut buffers = UpscaleBuffers::new();
318 let view = upscale_bilinear_u8(&src, 1, 1, 2, &mut buffers).unwrap();
319 assert_eq!(view.width, 2);
320 assert_eq!(view.height, 2);
321 assert!(view.data.iter().all(|&v| v == 77));
322 }
323
324 #[test]
325 fn upscale_preserves_linear_gradient_factor_2() {
326 let src: Vec<u8> = (0..8).map(|i| i * 10).collect();
328 let src = {
329 let mut row = Vec::with_capacity(8 * 3);
330 for _ in 0..3 {
331 row.extend_from_slice(&src);
332 }
333 row
334 };
335 let mut buffers = UpscaleBuffers::new();
336 let view = upscale_bilinear_u8(&src, 8, 3, 2, &mut buffers).unwrap();
337 for r in 0..view.height {
339 let row = &view.data[r * view.width..(r + 1) * view.width];
340 for w in row.windows(2) {
341 assert!(w[1] >= w[0].saturating_sub(1), "non-monotonic row: {row:?}");
342 }
343 }
344 }
345
346 #[test]
347 fn upscale_factor_3_doubles_dimensions_correctly() {
348 let src = vec![128u8; 5 * 4];
349 let mut buffers = UpscaleBuffers::new();
350 let view = upscale_bilinear_u8(&src, 5, 4, 3, &mut buffers).unwrap();
351 assert_eq!(view.width, 15);
352 assert_eq!(view.height, 12);
353 assert_eq!(view.data.len(), 180);
354 }
355
356 #[test]
357 fn buffers_are_reused_across_calls() {
358 let src1 = vec![10u8; 4 * 4];
359 let src2 = vec![200u8; 4 * 4];
360 let mut buffers = UpscaleBuffers::new();
361 let _ = upscale_bilinear_u8(&src1, 4, 4, 2, &mut buffers).unwrap();
362 let cap1 = buffers.buf.capacity();
363 let _ = upscale_bilinear_u8(&src2, 4, 4, 2, &mut buffers).unwrap();
364 assert_eq!(buffers.buf.capacity(), cap1, "buffer should be reused");
365 }
366
367 #[test]
368 fn rejects_invalid_factor_at_runtime() {
369 let src = vec![0u8; 4];
370 let mut buffers = UpscaleBuffers::new();
371 let err = upscale_bilinear_u8(&src, 2, 2, 5, &mut buffers).unwrap_err();
372 assert_eq!(err, UpscaleError::InvalidFactor(5));
373 }
374
375 #[test]
376 fn rescale_inverts_half_pixel_upscale() {
377 use chess_corners_core::{AxisEstimate, CornerDescriptor};
378
379 fn desc(x: f32, y: f32) -> CornerDescriptor {
386 CornerDescriptor::new(
387 x,
388 y,
389 1.0,
390 0.0,
391 0.0,
392 [AxisEstimate::new(0.0, 0.0), AxisEstimate::new(0.0, 0.0)],
393 )
394 }
395
396 for &(k, x_src, y_src) in &[
397 (2u32, 7.25f32, 3.0f32),
398 (3u32, 4.0f32, 8.5f32),
399 (4u32, 0.5f32, 12.25f32),
400 ] {
401 let kf = k as f32;
402 let x_out = (x_src + 0.5) * kf - 0.5;
403 let y_out = (y_src + 0.5) * kf - 0.5;
404
405 let mut d = [desc(x_out, y_out)];
406 rescale_descriptors_to_input(&mut d, k);
407 assert!(
408 (d[0].x - x_src).abs() < 1e-5,
409 "k={k}: x {} != expected {x_src}",
410 d[0].x
411 );
412 assert!(
413 (d[0].y - y_src).abs() < 1e-5,
414 "k={k}: y {} != expected {y_src}",
415 d[0].y
416 );
417 }
418 }
419
420 #[test]
421 fn rescale_is_noop_for_factor_1() {
422 use chess_corners_core::{AxisEstimate, CornerDescriptor};
423 let mut d = [CornerDescriptor::new(
424 2.5,
425 3.75,
426 1.0,
427 0.0,
428 0.0,
429 [AxisEstimate::new(0.0, 0.0), AxisEstimate::new(0.0, 0.0)],
430 )];
431 rescale_descriptors_to_input(&mut d, 1);
432 assert_eq!(d[0].x, 2.5);
433 assert_eq!(d[0].y, 3.75);
434 }
435}