💬Comments welcome. To leave a note, select any text and click the note / highlight button that pops up — or open the panel with the tab at the top-right (‹). Notes are visible only inside our private review group.
jump to

22.6 Problem Set 4 — High dynamic range

22.6.1 Summary

Important. To merge images to HDR, we will work with images encoded linearly. Most of the digital images you may encounter are encoded with a gamma of $2.2$ (in particular those of the handout). Therefore, in this problem set, you will have to undo the gamma $2.2$ from your inputs before merging them to HDR. You will also have to apply a gamma $2.2$ to the images you output in the linear domain (e.g. after tone-mapping). You can use the gamma_code function from problem set 1 that can be found in basicImageManipulation.cpp to do this. We assume all the inputs and outputs of the functions in this problem set are in the linear space (i.e. you will need to undo the gamma before you call the function and you will need to apply gamma after you tone map).

In the whole assignment, we will only work with images of static subjects and the camera hasn't moved between shots in the same sequence.

22.6.2 HDR merging

Consult the course slides for the overall HDR merging approach. We will first compute weights for each pixel and each channel to eliminate values that are too high or too low. We will then compute the scale factor between the values of two images, to determine their relative exposures.

Finally, we will merge a sequence of images using a weighted combination of the individual images leveraging the weights and scale factors. The slide "Assembling HDR" in lecture 6 might be helpful here.

1.

The weights computed from the `ante2` image sequence using the default parameters provided in `computeWeight`. You can see this output after running `testComputeFactor`. (a) w1
The weights computed from the ante2 image sequence using the default parameters provided in computeWeight. You can see this output after running testComputeFactor. (a) w1
The weights computed from the `ante2` image sequence using the default parameters provided in `computeWeight`. You can see this output after running `testComputeFactor`. (b) w2
The weights computed from the ante2 image sequence using the default parameters provided in computeWeight. You can see this output after running testComputeFactor. (b) w2

With these two methods, we know which pixels are usable in each image and what the relative exposures between the images is. With this information we are ready to merge a sequence of images to a single HDR image.

For the rest of the problem, you can assume that the first image in the sequence is the darkest, and that images are given in order of increasing exposure.

For each image in the given sequence, you need to compute its multiplicative scale factor ($k_i$ in the equation on the "Assembling HDR" slide from lecture 6). This requires computing the scale factor between adjacent images in the sequence using computeFactor, then chaining them together. For example, if you have the factor between image 3 and image 2 and image 2 and image 1, you can compute the factor between image 3 and image 1 by chaining the factors together:

$$\frac{k_3}{k_1} = \frac{k_3}{k_2}\frac{k_2}{k_1}$$

You should should pick one of the images as the 'reference' image (e.g. the darkest image), and compute the factors with respect to it. This means that each image's $k_i$ value will be relative to your 'reference' image i.e. you will compute it only up to a global scale factor.

Do not forget to handle the special case of pixels underexposed (or overexposed) in all images, see slide "Special Cases" in lecture 6. That is, when computing the weight of the darkest and brightest images, you should only threshold in one direction for these cases. If a pixel is not assigned to any of the weight images, then assign it the corresponding value from the first image in the sequence (by doing this you should avoid any divide by 0 errors as well). As you merge pixels do not forget to scale them with the factors computed above.

To test your method, you can write out the output image scaled by different scaling values. testMakeHDR in a4_main.cpp illustrates the process for the design image sequence.

The HDR image created from the `design` image sequence clipped to different ranges. You can see this output after running `testMakeHDR`. (a) 1
The HDR image created from the design image sequence clipped to different ranges. You can see this output after running testMakeHDR. (a) 1
The HDR image created from the `design` image sequence clipped to different ranges. (b) 2e2
The HDR image created from the design image sequence clipped to different ranges. (b) 2e2
The HDR image created from the `design` image sequence clipped to different ranges. (c) 2e4
The HDR image created from the design image sequence clipped to different ranges. (c) 2e4
The HDR image created from the `design` image sequence clipped to different ranges. (d) 2e6
The HDR image created from the design image sequence clipped to different ranges. (d) 2e6
The HDR image created from the `design` image sequence clipped to different ranges. (e) 2e8
The HDR image created from the design image sequence clipped to different ranges. (e) 2e8
The HDR image created from the `design` image sequence clipped to different ranges. (f) 2e10
The HDR image created from the design image sequence clipped to different ranges. (f) 2e10

22.6.3 Tone mapping

We have assembled our first HDR image, but this image still cannot be displayed properly on our low dynamic range screen. Let's implement tone mapping to remap the HDR information to a displayable range. Make sure you understand the slides "Contrast reduction in log domain" in order to combine the base luminance, detail and brightness scaling factor.

Your tone mapper will follow the method studied in class. The function is called toneMap. It takes as input an HDR image, a target contrast for the base (lowpass) layer, an amplification factor for the detail, and a Boolean to switch the lowpass/highpass separation between the bilateral filter or Gaussian blur.

As described in the lecture, our goal is to reduce the contrast from the HDR image (say, 1:10000) to what the display can show (say 1:100). Although gamma correction might seem like the first thing to consider, this results in washed out images, as shown on the gamma compression slide in lecture 7. The colors are actually okay (they're all there) but the high frequencies are washed out. We therefore want to work on the luminance, and increase the high frequencies. We also want to work in the log-domain since the human eye is sensitive to multiplicative contrast (recall lecture 1).

We want to modify only on the log-luminance:

1.

We recommend you do this by first implementing the helper function float image_minnonzero(const Image &im), followed by Image log10Image(const Image &im), for which we have written function signatures and comments for you. Then, call log10Image with the luminance image as the argument.

Next, we want to extract the detail of the (log) luminance channel. We do this by blurring the log luminance, and subtracting this from the original log luminance. If you recall problem set 2, this gives you the details (high frequencies).

You are welcome to use your own implementation of filtering methods, but you can also use our versions in filtering.cpp.

At this point we have a detail image and a base image. Our goal is to reduce the contrast on the base image while also preserving (or even amplifying) the details. The slides "Contrast Reduction in log domain" might be helpful here.

Make sure to add an offset that ensures that the largest base value will be mapped to 1 once the image is put back into the linear domain

We have provided two new functions for you that you might find useful: float Image::min() const and float Image::max() const to get the minimum (respectively maximum) value of an image.

Enjoy your results and compare the bilateral version with the Gaussian one. Use the functions testToneMapping_ante2, testToneMapping_ante3, testToneMapping_design, testToneMapping_boston in a4_main.cpp to help test your tone mapping function. Feel free to try them on your own images!

Note: The bilateral filter on the design image sequence takes a very long time. It is not necessary to test bilateral filter tone mapping for this image sequence. If you do test it, you likely won't get exactly the same image as given in the slides because it was generated with different parameters.

Tone mapping of the `ante2` image sequence using the parameters provided in `testToneMapping_ante2`. (a) Gaussian Tone Mapping
Tone mapping of the ante2 image sequence using the parameters provided in testToneMapping_ante2. (a) Gaussian Tone Mapping
Tone mapping of the `ante2` image sequence using the parameters provided in `testToneMapping_ante2`. (b) Bilateral Tone Mapping
Tone mapping of the ante2 image sequence using the parameters provided in testToneMapping_ante2. (b) Bilateral Tone Mapping
Tone mapping of the `boston` image sequence using the parameters provided in `testToneMapping_boston`. (a) Gaussian Tone Mapping
Tone mapping of the boston image sequence using the parameters provided in testToneMapping_boston. (a) Gaussian Tone Mapping
Tone mapping of the `boston` image sequence using the parameters provided in `testToneMapping_boston`. (b) Bilateral Tone Mapping
Tone mapping of the boston image sequence using the parameters provided in testToneMapping_boston. (b) Bilateral Tone Mapping

22.6.4 Extra credit (10% max)