Skip to main content

fast_canny/
lib.rs

1pub mod gaussian;
2pub mod hysteresis;
3pub mod kernel;
4pub mod nms;
5pub mod pipeline;
6pub mod workspace;
7
8// src/lib.rs
9#[cfg(target_arch = "x86_64")]
10pub mod avx2_kernel {
11    pub use crate::kernel::avx2::*;
12}
13
14mod pipeline_ptr;
15
16pub use workspace::CannyWorkspace;
17
18use gaussian::apply as gaussian_apply;
19use hysteresis::track_edges;
20use pipeline::execute_tiled_pipeline;
21
22// =====================================================================
23// CannyConfig
24// =====================================================================
25
26#[derive(Debug, Clone)]
27pub struct CannyConfig {
28    pub sigma: f32,
29    pub low_thresh: f32,
30    pub high_thresh: f32,
31}
32
33impl Default for CannyConfig {
34    fn default() -> Self {
35        Self {
36            sigma: 1.0,
37            low_thresh: 50.0,
38            high_thresh: 150.0,
39        }
40    }
41}
42
43impl CannyConfig {
44    pub fn builder() -> CannyConfigBuilder {
45        CannyConfigBuilder::default()
46    }
47}
48
49#[derive(Default)]
50pub struct CannyConfigBuilder {
51    sigma: Option<f32>,
52    low_thresh: Option<f32>,
53    high_thresh: Option<f32>,
54}
55
56impl CannyConfigBuilder {
57    pub fn sigma(mut self, v: f32) -> Self {
58        self.sigma = Some(v);
59        self
60    }
61    pub fn thresholds(mut self, low: f32, high: f32) -> Self {
62        self.low_thresh = Some(low);
63        self.high_thresh = Some(high);
64        self
65    }
66    pub fn build(self) -> Result<CannyConfig, CannyError> {
67        let cfg = CannyConfig {
68            sigma: self.sigma.unwrap_or(1.0),
69            low_thresh: self.low_thresh.unwrap_or(50.0),
70            high_thresh: self.high_thresh.unwrap_or(150.0),
71        };
72        if cfg.low_thresh > cfg.high_thresh {
73            return Err(CannyError::InvalidThresholds {
74                low: cfg.low_thresh,
75                high: cfg.high_thresh,
76            });
77        }
78        Ok(cfg)
79    }
80}
81
82// =====================================================================
83// CannyError / CannyStatus
84// =====================================================================
85
86#[derive(Debug, Clone, PartialEq)]
87pub enum CannyError {
88    InvalidDimensions { width: usize, height: usize },
89    InputLengthMismatch { expected: usize, actual: usize },
90    InvalidThresholds { low: f32, high: f32 },
91    NullPointer,
92}
93
94impl std::fmt::Display for CannyError {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::InvalidDimensions { width, height } => {
98                write!(f, "invalid dimensions: {}x{} (min 3x3)", width, height)
99            }
100            Self::InputLengthMismatch { expected, actual } => {
101                write!(f, "input length: expected {}, got {}", expected, actual)
102            }
103            Self::InvalidThresholds { low, high } => {
104                write!(f, "thresholds: low={} > high={}", low, high)
105            }
106            Self::NullPointer => write!(f, "null pointer"),
107        }
108    }
109}
110
111impl std::error::Error for CannyError {}
112
113#[repr(i32)]
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum CannyStatus {
116    Ok = 0,
117    NullPointer = -1,
118    InvalidDimensions = -2,
119    InputLengthMismatch = -3,
120    InvalidThresholds = -4,
121}
122
123impl From<&CannyError> for CannyStatus {
124    fn from(e: &CannyError) -> Self {
125        match e {
126            CannyError::NullPointer => Self::NullPointer,
127            CannyError::InvalidDimensions { .. } => Self::InvalidDimensions,
128            CannyError::InputLengthMismatch { .. } => Self::InputLengthMismatch,
129            CannyError::InvalidThresholds { .. } => Self::InvalidThresholds,
130        }
131    }
132}
133
134// =====================================================================
135// canny():接受 f32 切片的主 API(零拷贝路径)
136// =====================================================================
137
138pub fn canny<'ws>(
139    src: &[f32],
140    ws: &'ws mut CannyWorkspace,
141    cfg: &CannyConfig,
142) -> Result<&'ws [u8], CannyError> {
143    let expected = ws.width * ws.height;
144    if src.len() != expected {
145        return Err(CannyError::InputLengthMismatch {
146            expected,
147            actual: src.len(),
148        });
149    }
150    if cfg.low_thresh > cfg.high_thresh {
151        return Err(CannyError::InvalidThresholds {
152            low: cfg.low_thresh,
153            high: cfg.high_thresh,
154        });
155    }
156
157    ws.reset();
158
159    let w = ws.width;
160    let h = ws.height;
161
162    // 高斯平滑:写入 buffer_b,或直接使用 src(零拷贝)
163    if cfg.sigma > 0.0 {
164        gaussian_apply(src, &mut ws.buffer_b, w, h, cfg.sigma);
165        // 取裸指针后重新构造切片,脱离对 ws.buffer_b 的借用,
166        // 避免后续 execute_tiled_pipeline 同时可变借用 ws 时产生冲突
167        let blurred_ptr = ws.buffer_b.as_ptr();
168        let blurred_len = ws.buffer_b.len();
169        let blurred: &[f32] = unsafe { std::slice::from_raw_parts(blurred_ptr, blurred_len) };
170        execute_tiled_pipeline(blurred, ws, cfg.low_thresh, cfg.high_thresh);
171    } else {
172        execute_tiled_pipeline(src, ws, cfg.low_thresh, cfg.high_thresh);
173    }
174
175    track_edges(&mut ws.edge_map, w, h, &ws.arena);
176
177    Ok(&ws.edge_map)
178}
179
180// =====================================================================
181// canny_u8():新增 API,直接接受 &[u8] 输入
182//
183// 设计说明:
184//   u8 → f32 转换不可避免(Sobel 需要浮点运算),
185//   但转换结果写入 ws.buffer_b,后续所有阶段均零拷贝操作 workspace 内缓冲区。
186//   相比让调用方自行转换,此 API 封装了转换细节,提供更友好的接口。
187// =====================================================================
188
189/// 接受 u8 灰度图的 Canny 边缘检测 API。
190///
191/// # 参数
192/// - `src`:输入灰度图,每像素 1 字节,长度必须等于 `ws.width * ws.height`
193/// - `ws`:预分配的工作区,可跨帧复用
194/// - `cfg`:算法配置(sigma、低/高阈值)
195///
196/// # 返回值
197/// `&[u8]` 二值边缘图,仅含 0(非边缘)和 255(边缘),
198/// 生命周期绑定到 `ws`,下次调用前有效。
199///
200/// # 零拷贝说明
201/// u8→f32 转换写入 `ws.buffer_b`(预分配,无堆分配),
202/// 后续 Sobel/NMS/Hysteresis 均直接操作 workspace 内缓冲区。
203pub fn canny_u8<'ws>(
204    src: &[u8],
205    ws: &'ws mut CannyWorkspace,
206    cfg: &CannyConfig,
207) -> Result<&'ws [u8], CannyError> {
208    let expected = ws.width * ws.height;
209    if src.len() != expected {
210        return Err(CannyError::InputLengthMismatch {
211            expected,
212            actual: src.len(),
213        });
214    }
215    if cfg.low_thresh > cfg.high_thresh {
216        return Err(CannyError::InvalidThresholds {
217            low: cfg.low_thresh,
218            high: cfg.high_thresh,
219        });
220    }
221
222    ws.reset();
223
224    let w = ws.width;
225    let h = ws.height;
226
227    // u8 → f32 转换,写入预分配的 buffer_b(无堆分配)
228    // 值域保持 [0.0, 255.0],与 f32 API 语义一致
229    {
230        let dst = &mut ws.buffer_b;
231        for (d, &s) in dst.iter_mut().zip(src.iter()) {
232            *d = s as f32;
233        }
234    }
235
236    // 根据 sigma 决定是否再次高斯平滑
237    if cfg.sigma > 0.0 {
238        // 就地平滑:从 buffer_b 读,写回 buffer_b
239        // 需要临时借用,使用 buffer_a 作为中间缓冲
240        let src_f32: Vec<f32> = ws.buffer_b.clone();
241        gaussian_apply(&src_f32, &mut ws.buffer_b, w, h, cfg.sigma);
242    }
243
244    // 此时 buffer_b 已包含最终的平滑输入,直接传引用(零拷贝)
245    let blurred = ws.buffer_b.as_slice();
246
247    // SAFETY: blurred 是 ws.buffer_b 的切片,execute_tiled_pipeline 内部
248    // 通过裸指针访问 ws 的其他字段,不与 buffer_b 产生别名冲突
249    // (buffer_b 只读,其他缓冲区只写)
250    let blurred_ptr = blurred.as_ptr();
251    let blurred_len = blurred.len();
252    let blurred_static: &'static [f32] = unsafe {
253        // SAFETY: 生命周期延长仅用于传递给 execute_tiled_pipeline,
254        // 该函数在返回前不会 drop ws,blurred 在整个调用期间有效
255        std::slice::from_raw_parts(blurred_ptr, blurred_len)
256    };
257
258    execute_tiled_pipeline(blurred_static, ws, cfg.low_thresh, cfg.high_thresh);
259    track_edges(&mut ws.edge_map, w, h, &ws.arena);
260
261    Ok(&ws.edge_map)
262}
263
264// =====================================================================
265// C-FFI
266// =====================================================================
267
268#[unsafe(no_mangle)]
269pub extern "C" fn canny_workspace_new(width: usize, height: usize) -> *mut CannyWorkspace {
270    match CannyWorkspace::new(width, height) {
271        Ok(ws) => Box::into_raw(Box::new(ws)),
272        Err(e) => {
273            log::error!("[canny_workspace_new] {}", e);
274            std::ptr::null_mut()
275        }
276    }
277}
278
279#[unsafe(no_mangle)]
280pub unsafe extern "C" fn canny_workspace_free(ws: *mut CannyWorkspace) {
281    if !ws.is_null() {
282        // SAFETY: ws 由 canny_workspace_new 通过 Box::into_raw 创建
283        drop(unsafe { Box::from_raw(ws) });
284    }
285}
286
287#[unsafe(no_mangle)]
288pub unsafe extern "C" fn canny_process_ex(
289    ws: *mut CannyWorkspace,
290    src: *const f32,
291    src_len: usize,
292    sigma: f32,
293    low_thresh: f32,
294    high_thresh: f32,
295    out_edge: *mut *const u8,
296) -> i32 {
297    if ws.is_null() || src.is_null() || out_edge.is_null() {
298        log::error!("[canny_process_ex] null pointer");
299        return CannyStatus::NullPointer as i32;
300    }
301
302    // SAFETY: ws 由调用方保证有效;src 由调用方保证长度为 src_len
303    let ws_ref = unsafe { &mut *ws };
304    let src_slice = unsafe { std::slice::from_raw_parts(src, src_len) };
305
306    let cfg = match CannyConfig::builder()
307        .sigma(sigma)
308        .thresholds(low_thresh, high_thresh)
309        .build()
310    {
311        Ok(c) => c,
312        Err(e) => {
313            log::error!("[canny_process_ex] config: {}", e);
314            return CannyStatus::from(&e) as i32;
315        }
316    };
317
318    match canny(src_slice, ws_ref, &cfg) {
319        Ok(edge) => {
320            // SAFETY: out_edge 非空(上方已检查)
321            unsafe {
322                *out_edge = edge.as_ptr();
323            }
324            CannyStatus::Ok as i32
325        }
326        Err(e) => {
327            log::error!("[canny_process_ex] {}", e);
328            CannyStatus::from(&e) as i32
329        }
330    }
331}
332
333/// C-FFI:接受 u8 输入的 Canny 接口
334#[unsafe(no_mangle)]
335pub unsafe extern "C" fn canny_process_u8(
336    ws: *mut CannyWorkspace,
337    src: *const u8,
338    src_len: usize,
339    sigma: f32,
340    low_thresh: f32,
341    high_thresh: f32,
342    out_edge: *mut *const u8,
343) -> i32 {
344    if ws.is_null() || src.is_null() || out_edge.is_null() {
345        log::error!("[canny_process_u8] null pointer");
346        return CannyStatus::NullPointer as i32;
347    }
348
349    // SAFETY: 指针由调用方保证有效
350    let ws_ref = unsafe { &mut *ws };
351    let src_slice = unsafe { std::slice::from_raw_parts(src, src_len) };
352
353    let cfg = match CannyConfig::builder()
354        .sigma(sigma)
355        .thresholds(low_thresh, high_thresh)
356        .build()
357    {
358        Ok(c) => c,
359        Err(e) => {
360            log::error!("[canny_process_u8] config: {}", e);
361            return CannyStatus::from(&e) as i32;
362        }
363    };
364
365    match canny_u8(src_slice, ws_ref, &cfg) {
366        Ok(edge) => {
367            unsafe {
368                *out_edge = edge.as_ptr();
369            }
370            CannyStatus::Ok as i32
371        }
372        Err(e) => {
373            log::error!("[canny_process_u8] {}", e);
374            CannyStatus::from(&e) as i32
375        }
376    }
377}