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 #[cfg(debug_assertions)]
144 {
145 log::info!(
147 "[Edge::Detector] - [DEBUG] Threshold sensitivity scan \
148 (sigma=1.0, 3 trial configs):"
149 );
150 for &(trial_low, trial_high) in &[(2.0f32, 8.0), (5.0, 20.0), (10.0, 40.0)] {
151 if let Ok(cfg) = CannyConfig::builder()
152 .sigma(1.0)
153 .thresholds(trial_low, trial_high)
154 .build()
155 {
156 if let Ok(trial_slice) = canny_u8(&input_data, &mut self.workspace, &cfg) {
158 let trial_count = trial_slice.iter().filter(|&&v| v == 255).count();
159 let trial_density =
160 trial_count as f32 / (self.width * self.height) as f32 * 100.0;
161 log::info!(
162 "[Edge::Detector] - [DEBUG] low={:.1}, high={:.1} -> \
163 edge_pixels={}, density={:.2}%",
164 trial_low, trial_high, trial_count, trial_density
165 );
166 }
167 }
168 }
169
170 log::info!(
172 "[Edge::Detector] - [DEBUG] Low-sigma scan (sigma=0.5, low=5.0, high=20.0):"
173 );
174 if let Ok(cfg_low_sigma) = CannyConfig::builder()
175 .sigma(0.5)
176 .thresholds(5.0, 20.0)
177 .build()
178 {
179 if let Ok(trial_slice) = canny_u8(&input_data, &mut self.workspace, &cfg_low_sigma) {
180 let trial_count = trial_slice.iter().filter(|&&v| v == 255).count();
181 let trial_density =
182 trial_count as f32 / (self.width * self.height) as f32 * 100.0;
183 log::info!(
184 "[Edge::Detector] - [DEBUG] sigma=0.5, low=5.0, high=20.0 -> \
185 edge_pixels={}, density={:.2}%",
186 trial_count, trial_density
187 );
188 }
189 }
190 }
191
192 let cfg = CannyConfig::builder()
194 .sigma(1.0)
195 .thresholds(low, high)
196 .build()
197 .map_err(|e| {
198 log::error!("[Edge::Detector] - Invalid Canny config: {:?}", e);
199 DocQuadError::EdgeDetectionError
200 })?;
201
202 let canny_start = Instant::now();
203 let edge_slice = canny_u8(&input_data, &mut self.workspace, &cfg).map_err(|e| {
204 log::error!("[Edge::Detector] - canny_u8 failed: {:?}", e);
205 DocQuadError::EdgeDetectionError
206 })?;
207
208 let raw_edges = edge_slice.to_vec();
209 let canny_elapsed = canny_start.elapsed().as_millis();
210 let raw_edge_count = raw_edges.iter().filter(|&&v| v == 255).count();
211 let raw_density = raw_edge_count as f32 / (self.width * self.height) as f32 * 100.0;
212
213 log::info!(
214 "[Edge::Detector] - Canny raw result: edge_pixels={}, density={:.2}%, \
215 size={}x{}. Canny elapsed: {}ms",
216 raw_edge_count, raw_density, self.width, self.height, canny_elapsed
217 );
218
219 #[cfg(debug_assertions)]
221 Self::log_edge_spatial_distribution(&raw_edges, self.width, self.height, "raw_canny");
222
223 if let Some(dir) = debug_dir {
225 Self::save_binary_image(
226 &raw_edges,
227 self.width,
228 self.height,
229 &dir.join("debug_02_canny_raw.png"),
230 );
231 }
232
233 let morph_radius = Self::choose_morph_radius(self.width, self.height);
235 log::debug!(
236 "[Edge::Detector] - Applying morphological close: radius={} (image={}x{})",
237 morph_radius, self.width, self.height
238 );
239
240 let closed_edges = Morphology::close(&raw_edges, self.width, self.height, morph_radius);
241
242 let closed_edge_count = closed_edges.iter().filter(|&&v| v == 255).count();
243 let closed_density =
244 closed_edge_count as f32 / (self.width * self.height) as f32 * 100.0;
245 let net_change = closed_edge_count as i64 - raw_edge_count as i64;
246 let growth_pct = if raw_edge_count > 0 {
247 net_change as f32 / raw_edge_count as f32 * 100.0
248 } else {
249 0.0
250 };
251
252 log::info!(
253 "[Edge::Detector] - After morphological close: edge_pixels={}, density={:.2}%, \
254 net_change={:+} ({:+.1}%). Total elapsed: {}ms",
255 closed_edge_count, closed_density, net_change, growth_pct,
256 start.elapsed().as_millis()
257 );
258
259 #[cfg(debug_assertions)]
261 Self::log_edge_spatial_distribution(
262 &closed_edges,
263 self.width,
264 self.height,
265 "after_close",
266 );
267
268 if let Some(dir) = debug_dir {
270 Self::save_binary_image(
271 &closed_edges,
272 self.width,
273 self.height,
274 &dir.join("debug_03_after_close.png"),
275 );
276
277 #[cfg(debug_assertions)]
279 {
280 let closed_r2 = Morphology::close(&raw_edges, self.width, self.height, 2);
282 let r2_count = closed_r2.iter().filter(|&&v| v == 255).count();
283 log::info!(
284 "[Edge::Detector] - [DEBUG] radius=2 close result: \
285 edge_pixels={}, density={:.2}%",
286 r2_count,
287 r2_count as f32 / (self.width * self.height) as f32 * 100.0
288 );
289 Self::save_binary_image(
290 &closed_r2,
291 self.width,
292 self.height,
293 &dir.join("debug_04_close_radius2.png"),
294 );
295
296 let closed_r3 = Morphology::close(&raw_edges, self.width, self.height, 3);
298 let r3_count = closed_r3.iter().filter(|&&v| v == 255).count();
299 log::info!(
300 "[Edge::Detector] - [DEBUG] radius=3 close result: \
301 edge_pixels={}, density={:.2}%",
302 r3_count,
303 r3_count as f32 / (self.width * self.height) as f32 * 100.0
304 );
305 Self::save_binary_image(
306 &closed_r3,
307 self.width,
308 self.height,
309 &dir.join("debug_05_close_radius3.png"),
310 );
311
312 if let Ok(cfg_s05) = CannyConfig::builder()
314 .sigma(0.5)
315 .thresholds(5.0, 20.0)
316 .build()
317 {
318 if let Ok(s05_slice) = canny_u8(&input_data, &mut self.workspace, &cfg_s05) {
319 let s05_raw = s05_slice.to_vec();
320 let s05_closed = Morphology::close(&s05_raw, self.width, self.height, 2);
321 let s05_count = s05_closed.iter().filter(|&&v| v == 255).count();
322 log::info!(
323 "[Edge::Detector] - [DEBUG] sigma=0.5 + radius=2 close: \
324 edge_pixels={}, density={:.2}%",
325 s05_count,
326 s05_count as f32 / (self.width * self.height) as f32 * 100.0
327 );
328 Self::save_binary_image(
329 &s05_closed,
330 self.width,
331 self.height,
332 &dir.join("debug_06_sigma05_close_r2.png"),
333 );
334 }
335 }
336 }
337 }
338
339 if closed_density > 25.0 {
340 log::warn!(
341 "[Edge::Detector] - Post-close edge density {:.2}% is very high (>25%). \
342 Morphological close may have over-connected noise. \
343 Consider reducing morph_radius ({}) or raising Canny thresholds.",
344 closed_density, morph_radius
345 );
346 } else if closed_density < 0.05 {
347 log::warn!(
348 "[Edge::Detector] - Post-close edge density {:.2}% is very low (<0.05%). \
349 Document edges may be missing. \
350 Consider lowering Canny thresholds (current low={:.2}, high={:.2}).",
351 closed_density, low, high
352 );
353 }
354
355 if growth_pct > 100.0 {
356 log::warn!(
357 "[Edge::Detector] - Morphological close growth rate {:.1}% is very high (>100%). \
358 radius={} may be too large, causing noise over-connection.",
359 growth_pct, morph_radius
360 );
361 }
362
363 Ok(closed_edges)
364 }
365
366 fn log_edge_spatial_distribution(
371 edges: &[u8],
372 width: usize,
373 height: usize,
374 label: &str,
375 ) {
376 let grid_cols = 4usize;
377 let grid_rows = 4usize;
378 let cell_w = width / grid_cols;
379 let cell_h = height / grid_rows;
380
381 log::info!(
382 "[Edge::Detector] - [DEBUG] Edge spatial distribution ({}) \
383 in {}x{} grid (cell={}x{}px):",
384 label, grid_cols, grid_rows, cell_w, cell_h
385 );
386
387 let cell_total = (cell_w * cell_h) as f32;
388 let mut grid_lines = Vec::new();
389
390 for gy in 0..grid_rows {
391 let mut row_str = String::from(" |");
392 for gx in 0..grid_cols {
393 let x0 = gx * cell_w;
394 let y0 = gy * cell_h;
395 let x1 = ((gx + 1) * cell_w).min(width);
396 let y1 = ((gy + 1) * cell_h).min(height);
397
398 let mut count = 0usize;
399 for y in y0..y1 {
400 for x in x0..x1 {
401 if edges[y * width + x] == 255 {
402 count += 1;
403 }
404 }
405 }
406 let density = count as f32 / cell_total * 100.0;
407 let symbol = if density < 1.0 {
409 " "
410 } else if density < 5.0 {
411 "."
412 } else if density < 15.0 {
413 "o"
414 } else if density < 30.0 {
415 "O"
416 } else {
417 "#"
418 };
419 row_str.push_str(&format!("{:>5.1}%{}|", density, symbol));
420 }
421 grid_lines.push(row_str);
422 }
423
424 let separator = format!(" +{}+", "------+".repeat(grid_cols));
426 log::info!("[Edge::Detector] - [DEBUG] {}", separator);
427 for line in &grid_lines {
428 log::info!("[Edge::Detector] - [DEBUG] {}", line);
429 }
430 log::info!("[Edge::Detector] - [DEBUG] {}", separator);
431 }
432
433 fn save_gray_image(data: &[u8], width: usize, height: usize, path: &std::path::Path) {
435 match Self::write_pgm(data, width, height, path.with_extension("pgm").as_path()) {
438 Ok(_) => log::info!(
439 "[Edge::Detector] - [DEBUG] Saved gray image: {}",
440 path.with_extension("pgm").display()
441 ),
442 Err(e) => log::warn!(
443 "[Edge::Detector] - [DEBUG] Failed to save gray image {}: {}",
444 path.display(),
445 e
446 ),
447 }
448 }
449
450 fn save_binary_image(data: &[u8], width: usize, height: usize, path: &std::path::Path) {
452 match Self::write_pgm(data, width, height, path.with_extension("pgm").as_path()) {
453 Ok(_) => log::info!(
454 "[Edge::Detector] - [DEBUG] Saved binary image: {}",
455 path.with_extension("pgm").display()
456 ),
457 Err(e) => log::warn!(
458 "[Edge::Detector] - [DEBUG] Failed to save binary image {}: {}",
459 path.display(),
460 e
461 ),
462 }
463 }
464
465 fn write_pgm(
467 data: &[u8],
468 width: usize,
469 height: usize,
470 path: &std::path::Path,
471 ) -> std::io::Result<()> {
472 use std::io::Write;
473
474 if let Some(parent) = path.parent() {
476 std::fs::create_dir_all(parent)?;
477 }
478
479 let mut file = std::fs::File::create(path)?;
480 write!(file, "P5\n{} {}\n255\n", width, height)?;
482 file.write_all(&data[..width * height])?;
483 Ok(())
484 }
485
486 fn choose_morph_radius(width: usize, height: usize) -> usize {
487 let long_edge = width.max(height);
488 if long_edge <= 512 { 1 }
489 else if long_edge <= 1024 { 2 } else { 3 }
491 }
492}