Getting started

Let's assume you took the following image img, reshaped it to WHCN format (width, height, color channels, batch dimension) and ran it through a vision model:

using Images
img = load(joinpath(asset_dir, "img1.png")) # load image file
Example block output

You might use an input space attribution method (for example from ExplainableAI.jl) to determine which parts of the input contributed most to the "saxophone" class.

Let's load such an attribution x in WHCN format:

x = load(data_heatmap, "x") # load precomputed array from file
typeof(x)
Array{Float32, 4}
size(x)
(224, 224, 3, 1)

To make this attribution more interpretable, we can visualize it as a heatmap:

using VisionHeatmaps
heatmap(x)
(a vector displayed as a row to save space)

By default, to support batched explanations, a vector of heatmaps is returned. Since our following examples don't use batches, we will use the only function to unpack singleton heatmaps:

using VisionHeatmaps
heatmap(x) |> only
Example block output

Custom heatmapping pipelines

VisionHeatmaps internally applies a sequence of image transformations in what we call a Pipeline. The default pipeline corresponds to:

pipe = NormReduction() |> ExtremaColormap() |> FlipImage()
Pipeline(
  NormReduction(),
  ExtremaColormap(:batlow),
  FlipImage(),
)

We can apply this pipeline by passing it to heatmap:

heatmap(x, pipe) |> only
Example block output

In the following subsection, we will explain and modify this pipeline step by step.

Color channel reduction

For arrays with multiple color channels, the channels need to be reduced to a single scalar value for each pixel, which is later mapped onto a color scheme.

Several transformats are available for this purpose. Let's compare the two most commonly used ones. NormReduction reduces color channels in the array by taking their norm, whereas SumReduction takes the sum:

pipe = NormReduction() |> ExtremaColormap() |> FlipImage()
heatmap(x, pipe) |> only
Example block output
pipe = SumReduction() |> ExtremaColormap() |> FlipImage()
heatmap(x, pipe) |> only
Example block output

Colormaps

To map the now color-channel-reduced array onto a color scheme, we first need to normalize all values to the range $[0, 1]$.

For this purpose, two colormapping transforms are available:

  • ExtremaColormap: normalizes colorscheme to the minimum and maximum value in the array.
  • CenteredColormap: normalizes colorscheme to the maximum absolute value of the array. Values of zero will be mapped to the center of the color scheme.

Since NormReduction only yields positive values, it is well suited for ExtremaColormap. SumReduction on the other hand can yield positive and negative values. If zero-values are meaningful, using a divergent color scheme with CenteredColormap can be the right choice:

pipe = NormReduction() |> ExtremaColormap() |> FlipImage()
heatmap(x, pipe) |> only
Example block output
pipe = SumReduction() |> CenteredColormap() |> FlipImage()
heatmap(x, pipe) |> only
Example block output

Outlier removal

While this isn't part of the default heatmapping pipelines, previous heatmaps visibly emphasized three "dots" on the neck of the saxophone. Very high values in explanations tend to desaturate colors. For this purpose, we provide the adaptive PercentileClip. By default, it clips the 0.1-th and 99.9-th percentiles of values.

pipe = SumReduction() |> PercentileClip() |> CenteredColormap() |> FlipImage()
heatmap(x, pipe) |> only
Example block output

Custom color schemes

We can use a custom color scheme from ColorSchemes.jl in our colormap:

using ColorSchemes
pipe = NormReduction() |> ExtremaColormap(:jet) |> FlipImage()
heatmap(x, pipe) |> only
Example block output
pipe = NormReduction() |> ExtremaColormap(:viridis) |> FlipImage()
heatmap(x, pipe) |> only
Example block output

We strongly suggest to only use sequential color schemes with ExtremaColormap and divergent color schemes with CenteredColormap.

ColorSchemes.jl catalogue

Refer to the ColorSchemes.jl catalogue for a gallery of available color schemes.

Overlays

Singleton heatmaps can be overlaid on top of the original image. This can be used to recreate CAM-like heatmaps (usually in combination with ResizeToImage):

pipe = NormReduction() |> PercentileClip() |> ExtremaColormap(:jet) |> FlipImage() |> AlphaOverlay()
heatmap(x, img, pipe) |> only
Example block output

Heatmapping batches

heatmap can also be used to visualize input batches. Let's assume we computed an input space attribution batch for the following images.

imgs = [load(joinpath(asset_dir, f)) for f in ("img1.png", "img2.png", "img3.png", "img4.png", "img5.png")] # load image files
(a vector displayed as a row to save space)

Once again, we assume that batch is in WHCN format:

batch = load(data_heatmaps, "x") # load precomputed array from file
typeof(batch)
Array{Float32, 4}
size(batch)
(224, 224, 3, 5)

Calling heatmap will automatically return an vector of images:

heatmap(batch)
(a vector displayed as a row to save space)

These heatmaps can be customized as usual:

pipe = SumReduction() |> CenteredColormap() |> FlipImage()
heatmap(batch, pipe)
(a vector displayed as a row to save space)