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.radon_detector_params();
63 let mut rb = RadonBuffers::new();
64
65 if factor <= 1 {
66 let resp = radon_response_u8(
67 view.data(),
68 view.width(),
69 view.height(),
70 &radon_params,
71 &mut rb,
72 );
73 return Ok(resp.to_response_map());
74 }
75
76 let mut up_buffers = UpscaleBuffers::new();
77 let upscaled = upscale::upscale_bilinear_u8(img, src_w, src_h, factor, &mut up_buffers)?;
78 let resp = radon_response_u8(
79 upscaled.data(),
80 upscaled.width(),
81 upscaled.height(),
82 &radon_params,
83 &mut rb,
84 );
85 Ok(resp.to_response_map())
86}
87
88#[cfg(feature = "image")]
99pub fn radon_heatmap_image(
100 img: &::image::GrayImage,
101 cfg: &DetectorConfig,
102) -> Result<ResponseMap, ChessError> {
103 let (w, h) = img.dimensions();
104 radon_heatmap_u8(img.as_raw(), w, h, cfg)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::DetectorConfig;
111 use chess_corners_core::{radon_response_u8 as core_radon, RadonBuffers as CoreRadonBuffers};
112
113 fn synthetic_board(w: usize, h: usize) -> Vec<u8> {
114 let cell = (w.min(h) / 9).max(2);
117 let mut out = vec![0u8; w * h];
118 for y in 0..h {
119 for x in 0..w {
120 let cx = x / cell;
121 let cy = y / cell;
122 out[y * w + x] = if (cx + cy) & 1 == 0 { 220 } else { 35 };
123 }
124 }
125 out
126 }
127
128 #[test]
129 fn radon_heatmap_u8_reports_dimension_mismatch() {
130 let cfg = DetectorConfig::radon();
131 let img = vec![0u8; 10];
132 let err = radon_heatmap_u8(&img, 8, 8, &cfg).unwrap_err();
133 match err {
134 ChessError::DimensionMismatch { expected, actual } => {
135 assert_eq!(expected, 64);
136 assert_eq!(actual, 10);
137 }
138 other => panic!("expected ChessError::DimensionMismatch, got {other:?}"),
139 }
140 }
141
142 #[test]
143 fn heatmap_matches_core_path_no_upscale() {
144 let (w, h) = (96usize, 72usize);
145 let img = synthetic_board(w, h);
146 let cfg = DetectorConfig::radon();
147
148 let map = radon_heatmap_u8(&img, w as u32, h as u32, &cfg).unwrap();
149
150 let radon_params = cfg.radon_detector_params();
151 let mut rb = CoreRadonBuffers::new();
152 let view = core_radon(&img, w, h, &radon_params, &mut rb);
153 assert_eq!(map.width(), view.width());
154 assert_eq!(map.height(), view.height());
155 assert_eq!(map.data().len(), view.data().len());
156 assert_eq!(map.data(), view.data());
158 }
159
160 #[test]
161 fn heatmap_dimensions_match_working_resolution() {
162 let (w, h) = (96usize, 72usize);
163 let img = synthetic_board(w, h);
164 let cfg = DetectorConfig::radon();
165 let upsample = cfg.radon_detector_params().image_upsample.clamp(1, 2) as usize;
166
167 let map = radon_heatmap_u8(&img, w as u32, h as u32, &cfg).unwrap();
168 assert_eq!(map.width(), w * upsample);
169 assert_eq!(map.height(), h * upsample);
170 }
171
172 #[test]
173 fn heatmap_is_non_zero_on_a_board() {
174 let (w, h) = (96usize, 72usize);
175 let img = synthetic_board(w, h);
176 let cfg = DetectorConfig::radon();
177
178 let map = radon_heatmap_u8(&img, w as u32, h as u32, &cfg).unwrap();
179 let max = map.data().iter().copied().fold(f32::NEG_INFINITY, f32::max);
180 assert!(max > 0.0, "expected positive Radon response on a board");
181 }
182
183 #[test]
184 fn heatmap_honors_upscale_factor() {
185 use crate::upscale::UpscaleConfig;
186
187 let (w, h) = (48usize, 36usize);
188 let img = synthetic_board(w, h);
189 let mut cfg = DetectorConfig::radon();
190 cfg.upscale = UpscaleConfig::fixed(2);
191 let radon_upsample = cfg.radon_detector_params().image_upsample.clamp(1, 2) as usize;
192
193 let map = radon_heatmap_u8(&img, w as u32, h as u32, &cfg).unwrap();
194 assert_eq!(map.width(), w * 2 * radon_upsample);
196 assert_eq!(map.height(), h * 2 * radon_upsample);
197 }
198}