Rust图像处理第10节-浮雕/雕刻滤镜:邻域差值生成凹凸效果

🦀 Rust + WASM 实战系列 第 10 篇 阅读时间:约 5 分钟 | 实战可运行

📌 写在前面

Sobel(任务 9)用两个 3×3 核检测边缘。浮雕 = Sobel 的"亲戚" ------用 1 个核,把"边缘"变成"凹凸"。

效果像把图片刻在石板上:平坦 = 中灰,凸起 = 亮,凹陷 = 暗。


🚀 TL;DR

复制代码
原图:                   浮雕后:
┌──────────┐            ┌──────────┐
│  ████    │            │    ▓▓    │     中灰背景(128)
│  █  █    │    →       │   ▓  ▓   │     凸起 = 亮(> 128)
│  ████    │            │  ▓▓▓▓▓▓  │     凹陷 = 暗(< 128)
└──────────┘            └──────────┘

核心1 个 3×3 核):

diff 复制代码
-2  -1   0
-1   1   1
 0   1   2

公式:新像素 = 卷积结果 + 128(128 是中灰偏置)。


📖 目录

  1. 浮雕的物理直觉
  2. [核心 3×3 核解读](#核心 3×3 核解读 "#%E4%BA%8C%E6%A0%B8%E5%BF%83-33-%E6%A0%B8%E8%A7%A3%E8%AF%BB")
  3. [为什么加 128?](#为什么加 128? "#%E4%B8%89%E4%B8%BA%E4%BB%80%E4%B9%88%E5%8A%A0-128")
  4. 关键代码
  5. 前端效果展示
  6. [8 个方向的浮雕](#8 个方向的浮雕 "#%E5%85%AD8-%E4%B8%AA%E6%96%B9%E5%90%91%E7%9A%84%E6%B5%AE%E9%9B%95")
  7. [Sobel vs Emboss](#Sobel vs Emboss "#%E4%B8%83sobel-vs-emboss")
  8. 应用场景

一、浮雕的物理直觉

想象图片被刻在石板上,光从左上方照下来:

markdown 复制代码
光线从左上 → 凸起 → 朝光 → 亮
           凹陷 → 背光 → 暗
           平坦 → 中间色

算法本质:算"对角邻居差值"------凸起 = 远端比近端亮,凹陷 = 反过来。


二、核心 3×3 核解读

rust 复制代码
let kernel = Matrix3::from_row_slice(&[
    -2, -1,  0,    // 左上    系数负("减")
    -1,  1,  1,    // 正中    系数 1("自")
     0,  1,  2,    // 右下    系数正("加")
]);

每个系数的意思

arduino 复制代码
   左上 ──── 中上 ──── 右上
     │         │         │
   左中  ─┼─ ★正中  ─┼─ 右中   ← "现在"这个像素
    │         │         │
   左下 ──── 中下 ──── 右下
系数 含义
-2(左上) "远离自己" → 大幅扣除
-1(左、左上) "略远" → 少量扣除
0(左上角 + 中) "无影响"
+1(右、右下) "略近" → 少量加成
+2(右下) "最近自己" → 大幅加成
+1(正中) "自身" → 1 倍贡献

几何意义 :对角线(左上 ↔ 右下)的梯度------凸起 = 远端暗,凹陷 = 远端亮。


三、为什么加 128?

前面说了 新像素 = 卷积结果 + 128(128 是中灰偏置)

没有 128 偏置会怎样?

rust 复制代码
let sum: i32 = convolution_sum;  // 通常 -255 ~ +255
// 不加 128:sum = 200 → 200  → 太亮
//          sum = -200 → -200 → 负数(u8 下溢 → 56)

加上 128

rust 复制代码
let embossed = (sum + 128).clamp(0, 255) as u8;
// sum =    0 → 128 (中灰,无边缘 = 平坦)
// sum = +100 → 228 (亮,凸起)
// sum = -100 →  28 (暗,凹陷)
sum 卷积结果 +128 后 视觉
0 128 中灰(平)
+127 255 最白(最强凸)
-128 0 最黑(最强凹)

128 是个"魔法数字" = u8 范围的中点。


四、关键代码

rust 复制代码
// 1. 收集 3×3 邻域灰度(用 Matrix3 存,跟核矩阵同结构)
let neighborhood: Matrix3<i32> = Matrix3::from_row_slice(&[
    gray_at(pixels, w, h, x - 1, y - 1),  // 左上
    gray_at(pixels, w, h, x,     y - 1),  // 上
    gray_at(pixels, w, h, x + 1, y - 1),  // 右上
    gray_at(pixels, w, h, x - 1, y    ),  // 左
    gray_at(pixels, w, h, x,     y    ),  // 中
    gray_at(pixels, w, h, x + 1, y    ),  // 右
    gray_at(pixels, w, h, x - 1, y + 1),  // 左下
    gray_at(pixels, w, h, x,     y + 1),  // 下
    gray_at(pixels, w, h, x + 1, y + 1),  // 右下
]);

// 2. 卷积(zip + map + sum,nalgebra 风格)
let sum: i32 = neighborhood.iter()
    .zip(kernel.iter())
    .map(|(a, b)| a * b)
    .sum();

// 3. 加 128 偏置 + clamp
let embossed = (sum + 128).clamp(0, 255) as u8;

// 4. 三通道都填这个灰度(浮雕是"灰度感"的)
result[o]     = embossed;
result[o + 1] = embossed;
result[o + 2] = embossed;

五、前端效果展示

六、8 个方向的浮雕

不同方向的光源 → 不同方向的凸起:

浮凸方向
-2 -1 0 / -1 1 1 / 0 1 2 左 → 右(光从左上)
0 1 2 / -1 1 1 / -2 -1 0 右 → 左
-2 -1 0 / -1 1 1 / 0 1 2(转置) 上 → 下
... 8 个方向

简单实现

rust 复制代码
// 转置核 = 改变方向
let transposed: Matrix3<i32> = kernel.transpose();

完整 8 方向 在专业图像处理软件里都有,但我们只做最常用的左上 → 右下


七、Sobel vs Emboss:相同的"卷积芯",不同的"外围"

很多人第一次看完 Sobel 和浮雕的代码会有疑问:"是不是换了个矩阵,其他都一样?"

答案是:卷积芯完全一样,外围有 4 个不同

7.1 相同的部分(卷积算法本身)

两者都用这一段(来自任务 9 讨论过的 zip + map + sum 模式):

rust 复制代码
// 1. 收集 3×3 邻域
let neighborhood: Matrix3<i32> = Matrix3::from_row_slice(&[
    gray_at(pixels, w, h, x-1, y-1),  // ... 9 个像素
]);

// 2. 卷积 = 对应元素相乘再求和
let sum: i32 = neighborhood.iter()
    .zip(kernel.iter())
    .map(|(a, b)| a * b)
    .sum();

这一步 100% 相同------不管你是 Sobel、浮雕、模糊、锐化,卷积"芯"都是同一段代码。

7.2 4 个不同点

维度 Sobel Emboss
核数量 2 个(Gx + Gy,两个方向差分) 1 个(单一对角方向差分)
结果合成 `( gx + gy ).min(255)` 矢量合成 直接用卷积结果sum
偏置 不加(mag 本来就 ≥ 0) + 128(把 0 映射到中灰)
最终输出 二值化(黑或白) 连续灰度(0~255)

7.3 算法对照(伪代码)

把"卷积芯"抽出来成 convolve() 函数,两者差异立刻清晰:

rust 复制代码
// 卷积芯(共用)
fn convolve(neighborhood: Matrix3<i32>, kernel: Matrix3<i32>) -> i32 {
    neighborhood.iter()
        .zip(kernel.iter())
        .map(|(a, b)| a * b)
        .sum()
}

// ============ Sobel ============
let gx = convolve(neighborhood, SOBEL_X);
let gy = convolve(neighborhood, SOBEL_Y);
let mag = (gx.abs() + gy.abs()).min(255);
let pixel = if mag > threshold { 255 } else { 0 };  // ← 二值化

// ============ Emboss ============
let sum = convolve(neighborhood, EMBOSS_KERNEL);
let pixel = (sum + 128).clamp(0, 255);              // ← +128 偏置

共同convolve(neighborhood, kernel) 那段 差异:核的数量、结果怎么合成、最终怎么映射到像素

7.4 为什么浮雕只用 1 个核?

Sobel 必须用 2 个核是因为梯度本身是矢量------既有大小又有方向,Gx 和 Gy 两个分量才能完整描述"明暗在哪里突变"。

浮雕关心的是单一方向(默认是左上→右下)的明暗差异,本身就是标量,1 个核够用:

  • 凸起 = 右下像素 > 左上像素(卷积结果 > 0)
  • 凹陷 = 右下像素 < 左上像素(卷积结果 < 0)
  • 平坦 = 两者相等(卷积结果 ≈ 0)

7.5 为什么浮雕要 +128,Sobel 不要?

Sobel Emboss
卷积结果范围 mag =|gx| + |gy|,本来就 ≥ 0 sum 可以是**-255 ~ +255** 任意值
0 的含义 "完全没有边缘"(不显示) "平坦区域"(要显示成中灰背景)
加偏置? 不需要(已经 ≥ 0) 必须 +128,否则平坦区显示成纯黑

7.6 一句话总结

核决定"算什么",合成方式决定"怎么用结果"

Sobel 核 → 检测边缘;浮雕核 → 检测对角差值。 Sobel 用 abs + sum + threshold → 输出黑白二值图; 浮雕用 +128 → 输出连续灰度图。

换核换主题,换合成方式换玩法------这就是卷积类算法的"组件化"。


八、应用场景

场景 说明
艺术化照片 "老照片 / 浮雕版"
UI 装饰按钮 Material Design 风格
盲文 / 触觉图 转成可触摸的灰度图
游戏 / 复古风 "老游戏" 像素感
专业印刷制版 模拟雕刻效果

🎁 写在最后

浮雕是最便宜的"立体感"特效

  • 1 个 3×3 核
  • 1 行核心公式
  • 几行 nalgebra 代码

但效果立竿见影 。掌握了 Sobel,再学浮雕就是5 分钟的事

下篇预告:《故障风 RGB 通道偏移:错位错色制造电子故障 》------ 最后一个邻域卷积,不用卷积核也能做出"赛博朋克"风格,敬请期待。


📦 项目地址pixel-math-wasm 🦀 Rust + WebAssembly 实战系列


🏷️ 标签#Rust #WebAssembly #图像处理 #浮雕 #Emboss #卷积 #算法 #nalgebra

相关推荐
Rockbean2 小时前
10分钟Solana-性能web3-2.4 Rust 编程基础三:结构体、枚举、错误处理与集合
rust·web3·智能合约
doiito3 小时前
【Agent Harness】Gliding Horse 上下文感知与智能压缩:让 Agent 的“注意力”永不偏移
ai·rust·架构设计·系统设计·ai agent
SmalBox1 天前
【节点】[CorneaRefraction节点]原理解析与实际应用
unity3d·游戏开发·图形学
花褪残红青杏小1 天前
Rust图像处理第9节-Sobel 边缘检测:第一个真正用卷积的算法
rust·webassembly·图形学
doiito1 天前
【Agent Harness】Gliding Horse L2 作战地图深度优化:给多 Agent 上下文装上“精准导航”
ai·rust·架构设计·系统设计·ai agent
花褪残红青杏小1 天前
Rust图像处理第8节-暗角 & 复古胶片特效:四周衰减中心高亮
rust·webassembly·图形学
SmalBox2 天前
【节点】[CirclePupilAnimation节点]原理解析与实际应用
unity3d·游戏开发·图形学