---
title: "Tone Mapping"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Tone Mapping}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.width = 4
)
```
```{r setup}
library(picohdr)
```
# HDR images
High Dynamic Range (HDR) images contain information about the luminosity in
a scene that can be well outside the range of standard file formats like PNG
or JPEG.
In non-HDR images (Low Dynamic Range (LDR) images), the value for any particular
pixel usually lie in the range [0, 1] or [0, 255]. In HDR images, there is
no real limit to the upper value.
Other considerations for HDR image
* There may be more than 4 channels of data. EXR images are often used to stored
extra information such as rendering depth, UV texture coordinates, coordinates etc
* It will often be necessary to extract the channels of interest for plotting.
In the EXR image format, channels are stored *alphabetically*, which means
that careful selection will be needed to extract RGBA channels in order.
Tone mapping is the process of manipulating the HDR values into a lower dynamic
range - usually with values in the range [0, 1].
There is no "correct" tone mapping operation - just different techniques depending
on your requirements.
## Example image
When an EXR image is loaded it is just a numeric array of data.
```{r}
file <- system.file("image/rstats.exr", package = "picohdr")
im <- picohdr::read_exr(file)
dim(im)
```
The names on the array indicate the channel names. EXR stored all channels
in alphabetical order.
Here there are RGBA channels
but also extra information from the 3D renderer which created this image
e.g. the `u` and `v` texture coordinates at each output pixel.
```{r}
dimnames(im)[[3]]
```
A peek at the first plane in the array shows that it is just numeric data
```{r}
im[1:5, 1:5, 1]
```
We can create a standard RGB array from this image data
```{r}
rgb_arr <- im[, , c('R', 'G', 'B')]
```
If this was a normal image loaded from PNG or JPEG, we could do the following
to view it in R - but this code will cause an error with an HDR image as
there is no guarantee that the pixel values lie between 0 and 1 (which is
what `as.raster()` requires)
```{r error=TRUE}
plot(as.raster(rgb_arr))
```
```{r}
library(ggplot2)
df <- array_to_df(rgb_arr)
ggplot(df) +
geom_density(aes(value, group = channel, colour = channel)) +
theme_bw() +
theme(legend.position = 'bottom') +
coord_cartesian(xlim = c(0, 1.5), ylim = c(0, 5)) +
scale_color_manual(values = c('blue', 'green', 'red')) +
labs(
title = "Raw HDR pixel values",
subtitle = "Some values greater than 1.0 in this image"
)
```
**Tone mapping** is then the process by which these pixel values can be
shifted, truncated, adapted to squeeze the pixel values into the range [0, 1]
so that we can view it properly in our *low dynamic range* R session.
## Tone-map by clamping
A simple technique for tone-mapping is to just clamp the values at [0, 1] with
values outside this range being pulled back to these limits.
If we do this, then the image is now viewable as a raster, but some parts of
it look blown out and overexposed.
```{r}
rgb_clamped <- rgb_arr |>
adj_clamp(lo = 0, hi = 1) |>
adj_gamma()
oldpar <- par(mai = c(0, 0, 0, 0))
plot(as.raster(rgb_clamped))
par(oldpar)
```
```{r echo=FALSE}
df <- array_to_df(rgb_clamped)
ggplot(df) +
geom_density(aes(value, group = channel, colour = channel)) +
theme_bw() +
theme(legend.position = 'bottom') +
coord_cartesian(xlim = c(0, 1.5), ylim = c(0, 5)) +
scale_color_manual(values = c('blue', 'green', 'red')) +
labs(
title = "Pixel values clamped to [0, 1]"
)
```
## Tone-map by linear rescaling
```{r}
rgb_rescale <- rgb_arr |>
adj_rescale(lo = 0, hi = 1) |>
adj_gamma()
oldpar <- par(mai = c(0, 0, 0, 0))
plot(as.raster(rgb_rescale))
par(oldpar)
```
```{r echo=FALSE}
df <- array_to_df(rgb_rescale)
ggplot(df) +
geom_density(aes(value, group = channel, colour = channel)) +
theme_bw() +
theme(legend.position = 'bottom') +
coord_cartesian(xlim = c(0, 1.5), ylim = c(0, 5)) +
scale_color_manual(values = c('blue', 'green', 'red')) +
labs(
title = "Pixel values rescaled to [0, 1]"
)
```
## Tone-map with Reinhard's technique
At its core, Reinhard's technique is a non-linear rescaling of the values
back into the range [0, 1].
```{r}
rgb_rh1<- rgb_arr |>
tm_reinhard() |>
adj_gamma()
oldpar <- par(mai = c(0, 0, 0, 0))
plot(as.raster(rgb_rh1))
par(oldpar)
```
```{r echo=FALSE}
df <- array_to_df(rgb_rh1)
ggplot(df) +
geom_density(aes(value, group = channel, colour = channel)) +
theme_bw() +
theme(legend.position = 'bottom') +
coord_cartesian(xlim = c(0, 1.5), ylim = c(0, 5)) +
scale_color_manual(values = c('blue', 'green', 'red')) +
labs(
title = "Tone mapping: Reinhard"
)
```
## Tone-map with Reinhard's technique (extended)
This basic version of Reinhard's technique does uses a simpler version of the
algorithm. Results may be a bit darker and/or washed out.
```{r}
rgb_rh2 <- rgb_arr |>
tm_reinhard_basic() |>
adj_gamma()
oldpar <- par(mai = c(0, 0, 0, 0))
plot(as.raster(rgb_rh2))
par(oldpar)
```
```{r echo=FALSE}
df <- array_to_df(rgb_rh2)
ggplot(df) +
geom_density(aes(value, group = channel, colour = channel)) +
theme_bw() +
theme(legend.position = 'bottom') +
coord_cartesian(xlim = c(0, 1.5), ylim = c(0, 5)) +
scale_color_manual(values = c('blue', 'green', 'red')) +
labs(
title = "Tone mapping: Reinhard (extended)"
)
```
## Tone-map with Reinhard's technique (variant)
This variant on Reinhard's technique is a hybrid between
the standard and basic techniques.
```{r}
rgb_rh3 <- rgb_arr |>
tm_reinhard_variant() |>
adj_gamma()
oldpar <- par(mai = c(0, 0, 0, 0))
plot(as.raster(rgb_rh3))
par(oldpar)
```
```{r echo=FALSE}
df <- array_to_df(rgb_rh3)
ggplot(df) +
geom_density(aes(value, group = channel, colour = channel)) +
theme_bw() +
theme(legend.position = 'bottom') +
coord_cartesian(xlim = c(0, 2), ylim = c(0, 5)) +
scale_color_manual(values = c('blue', 'green', 'red')) +
labs(
title = "Tone mapping: Reinhard (variant)"
)
```