Rust图像处理第16节-曼德博集合 Mandelbrot:用一行公式画出无限细节

曼德博集合 Mandelbrot:四个步骤学会算法

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

📌 写在前面

前三部分做的所有算法都是**"对每个像素 → 用某种公式算出颜色"**------几何变换是改坐标,卷积是算加权和,亮度是改 RGB 值。

从这一篇开始,进入全新的领域 ------分形

对每个像素 → 在复平面迭代 → 根据迭代次数着色

公式超简单:
zn+1 =zn2+c,z0=0 z_{n+1} = z_n^2 + c, \quad z_0 = 0 zn+1=zn2+c,z0=0

它生成的图案是无限的、自相似的、无限复杂的------这就是 Mandelbrot 集合。

这一篇不讲历史,不讲分形几何理论 ------只讲怎么算。跟着算法一步步走,看一个像素怎么变成颜色。


🚀 TL;DR

整个算法 4 步

r 复制代码
① 像素 → 复数 c              (算一个数字 c)
② 公式 z = z² + c            (z 反复变)
③ 什么是"逃逸"                (z 大到一定程度就停)
④ 颜色怎么定                  (按 iter 涂颜色)

核心代码就 7 行

rust 复制代码
while iter < 200 && (zr*zr + zi*zi) < 4.0 {
    let new_zr = zr*zr - zi*zi + c_re;
    let new_zi = 2.0*zr*zi + c_im;
    zr = new_zr; zi = new_zi;
    iter += 1;
}

600×600 画布 + 200 步 ≈ 7200 万次浮点运算------WASM 比 JS 快 5~10 倍。


📖 目录

  1. [像素 → 复数](#像素 → 复数 "#%E4%B8%80%E5%83%8F%E7%B4%A0--%E5%A4%8D%E6%95%B0")
  2. [公式 z = z² + c 怎么算](#公式 z = z² + c 怎么算 "#%E4%BA%8C%E5%85%AC%E5%BC%8F-z--z%C2%B2--c-%E6%80%8E%E4%B9%88%E7%AE%97")
  3. 什么是"逃逸"
  4. 颜色怎么定
  5. 跟着一个像素走一遍算法
  6. 关键代码
  7. 前端效果展示
  8. 踩坑提醒

一、像素 → 复数

画布和复平面

画布是 600×600600 \times 600 600×600 个离散的 像素网格,每个像素是整数 (px,py)(\text{px}, \text{py}) (px,py)。

复平面是连续的 二维实数平面,每个点是 (a,b)(a, b) (a,b) 实数。

我们要给每个像素 (px,py)(\text{px}, \text{py}) (px,py) 对应到一个复数 c=a+bic = a + bi c=a+bi。

映射公式

cre =centerx+ px−w/2zoom cim =centery+ py−h/2zoom \begin{aligned} c_{\text{re}} &= \text{center}x + \frac{\text{px} - w/2}{\text{zoom}} \\ c{\text{im}} &= \text{center}_y + \frac{\text{py} - h/2}{\text{zoom}} \end{aligned} crecim=centerx+zoompx−w/2=centery+zoompy−h/2

参数 含义
centerx \text{center}_x centerx, centery \text{center}_y centery 画布中心对应复平面哪个点
zoom\text{zoom} zoom 缩放倍率(多少像素 = 1 个复平面单位)

例子 (取 center=(−0.5,0)\text{center} = (-0.5, 0) center=(−0.5,0), zoom=200\text{zoom} = 200 zoom=200):

像素位置 cre c_{\text{re}} cre cim c_{\text{im}} cim 复数 cc c
(300,300)(300, 300) (300,300) 画布中心 −0.5-0.5 −0.5 00 0 −0.5+0i-0.5 + 0i −0.5+0i
(0,0)(0, 0) (0,0) 左上角 −2-2 −2 −1.5-1.5 −1.5 −2−1.5i-2 - 1.5i −2−1.5i
(599,599)(599, 599) (599,599) 右下角 ≈1\approx 1 ≈1 ≈1.5\approx 1.5 ≈1.5 1+1.5i1 + 1.5i 1+1.5i

默认 center 为什么是 (-0.5, 0)?

曼德博集合整体偏左 ------主体在 c ∈ [-2, 0.25]。把画布中心定在 (-0.5, 0),集合刚好被框在画布中间

如果用 center=(0, 0),集合会跑到画布左边,右边一大片空白

代码

rust 复制代码
let inv_zoom = 1.0 / zoom;
for py in 0..600 {
    for px in 0..600 {
        let c_re = center_x + (px as f64 - 300.0) * inv_zoom;
        let c_im = center_y + (py as f64 - 300.0) * inv_zoom;
        // ... 后面用这个 c ...
    }
}

二、公式 z = z² + c 怎么算

z 是复数

复数 z=zr+zi⋅iz = \text{zr} + \text{zi} \cdot i z=zr+zi⋅i,两部分:
zr =实部(普通的数) zi =虚部(带 i 的部分,i2=−1) \begin{aligned} \text{zr} &= \text{实部(普通的数)}\\ \text{zi} &= \text{虚部(带 } i \text{ 的部分,} i^2 = -1 \text{)} \end{aligned} zrzi=实部(普通的数)=虚部(带 i 的部分,i2=−1)

比如 z=3+4iz = 3 + 4i z=3+4i 意思是 zr=3\text{zr} = 3 zr=3, zi=4\text{zi} = 4 zi=4。

复数乘法公式

复数乘法有一个固定公式:
(a+bi)×(a+bi)=(a2−b2)+2abi(a + bi) \times (a + bi) = (a^2 - b^2) + 2abi (a+bi)×(a+bi)=(a2−b2)+2abi

  • 实部 a2−b2a^2 - b^2 a2−b2
  • 虚部 2ab2ab 2ab

套用到 z2z^2 z2

z=zr+zi⋅iz = \text{zr} + \text{zi} \cdot i z=zr+zi⋅i,代入上面的公式( a=zra = \text{zr} a=zr, b=zib = \text{zi} b=zi):
Re(z2) =zr2−zi2 Im(z2) =2⋅zr⋅zi \begin{aligned} \text{Re}(z^2) &= \text{zr}^2 - \text{zi}^2 \\ \text{Im}(z^2) &= 2 \cdot \text{zr} \cdot \text{zi} \end{aligned} Re(z2)Im(z2)=zr2−zi2=2⋅zr⋅zi

再加 cc c

Re(z2+c) =(zr2−zi2)+cre Im(z2+c) =(2⋅zr⋅zi)+cim \begin{aligned} \text{Re}(z^2 + c) &= (\text{zr}^2 - \text{zi}^2) + c_{\text{re}} \\ \text{Im}(z^2 + c) &= (2 \cdot \text{zr} \cdot \text{zi}) + c_{\text{im}} \end{aligned} Re(z2+c)Im(z2+c)=(zr2−zi2)+cre=(2⋅zr⋅zi)+cim

代码

rust 复制代码
let new_zr = zr * zr - zi * zi + c_re;   // 实部公式
let new_zi = 2.0 * zr * zi + c_im;       // 虚部公式
zr = new_zr;
zi = new_zi;

这两行就是 z=z2+cz = z^2 + c z=z2+c 的代码版------把一行公式拆成"实部"和"虚部"两部分。

c=0.5c = 0.5 c=0.5(纯实数)验证

步骤 zz z z2z^2 z2 z2+cz^2 + c z2+c
0 00 0 00 0 0.50.5 0.5
1 0.50.5 0.5 0.250.25 0.25 0.750.75 0.75
2 0.750.75 0.75 0.56250.5625 0.5625 1.06251.0625 1.0625

虚部一直是 0------因为 cc c 是纯实数,从 0 出发永远不会有虚部。


三、什么是"逃逸"

概念

迭代过程中,z 越算越大 → "逃逸"(爆炸到无穷)。

我们关心的是"z 在变大的过程中超过多少就算跑飞了"。

为什么阈值是 2?

关键观察:复数乘法会让模长平方。
∣z2∣=∣z∣⋅∣z∣=∣z∣2|z^2| = |z| \cdot |z| = |z|^2 ∣z2∣=∣z∣⋅∣z∣=∣z∣2

也就是说, ∣z∣=3|z| = 3 ∣z∣=3 时,下一步 ∣z2∣=9|z^2| = 9 ∣z2∣=9。

如果某一步 ∣z∣>2|z| > 2 ∣z∣>2 ,那 ∣z2∣>4|z^2| > 4 ∣z2∣>4,下一步 ∣z4∣>16|z^4| > 16 ∣z4∣>16,再下一步 >256> 256 >256......

指数级爆炸,再也回不来

所以 ∣z∣>2|z| > 2 ∣z∣>2 一定跑飞------定 2 就够了。

为什么代码里写 < 4.0

∣z∣>2|z| > 2 ∣z∣>2 等价于 ∣z∣2>4|z|^2 > 4 ∣z∣2>4,等价于 zr2+zi2>4\text{zr}^2 + \text{zi}^2 > 4 zr2+zi2>4。

zr2+zi2 \sqrt{\text{zr}^2 + \text{zi}^2} zr2+zi2 比 zr2+zi2\text{zr}^2 + \text{zi}^2 zr2+zi2 慢几倍 ------所以我们省掉 sqrt,直接判断平方和:
zr2+zi2<4  ⟺  ∣z∣<2  ⟺  还没逃逸\text{zr}^2 + \text{zi}^2 < 4 \iff |z| < 2 \iff \text{还没逃逸} zr2+zi2<4⟺∣z∣<2⟺还没逃逸


四、颜色怎么定

每个像素迭代完后得到一个 iter(0~200 之间)。根据 iter 决定涂什么颜色。

为了直观,我们用 5 色分类(不是渐变,方便表格对照):

迭代次数 iter 颜色 含义
1 ~ 4 红色 跑飞快(1~4 步就逃)
5 ~ 9 橙色 较快
10 ~ 29 黄色 中等
30 ~ 99 绿色 较慢
100 ~ 199 蓝色 很慢(接近集合边缘)
200 黑色 200 步都没逃出(在集合内)

记忆口诀

python 复制代码
iter 越小 → 跑得越快 → 颜色越亮(红橙)
iter 越大 → 跑得越慢 → 颜色越深(蓝)
iter = 200 → 不跑 → 黑色

颜色 vs 集合边界的关系

makefile 复制代码
              远离边界           接近边界        在边界内
iter:           1                  100             200
颜色:           红 ──────────→ 蓝 ──────────→ 黑

五、跟着一个像素走一遍算法

画布中心 (300, 300) ,对应 c = -0.5 + 0i

ini 复制代码
z = 0 + 0i

逐行迭代 + 颜色判定

步骤 zr zi zr² + zi² 还在算? 如果停 → 颜色
0 0 0 0 -
1 -0.5 0 0.25 1 步 →
2 -0.25 0 0.0625 2 步 →
3 -0.4375 0 0.191 3 步 →
4 -0.309 0 0.095 4 步 →
5 -0.405 0 0.164 5 步 →
... ... ... ... 一直在 -1, 1 之间晃
50 -0.358 0 0.128 50 步 →绿
... ... ... ... ...
199 -0.367 0 0.135 199 步 →
200 -0.367 0 0.135 ✗ 达到上限 黑色(在集合内)

结论:c = -0.5 永不逃 → 涂黑色


再选另一个像素 c = 0.5(画布中心偏右):

步骤 zr zi zr² + zi² 还在算? 如果停 → 颜色
0 0 0 0 -
1 0.5 0 0.25 1 步 →
2 0.75 0 0.5625 2 步 →
3 1.0625 0 1.129 3 步 →
4 1.629 0 2.654 4 步 →
5 3.154 0 9.95 > 4 逃了! 红色(5 步就逃,最亮的红)

结论:c = 0.5 5 步就跑飞 → 涂红色


算法总结

ruby 复制代码
对每个像素 (px, py):
    1. 像素 → 复数 $c$              ← 第 ① 节
2. $z = 0$
3. 重复:
   - if $\text{zr}^2 + \text{zi}^2 > 4$:停止  ← 第 ③ 节
   - $z = z^2 + c$                       ← 第 ② 节
4. 用 $\text{iter}$ 涂颜色              ← 第 ④ 节

六、关键代码

rust 复制代码
#[wasm_bindgen]
pub fn mandelbrot(
    width: u32, height: u32,
    center_x: f64, center_y: f64, zoom: f64,
    max_iter: u32, palette: u32,
) -> Vec<u8> {
    let w = width as usize;
    let h = height as usize;
    let mut pixels = vec![0u8; w * h * 4];
    let inv_zoom = 1.0 / zoom;

    for py in 0..h {
        for px in 0..w {
            // ① 像素 → 复数 c
            let c_re = center_x + (px as f64 - w as f64 / 2.0) * inv_zoom;
            let c_im = center_y + (py as f64 - h as f64 / 2.0) * inv_zoom;

            // ②③ 迭代 + 逃逸判断
            let mut zr = 0.0;
            let mut zi = 0.0;
            let mut iter = 0;
            while iter < max_iter && (zr * zr + zi * zi) < 4.0 {
                let new_zr = zr * zr - zi * zi + c_re;
                let new_zi = 2.0 * zr * zi + c_im;
                zr = new_zr;
                zi = new_zi;
                iter += 1;
            }

            // ④ 涂色(按 iter 分桶)
            let (r, g, b) = color_from_iter(iter, max_iter, palette);
            let idx = (py * w + px) * 4;
            pixels[idx]     = r;
            pixels[idx + 1] = g;
            pixels[idx + 2] = b;
            pixels[idx + 3] = 255;
        }
    }

    pixels
}

核心循环 7 行。

涂色函数(按上面 ④ 节的分桶)

rust 复制代码
fn color_from_iter(iter: u32, max_iter: u32, palette: u32) -> (u8, u8, u8) {
    if iter >= max_iter {
        return (0, 0, 0);   // 黑:200 步都没逃
    }
    let t = iter as f64 / max_iter as f64;  // 0~1
    match palette {
        0 => fire(t),       // 经典红橙黄
        1 => ocean(t),      // 蓝色系
        2 => forest(t),     // 绿色系
        _ => grayscale(t),  // 灰度
    }
}

fn fire(t: f64) -> (u8, u8, u8) {
    let r = (255.0 * (1.0 - (1.0 - t).powi(2))) as u8;
    let g = (255.0 * t.powi(2)) as u8;
    let b = (255.0 * (t * 4.0).min(1.0) * t.powi(3) * 4.0) as u8;
    (r, g, b)
}

七、前端效果展示

八、必须用 WASM

1. 性能坑:JS 跑不动,必须用 WASM

总工作量
600×600=360,000 像素600 \times 600 = 360{,}000 \text{ 像素} 600×600=360,000 像素

每个像素平均迭代 ∼50\sim 50 ∼50 次,总浮点运算 ≈7200\approx 7200 ≈7200 万次。

实测:

实现 耗时 体感
纯 JS 800~1500 ms 卡顿
WASM (Rust) 100~200 ms 流畅

加速 5~10 倍Mandelbrot 是 CPU 密集型算法,JS 原生跑不动------必须用 WASM。


一句话总结

Mandelbrot 算法五步

① 像素 → 复数 c ② 公式 z = z² + c(代码拆成实部/虚部两行) ③ 逃逸判断:|z|² > 4(即 |z| > 2) ④ 颜色:iter 小 → 红,iter 大 → 蓝,iter=200 → 黑 ⑤ 跟着 c = -0.5(涂黑)和 c = 0.5(涂红)走一遍

整个代码核心 7 行,WASM 比 JS 快 5~10 倍


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


🏷️ 标签#Rust #WebAssembly #分形 #Mandelbrot #复数 #迭代 #算法

相关推荐
Arman37681 小时前
rusty-cat × 阿里云 OSS 直传直下实战
rust·ai编程
临风细雨3 小时前
从 Bun 的 Rust 重写,看 C# 如何重建 AI 基础设施层
人工智能·rust·c#
x-cmd3 小时前
Linus 9 年前说「模糊测试有效」——今天已成 OSS-Fuzz 时代铁律
linux·ai·rust·开源·自动化·agent·安全性测试
CodeForWater4 小时前
RUST设置临时环境变量在PowerShell
rust
独孤留白5 小时前
从C到Rust:Trait Deref + DerefMut 智能指针与自动解引用
rust
AvaLee5 小时前
一文推导 2D 旋转矩阵:从三角函数到工程实践
图形学
程序员爱钓鱼6 小时前
Rust 数据类型详解:整数、浮点数、布尔、字符、元组和数组
后端·rust
梦帮科技7 小时前
Rust+Tauri+EVM:构建下一代内存安全级数字版权(DRM)主权音频网络与跨链金融系统的技术实践与全景架构
网络·安全·架构·rust·区块链·音视频·web3.py
明理的信封18 小时前
AI 基础设施的“去 Python 化“:Rust 与 C# 的两条替代路径
人工智能·python·rust