1use crate::core::buffer::DocBuffer;
3use crate::edge::morphology::Morphology;
4use crate::edge::threshold::AdaptiveThreshold;
5use crate::error::DocQuadError;
6use fast_canny::{CannyConfig, CannyWorkspace, canny_u8};
7use std::time::Instant;
8
9pub struct EdgeDetector {
11 workspace: CannyWorkspace,
12 width: usize,
13 height: usize,
14}
15
16impl EdgeDetector {
17 pub fn new(width: usize, height: usize) -> Result<Self, DocQuadError> {
18 let workspace = CannyWorkspace::new(width, height).map_err(|e| {
19 log::error!(
20 "[Edge::Detector] - Failed to create CannyWorkspace: {:?}",
21 e
22 );
23 DocQuadError::EdgeDetectionError
24 })?;
25
26 log::info!(
27 "[Edge::Detector] - CannyWorkspace pre-allocated for {}x{}.",
28 width,
29 height
30 );
31
32 Ok(Self {
33 workspace,
34 width,
35 height,
36 })
37 }
38
39 pub fn detect(&mut self, buffer: &DocBuffer<'_>) -> Result<Vec<u8>, DocQuadError> {
40 self.detect_with_debug(buffer, None)
41 }
42
43 pub fn detect_with_debug(
49 &mut self,
50 buffer: &DocBuffer<'_>,
51 debug_dir: Option<&std::path::Path>,
52 ) -> Result<Vec<u8>, DocQuadError> {
53 let start = Instant::now();
54
55 let view = buffer.as_array_view()?;
56
57 let (low, high) = AdaptiveThreshold::calculate(&view, 0.33);
59
60 let mut hist = [0u32; 256];
62 for &pixel in view.iter() {
63 hist[pixel as usize] += 1;
64 }
65 let total_pixels = (buffer.width * buffer.height) as f32;
66
67 let dark_pct = hist[0..64].iter().sum::<u32>() as f32 / total_pixels * 100.0;
69 let mid_pct = hist[64..192].iter().sum::<u32>() as f32 / total_pixels * 100.0;
70 let bright_pct = hist[192..256].iter().sum::<u32>() as f32 / total_pixels * 100.0;
71
72 log::info!(
73 "[Edge::Detector] - Pixel brightness distribution: \
74 dark(0-63)={:.1}%, mid(64-191)={:.1}%, bright(192-255)={:.1}%",
75 dark_pct, mid_pct, bright_pct
76 );
77
78 let mut max_bucket_count = 0u32;
80 let mut max_bucket_start = 0usize;
81 for i in (0..256).step_by(16) {
82 let bucket_sum: u32 = hist[i..i + 16].iter().sum();
83 if bucket_sum > max_bucket_count {
84 max_bucket_count = bucket_sum;
85 max_bucket_start = i;
86 }
87 }
88 log::info!(
89 "[Edge::Detector] - Histogram peak bucket: [{}-{}] = {} pixels ({:.1}%)",
90 max_bucket_start,
91 max_bucket_start + 15,
92 max_bucket_count,
93 max_bucket_count as f32 / total_pixels * 100.0
94 );
95
96 log::info!(
97 "[Edge::Detector] - Adaptive thresholds: low={:.2}, high={:.2} \
98 (image={}x{}, sigma=0.33)",
99 low, high, buffer.width, buffer.height
100 );
101
102 if high > 200.0 {
103 log::warn!(
104 "[Edge::Detector] - Canny high threshold {:.2} is very high (>200). \
105 Document edges with weak gradients may be missed.",
106 high
107 );
108 }
109
110 let input_data: Vec<u8> = if buffer.stride == buffer.width {
112 log::debug!(
113 "[Edge::Detector] - Input is contiguous (stride==width={}), direct copy.",
114 buffer.width
115 );
116 buffer.data[..(buffer.width * buffer.height) as usize].to_vec()
117 } else {
118 log::debug!(
119 "[Edge::Detector] - Input has stride padding (stride={} > width={}), compacting rows.",
120 buffer.stride, buffer.width
121 );
122 let mut compact = Vec::with_capacity((buffer.width * buffer.height) as usize);
123 for row in view.rows() {
124 compact.extend(row.iter().copied());
125 }
126 compact
127 };
128
129 if let Some(dir) = debug_dir {
131 Self::save_gray_image(
132 &input_data,
133 self.width,
134 self.height,
135 &dir.join("debug_01_input_gray.png"),
136 );
137 }
138
139 log::info!(
142 "[Edge::Detector] - [DEBUG] Threshold sensitivity scan \
143 (sigma=1.0, 3 trial configs):"
144 );
145 for &(trial_low, trial_high) in &[(2.0f32, 8.0), (5.0, 20.0), (10.0, 40.0)] {
146 if let Ok(cfg) = CannyConfig::builder()
147 .sigma(1.0)
148 .thresholds(trial_low, trial_high)
149 .build()
150 {
151 if let Ok(trial_slice) = canny_u8(&input_data, &mut self.workspace, &cfg) {
153 let trial_count = trial_slice.iter().filter(|&&v| v == 255).count();
154 let trial_density =
155 trial_count as f32 / (self.width * self.height) as f32 * 100.0;
156 log::info!(
157 "[Edge::Detector] - [DEBUG] low={:.1}, high={:.1} -> \
158 edge_pixels={}, density={:.2}%",
159 trial_low, trial_high, trial_count, trial_density
160 );
161 }
162 }
163 }
164
165 log::info!(
167 "[Edge::Detector] - [DEBUG] Low-sigma scan (sigma=0.5, low=5.0, high=20.0):"
168 );
169 if let Ok(cfg_low_sigma) = CannyConfig::builder()
170 .sigma(0.5)
171 .thresholds(5.0, 20.0)
172 .build()
173 {
174 if let Ok(trial_slice) = canny_u8(&input_data, &mut self.workspace, &cfg_low_sigma) {
175 let trial_count = trial_slice.iter().filter(|&&v| v == 255).count();
176 let trial_density =
177 trial_count as f32 / (self.width * self.height) as f32 * 100.0;
178 log::info!(
179 "[Edge::Detector] - [DEBUG] sigma=0.5, low=5.0, high=20.0 -> \
180 edge_pixels={}, density={:.2}%",
181 trial_count, trial_density
182 );
183 }
184 }
185
186 let cfg = CannyConfig::builder()
188 .sigma(1.0)
189 .thresholds(low, high)
190 .build()
191 .map_err(|e| {
192 log::error!("[Edge::Detector] - Invalid Canny config: {:?}", e);
193 DocQuadError::EdgeDetectionError
194 })?;
195
196 let canny_start = Instant::now();
197 let edge_slice = canny_u8(&input_data, &mut self.workspace, &cfg).map_err(|e| {
198 log::error!("[Edge::Detector] - canny_u8 failed: {:?}", e);
199 DocQuadError::EdgeDetectionError
200 })?;
201
202 let raw_edges = edge_slice.to_vec();
203 let canny_elapsed = canny_start.elapsed().as_millis();
204 let raw_edge_count = raw_edges.iter().filter(|&&v| v == 255).count();
205 let raw_density = raw_edge_count as f32 / (self.width * self.height) as f32 * 100.0;
206
207 log::info!(
208 "[Edge::Detector] - Canny raw result: edge_pixels={}, density={:.2}%, \
209 size={}x{}. Canny elapsed: {}ms",
210 raw_edge_count, raw_density, self.width, self.height, canny_elapsed
211 );
212
213 Self::log_edge_spatial_distribution(&raw_edges, self.width, self.height, "raw_canny");
215
216 if let Some(dir) = debug_dir {
218 Self::save_binary_image(
219 &raw_edges,
220 self.width,
221 self.height,
222 &dir.join("debug_02_canny_raw.png"),
223 );
224 }
225
226 let morph_radius = Self::choose_morph_radius(self.width, self.height);
228 log::debug!(
229 "[Edge::Detector] - Applying morphological close: radius={} (image={}x{})",
230 morph_radius, self.width, self.height
231 );
232
233 let closed_edges = Morphology::close(&raw_edges, self.width, self.height, morph_radius);
234
235 let closed_edge_count = closed_edges.iter().filter(|&&v| v == 255).count();
236 let closed_density =
237 closed_edge_count as f32 / (self.width * self.height) as f32 * 100.0;
238 let net_change = closed_edge_count as i64 - raw_edge_count as i64;
239 let growth_pct = if raw_edge_count > 0 {
240 net_change as f32 / raw_edge_count as f32 * 100.0
241 } else {
242 0.0
243 };
244
245 log::info!(
246 "[Edge::Detector] - After morphological close: edge_pixels={}, density={:.2}%, \
247 net_change={:+} ({:+.1}%). Total elapsed: {}ms",
248 closed_edge_count, closed_density, net_change, growth_pct,
249 start.elapsed().as_millis()
250 );
251
252 Self::log_edge_spatial_distribution(
254 &closed_edges,
255 self.width,
256 self.height,
257 "after_close",
258 );
259
260 if let Some(dir) = debug_dir {
262 Self::save_binary_image(
263 &closed_edges,
264 self.width,
265 self.height,
266 &dir.join("debug_03_after_close.png"),
267 );
268
269 let closed_r2 = Morphology::close(&raw_edges, self.width, self.height, 2);
271 let r2_count = closed_r2.iter().filter(|&&v| v == 255).count();
272 log::info!(
273 "[Edge::Detector] - [DEBUG] radius=2 close result: \
274 edge_pixels={}, density={:.2}%",
275 r2_count,
276 r2_count as f32 / (self.width * self.height) as f32 * 100.0
277 );
278 Self::save_binary_image(
279 &closed_r2,
280 self.width,
281 self.height,
282 &dir.join("debug_04_close_radius2.png"),
283 );
284
285 let closed_r3 = Morphology::close(&raw_edges, self.width, self.height, 3);
287 let r3_count = closed_r3.iter().filter(|&&v| v == 255).count();
288 log::info!(
289 "[Edge::Detector] - [DEBUG] radius=3 close result: \
290 edge_pixels={}, density={:.2}%",
291 r3_count,
292 r3_count as f32 / (self.width * self.height) as f32 * 100.0
293 );
294 Self::save_binary_image(
295 &closed_r3,
296 self.width,
297 self.height,
298 &dir.join("debug_05_close_radius3.png"),
299 );
300
301 if let Ok(cfg_s05) = CannyConfig::builder()
303 .sigma(0.5)
304 .thresholds(5.0, 20.0)
305 .build()
306 {
307 if let Ok(s05_slice) = canny_u8(&input_data, &mut self.workspace, &cfg_s05) {
308 let s05_raw = s05_slice.to_vec();
309 let s05_closed = Morphology::close(&s05_raw, self.width, self.height, 2);
310 let s05_count = s05_closed.iter().filter(|&&v| v == 255).count();
311 log::info!(
312 "[Edge::Detector] - [DEBUG] sigma=0.5 + radius=2 close: \
313 edge_pixels={}, density={:.2}%",
314 s05_count,
315 s05_count as f32 / (self.width * self.height) as f32 * 100.0
316 );
317 Self::save_binary_image(
318 &s05_closed,
319 self.width,
320 self.height,
321 &dir.join("debug_06_sigma05_close_r2.png"),
322 );
323 }
324 }
325 }
326
327 if closed_density > 25.0 {
328 log::warn!(
329 "[Edge::Detector] - Post-close edge density {:.2}% is very high (>25%). \
330 Morphological close may have over-connected noise. \
331 Consider reducing morph_radius ({}) or raising Canny thresholds.",
332 closed_density, morph_radius
333 );
334 } else if closed_density < 0.05 {
335 log::warn!(
336 "[Edge::Detector] - Post-close edge density {:.2}% is very low (<0.05%). \
337 Document edges may be missing. \
338 Consider lowering Canny thresholds (current low={:.2}, high={:.2}).",
339 closed_density, low, high
340 );
341 }
342
343 if growth_pct > 100.0 {
344 log::warn!(
345 "[Edge::Detector] - Morphological close growth rate {:.1}% is very high (>100%). \
346 radius={} may be too large, causing noise over-connection.",
347 growth_pct, morph_radius
348 );
349 }
350
351 Ok(closed_edges)
352 }
353
354 fn log_edge_spatial_distribution(
359 edges: &[u8],
360 width: usize,
361 height: usize,
362 label: &str,
363 ) {
364 let grid_cols = 4usize;
365 let grid_rows = 4usize;
366 let cell_w = width / grid_cols;
367 let cell_h = height / grid_rows;
368
369 log::info!(
370 "[Edge::Detector] - [DEBUG] Edge spatial distribution ({}) \
371 in {}x{} grid (cell={}x{}px):",
372 label, grid_cols, grid_rows, cell_w, cell_h
373 );
374
375 let cell_total = (cell_w * cell_h) as f32;
376 let mut grid_lines = Vec::new();
377
378 for gy in 0..grid_rows {
379 let mut row_str = String::from(" |");
380 for gx in 0..grid_cols {
381 let x0 = gx * cell_w;
382 let y0 = gy * cell_h;
383 let x1 = ((gx + 1) * cell_w).min(width);
384 let y1 = ((gy + 1) * cell_h).min(height);
385
386 let mut count = 0usize;
387 for y in y0..y1 {
388 for x in x0..x1 {
389 if edges[y * width + x] == 255 {
390 count += 1;
391 }
392 }
393 }
394 let density = count as f32 / cell_total * 100.0;
395 let symbol = if density < 1.0 {
397 " "
398 } else if density < 5.0 {
399 "."
400 } else if density < 15.0 {
401 "o"
402 } else if density < 30.0 {
403 "O"
404 } else {
405 "#"
406 };
407 row_str.push_str(&format!("{:>5.1}%{}|", density, symbol));
408 }
409 grid_lines.push(row_str);
410 }
411
412 let separator = format!(" +{}+", "------+".repeat(grid_cols));
414 log::info!("[Edge::Detector] - [DEBUG] {}", separator);
415 for line in &grid_lines {
416 log::info!("[Edge::Detector] - [DEBUG] {}", line);
417 }
418 log::info!("[Edge::Detector] - [DEBUG] {}", separator);
419 }
420
421 fn save_gray_image(data: &[u8], width: usize, height: usize, path: &std::path::Path) {
423 match Self::write_pgm(data, width, height, path.with_extension("pgm").as_path()) {
426 Ok(_) => log::info!(
427 "[Edge::Detector] - [DEBUG] Saved gray image: {}",
428 path.with_extension("pgm").display()
429 ),
430 Err(e) => log::warn!(
431 "[Edge::Detector] - [DEBUG] Failed to save gray image {}: {}",
432 path.display(),
433 e
434 ),
435 }
436 }
437
438 fn save_binary_image(data: &[u8], width: usize, height: usize, path: &std::path::Path) {
440 match Self::write_pgm(data, width, height, path.with_extension("pgm").as_path()) {
441 Ok(_) => log::info!(
442 "[Edge::Detector] - [DEBUG] Saved binary image: {}",
443 path.with_extension("pgm").display()
444 ),
445 Err(e) => log::warn!(
446 "[Edge::Detector] - [DEBUG] Failed to save binary image {}: {}",
447 path.display(),
448 e
449 ),
450 }
451 }
452
453 fn write_pgm(
455 data: &[u8],
456 width: usize,
457 height: usize,
458 path: &std::path::Path,
459 ) -> std::io::Result<()> {
460 use std::io::Write;
461
462 if let Some(parent) = path.parent() {
464 std::fs::create_dir_all(parent)?;
465 }
466
467 let mut file = std::fs::File::create(path)?;
468 write!(file, "P5\n{} {}\n255\n", width, height)?;
470 file.write_all(&data[..width * height])?;
471 Ok(())
472 }
473
474fn choose_morph_radius(width: usize, height: usize) -> usize {
475 let long_edge = width.max(height);
476 if long_edge <= 512 { 1 }
477 else if long_edge <= 1024 { 2 } else { 3 }
479}
480}