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        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                // 注意:此处复用 workspace,trial 结果会覆盖,仅用于统计密度
152                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        // 同时尝试 sigma=0.5 的低平滑版本,观察细边缘保留情况
166        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        // 正式 Canny 检测(使用自适应阈值)
187        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        // 分析边缘像素的空间分布(分 4×4 网格统计密度)
214        Self::log_edge_spatial_distribution(&raw_edges, self.width, self.height, "raw_canny");
215
216        // 保存 Canny 原始边缘图
217        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        // ── 形态学闭运算 ──────────────────────────────────────────────────────
227        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        // 分析闭运算后边缘的空间分布
253        Self::log_edge_spatial_distribution(
254            &closed_edges,
255            self.width,
256            self.height,
257            "after_close",
258        );
259
260        // 保存闭运算后边缘图
261        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            // 额外:用 radius=2 再做一次闭运算,对比效果
270            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            // 额外:用 radius=3 再做一次闭运算
286            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            // 额外:sigma=0.5 + radius=2 组合
302            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    /// 将边缘图按 4×4 网格分区,统计每个分区的边缘密度,辅助判断边缘的空间分布。
355    ///
356    /// 若文档边框存在,应在图像四周(边缘区域)出现高密度;
357    /// 若只有内容噪声,则密度集中在图像中央。
358    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                // 用简单字符可视化密度:空格<1%, .=1-5%, o=5-15%, O=15-30%, #>30%
396                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        // 输出分隔线
413        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    /// 将灰度字节数组保存为 PNG 图像(调试用,仅在 debug_dir 存在时调用)。
422    fn save_gray_image(data: &[u8], width: usize, height: usize, path: &std::path::Path) {
423        // 使用标准库写入 PGM 格式(无需 image crate,避免循环依赖)
424        // PGM 是最简单的灰度图格式,可用 GIMP/Photoshop/ImageMagick 打开
425        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    /// 将二值边缘图保存为 PGM 图像(255=白色边缘,0=黑色背景)。
439    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    /// 写入 PGM(Portable GrayMap)文件,格式简单,无需外部依赖。
454    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        // 确保父目录存在
463        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        // PGM 头部:P5 = 二进制灰度图,最大值 255
469        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 }  // 当前固定返回 1,建议改为 2
478    else { 3 }
479}
480}