<!doctype html><htmllang=endir=ltrclass="blog-wrapper blog-post-page plugin-blog plugin-id-default"data-has-hydrated=false><metacharset=UTF-8><metaname=generatorcontent="Docusaurus v3.7.0"><titledata-rh=true>Playing with fire: Tone mapping and color | The Old Speice Guy</title><metadata-rh=truename=viewportcontent="width=device-width, initial-scale=1.0"><metadata-rh=truename=twitter:cardcontent=summary_large_image><metadata-rh=trueproperty=og:urlcontent=https://speice.io/2024/11/playing-with-fire-log-density/><metadata-rh=trueproperty=og:localecontent=en><metadata-rh=truename=docusaurus_localecontent=en><metadata-rh=truename=docusaurus_tagcontent=default><metadata-rh=truename=docsearch:languagecontent=en><metadata-rh=truename=docsearch:docusaurus_tagcontent=default><metadata-rh=trueproperty=og:titlecontent="Playing with fire: Tone mapping and color | The Old Speice Guy"><metadata-rh=truename=descriptioncontent="So far, our plot() function has been fairly simple: map a fractal flame coordinate to a specific pixel,"><metadata-rh=trueproperty=og:descriptioncontent="So far, our plot() function has been fairly simple: map a fractal flame coordinate to a specific pixel,"><metadata-rh=trueproperty=og:typecontent=article><metadata-rh=trueproperty=article:published_timecontent=2024-12-16T21:32:00.000Z><linkdata-rh=truerel=iconhref=/img/favicon.ico><linkdata-rh=truerel=canonicalhref=https://speice.io/2024/11/playing-with-fire-log-density/><linkdata-rh=truerel=alternatehref=https://speice.io/2024/11/playing-with-fire-log-density/hreflang=en><linkdata-rh=truerel=alternatehref=https://speice.io/2024/11/playing-with-fire-log-density/hreflang=x-default><scriptdata-rh=truetype=application/ld+json>{"@context":"https://schema.org","@id":"https://speice.io/2024/11/playing-with-fire-log-density","@type":"BlogPosting","author":{"@type":"Person","name":"Bradlee Speice"},"dateModified":"2024-12-17T02:30:05.000Z","datePublished":"2024-12-16T21:32:00.000Z","description":"So far, our plot() function has been fairly simple: map a fractal flame coordinate to a specific pixel,","headline":"Playing with fire: Tone mapping and color","isPartOf":{"@id":"https://speice.io/","@type":"Blog","name":"Blog"},"keywords":[],"mainEntityOfPage":"https://speice.io/2024/11/playing-with-fire-log-density","name":"Playing with fire: Tone mapping and color","url":"https://speice.io/2024/11/playing-with-fire-log-density"}</script><linkrel=alternatetype=application/rss+xmlhref=/rss.xmltitle="The Old Speice Guy RSS Feed"><linkrel=alternatetype=application/atom+xmlhref=/atom.xmltitle="The Old Speice Guy Atom Feed"><linkrel=stylesheethref=/katex/katex.min.csstype=text/css><linkrel=stylesheethref=/assets/css/styles.24ac2c37.css><scriptsrc=/assets/js/runtime~main.75ada3c5.jsdefer></script><scriptsrc=/assets/js/main.d0bb06d2.jsdefer></script><bodyclass=navigation-with-keyboard><script>!function(){vart,e=function(){try{returnnewURLSearchParams(window.location.search).get("docusaurus-theme")}catch(t){}}()||function(){try{returnwindow.localStorage.getItem("theme")}catch(t){}}();t=null!==e?e:"light",document.documentElement.setAttribute("data-theme",t)}(),function(){try{for(var[t,e]ofnewURLSearchParams(window.location.search).entries())if(t.startsWith("docusaurus-data-")){vara=t.replace("docusaurus-data-","data-");document.documentElement.setAttribute(a,e)}}catch(t){}}()</script><divid=__docusaurus><divrole=regionaria-label="Skip to main content"><aclass=skipToContent_fXgnhref=#__docusaurus_skipToContent_fallback>Skip to main content</a></div><navaria-label=Mainclass="navbar navbar--fixed-top"><divclass=navbar__inner><divclass=navbar__items><buttonaria-label="Toggle navigation bar"aria-expanded=falseclass="navbar__toggle clean-btn"type=button><svgwidth=30height=30viewBox="0 0 30 30"aria-hidden=true><pathstroke=currentColorstroke-linecap=roundstroke-miterlimit=10stroke-width=2d="M4 7h22M4 15h22M4 23h22"/></svg></button><aclass=navbar__brandhref=/><divclass=navbar__logo
and color in that pixel. This works well for simple function systems (like Sierpinski's Gasket),
but more complex systems (like the reference parameters) produce grainy images.</p>
<p>In this post, we'll refine the image quality and add color to really make things shine.</p>
<h2class="anchor anchorWithStickyNavbar_LWe7"id=image-histograms>Image histograms<ahref=#image-histogramsclass=hash-linkaria-label="Direct link to Image histograms"title="Direct link to Image histograms"></a></h2>
<h2class="anchor anchorWithStickyNavbar_LWe7"id=tone-mapping>Tone mapping<ahref=#tone-mappingclass=hash-linkaria-label="Direct link to Tone mapping"title="Direct link to Tone mapping"></a></h2>
<p>While using a histogram reduces the "graining," it also leads to some parts vanishing entirely.
In the reference parameters, the outer circle is still there, but the interior is gone!</p>
<p>To fix this, we'll introduce the second major innovation of the fractal flame algorithm: <ahref=https://en.wikipedia.org/wiki/Tone_mappingtarget=_blankrel="noopener noreferrer">tone mapping</a>.
This is a technique used in computer graphics to compensate for differences in how
computers represent brightness, and how people actually see brightness.</p>
<p>As a concrete example, high-dynamic-range (HDR) photography uses this technique to capture
scenes with a wide range of brightnesses. To take a picture of something dark,
you need a long exposure time. However, long exposures lead to "hot spots" (sections that are pure white).
By taking multiple pictures with different exposure times, we can combine them to create
a final image where everything is visible.</p>
<p>In fractal flames, this "tone map" is accomplished by scaling brightness according to the <em>logarithm</em>
of how many times we encounter a pixel. This way, "cold spots" (pixels the chaos game visits infrequently)
are still visible, and "hot spots" (pixels the chaos game visits frequently) won't wash out.</p>
<detailsclass="details_lb9f alert alert--info details_b_Ee"data-collapsed=true><summary>Log-scale vibrancy also explains fractal flames appear to be 3D...</summary><div><divclass=collapsibleContent_i85q><p>As mentioned in the paper:<blockquote>
<p>Where one branch of the fractal crosses another, one may appear to occlude the other
if their densities are different enough because the lesser density is inconsequential in sum.
For example, branches of densities 1000 and 100 might have brightnesses of 30 and 20.
Where they cross the density is 1100, whose brightness is 30.4, which is
<h2class="anchor anchorWithStickyNavbar_LWe7"id=color>Color<ahref=#colorclass=hash-linkaria-label="Direct link to Color"title="Direct link to Color"></a></h2>
<p>Now we'll introduce the last innovation of the fractal flame algorithm: color.
By including a third coordinate (<spanclass=katex><spanclass=katex-mathml><mathxmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>c</mi></mrow><annotationencoding=application/x-tex>c</annotation></semantics></math></span><spanclass=katex-htmlaria-hidden=true><spanclass=base><spanclass=strutstyle=height:0.4306em></span><spanclass="mord mathnormal">c</span></span></span></span>) in the chaos game, we can illustrate the transforms
<h3class="anchor anchorWithStickyNavbar_LWe7"id=color-coordinate>Color coordinate<ahref=#color-coordinateclass=hash-linkaria-label="Direct link to Color coordinate"title="Direct link to Color coordinate"></a></h3>
<p>Color in a fractal flame is continuous on the range <spanclass=katex><spanclass=katex-mathml><mathxmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mostretchy=false>[</mo><mn>0</mn><moseparator=true>,</mo><mn>1</mn><mostretchy=false>]</mo></mrow><annotationencoding=application/x-tex>[0, 1]</annotation></semantics></math></span><spanclass=katex-htmlaria-hidden=true><spanclass=base><spanclass=strutstyle=height:1em;vertical-align:-0.25em></span><spanclass=mopen>[</span><spanclass=mord>0</span><spanclass=mpunct>,</span><spanclass=mspacestyle=margin-right:0.1667em></span><spanclass=mord>1</span><spanclass=mclose>]</span></span></span></span>. This is important for two reasons:</p>
<p>We'll give each transform a color value (<spanclass=katex><spanclass=katex-mathml><mathxmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mi>c</mi><mi>i</mi></msub></mrow><annotationencoding=application/x-tex>c_i</annotation></semantics></math></span><spanclass=katex-htmlaria-hidden=true><spanclass=base><spanclass=strutstyle=height:0.5806em;vertical-align:-0.15em></span><spanclass=mord><spanclass="mord mathnormal">c</span><spanclass=msupsub><spanclass="vlist-t vlist-t2"><spanclass=vlist-r><spanclass=vliststyle=height:0.3117em><spanstyle=top:-2.55em;margin-left:0em;margin-right:0.05em><spanclass=pstrutstyle=height:2.7em></span><spanclass="sizing reset-size6 size3 mtight"><spanclass="mord mathnormal mtight">i</span></span></span></span><spanclass=vlist-s></span></span><spanclass=vlist-r><spanclass=vliststyle=height:0.15em><span></span></span></span></span></span></span></span></span></span>) in the <spanclass=katex><spanclass=katex-mathml><mathxmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mostretchy=false>[</mo><mn>0</mn><moseparator=true>,</mo><mn>1</mn><mostretchy=false>]</mo></mrow><annotationencoding=application/x-tex>[0, 1]</annotation></semantics></math></span><spanclass=katex-htmlaria-hidden=true><spanclass=base><spanclass=strutstyle=height:1em;vertical-align:-0.25em></span><spanclass=mopen>[</span><spanclass=mord>0</span><spanclass=mpunct>,</span><spanclass=mspacestyle=margin-right:0.1667em></span><spanclass=mord>1</span><spanclass=mclose>]</span></span></span></span> range.
The final transform gets a value too (<spanclass=katex><spanclass=katex-mathml><mathxmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mi>c</mi><mi>f</mi></msub></mrow><annotationencoding=application/x-tex>c_f</annotation></semantics></math></span><spanclass=katex-htmlaria-hidden=true><spanclass=base><spanclass=strutstyle=height:0.7167em;vertical-align:-0.2861em></span><spanclass=mord><spanclass="mord mathnormal">c</span><spanclass=msupsub><spanclass="vlist-t vlist-t2"><spanclass=vlist-r><spanclass=vliststyle=height:0.3361em><spanstyle=top:-2.55em;margin-left:0em;margin-right:0.05em><spanclass=pstrutstyle=height:2.7em></span><spanclass="sizing reset-size6 size3 mtight"><spanclass="mord mathnormal mtight"style=margin-right:0.10764em>f</span></span></span></span><spanclass=vlist-s></span></span><spanclass=vlist-r><spanclass=vliststyle=height:0.2861em><span></span></span></span></span></span></span></span></span></span>).
<h3class="anchor anchorWithStickyNavbar_LWe7"id=color-speed>Color speed<ahref=#color-speedclass=hash-linkaria-label="Direct link to Color speed"title="Direct link to Color speed"></a></h3>
<divclass="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><divclass=admonitionHeading_Gvgb><spanclass=admonitionIcon_Rf37><svgviewBox="0 0 16 16"><pathfill-rule=evenoddd="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"/></svg></span>warning</div><divclass=admonitionContent_BuS1><p>Color speed isn't introduced in the Fractal Flame Algorithm paper.<p>It is included here because <ahref=https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c#L2140target=_blankrel="noopener noreferrer"><code>flam3</code> implements it</a>,
and because it's fun to play with.</div></div>
<p>Next, we'll add a parameter to each transform that controls how much it changes the current color.
This is known as the "color speed" (<spanclass=katex><spanclass=katex-mathml><mathxmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mi>s</mi><mi>i</mi></msub></mrow><annotationencoding=application/x-tex>s_i</annotation></semantics></math></span><spanclass=katex-htmlaria-hidden=true><spanclass=base><spanclass=strutstyle=height:0.5806em;vertical-align:-0.15em></span><spanclass=mord><spanclass="mord mathnormal">s</span><spanclass=msupsub><spanclass="vlist-t vlist-t2"><spanclass=vlist-r><spanclass=vliststyle=height:0.3117em><spanstyle=top:-2.55em;margin-left:0em;margin-right:0.05em><spanclass=pstrutstyle=height:2.7em></span><spanclass="sizing reset-size6 size3 mtight"><spanclass="mord mathnormal mtight">i</span></span></span></span><spanclass=vlist-s></span></span><spanclass=vlist-r><spanclass=vliststyle=height:0.15em><span></span></span></span></span></span></span></span></span></span>):</p>
<p>Color speed values work just like transform weights. A value of 1
means we take the transform color and ignore the previous color state.
A value of 0 means we keep the current color state and ignore the
transform color.</p>
<h3class="anchor anchorWithStickyNavbar_LWe7"id=palette>Palette<ahref=#paletteclass=hash-linkaria-label="Direct link to Palette"title="Direct link to Palette"></a></h3>
<p>Now, we need to map the color coordinate to a pixel color. Fractal flames typically use
256 colors (each color has 3 values - red, green, blue) to define a palette.
The color coordinate then becomes an index into the palette.</p>
<p>There's one small complication: the color coordinate is continuous, but the palette
uses discrete colors. How do we handle situations where the color coordinate is
"in between" the colors of our palette?</p>
<p>One way to handle this is a step function. In the code below, we multiply the color coordinate
by the number of colors in the palette, then truncate that value. This gives us a discrete index:</p>
<detailsclass="details_lb9f alert alert--info details_b_Ee"data-collapsed=true><summary>As an alternative...</summary><div><divclass=collapsibleContent_i85q><p>...you could interpolate between colors in the palette.
For example, <code>flam3</code> uses <ahref=https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L483-L486target=_blankrel="noopener noreferrer">linear interpolation</a></div></div></details>
<p>In the diagram below, each color in the palette is plotted on a small vertical strip.
Putting the strips side by side shows the full palette used by the reference parameters:</p>
<h3class="anchor anchorWithStickyNavbar_LWe7"id=plotting>Plotting<ahref=#plottingclass=hash-linkaria-label="Direct link to Plotting"title="Direct link to Plotting"></a></h3>
<p>We're now ready to plot our <spanclass=katex><spanclass=katex-mathml><mathxmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mostretchy=false>(</mo><msub><mi>x</mi><mi>f</mi></msub><moseparator=true>,</mo><msub><mi>y</mi><mi>f</mi></msub><moseparator=true>,</mo><msub><mi>c</mi><mi>f</mi></msub><mostretchy=false>)</mo></mrow><annotationencoding=application/x-tex>(x_f,y_f,c_f)</annotation></semantics></math></span><spanclass=katex-htmlaria-hidden=true><spanclass=base><spanclass=strutstyle=height:1.0361em;vertical-align:-0.2861em></span><spanclass=mopen>(</span><spanclass=mord><spanclass="mord mathnormal">x</span><spanclass=msupsub><spanclass="vlist-t vlist-t2"><spanclass=vlist-r><spanclass=vliststyle=height:0.3361em><spanstyle=top:-2.55em;margin-left:0em;margin-right:0.05em><spanclass=pstrutstyle=height:2.7em></span><spanclass="sizing reset-size6 size3 mtight"><spanclass="mord mathnormal mtight"style=margin-right:0.10764em>f</span></span></span></span><spanclass=vlist-s></span></span><spanclass=vlist-r><spanclass=vliststyle=height:0.2861em><span></span></span></span></span></span></span><spanclass=mpunct>,</span><spanclass=mspacestyle=margin-right:0.1667em></span><spanclass=mord><spanclass="mord mathnormal"style=margin-right:0.03588em>y</span><spanclass=msupsub><spanclass="vlist-t vlist-t2"><spanclass=vlist-r><spanclass=vliststyle=height:0.3361em><spanstyle=top:-2.55em;margin-left:-0.0359em;margin-right:0.05em><spanclass=pstrutstyle=height:2.7em></span><spanclass="sizing reset-size6 size3 mtight"><spanclass="mord mathnormal mtight"style=margin-right:0.10764em>f</span></span></span></span><spanclass=vlist-s></span></span><spanclass=vlist-r><spanclass=vliststyle=height:0.2861em><span></span></span></span></span></span></span><spanclass=mpunct>,</span><spanclass=mspacestyle=margin-right:0.1667em></span><spanclass=mord><spanclass="mord mathnormal">c</span><spanclass=msupsub><spanclass="vlist-t vlist-t2"><spanclass=vlist-r><spanclass=vliststyle=height:0.3361em><spanstyle=top:-2.55em;margin-left:0em;margin-right:0.05em><spanclass=pstrutstyle=height:2.7em></span><spanclass="sizing reset-size6 size3 mtight"><spanclass="mord mathnormal mtight"style=margin-right:0.10764em>f</span></span></span></span><spanclass=vlist-s></span></span><spanclass=vlist-r><spanclass=vliststyle=height:0.2861em><span></span></span></span></span></span></span><spanclass=mclose>)</span></span></span></span> coordinates. This time, we'll use a histogram
for each color channel (red, green, blue, alpha). After translating from color coordinate (<spanclass=katex><spanclass=katex-mathml><mathxmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mi>c</mi><mi>f</mi></msub></mrow><annotationencoding=application/x-tex>c_f</annotation></semantics></math></span><spanclass=katex-htmlaria-hidden=true><spanclass=base><spanclass=strutstyle=height:0.7167em;vertical-align:-0.2861em></span><spanclass=mord><spanclass="mord mathnormal">c</span><spanclass=msupsub><spanclass="vlist-t vlist-t2"><spanclass=vlist-r><spanclass=vliststyle=height:0.3361em><spanstyle=top:-2.55em;margin-left:0em;margin-right:0.05em><spanclass=pstrutstyle=height:2.7em></span><spanclass="sizing reset-size6 size3 mtight"><spanclass="mord mathnormal mtight"style=margin-right:0.10764em>f</span></span></span></span><spanclass=vlist-s></span></span><spanclass=vlist-r><spanclass=vliststyle=height:0.2861em><span></span></span></span></span></span></span></span></span></span>)
<h2class="anchor anchorWithStickyNavbar_LWe7"id=summary>Summary<ahref=#summaryclass=hash-linkaria-label="Direct link to Summary"title="Direct link to Summary"></a></h2>
<p>Tone mapping is the second major innovation of the fractal flame algorithm.
By tracking how often the chaos game encounters each pixel, we can adjust
brightness/transparency to reduce the visual "graining" of previous images.</p>
<p>Next, introducing a third coordinate to the chaos game makes color images possible,
the third major innovation of the fractal flame algorithm. Using a continuous
color scale and color palette adds a splash of excitement to the image.</p>
<p>The Fractal Flame Algorithm paper goes on to describe more techniques
not covered here. For example, image quality can be improved with density estimation
and filtering. New parameters can be generated by "mutating" existing
fractal flames. And fractal flames can even be animated to produce videos!</p>
<p>That said, I think this is a good place to wrap up. We went from
an introduction to the mathematics of fractal systems all the way to
generating full-color images. Fractal flames are a challenging topic,