3

I'm trying to write a program that takes in a matrix full of 12-bit RAW sensor values (that range from [512-4096]) (512 is the Bayer sensor black level--> i.e. what pure black is defined as) and adjusts the EV of each pixel, excatly like the Adobe Camera Raw (ACR) "exposure" slider. I'm trying to figure out how it is done basically. I've looked up dozens of blogs that explain how EV is calculated and it seems to be:

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

This just seems very wrong because an adjustment of 5 EV blows the picture wayyy out of proportion.. and I can't find any other resources online. Wikipedia has an excellent entry on Exposure Value but it doesn't seem to have what I'm looking for either... any help on this?

Thanks!

Here is an example of what I mean:

RAW file in ACR with EV 0: 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 on any EV value that is not 5, so the formula is not correct. I also know this formula is wrong because it doesn't work if EV = 0. I have found dozens of sites that explain that the formula is just new_pixel = pixel * 2^exposure but this doesn't seem to work with Raw photos... am I missing something?

Any thoughts?

Here is some python code and some files that I'm using 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
Images:

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

I've spent 14 hours on this for some reason... I have no idea what I'm doing wrong as I can't get it to consistently work (with multiple EVs) there's always either a green or a magenta hue. I think the fact that this is a RAW photo fucks up the green channel...

JoeVictor
  • 1,806
  • 1
  • 17
  • 38

1 Answers1

3

Assuming you start off with a raw image, you are in a linear space. In this case, changing exposure is a multiplicative operation.

Increasing Exposure Value (EV) by 1 corresponds to doubling the exposure. Exposure is a linear measure of the amount of light that reaches each pixel. Doubling the exposure doubles the amount of light. Because in photography one usually thinks in terms of fractions of current exposure, it makes sense to talk about "increasing EV by 1", rather than "multiplying exposure by 2".

Thus, indeed, to increase the Exposure Value by n, multiply the pixel values by 2n.

If the input image is a JPEG or TIFF file, it likely is in sRGB color space. This is a non-linear color space meant to increase the apparent range of the 8-bit image file. It is necessary to convert sRGB to linear RGB first, before modifying exposure. This can be accomplished approximately by raising each pixel value to a power of 2.2, Wikipedia has the exact formulation.


The problems in the OP are caused by an inexact black level. raw.black_level_per_channel returns 528 for the given image (it's the same value for each of the channels, though I guess this is not necessarily the case for other camera models), not 512. Furthermore, the code writes raw.raw_image_visible back into raw.raw_image, which is not correct.

The following code produces correct results:

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()
Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
  • doesn't seem to give me the same results as Adobe Camera Raw, I even tried to multiply the RGB value by `2**p`, it still doesn't work. – JoeVictor Jan 19 '19 at 23:52
  • According to the inspector, I'm working already in an RGB space, but the file is raw, so pixel values are actually Bayer sensor values – JoeVictor Jan 19 '19 at 23:58
  • @QuantumHoneybees: see edited answer. But you might need to compute the Exposure Value of the raw data to know by how much to increase it. – Cris Luengo Jan 20 '19 at 00:10
  • So I've tried this and it doesn't work because of the following: My raw picture data is a 12-bit data with a black level of 512, which means each sensor has a value [512,4095] with 512 meaning no light and 4095 meaning max light. Now, for example, if I want to add `+5 EV`, I'll increase a pixel with a value of `1500` to be: `1500 * 2^5 = 48,000`, which is WAY over the maximum value of 4095. So how is that formula supposed to work exactly? – JoeVictor Jan 20 '19 at 00:19
  • @QuantumHoneybees: Increasing your EV by 5 is equivalent to increasing your F-stop by 5, or multiplying your exposure time by 32. If you start with a properly exposed picture, it will blow out if you do that. – Cris Luengo Jan 20 '19 at 00:41
  • Then why is it when I increase my black photo's exposure to 5 (from 0) in ACR, it brightens up so cleanly? I am definitely missing something here... – JoeVictor Jan 20 '19 at 00:46
  • @QuantumHoneybees: The black picture has mostly very low values, you can multiply them by 32 without exceeding the max value. Do remember to first subtract the offset (black value). – Cris Luengo Jan 20 '19 at 01:30
  • I have no idea what i'm doing wrong.. I'm doing exactly what you're saying but still for some reason, bumping up the exposure by 5 works, but by EV 4 works horrendously. I've attached the code and a link to the picture if you want to look at it to see what I'm doing wrong... but I have **no idea** what could be the problem. This is one of the weirdest bugs I've seen in my life.. – JoeVictor Jan 20 '19 at 02:22
  • Just for full clarity's sake... I took another photo. The `raw.black_level_per_channel` still returns 528, yet the raw_image still has a minimum of 513 when I run through all the numbers (if the image is dark enough) . Which is weird. But who cares now it works :) – JoeVictor Jan 20 '19 at 08:32
  • @QuantumHoneybees: This is noise. It is the reason that the black level is not 0. It allows for better noise reduction if that is not clipped off. I just made a small change to account for the possibility of creating negative values when subtracting the black level—I think this is all in unsigned integer representation, and therefore negative values are not possible. But I always find it easiest to convert data to floating-point representation for computation, maybe that would be better here too. – Cris Luengo Jan 20 '19 at 14:53