1use chess_corners_core::{radon_response_u8, ImageView, RadonBuffers, ResponseMap};
22
23use crate::config::DetectorConfig;
24use crate::error::ChessError;
25use crate::upscale::{self, UpscaleBuffers};
26
27pub fn radon_heatmap_u8(
43 img: &[u8],
44 width: u32,
45 height: u32,
46 cfg: &DetectorConfig,
47) -> Result<ResponseMap, ChessError> {
48 cfg.upscale.validate()?;
49
50 let src_w = width as usize;
51 let src_h = height as usize;
52 let expected = src_w * src_h;
53 if img.len() != expected {
54 return Err(ChessError::DimensionMismatch {
55 expected,
56 actual: img.len(),
57 });
58 }
59 let view = ImageView::from_u8_slice(src_w, src_h, img).expect("dimensions were checked above");
60
61 let factor = cfg.upscale.effective_factor();
62 let radon_params = cfg.to_radon_detector_params();
63 let mut rb = RadonBuffers::new();
64
65 if factor <= 1 {
66 let resp = radon_response_u8(view.data, view.width, view.height, &radon_params, &mut rb);
67 return Ok(resp.to_response_map());
68 }
69
70 let mut up_buffers = UpscaleBuffers::new();
71 let upscaled = upscale::upscale_bilinear_u8(img, src_w, src_h, factor, &mut up_buffers)?;
72 let resp = radon_response_u8(
73 upscaled.data,
74 upscaled.width,
75 upscaled.height,
76 &radon_params,
77 &mut rb,
78 );
79 Ok(resp.to_response_map())
80}
81
82#[cfg(feature = "image")]
93pub fn radon_heatmap_image(
94 img: &::image::GrayImage,
95 cfg: &DetectorConfig,
96) -> Result<ResponseMap, ChessError> {
97 let (w, h) = img.dimensions();
98 radon_heatmap_u8(img.as_raw(), w, h, cfg)
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::DetectorConfig;
105 use chess_corners_core::{radon_response_u8 as core_radon, RadonBuffers as CoreRadonBuffers};
106
107 fn synthetic_board(w: usize, h: usize) -> Vec<u8> {
108 let cell = (w.min(h) / 9).max(2);
111 let mut out = vec![0u8; w * h];
112 for y in 0..h {
113 for x in 0..w {
114 let cx = x / cell;
115 let cy = y / cell;
116 out[y * w + x] = if (cx + cy) & 1 == 0 { 220 } else { 35 };
117 }
118 }
119 out
120 }
121
122 #[test]
123 fn heatmap_matches_core_path_no_upscale() {
124 let (w, h) = (96usize, 72usize);
125 let img = synthetic_board(w, h);
126 let cfg = DetectorConfig::radon();
127
128 let map = radon_heatmap_u8(&img, w as u32, h as u32, &cfg).unwrap();
129
130 let radon_params = cfg.to_radon_detector_params();
131 let mut rb = CoreRadonBuffers::new();
132 let view = core_radon(&img, w, h, &radon_params, &mut rb);
133 assert_eq!(map.width(), view.width());
134 assert_eq!(map.height(), view.height());
135 assert_eq!(map.data().len(), view.data().len());
136 assert_eq!(map.data(), view.data());
138 }
139
140 #[test]
141 fn heatmap_dimensions_match_working_resolution() {
142 let (w, h) = (96usize, 72usize);
143 let img = synthetic_board(w, h);
144 let cfg = DetectorConfig::radon();
145 let upsample = cfg.to_radon_detector_params().image_upsample.clamp(1, 2) as usize;
146
147 let map = radon_heatmap_u8(&img, w as u32, h as u32, &cfg).unwrap();
148 assert_eq!(map.width(), w * upsample);
149 assert_eq!(map.height(), h * upsample);
150 }
151
152 #[test]
153 fn heatmap_is_non_zero_on_a_board() {
154 let (w, h) = (96usize, 72usize);
155 let img = synthetic_board(w, h);
156 let cfg = DetectorConfig::radon();
157
158 let map = radon_heatmap_u8(&img, w as u32, h as u32, &cfg).unwrap();
159 let max = map.data().iter().copied().fold(f32::NEG_INFINITY, f32::max);
160 assert!(max > 0.0, "expected positive Radon response on a board");
161 }
162
163 #[test]
164 fn heatmap_honors_upscale_factor() {
165 use crate::upscale::UpscaleConfig;
166
167 let (w, h) = (48usize, 36usize);
168 let img = synthetic_board(w, h);
169 let mut cfg = DetectorConfig::radon();
170 cfg.upscale = UpscaleConfig::fixed(2);
171 let radon_upsample = cfg.to_radon_detector_params().image_upsample.clamp(1, 2) as usize;
172
173 let map = radon_heatmap_u8(&img, w as u32, h as u32, &cfg).unwrap();
174 assert_eq!(map.width(), w * 2 * radon_upsample);
176 assert_eq!(map.height(), h * 2 * radon_upsample);
177 }
178}