1use chess_corners_core::{CornerDescriptor, ImageView};
14use serde::{Deserialize, Serialize};
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23#[non_exhaustive]
24pub enum UpscaleMode {
25 #[default]
27 Disabled,
28 Fixed,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(default)]
37#[non_exhaustive]
38pub struct UpscaleConfig {
39 pub mode: UpscaleMode,
41 pub factor: u32,
44}
45
46impl Default for UpscaleConfig {
47 fn default() -> Self {
48 Self {
49 mode: UpscaleMode::Disabled,
50 factor: 2,
51 }
52 }
53}
54
55impl UpscaleConfig {
56 pub fn disabled() -> Self {
58 Self::default()
59 }
60
61 pub fn fixed(factor: u32) -> Self {
63 Self {
64 mode: UpscaleMode::Fixed,
65 factor,
66 }
67 }
68
69 #[inline]
71 pub fn effective_factor(&self) -> u32 {
72 match self.mode {
73 UpscaleMode::Disabled => 1,
74 UpscaleMode::Fixed => self.factor,
75 }
76 }
77
78 pub fn validate(&self) -> Result<(), UpscaleError> {
80 if matches!(self.mode, UpscaleMode::Fixed) && !matches!(self.factor, 2..=4) {
81 return Err(UpscaleError::InvalidFactor(self.factor));
82 }
83 Ok(())
84 }
85}
86
87#[derive(Debug, PartialEq, Eq)]
89#[non_exhaustive]
90pub enum UpscaleError {
91 InvalidFactor(u32),
93 DimensionOverflow { src: (usize, usize), factor: u32 },
95 DimensionMismatch {
97 actual: usize,
99 expected: usize,
101 },
102}
103
104impl core::fmt::Display for UpscaleError {
105 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
106 match self {
107 Self::InvalidFactor(k) => {
108 write!(f, "upscale factor {k} not supported (expected 2, 3, or 4)")
109 }
110 Self::DimensionOverflow { src, factor } => write!(
111 f,
112 "upscaled dimensions overflow: {}x{} * {} exceeds usize",
113 src.0, src.1, factor
114 ),
115 Self::DimensionMismatch { actual, expected } => write!(
116 f,
117 "image buffer length mismatch: expected {expected} bytes (src_w*src_h), got {actual}"
118 ),
119 }
120 }
121}
122
123impl std::error::Error for UpscaleError {}
124
125#[derive(Debug, Default, Clone)]
131pub struct UpscaleBuffers {
132 buf: Vec<u8>,
133 w: usize,
134 h: usize,
135}
136
137impl UpscaleBuffers {
138 pub fn new() -> Self {
140 Self::default()
141 }
142
143 fn ensure(&mut self, w: usize, h: usize) {
144 self.w = w;
145 self.h = h;
146 let needed = w.saturating_mul(h);
147 if self.buf.len() < needed {
148 self.buf.resize(needed, 0);
149 }
150 }
151
152 pub fn width(&self) -> usize {
154 self.w
155 }
156
157 pub fn height(&self) -> usize {
159 self.h
160 }
161}
162
163pub fn upscale_bilinear_u8<'a>(
168 src: &[u8],
169 src_w: usize,
170 src_h: usize,
171 factor: u32,
172 buffers: &'a mut UpscaleBuffers,
173) -> Result<ImageView<'a>, UpscaleError> {
174 if !matches!(factor, 2..=4) {
175 return Err(UpscaleError::InvalidFactor(factor));
176 }
177 let k = factor as usize;
178 let dst_w = src_w
179 .checked_mul(k)
180 .ok_or(UpscaleError::DimensionOverflow {
181 src: (src_w, src_h),
182 factor,
183 })?;
184 let dst_h = src_h
185 .checked_mul(k)
186 .ok_or(UpscaleError::DimensionOverflow {
187 src: (src_w, src_h),
188 factor,
189 })?;
190
191 let expected = src_w * src_h;
192 if src.len() != expected {
193 return Err(UpscaleError::DimensionMismatch {
194 actual: src.len(),
195 expected,
196 });
197 }
198 buffers.ensure(dst_w, dst_h);
199
200 if src_w == 0 || src_h == 0 {
201 return Ok(ImageView::from_u8_slice(dst_w, dst_h, &buffers.buf[..dst_w * dst_h]).unwrap());
202 }
203
204 let inv_k = 1.0f32 / factor as f32;
205 let max_x = src_w as i32 - 1;
206 let max_y = src_h as i32 - 1;
207
208 let mut xw: Vec<(usize, usize, f32)> = Vec::with_capacity(dst_w);
212 for x_out in 0..dst_w {
213 let xf = (x_out as f32 + 0.5) * inv_k - 0.5;
214 let x0 = xf.floor() as i32;
215 let wx = xf - x0 as f32;
216 let x0c = x0.clamp(0, max_x) as usize;
217 let x1c = (x0 + 1).clamp(0, max_x) as usize;
218 xw.push((x0c, x1c, wx));
219 }
220
221 for y_out in 0..dst_h {
222 let yf = (y_out as f32 + 0.5) * inv_k - 0.5;
223 let y0 = yf.floor() as i32;
224 let wy = yf - y0 as f32;
225 let y0c = y0.clamp(0, max_y) as usize;
226 let y1c = (y0 + 1).clamp(0, max_y) as usize;
227 let row0 = y0c * src_w;
228 let row1 = y1c * src_w;
229 let dst_row = y_out * dst_w;
230
231 for (x_out, &(x0, x1, wx)) in xw.iter().enumerate().take(dst_w) {
232 let i00 = src[row0 + x0] as f32;
233 let i10 = src[row0 + x1] as f32;
234 let i01 = src[row1 + x0] as f32;
235 let i11 = src[row1 + x1] as f32;
236 let top = i00 + (i10 - i00) * wx;
237 let bot = i01 + (i11 - i01) * wx;
238 let v = top + (bot - top) * wy;
239 let rounded = v + 0.5;
241 buffers.buf[dst_row + x_out] = rounded.clamp(0.0, 255.0) as u8;
242 }
243 }
244
245 let slice = &buffers.buf[..dst_w * dst_h];
246 Ok(ImageView::from_u8_slice(dst_w, dst_h, slice).expect("dims match"))
247}
248
249pub fn rescale_descriptors_to_input(descriptors: &mut [CornerDescriptor], factor: u32) {
265 if factor <= 1 {
266 return;
267 }
268 let inv = 1.0f32 / factor as f32;
269 let shift = 0.5 * (1.0 - inv);
270 for d in descriptors.iter_mut() {
271 d.x = d.x * inv - shift;
272 d.y = d.y * inv - shift;
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn config_default_is_disabled() {
282 let cfg = UpscaleConfig::default();
283 assert_eq!(cfg.effective_factor(), 1);
284 assert!(cfg.validate().is_ok());
285 }
286
287 #[test]
288 fn config_rejects_invalid_factors() {
289 for bad in [0u32, 1, 5, 8] {
290 let cfg = UpscaleConfig::fixed(bad);
291 assert_eq!(cfg.validate(), Err(UpscaleError::InvalidFactor(bad)));
292 }
293 }
294
295 #[test]
296 fn config_accepts_valid_factors() {
297 for good in [2u32, 3, 4] {
298 let cfg = UpscaleConfig::fixed(good);
299 assert!(cfg.validate().is_ok());
300 assert_eq!(cfg.effective_factor(), good);
301 }
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}