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 {
76 src: (usize, usize),
78 factor: u32,
80 },
81 DimensionMismatch {
83 actual: usize,
85 expected: usize,
87 },
88}
89
90impl core::fmt::Display for UpscaleError {
91 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
92 match self {
93 Self::InvalidFactor(k) => {
94 write!(f, "upscale factor {k} not supported (expected 2, 3, or 4)")
95 }
96 Self::DimensionOverflow { src, factor } => write!(
97 f,
98 "upscaled dimensions overflow: {}x{} * {} exceeds usize",
99 src.0, src.1, factor
100 ),
101 Self::DimensionMismatch { actual, expected } => write!(
102 f,
103 "image buffer length mismatch: expected {expected} bytes (src_w*src_h), got {actual}"
104 ),
105 }
106 }
107}
108
109impl std::error::Error for UpscaleError {}
110
111#[derive(Debug, Default, Clone)]
117pub struct UpscaleBuffers {
118 buf: Vec<u8>,
119 w: usize,
120 h: usize,
121}
122
123impl UpscaleBuffers {
124 pub fn new() -> Self {
126 Self::default()
127 }
128
129 fn ensure(&mut self, w: usize, h: usize) {
130 self.w = w;
131 self.h = h;
132 let needed = w.saturating_mul(h);
133 if self.buf.len() < needed {
134 self.buf.resize(needed, 0);
135 }
136 }
137
138 pub fn width(&self) -> usize {
140 self.w
141 }
142
143 pub fn height(&self) -> usize {
145 self.h
146 }
147}
148
149pub fn upscale_bilinear_u8<'a>(
154 src: &[u8],
155 src_w: usize,
156 src_h: usize,
157 factor: u32,
158 buffers: &'a mut UpscaleBuffers,
159) -> Result<ImageView<'a>, UpscaleError> {
160 if !matches!(factor, 2..=4) {
161 return Err(UpscaleError::InvalidFactor(factor));
162 }
163 let k = factor as usize;
164 let dst_w = src_w
165 .checked_mul(k)
166 .ok_or(UpscaleError::DimensionOverflow {
167 src: (src_w, src_h),
168 factor,
169 })?;
170 let dst_h = src_h
171 .checked_mul(k)
172 .ok_or(UpscaleError::DimensionOverflow {
173 src: (src_w, src_h),
174 factor,
175 })?;
176
177 let expected = src_w * src_h;
178 if src.len() != expected {
179 return Err(UpscaleError::DimensionMismatch {
180 actual: src.len(),
181 expected,
182 });
183 }
184 buffers.ensure(dst_w, dst_h);
185
186 if src_w == 0 || src_h == 0 {
187 return Ok(
188 ImageView::from_u8_slice(dst_w, dst_h, &buffers.buf[..dst_w * dst_h])
189 .expect("dims match"),
190 );
191 }
192
193 let inv_k = 1.0f32 / factor as f32;
194 let max_x = src_w as i32 - 1;
195 let max_y = src_h as i32 - 1;
196
197 let mut xw: Vec<(usize, usize, f32)> = Vec::with_capacity(dst_w);
201 for x_out in 0..dst_w {
202 let xf = (x_out as f32 + 0.5) * inv_k - 0.5;
203 let x0 = xf.floor() as i32;
204 let wx = xf - x0 as f32;
205 let x0c = x0.clamp(0, max_x) as usize;
206 let x1c = (x0 + 1).clamp(0, max_x) as usize;
207 xw.push((x0c, x1c, wx));
208 }
209
210 for y_out in 0..dst_h {
211 let yf = (y_out as f32 + 0.5) * inv_k - 0.5;
212 let y0 = yf.floor() as i32;
213 let wy = yf - y0 as f32;
214 let y0c = y0.clamp(0, max_y) as usize;
215 let y1c = (y0 + 1).clamp(0, max_y) as usize;
216 let row0 = y0c * src_w;
217 let row1 = y1c * src_w;
218 let dst_row = y_out * dst_w;
219
220 for (x_out, &(x0, x1, wx)) in xw.iter().enumerate().take(dst_w) {
221 let i00 = src[row0 + x0] as f32;
222 let i10 = src[row0 + x1] as f32;
223 let i01 = src[row1 + x0] as f32;
224 let i11 = src[row1 + x1] as f32;
225 let top = i00 + (i10 - i00) * wx;
226 let bot = i01 + (i11 - i01) * wx;
227 let v = top + (bot - top) * wy;
228 let rounded = v + 0.5;
230 buffers.buf[dst_row + x_out] = rounded.clamp(0.0, 255.0) as u8;
231 }
232 }
233
234 let slice = &buffers.buf[..dst_w * dst_h];
235 Ok(ImageView::from_u8_slice(dst_w, dst_h, slice).expect("dims match"))
236}
237
238pub fn rescale_descriptors_to_input(descriptors: &mut [CornerDescriptor], factor: u32) {
254 if factor <= 1 {
255 return;
256 }
257 let inv = 1.0f32 / factor as f32;
258 let shift = 0.5 * (1.0 - inv);
259 for d in descriptors.iter_mut() {
260 d.x = d.x * inv - shift;
261 d.y = d.y * inv - shift;
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn config_default_is_disabled() {
271 let cfg = UpscaleConfig::default();
272 assert_eq!(cfg, UpscaleConfig::Disabled);
273 assert_eq!(cfg.effective_factor(), 1);
274 assert!(cfg.validate().is_ok());
275 }
276
277 #[test]
278 fn config_rejects_invalid_factors() {
279 for bad in [0u32, 1, 5, 8] {
280 let cfg = UpscaleConfig::fixed(bad);
281 assert_eq!(cfg.validate(), Err(UpscaleError::InvalidFactor(bad)));
282 }
283 }
284
285 #[test]
286 fn config_accepts_valid_factors() {
287 for good in [2u32, 3, 4] {
288 let cfg = UpscaleConfig::fixed(good);
289 assert!(cfg.validate().is_ok());
290 assert_eq!(cfg.effective_factor(), good);
291 }
292 }
293
294 #[test]
295 fn disabled_round_trips_through_serde() {
296 let cfg = UpscaleConfig::Disabled;
297 let json = serde_json::to_string(&cfg).expect("serialize disabled");
298 assert!(json.contains("disabled"));
299 let decoded: UpscaleConfig = serde_json::from_str(&json).expect("deserialize disabled");
300 assert_eq!(decoded, cfg);
301 }
302
303 #[test]
304 fn fixed_round_trips_through_serde() {
305 let cfg = UpscaleConfig::Fixed(3);
306 let json = serde_json::to_string(&cfg).expect("serialize fixed");
307 assert!(json.contains("fixed"));
308 let decoded: UpscaleConfig = serde_json::from_str(&json).expect("deserialize fixed");
309 assert_eq!(decoded, cfg);
310 }
311
312 #[test]
313 fn upscale_bilinear_u8_reports_dimension_mismatch() {
314 let src = vec![0u8; 10]; let mut buffers = UpscaleBuffers::new();
316 let err = upscale_bilinear_u8(&src, 4, 4, 2, &mut buffers).unwrap_err();
317 assert_eq!(
318 err,
319 UpscaleError::DimensionMismatch {
320 expected: 16,
321 actual: 10,
322 }
323 );
324 }
325
326 #[test]
327 fn upscale_factor_2_uniform_image_is_uniform() {
328 let src = vec![42u8; 8 * 6];
329 let mut buffers = UpscaleBuffers::new();
330 let view = upscale_bilinear_u8(&src, 8, 6, 2, &mut buffers).unwrap();
331 assert_eq!(view.width(), 16);
332 assert_eq!(view.height(), 12);
333 assert!(view.data().iter().all(|&v| v == 42));
334 }
335
336 #[test]
337 fn upscale_factor_2_of_1x1_fills_buffer() {
338 let src = [77u8];
339 let mut buffers = UpscaleBuffers::new();
340 let view = upscale_bilinear_u8(&src, 1, 1, 2, &mut buffers).unwrap();
341 assert_eq!(view.width(), 2);
342 assert_eq!(view.height(), 2);
343 assert!(view.data().iter().all(|&v| v == 77));
344 }
345
346 #[test]
347 fn upscale_preserves_linear_gradient_factor_2() {
348 let src: Vec<u8> = (0..8).map(|i| i * 10).collect();
350 let src = {
351 let mut row = Vec::with_capacity(8 * 3);
352 for _ in 0..3 {
353 row.extend_from_slice(&src);
354 }
355 row
356 };
357 let mut buffers = UpscaleBuffers::new();
358 let view = upscale_bilinear_u8(&src, 8, 3, 2, &mut buffers).unwrap();
359 for r in 0..view.height() {
361 let row = &view.data()[r * view.width()..(r + 1) * view.width()];
362 for w in row.windows(2) {
363 assert!(w[1] >= w[0].saturating_sub(1), "non-monotonic row: {row:?}");
364 }
365 }
366 }
367
368 #[test]
369 fn upscale_factor_3_doubles_dimensions_correctly() {
370 let src = vec![128u8; 5 * 4];
371 let mut buffers = UpscaleBuffers::new();
372 let view = upscale_bilinear_u8(&src, 5, 4, 3, &mut buffers).unwrap();
373 assert_eq!(view.width(), 15);
374 assert_eq!(view.height(), 12);
375 assert_eq!(view.data().len(), 180);
376 }
377
378 #[test]
379 fn buffers_are_reused_across_calls() {
380 let src1 = vec![10u8; 4 * 4];
381 let src2 = vec![200u8; 4 * 4];
382 let mut buffers = UpscaleBuffers::new();
383 let _ = upscale_bilinear_u8(&src1, 4, 4, 2, &mut buffers).unwrap();
384 let cap1 = buffers.buf.capacity();
385 let _ = upscale_bilinear_u8(&src2, 4, 4, 2, &mut buffers).unwrap();
386 assert_eq!(buffers.buf.capacity(), cap1, "buffer should be reused");
387 }
388
389 #[test]
390 fn rejects_invalid_factor_at_runtime() {
391 let src = vec![0u8; 4];
392 let mut buffers = UpscaleBuffers::new();
393 let err = upscale_bilinear_u8(&src, 2, 2, 5, &mut buffers).unwrap_err();
394 assert_eq!(err, UpscaleError::InvalidFactor(5));
395 }
396
397 #[test]
398 fn rescale_inverts_half_pixel_upscale() {
399 use chess_corners_core::{AxisEstimate, CornerDescriptor};
400
401 fn desc(x: f32, y: f32) -> CornerDescriptor {
408 CornerDescriptor::new(
409 x,
410 y,
411 1.0,
412 [AxisEstimate::new(0.0, 0.0), AxisEstimate::new(0.0, 0.0)],
413 )
414 }
415
416 for &(k, x_src, y_src) in &[
417 (2u32, 7.25f32, 3.0f32),
418 (3u32, 4.0f32, 8.5f32),
419 (4u32, 0.5f32, 12.25f32),
420 ] {
421 let kf = k as f32;
422 let x_out = (x_src + 0.5) * kf - 0.5;
423 let y_out = (y_src + 0.5) * kf - 0.5;
424
425 let mut d = [desc(x_out, y_out)];
426 rescale_descriptors_to_input(&mut d, k);
427 assert!(
428 (d[0].x - x_src).abs() < 1e-5,
429 "k={k}: x {} != expected {x_src}",
430 d[0].x
431 );
432 assert!(
433 (d[0].y - y_src).abs() < 1e-5,
434 "k={k}: y {} != expected {y_src}",
435 d[0].y
436 );
437 }
438 }
439
440 #[test]
441 fn rescale_is_noop_for_factor_1() {
442 use chess_corners_core::{AxisEstimate, CornerDescriptor};
443 let mut d = [CornerDescriptor::new(
444 2.5,
445 3.75,
446 1.0,
447 [AxisEstimate::new(0.0, 0.0), AxisEstimate::new(0.0, 0.0)],
448 )];
449 rescale_descriptors_to_input(&mut d, 1);
450 assert_eq!(d[0].x, 2.5);
451 assert_eq!(d[0].y, 3.75);
452 }
453}