1pub mod gaussian;
2pub mod hysteresis;
3pub mod kernel;
4pub mod nms;
5pub mod pipeline;
6pub mod workspace;
7
8#[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#[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#[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
134pub 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 if cfg.sigma > 0.0 {
164 gaussian_apply(src, &mut ws.buffer_b, w, h, cfg.sigma);
165 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
180pub 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 {
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 if cfg.sigma > 0.0 {
238 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 let blurred = ws.buffer_b.as_slice();
246
247 let blurred_ptr = blurred.as_ptr();
251 let blurred_len = blurred.len();
252 let blurred_static: &'static [f32] = unsafe {
253 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#[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 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 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 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#[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 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}