fast-canny 0.1.0

Industrial-grade Zero-Allocation SIMD Canny Edge Detector
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# fast-canny 完整技术架构与实现详解


---

## 1. 项目核心功能与技术定位


### 核心定位


`fast-canny` 是一个**工业级零分配 SIMD Canny 边缘检测器**,用 Rust 实现,专为实时图像处理场景设计。其核心价值在于:

| 痛点              | 解决方案                       |
| ----------------- | ------------------------------ |
| 每帧大量堆分配    | Bump Arena + 预分配 Workspace  |
| 单线程计算瓶颈    | Rayon 多核 Tiling 流水线       |
| 标量 Sobel 计算慢 | AVX2+FMA / NEON SIMD 内核      |
| 硬件耦合难移植    | `SobelKernel` trait 运行时分发 |

### 适用场景


- 工业质检(缺陷轮廓提取)
- 自动驾驶(车道线、障碍物检测)
- 医学影像(病灶边界分割)
- 实时视频流处理(摄像头帧处理)

---

## 2. 整体架构设计与技术栈


### 技术栈组成


```
Rust 2024 Edition
├── rayon 1.10        — 数据并行框架
├── bumpalo 3.16      — 零分配 Bump Arena(BFS 栈)
├── bytemuck 1.15     — 安全类型转换
├── log 0.4           — 结构化日志
└── libblur 0.23.3    — 高性能高斯模糊(f32 单通道)
```

### 模块划分逻辑


```
src/
├── lib.rs              ← 对外 API 入口(canny/canny_u8/C-FFI)
├── workspace.rs        ← 预分配缓冲区管理
├── gaussian.rs         ← 高斯平滑(委托 libblur)
├── kernel/
│   ├── mod.rs          ← SobelKernel trait + 运行时分发
│   ├── avx2.rs         ← x86_64 AVX2+FMA 实现
│   └── aarch64.rs      ← AArch64 NEON 实现
├── pipeline.rs         ← Tiling 多核流水线
├── nms.rs              ← NMS + 双阈值化融合算子
├── hysteresis.rs       ← 零分配 BFS Hysteresis
└── pipeline_ptr.rs     ← 跨线程裸指针安全包装
```

### Rust 语言特性运用


**生命周期与借用检查**:`canny()` 返回 `&'ws [u8]`,生命周期绑定到 `CannyWorkspace`,编译器静态保证结果有效期。

**Trait 对象运行时分发**:`Box<dyn SobelKernel>` 存储在 `CannyWorkspace` 中,运行时根据 CPU 特性选择最优实现。

**unsafe 隔离**:所有裸指针操作集中在 `pipeline.rs` 和 `kernel/` 内部,对外 API 全部安全。

---

## 3. 关键算法实现原理


### 五阶段流水线


```
输入 f32/u8 灰度图
    ▼ ① 高斯平滑(gaussian.rs)
    │   sigma > 0 时执行,sigma ≤ 0 跳过
    ▼ ② Sobel 梯度(kernel/)
    │   融合幅值 + 方向量化,SIMD 加速
    ▼ ③ NMS(nms.rs)
    │   非极大值抑制,细化为单像素宽边缘
    ▼ ④ 双阈值化(nms.rs 内融合)
    │   强边缘(255) / 弱边缘(127) / 非边缘(0)
    ▼ ⑤ Hysteresis(hysteresis.rs)
    │   零分配 BFS,弱边缘连通强边缘则提拔
输出 u8 二值边缘图(仅含 0 / 255)
```

### ① 高斯平滑(`src/gaussian.rs`

委托 `libblur::gaussian_blur_f32` 实现,使用 `EdgeMode::Clamp` 边界处理:

```rust
pub fn apply(src: &[f32], dst: &mut [f32], width: usize, height: usize, sigma: f32) {
    let src_image = BlurImage::borrow(src, w, h, FastBlurChannels::Plane);
    let mut dst_image = BlurImageMut::borrow(dst, w, h, FastBlurChannels::Plane);
    gaussian_blur_f32(
        &src_image, &mut dst_image,
        GaussianBlurParams::new_from_sigma(sigma as f64),
        EdgeMode2D::new(EdgeMode::Clamp),
        ThreadingPolicy::Adaptive,
        IeeeBinaryConvolutionMode::Normal,
    ).expect("...");
}
```

**设计要点**:`sigma ≤ 0` 时完全跳过高斯阶段,直接使用原始输入,适合已预处理的输入。

### ② Sobel 梯度计算(`src/kernel/`

Sobel 算子定义:

```
Gx = [-1  0  +1]     Gy = [-1  -2  -1]
     [-2  0  +2]          [ 0   0   0]
     [-1  0  +1]          [+1  +2  +1]
```

**方向量化规则**(标量实现,`kernel/mod.rs`):

```rust
let ax = gx.abs();
let ay = gy.abs();
dir_out[idx] = if ay <= ax * 0.414_213_56 {
    0u8  // 0°  水平方向
} else if ay >= ax * 2.414_213_56 {
    2u8  // 90° 垂直方向
} else if (gx >= 0.0) == (gy >= 0.0) {
    1u8  // 45° 右上/左下
} else {
    3u8  // 135° 左上/右下
};
```

四个方向对应 `tan(22.5°)≈0.414` 和 `tan(67.5°)≈2.414` 两个分界阈值。

### ③ 非极大值抑制(`src/nms.rs`

NMS 核心逻辑:沿梯度方向比较相邻像素,只保留局部极大值:

```rust
// 4个方向的邻域偏移
let offsets: [[isize; 2]; 4] = [
    [-1, 1],         // 0°   水平
    [-w + 1, w - 1], // 45°  右上/左下
    [-w, w],         // 90°  垂直
    [-w - 1, w + 1], // 135° 左上/右下
];

// 非对称比较:确保等值相邻像素只保留一个
if m >= m1 && m > m2 {
    edge_out[idx] = if m >= high_thresh { 255 } else { 127 };
} else {
    edge_out[idx] = 0;
}
```

**NMS 与双阈值化融合**:两个阶段在同一函数内完成,避免额外的内存遍历,减少 Cache Miss。

### ④ Hysteresis 滞后处理(`src/hysteresis.rs`

基于零分配 BFS 的连通域追踪:

```
步骤1: 主动清零四周边界(防止 BFS 越界)
步骤2: 扫描所有强边缘(255)作为 BFS 种子
步骤3: BFS 扩散 —— 弱边缘(127)连通强边缘则提升为255
步骤4: 清除所有残余弱边缘(127 → 0)
```

**零分配关键**:BFS 栈使用 `bumpalo::collections::Vec`,分配在 `Bump` Arena 上,帧间通过 `arena.reset()` O(1) 复位,无堆分配。

---

## 4. 性能优化策略


### SIMD 加速


**AVX2+FMA(`src/kernel/avx2.rs`)**:每次处理 8 个 f32 像素:

```
阶段1: 加载 3×3 邻居(8列 × 3行 = 24次 _mm256_loadu_ps)
阶段2: Sobel 卷积(使用 _mm256_fmadd_ps 融合乘加)
阶段3: 幅值 = sqrt(gx² + gy²)
阶段4: 方向量化(无分支,使用 _mm256_blendv_epi8)
阶段5: Pack 32→8 bit(packus_epi32 → packus_epi16 → 存储)
```

**NEON(`src/kernel/aarch64.rs`)**:每次处理 4 个 f32 像素:

```
阶段1: vld1q_f32 加载邻居
阶段2: vfmaq_f32 融合乘加(a + b*c)
阶段3: vsqrtq_f32 向量平方根
阶段4: vcleq_f32/vcgeq_f32 无分支方向判断
阶段5: vqmovn_u32 → vqmovn_u16 饱和窄化到 u8
```

**运行时分发**(`src/kernel/mod.rs`):

```rust
pub fn detect() -> Box<dyn SobelKernel> {
    #[cfg(target_arch = "x86_64")]
    {
        if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
            return Box::new(avx2::Avx2SobelKernel);
        }
    }
    #[cfg(target_arch = "aarch64")]
    {
        return Box::new(aarch64::NeonSobelKernel);
    }
    Box::new(ScalarSobelKernel)  // 标量后备
}
```

### 多核并行(`src/pipeline.rs`

采用 `TILE_SIZE=64` 的 L1 Cache 友好分块,Rayon 并行处理行 Tile:

```rust
const TILE_SIZE: usize = 64;

(0..num_tiles_y).into_par_iter().for_each(move |ty| {
    // 每个 Tile 独立处理:Sobel → NMS → 双阈值
    // 不同 ty 写入不同行范围,无数据竞争
});
```

**跨线程指针安全**(`src/pipeline_ptr.rs`):`SendPtr<T>` 和 `SendConstPtr<T>` 包装裸指针,手动实现 `Send + Sync`,使闭包可跨线程捕获,同时通过行分区保证无竞争。

### 零帧间分配


```
CannyWorkspace::reset() {
    arena.reset();      // O(1):仅重置 bump 指针
    edge_map.fill(0);   // O(W×H):清零输出缓冲区
    // buffer_a/buffer_b/dir_map 由各阶段完整覆盖,无需清零
}
```

---

## 5. 代码组织结构与模块协作


### 内存布局


```
CannyWorkspace {
    buffer_b  [f32 × W×H]  ← 高斯平滑输出(Sobel 输入)
    buffer_a  [f32 × W×H]  ← 梯度幅值 (mag)
    dir_map   [u8  × W×H]  ← 梯度方向 (0/1/2/3)
    edge_map  [u8  × W×H]  ← NMS输出 / Hysteresis最终结果
    arena     [Bump]        ← BFS 栈(帧间 O(1) reset)
    kernel    [Box<dyn SobelKernel>]  ← 运行时选定的硬件实现
}
```

### 数据流与模块协作


```
lib.rs::canny()
    ├─→ gaussian::apply(src → buffer_b)          [gaussian.rs]
    ├─→ pipeline::execute_tiled_pipeline(buffer_b → buffer_a, dir_map, edge_map)
    │       │
    │       ├─→ kernel.process_row_slice(...)     [kernel/avx2.rs 或 aarch64.rs]
    │       │       写入: buffer_a(mag), dir_map
    │       │
    │       └─→ nms_and_threshold_slice(...)      [nms.rs]
    │               读取: buffer_a, dir_map
    │               写入: edge_map (0/127/255)
    └─→ hysteresis::track_edges(edge_map)         [hysteresis.rs]
            BFS: 127→255 或 127→0
            最终: edge_map 仅含 0/255
```

### 借用冲突解决方案


`execute_tiled_pipeline` 需要同时读 `buffer_b` 和写 `buffer_a/dir_map/edge_map`,Rust 借用检查器会拒绝。解决方案:

```rust
// lib.rs 中的处理
let blurred_ptr = ws.buffer_b.as_ptr();
let blurred_len = ws.buffer_b.len();
// 通过裸指针重建切片,脱离对 ws.buffer_b 的借用
let blurred: &[f32] = unsafe {
    std::slice::from_raw_parts(blurred_ptr, blurred_len)
};
execute_tiled_pipeline(blurred, ws, cfg.low_thresh, cfg.high_thresh);
```

---

## 6. 使用方法与集成示例


### f32 输入(主 API)


```rust
use fast_canny::{CannyConfig, CannyWorkspace, canny};

// 1. 创建 Workspace(一次性,跨帧复用)
let mut ws = CannyWorkspace::new(1920, 1080).expect("尺寸 >= 3x3");

// 2. 配置参数
let cfg = CannyConfig::builder()
    .sigma(1.0)
    .thresholds(50.0, 150.0)
    .build()
    .expect("low <= high");

// 3. 执行检测
let src: Vec<f32> = vec![0.0f32; 1920 * 1080];
let edge_map: &[u8] = canny(&src, &mut ws, &cfg).unwrap();
// edge_map 生命周期绑定到 ws,下次调用前有效

// 4. 下一帧直接复用 ws(零分配)
let next_frame: Vec<f32> = vec![128.0f32; 1920 * 1080];
let _ = canny(&next_frame, &mut ws, &cfg).unwrap();
```

### u8 输入(摄像头直接接入)


```rust
use fast_canny::{CannyConfig, CannyWorkspace, canny_u8};

let mut ws = CannyWorkspace::new(640, 480).unwrap();
let cfg = CannyConfig::builder()
    .sigma(1.0).thresholds(30.0, 90.0).build().unwrap();

// 直接传入摄像头 u8 帧,内部自动 u8→f32 转换(写入预分配 buffer_b)
let frame: Vec<u8> = vec![128u8; 640 * 480];
let edge_map = canny_u8(&frame, &mut ws, &cfg).unwrap();
```

### C-FFI 集成(Android/JNI 场景)


```c
void* ws = canny_workspace_new(640, 480);

float src[640 * 480] = {0};
const uint8_t* edge = NULL;

int status = canny_process_ex(
    ws, src, 640 * 480,
    1.0f, 50.0f, 150.0f,
    &edge
);
// edge 指针生命周期与 ws 相同

canny_workspace_free(ws);
```

### `detect_image.rs` 实际调用流程


```
parse_args()                    ← 解析命令行参数
load_image_as_f32(path)         ← image crate 加载,转 f32 灰度
CannyWorkspace::new(w, h)       ← 预分配所有缓冲区
canny(&pixels, &mut ws, &cfg)   ← 执行五阶段流水线
save_edge_map(edge_map, path)   ← 保存结果图像
```

### `visual_demo.rs` 验证维度


该示例生成 10 类合成图像并验证算法正确性:均匀图(零边缘)、阶跃图、棋盘格、圆形、噪声图、渐变图、同心矩形框、最小 3×3 尺寸,同时进行阈值和 sigma 敏感性扫描。

---

## 7. 配置参数说明与性能调优


### CannyConfig 参数


| 参数          | 类型  | 说明                  | 推荐范围                  |
| ------------- | ----- | --------------------- | ------------------------- |
| `sigma`       | `f32` | 高斯平滑强度,≤0 跳过 | 0.5~2.0(噪声越大取越大) |
| `low_thresh`  | `f32` | 弱边缘阈值下界        | 高阈值的 1/3~1/2          |
| `high_thresh` | `f32` | 强边缘阈值            | 根据图像梯度分布调整      |

**典型配置场景**:

```rust
// 工业质检(清晰图像,精确边缘)
CannyConfig::builder().sigma(0.5).thresholds(30.0, 90.0).build()

// 自然图像(含噪声,需平滑)
CannyConfig::builder().sigma(1.4).thresholds(50.0, 150.0).build()

// 已预处理输入(跳过高斯,最快速度)
CannyConfig::builder().sigma(0.0).thresholds(20.0, 60.0).build()
```

### 性能调优指南


**1. 启用 AVX2 编译优化**(`.cargo/config.toml`):

```toml
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "target-feature=+avx2,+fma"]
```

不设置时库会运行时自动检测并回退标量,无需手动配置。

**2. Release 构建配置**(已在 `Cargo.toml` 中配置):

```toml
[profile.release]
opt-level = 3
lto = "fat"        # 全程序链接时优化
codegen-units = 1  # 单编译单元,最大优化
panic = "abort"    # 减小二进制体积
strip = true       # 去除调试符号
```

**3. Workspace 复用原则**:`CannyWorkspace` 创建开销较高(分配 4×W×H 缓冲区),应在程序启动时创建一次,跨帧复用。每帧调用 `reset()` 的开销仅为 O(W×H) 的 `edge_map` 清零。

**4. 阈值调优策略**:

- 边缘过多 → 提高 `high_thresh`
- 边缘断裂 → 降低 `low_thresh`(扩大弱边缘范围)
- 噪声边缘多 → 增大 `sigma` 或提高 `low_thresh`

**5. 线程策略**:`libblur` 的高斯模糊使用 `ThreadingPolicy::Adaptive`,小图(< 256×256)自动退化为单线程,避免线程调度开销。Sobel/NMS 阶段通过 Rayon 的 `into_par_iter` 自动适配可用 CPU 核心数。