Skip to main content

doc_quad/edge/
detector.rs

1// src/edge/detector.rs
2use 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
9/// 边缘检测器封装,持有预分配的 CannyWorkspace 以避免帧间堆分配。
10pub 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    /// 执行 Canny 边缘检测,可选输出调试中间图到指定目录。
44    ///
45    /// # 参数
46    /// - `buffer`:输入图像缓冲区
47    /// - `debug_dir`:若 Some(path),则将各阶段中间图保存到该目录
48    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        // ── 阈值计算 ──────────────────────────────────────────────────────────
58        let (low, high) = AdaptiveThreshold::calculate(&view, 0.33);
59
60        // 输出详细的直方图统计,辅助判断阈值是否合理
61        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        // 统计各亮度区间的像素占比,辅助诊断图像亮度分布
68        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        // 输出直方图峰值区间(找出像素最集中的 16 级区间)
79        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        // ── 内存紧凑化 ────────────────────────────────────────────────────────
111        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        // 保存输入灰度图(调试用)
130        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        // ── Canny 检测 ────────────────────────────────────────────────────────
140
141        // 【重构点】:使用 cfg(debug_assertions) 包裹调试性质的试探循环
142        // 生产环境 Release 下不会编译以下多余的计算逻辑
143        #[cfg(debug_assertions)]
144        {
145            // 尝试三组阈值,输出各自的边缘密度,辅助判断最佳阈值区间
146            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                    // 注意:此处复用 workspace,trial 结果会覆盖,仅用于统计密度
157                    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            // 同时尝试 sigma=0.5 的低平滑版本,观察细边缘保留情况
171            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        // 正式 Canny 检测(使用自适应阈值)
193        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        // 分析边缘像素的空间分布(分 4×4 网格统计密度)- 这个全图扫描函数也很耗时,故限定 Debug 模式
220        #[cfg(debug_assertions)]
221        Self::log_edge_spatial_distribution(&raw_edges, self.width, self.height, "raw_canny");
222
223        // 保存 Canny 原始边缘图
224        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        // ── 形态学闭运算 ──────────────────────────────────────────────────────
234        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        // 分析闭运算后边缘的空间分布
260        #[cfg(debug_assertions)]
261        Self::log_edge_spatial_distribution(
262            &closed_edges,
263            self.width,
264            self.height,
265            "after_close",
266        );
267
268        // 保存闭运算后边缘图以及更多的调试用闭运算组合
269        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            // 【重构点】:其他试探性的形态学闭运算仅在 debug 模式执行
278            #[cfg(debug_assertions)]
279            {
280                // 额外:用 radius=2 再做一次闭运算,对比效果
281                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                // 额外:用 radius=3 再做一次闭运算
297                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                // 额外:sigma=0.5 + radius=2 组合
313                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    /// 将边缘图按 4×4 网格分区,统计每个分区的边缘密度,辅助判断边缘的空间分布。
367    ///
368    /// 若文档边框存在,应在图像四周(边缘区域)出现高密度;
369    /// 若只有内容噪声,则密度集中在图像中央。
370    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                // 用简单字符可视化密度:空格<1%, .=1-5%, o=5-15%, O=15-30%, #>30%
408                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        // 输出分隔线
425        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    /// 将灰度字节数组保存为 PNG 图像(调试用,仅在 debug_dir 存在时调用)。
434    fn save_gray_image(data: &[u8], width: usize, height: usize, path: &std::path::Path) {
435        // 使用标准库写入 PGM 格式(无需 image crate,避免循环依赖)
436        // PGM 是最简单的灰度图格式,可用 GIMP/Photoshop/ImageMagick 打开
437        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    /// 将二值边缘图保存为 PGM 图像(255=白色边缘,0=黑色背景)。
451    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    /// 写入 PGM(Portable GrayMap)文件,格式简单,无需外部依赖。
466    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        // 确保父目录存在
475        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        // PGM 头部:P5 = 二进制灰度图,最大值 255
481        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 }  // 当前固定返回 1,建议改为 2
490        else { 3 }
491    }
492}