Python – Adjusts the exposure of RAW images based on EV values

Adjusts the exposure of RAW images based on EV values… here is a solution to the problem.

Adjusts the exposure of RAW images based on EV values

I’m trying to write a program that takes a matrix full of 12-bit RAW sensor values (ranging from [512-4096]) (512 is the Bayer sensor black level –> i.e. what is pure black defined as) and adjusts the EV of each pixel, just like the Adobe Camera Raw (ACR) “exposure” slider. I’m trying to figure out how it’s basically done. I’ve consulted dozens of blogs explaining how EV calculations are calculated, and it seems:

This link seems to give the following formula: PixelAdjusted = Pixel * 2^EV

This seems very wrong, as the 5 EV adjustment would make the picture disproportionate. And I can’t find any other resources online. Wikipedia has a good entry on Exposure Value, but it doesn’t seem to have what I’m looking for either… Does it help in any way?

Thanks!

Here’s an example of what I mean:

RAW file with EV 0 in ACR:
Before

With EV 5:
After

I currently have this formula:

black_level = 512
bit_depth = 2**12
normalized = max((raw_pixel - black_level),0) / (bit_depth) ## normalize to [0,1]
exposed = normalized * (2**EV)    ## expose by desired EV value

## scale back to normal level:
raw_pixel_new = exposed * bit_depth  

But this fails for any EV value other than 5, so the formula is incorrect. I also know that this formula is wrong because it doesn’t work when EV = 0. I’ve found that many websites explain the formula just new_pixel = pixel * 2^exposure but that doesn’t seem to work for Raw photos… Am I missing something?

Any ideas?

Here is some python code and some files I used to test:

Code:

import rawpy
import numpy as np

from PIL import Image

bit_depth = 12
black_level = 512
exposure = 4

path = "/001_ev0. DNG"
raw = rawpy.imread(path)

im = raw.raw_image_visible
im = np.maximum(im - black_level, 0)
im *= 2**exposure
# im = im + black_level # for some reason commenting this out makes it slightly better
im = np.minimum(im,2**12 - 1)
raw.raw_image[:,:] = im
im = raw.postprocess(use_camera_wb=True,no_auto_bright=True)
img = Image.fromarray(im, 'RGB')
img.show() #This should look like the file: 001_ev4.tif

Image:

https://drive.google.com/open?id=1T0ru_Vid8ctM3fDdbx1hvxNojOXOzXxg

For some reason I’ve spent 14 hours on this… I don’t know what I’m doing wrong because I can’t get it to work consistently (with multiple EVs) always green or magenta tones. I think the fact that it’s a RAW photo screws up the green channel ….

Solution

Let’s say you start with the original image and you are in linear space. In this case, changing the exposure is a multiplication operation.

An

increase of 1 in the exposure value (EV) corresponds to a doubling of exposure. Exposure is a linear measure of the amount of light reaching each pixel. Doubling the exposure doubles the amount of light. Because in photography people usually think in terms of the score of the current exposure, it makes sense to talk about “increasing the EV by 1” instead of “multiplying the exposure by 2”.

So, in effect, to increase the exposure value by n, multiply the pixel value by 2n.

If the input image is a JPEG or TIFF file, it may be in the sRGB color space. This is a non-linear color space designed to increase the apparent range of an 8-bit image file. Before modifying the exposure, you need to convert sRGB to linear RGB. This can be approximated by increasing each pixel value to a power of 2.2, Wikipedia has the exact formulation .


Problems in OP are caused by inaccurate black levels. raw.black_level_per_channel returns 528 for a given image (the value is the same for each channel, but I guess this is not necessarily the case for other camera models), not 512. Also, the code says that raw.raw_image_visible changes back to raw.raw_image, which is incorrect.

The following code produces the correct result:

import rawpy
import numpy as np
from PIL import Image

bit_depth = 12
exposure = 5

path = "/001_ev0. DNG"
raw = rawpy.imread(path)
black_level = raw.black_level_per_channel[0] # assume they're all the same

im = raw.raw_image
im = np.maximum(im, black_level) - black_level # changed order of computation
im *= 2**exposure
im = im + black_level
im = np.minimum(im, 2**12 - 1)
raw.raw_image[:,:] = im
im = raw.postprocess(use_camera_wb=True, no_auto_bright=True)
img = Image.fromarray(im, 'RGB')
img.show()

Related Problems and Solutions