On this page:
4.1.1 Motivation
4.1.2 Conceptual Model
4.1.3 Opacity (Alpha Components)
4.1.4 Data Layout

4.1 Overview

Contents:

4.1.1 Motivation

There are three main reasons to use flomaps:
  • Precision. A point in a typical bitmap is represented by a few bytes, each of which can have one of 256 distinct values. In contrast, a point in a flomap is represented by double-precision floating-point numbers, typically between 0.0 and 1.0 inclusive. This range contains about 4.6 quintillion (or 4.6×1018) distinct values. While bytes are fine for many applications, their low precision becomes a problem when images are repeatedly operated on, or when their values are built by adding many small amounts—which are often rounded to zero.

  • Range. A floating-point value can also represent about 4.6 quintillion distinct intensities above saturation (1.0). If distinguishing oversaturated values is important, flomaps have the range for it. Further, floating-point images are closed under pointwise arithmetic (up to floating-point error).

  • Speed. The images/flomap module benefits greatly from Typed Racket’s type-directed optimizations. Even getting individual color values—interpolated between points, if desired—is fast.

For these reasons, other parts of the images library use flomaps internally, to represent and operate on RGB and ARGB images, light maps, shadow maps, height maps, and normal maps.

4.1.2 Conceptual Model

A flomap is conceptually infinite in its width and height, but has nonzero values in a finite rectangle starting at coordinate 0 0 and extending to its width and height (exclusive). A flomap is not conceptually infinite in its components because there is no natural linear order on component coordinates, as the meaning of components depends on programmer intent.

The following example creates a 10×10 bitmap with RGB components, and indexes its top-left red value and two values outside the finite, nonzero rectangle. It also attempts to index component 3, which doesn’t exist. Note that flomap-ref accepts its coordinate arguments in a standard order: k x y (with k for komponent).
> (define magenta-fm (make-flomap* 10 10 #(0.5 0.0 1.0)))
> (flomap->bitmap magenta-fm)

image

> (flomap-ref* magenta-fm 0 0)

(flvector 0.5 0.0 1.0)

> (flomap-ref* magenta-fm -1 0)

(flvector 0.0 0.0 0.0)

> (flomap-ref* magenta-fm 0 1000)

(flvector 0.0 0.0 0.0)

> (flomap-ref magenta-fm 3 0 0)

flomap-ref: expected argument of type <nonnegative fixnum <

3>; given: 3

Many flomap functions, such as flomap-bilinear-ref and flomap-rotate, treat their arguments as if every real x y coordinate has values. In all such cases, known values are at half-integer coordinates and others are interpolated.

Examples:

> (flomap-bilinear-ref* magenta-fm 0.5 0.5)

(flvector 0.5 0.0 1.0)

> (flomap-bilinear-ref* magenta-fm 0.25 0.25)

(flvector 0.28125 0.0 0.5625)

> (flomap-bilinear-ref* magenta-fm 0.0 0.0)

(flvector 0.125 0.0 0.25)

> (flomap-bilinear-ref* magenta-fm -0.25 -0.25)

(flvector 0.03125 0.0 0.0625)

This conceptual model allows us to treat flomaps as if they were multi-valued functions on Real×Real. For example, we might plot the red component of an ARGB icon:

> (require images/icons/misc plot)
> (define icon-fm (bomb-flomap #:bomb-color "orange" #:height 48))
> (flomap->bitmap icon-fm)

image

> (define-values (icon-width icon-height) (flomap-size icon-fm))
> (plot3d (contour-intervals3d
           (λ (x y) (flomap-bilinear-ref icon-fm 1 x y))
           -0.5 (+ 0.5 icon-width) -0.5 (+ 0.5 icon-height)))

image

Notice that the plot’s maximum height is above saturation (1.0). The tallest peak corresponds to the specular highlight (the shiny part) on the bomb. Specular highlights are one case where it is important to operate on oversaturated values without truncating them—until it is time to display the image.

If we have a w×h flomap and consider its known values as being at half-integer coordinates, the exact center of the flomap is at (* 1/2 w) (* 1/2 h). When unknown values are estimated using bilinear interpolation, the finite rectangle containing all the known and estimated nonzero values is from -1/2 -1/2 to (+ w 1/2) (+ h 1/2).

4.1.3 Opacity (Alpha Components)

A partially transparent flomap is simply a flomap in which component 0 is assumed to be an alpha (opacity) component. The other components should be multiplied by their corresponding alpha value; i.e. an RGB triple 1.0 0.5 0.25 with opacity 0.5 should be represented by 0.5 0.5 0.25 0.125.

This representation generally goes by the unfortunate misnomer “premultiplied alpha,” which makes it seem as if the alpha component is multiplied by something. We will refer to this representation as alpha-multiplied because the color components are multiplied by the alpha component. All alpha-aware functions consume alpha-multiplied flomaps and produce alpha-multiplied flomaps.

There are many good reasons to use alpha-multiplied flomaps instead of non-alpha-multiplied flomaps. Some are:
  • Compositing requires fewer operations per point.

  • Compositing is associative; i.e. (flomap-lt-superimpose fm1 (flomap-lt-superimpose fm2 fm3)) is the same as (flomap-lt-superimpose (flomap-lt-superimpose fm1 fm2) fm3), up to floating-point error.

  • There is only one transparent point: all zeros. We could not conceptualize partially transparent flomaps as being infinite in size without a unique transparent point.

  • Many functions can operate on flomaps without treating the alpha component specially and still be correct.

As an example of the last point, consider blur. The following example creates an alpha-multiplied flomap using draw-flomap. It blurs the flomap using a general-purpose (i.e. non-alpha-aware) blur function, then converts the flomap to non-alpha-multiplied and does the same.
> (define circle-fm (draw-flomap (λ (fm-dc)
                                   (send fm-dc set-pen "black" 1 'transparent)
                                   (send fm-dc set-brush "green" 'solid)
                                   (send fm-dc draw-ellipse 10 10 30 30))
                                 50 50))
> (flomap->bitmap circle-fm)

image

> (flomap->bitmap (flomap-blur circle-fm 4 4))

image

> (let* ([fm  (flomap-divide-alpha circle-fm)]
         [fm  (flomap-blur fm 4 4)]
         [fm  (flomap-multiply-alpha fm)])
    (flomap->bitmap fm))

image

Notice the dark band around the second blurred circle.

Of course, this could be fixed by making flomap-blur operate differently on flomaps with an alpha component. But the implementation would amount to converting them to alpha-multiplied flomaps anyway.

The only valid reason to not multiply color components by alpha is loss of precision, which is not an issue with flomaps.

4.1.4 Data Layout

For most applications, there should be enough flomap functions available that you should not need to access their fields directly. However, there will always be use cases for direct manipulation, so the fields are public.

The color values in a flomap are stored flattened in a single FlVector, in row-major order with adjacent color components. For example, a 2×2 RGB flomap can be visualized as

image

In a flomap, it would be stored as

image

Mathematically, for a c-component, w-width flomap, the kth color component at position x y is at index

(+ k (* c (+ x (* y w))))

The coords->index function carries out this calculation quickly using only fixnum arithmetic.

If i is a calculated index for the value at k x y, then the (+ k 1)th value is at index (+ i 1), the (+ x 1)th value is at index (+ i c), and the (+ y 1)th value is at index (+ i (* c w)).