跳转到内容

分形/彩色曼德勃罗集

来自维基教科书,开放的书籍,为开放的世界

着色算法

[编辑 | 编辑源代码]

除了绘制集合本身,还开发了一系列算法来

  • 以美观的方式有效地为集合着色
  • 显示数据的结构(科学可视化)


通用着色算法: ( 像素特征 -> 颜色 )

  • 离散梯度
    • 最后一次迭代(整数值)= 水平集方法 = LSM
  • 连续梯度:将归一化位置([0.0, 1.0] 范围内的浮点数)作为输入,并给出颜色作为输出,与分形无关,但在通用计算机图形中使用;



将迭代次数直接转换为颜色

[编辑 | 编辑源代码]

我们可能想要考虑的一件事是避免完全处理调色板或颜色混合。实际上,我们可以利用一些方法通过当场构建颜色来生成平滑、一致的着色。

这里

  • v 指的是归一化的指数映射循环迭代计数
  • f(v) 指的是 sRGB 传输函数


一种生成这种颜色天真方法是通过直接将 v 缩放至 255 并将其传递到 RGB 中

rgb = [v * 255, v * 255, v * 255]

这样做的一个缺陷是 RGB 由于伽马而是非线性的;考虑使用线性的 sRGB。从 RGB 到 sRGB 使用通道上的逆压缩函数。这使伽马线性化,并允许我们对颜色进行正确采样。

srgb = [v * 255, v * 255, v * 255]



连续(平滑)着色

[编辑 | 编辑源代码]
此图像是使用逃逸时间算法渲染的。颜色有非常明显的“条带”
此图像是使用归一化迭代次数算法渲染的。颜色条带已被平滑的渐变所取代。此外,颜色呈现出与使用逃逸时间算法观察到的相同模式。

逃逸时间算法因其简单性而受欢迎。然而,它会产生颜色条带,作为一种类型的 混叠,可能会降低图像的美观性。这可以通过使用一种称为“归一化迭代次数”的算法来改进,[1][2] 该算法提供迭代之间颜色的平滑过渡。该算法通过使用迭代次数与 势函数的联系,将一个实数 与每个 z 值相关联。该函数由下式给出

其中 zn 是经过 n 次迭代后的值,而 Pz 在曼德勃罗集方程中被提升的幂(zn+1 = znP + cP 通常为 2)。

如果我们选择一个较大的逃逸半径 N(例如,10100),我们有

对于某个实数 ,这是

并且由于 n 是第一个使得 |zn| > N 的迭代次数,我们从 n 中减去的数字在 [0, 1) 区间内。

为了着色,我们必须具有循环颜色范围(以数学方式构建,例如)并且包含 H 种颜色,编号从 0 到 H − 1(例如,H = 500)。我们将实数 乘以一个固定的实数,该实数确定图像中颜色的密度,取此数字对 H 模的整数部分,并用它来在颜色表中查找相应的颜色。

例如,修改上面的伪代码并使用 w:线性插值 的概念将产生

for each pixel (Px, Py) on the screen do
    x0:= scaled x coordinate of pixel (scaled to lie in the Mandelbrot X scale (-2.5, 1))
    y0:= scaled y coordinate of pixel (scaled to lie in the Mandelbrot Y scale (-1, 1))
    x:= 0.0
    y:= 0.0
    iteration:= 0
    max_iteration:= 1000
    // Here N = 2^8 is chosen as a reasonable bailout radius.

    while x*x + y*y ≤ (1 << 16) and iteration < max_iteration do
        xtemp:= x*x - y*y + x0
        y:= 2*x*y + y0
        x:= xtemp
        iteration:= iteration + 1

    // Used to avoid floating point issues with points inside the set.
    if iteration < max_iteration then
        // sqrt of inner term removed using log simplification rules.
        log_zn:= log(x*x + y*y) / 2
        nu:= log(log_zn / log(2)) / log(2)
        // Rearranging the potential function.
        // Dividing log_zn by log(2) instead of log(N = 1<<8)
        // because we want the entire palette to range from the
        // center to radius 2, NOT our bailout radius.
        iteration:= iteration + 1 - nu

    color1:= palette[floor(iteration)]
    color2:= palette[floor(iteration) + 1]
    // iteration % 1 = fractional part of iteration.
    color:= linear_interpolate(color1, color2, iteration % 1)
    plot(Px, Py, color)


指数映射和循环迭代

[编辑 | 编辑源代码]

通常,当我们渲染分形时,给定调色板中的颜色在分形上出现的范围是静态的。如果我们希望从分形的边界偏移位置,或调整它们的调色板以特定方式循环,我们可以在将最终迭代次数传递给从调色板中选择项目之前,进行一些简单的更改。


当我们获得迭代次数后,我们可以使颜色范围非线性。将归一化为范围 [0,1] 的值提高到 n 次幂,将线性范围映射到指数范围,在本例中,这可以微调颜色在分形外部的出现方式,并让我们可以显示其他颜色,或将整个调色板推向边界。


其中

  • i 是我们迭代次数,迭代结束后的次数
  • max_i 是我们的迭代限制
  • S 是我们迭代次数所要提升的指数
  • N 是调色板中项目的数量。

这将以非线性方式缩放迭代次数,并缩放调色板以近似地与缩放成比例地循环。

然后我们可以将 v 插入我们想要的任何用于生成颜色的算法中。

HSV 着色

[edit | edit source]

HSV 着色可以通过将迭代次数从 [0,max_iter) 映射到 [0,360),将其提高到 1.5 次幂,然后对 360 取模来实现。

然后我们可以简单地将指数映射的迭代次数带入值并返回

hsv = [powf((i / max) * 360, 1.5) % 360, 100, (i / max) * 100]

此方法也适用于 HSL,只是我们将饱和度设置为 50% 而不是 100%。

hsl = [powf((i / max) * 360, 1.5) % 360, 50, (i / max) * 100]

其中

LCH 着色

[edit | edit source]

最具感知均匀性的着色方法之一是将处理过的迭代次数传递给 LCH。如果我们使用上面提到的指数映射和循环方法,我们可以将结果带入亮度和色度通道。我们也可以对迭代次数进行指数映射并将其缩放到 360,并将此结果对 360 取模传递到色相中。

我们希望避免这里出现的一个问题是超出色域的颜色。这可以通过基于色域内颜色相对于亮度和色度的变化的小技巧来实现。随着亮度的增加,我们需要降低色度以保持在色域内。

s = iters/max_i;
v = 1.0 - powf(cos(pi * s), 2.0);
LCH = [75 - (75 * v), 28 + (75 - (75 * v)), powf(360 * s, 1.5) % 360];

直方图着色

[edit | edit source]

一种更复杂的着色方法涉及使用 直方图,该直方图将每个像素与其逃逸/退出之前的最大迭代次数配对。这种方法将颜色均匀地分布到相同的总区域,重要的是,它独立于选择的最大迭代次数。[3]

此算法有四个步骤。第一步涉及计算与每个像素相关的迭代次数(但没有绘制任何像素)。这些存储在一个数组中:IterationCounts[x][y],其中 x 和 y 分别表示屏幕上该像素的 x 和 y 坐标。

顶行是一系列使用逃逸时间算法绘制的图像,每个像素分别进行 10000 次、1000 次和 100 次最大迭代。底行使用相同的最大迭代值,但使用直方图着色方法。请注意,直方图着色方法绘制的图像中,着色如何随最大迭代次数的变化而变化很小。

第二步的第一步是创建一个大小为 n 的数组,即最大迭代次数:NumIterationsPerPixel。接下来,必须遍历像素迭代次数对数组 IterationCounts[][],并通过例如 i = IterationCounts[x][y] 检索每个像素的保存迭代次数 i。检索到每个像素的迭代次数 i 后,需要按 i 对 NumIterationsPerPixel 进行索引,并递增索引值(最初为零)-- 例如 NumIterationsPerPixel[i] = NumIterationsPerPixel[i] + 1。

for (x = 0; x < width; x++) do
    for (y = 0; y < height; y++) do
        i:= IterationCounts[x][y]
        NumIterationsPerPixel[i]++

第三步遍历 NumIterationsPerPixel 数组,并将所有存储的值加起来,并将它们保存在 total 中。数组索引表示达到该迭代次数后逃逸的像素数量。

total: = 0
for (i = 0; i < max_iterations; i++) do
    total += NumIterationsPerPixel[i]

在此之后,第四步开始,对 IterationCounts 数组中的所有值进行索引,并针对与每个像素相关的每个迭代次数 i,将该计数添加到 NumIterationsPerPixel 数组中从 1 到 i 的所有迭代计数的全局总和中。然后将该值通过将总和除以前面计算的 total 值进行归一化。

hue[][]:= 0.0
for (x = 0; x < width; x++) do
    for (y = 0; y < height; y++) do
        iteration:= IterationCounts[x][y]
        for (i = 0; i <= iteration; i++) do
            hue[x][y] += NumIterationsPerPixel[i] / total /* Must be floating-point division. */

...

color = palette[hue[m, n]]

...

最后,使用计算出的值,例如作为调色板的索引。

此方法可以与下面的平滑着色方法结合使用,以获得更美观的图像。

Muency

[edit | edit source]

来自曼德尔布罗特集的词汇表和百科全书,作者 Robert Munafo,(c) 1987-2023

Albert Lobo 编写的 JavaScript 版本

// https://github.com/llop/mandelbrot-viewer-js/blob/master/mandelbrot-viewer.js
// http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
  _hsvToRgb(h, s, v) {
    let r, g, b;
    const i = Math.floor(h * 6);
    const f = h * 6 - i;
    const p = v * (1 - s);
    const q = v * (1 - f * s);
    const t = v * (1 - (1 - f) * s);
    switch (i % 6) {
      case 0: r = v, g = t, b = p; break;
      case 1: r = q, g = v, b = p; break;
      case 2: r = p, g = v, b = t; break;
      case 3: r = p, g = q, b = v; break;
      case 4: r = t, g = p, b = v; break;
      case 5: r = v, g = p, b = q; break;
    }
    return [ r * 255, g * 255, b * 255 ];
  }
  
  
  // https://mrob.com/pub/muency/color.html
  _colorCheckered(k) {
    if (this.ns[k] >= this.maxN) return [ 255, 255, 255 ];
    
    const dwell = Math.floor(this.dwell[k]);
    const finalrad = this.dwell[k] - Math.floor(this.dwell[k]);
    const dscale = Math.log2(this.dist[k] / this.inc);
    
    let value = 0.0;
    if (dscale > 0.0) value = 1.0;
    else if (dscale > -10.0) value = (10.0 + dscale) / 10.0;
    
    let p = Math.log(dwell) / Mandelbrot.LOG_BIG_NUM;
    let angle = 0.0;
    let radius = 0.0;
    if (p < 0.5) {
      p = 1.0 - 1.5 * p;
      angle = 1.0 - p;
      radius = Math.sqrt(p);
    } else {
      p = 1.5 * p - 0.5;
      angle = p;
      radius = Math.sqrt(p);
    }
    
    if (dwell % 2) {
      value *= 0.85;
      radius *= 0.667;
    }
    
    if (this.finalang[k] > 0.0) {
      angle += 0.02;
    }
    angle += 0.0001 * finalrad;
    
    let hue = angle * 10.0;
    hue -= Math.floor(hue);
    let saturation = radius - Math.floor(radius);
    
    return this._hsvToRgb(hue, saturation, value);
  }
  
  // b&w version of checkerboard
  _colorCheckeredBlackAndWhite(k) {
    const color = this._colorCheckered(k);
    const gray = Math.round(color[0] * 0.299 + color[1] * 0.587 + color[2] * 0.114);
    return [ gray, gray, gray ];
  }
  
  
  // color pixel k
  _color(k) {
    if (this.ns[k] == 0) return [ 0, 0, 0 ];  // C not yet processed: color black
    this.pix[k] = 0;                          // mark pixel as colored
    return this.colorFunc(k);                 // return color for pixel
  }
  
  
  // paint what we have on the canvas
  render() {
    if (!this.scanDone) {
      let offset = 0;
      for (let k = 0; k < this.imgSize; ++k, offset += 4) {
        if (this.pix[k]) {
          const color = this._color(k);
          this.image.data[offset] = color[0];
          this.image.data[offset + 1] = color[1];
          this.image.data[offset + 2] = color[2];
          this.image.data[offset + 3] = 255;
        }
      }
      if (!this.scanning) this.scanDone = true;
    }
    this.context.clearRect(0, 0, this.imgWidth, this.imgHeight);
    this.context.putImageData(this.image, 0, 0);
  }
  
  
  // sleep function. use 'await this.sleep()' in async functions
  _sleep() { return new Promise(requestAnimationFrame); }
  
}

另请参阅

[edit | edit source]

参考文献

[编辑 | 编辑源代码]
  1. García, Francisco; Ángel Fernández; Javier Barrallo; Luis Martín. "在复平面上对动力系统进行着色" (PDF). 已存档 (PDF) from the original on 30 November 2019. Retrieved 2008-01-21. {{cite journal}}: Cite journal requires |journal= (help)
  2. Linas Vepstas. "对曼德布罗特逃逸进行重归一化". 已存档 from the original on 14 February 2020. Retrieved 11 February 2020.
  3. "新手:如何在曼德布罗特集合中映射颜色?". www.fractalforums.com. May 2007. 已存档 from the original on 9 September 2022. Retrieved February 11, 2020.
华夏公益教科书