# 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 核心数。