snibgo's ImageMagick pages

Linear camera raw

How linear is my camera raw data? How can I make it linear?

Commands of this page use utility programs such as cSelect.exe. I don't publish the source or binary of these.

What is linear, and why does it matter?

A digital camera measures light received from a subject. In particular, it measures the energy emitted within particular parts of the spectrum. For some purposes, we want to ensure that the numerical values at the pixels are proportional to the energy received (technically, proportional to the object luminance). This ensures that certain operation such as blurring will replicate physical processes, and help transformations to output colourspaces to be technically correct, and helps images from multiple sources (photos, videos, film scans, paintings, CGI) to have a common standard ("scene-referred") so they can be composited together easily and accurately. If we have multiple photos at different exposures of the same scene, we can multiply each by a number to make the same object the same tone and color on all the photos, so they can be merged for High Dynamic Range.

(Accurate processing also requires us to know the precise colours that the camera records as pure red, green, blue and white -- the three primaries and the white point, but that is beyond the scope of this web page.)

The real world has a wide range of possible luminances, from zero to infinity. A camera captures only a narrow range of luminances, and we want linearity within that range. There is no upper limit to luminance, it is unbounded, so linear vales are often recorded as floating point to allow for a higher dynamic range than a camera can record. Even the lower limit has problems; we need to go deep underground to experience true darkness. A camera might record zero even when there is light. Conversely, even when the shutter hasn't been opened, a camera might record non-zero, due to noise. Floating point also helps in the shadows, because the bottom few stops will have integer values 1, 2, 4, 8... which doesn't allow for much detail to be recorded between the stops. Merely converting from integer to floating-point doesn't help the shadows, but we can combine data from multiple exposures to supply shadow detail.

We can measure the linearity of the camera. If it isn't linear, we can derive a transformation that will make the values linear, and the different channels may need different transformations.

The process only applies to raw camera files. In-camera JPEG images are certainly not linear due to the sRGB tonal transformation. That is reversible, but the camera has probably also made local variations, boosting contrast and saturation, increasing sharpness and so on, to make the image more readable and attractive to humans. A JPEG or other processed image can often be made approximately linear, but attempts at accurate linearity are doomed.

The method

We apply the method to a Nikon D800 camera. The same method can be applied to any camera that makes raw images that dcraw can read.

We want to know if the raw sensor values are proportional to object luminance. We could photograph a step wedge on a light box and measure the value at each step.

Another method is to photograph a plain gray-ish card at different (but known) manual exposures. If we open the aperture by one stop or double the exposure time, the average value should double. The value should double for every extra stop. We repeat at a range of exposures, until a sensor records zero or 100%. At or beyond those limits, the average will be misleading, so we ignore those readings.

The JPEG verson of a typical photo looks like this:

set NEFDIR=\pictures\20180406\

%IM%convert ^
  %NEFDIR%AGA_3536.JPG ^
  -resize 400x400 ^

The camera records the difference between the manual exposure and what it thinks is the correct exposure, shown as "ExpDiff" in tables below.

We assume the lens apertures and shutter speeds are accurate.

We use dcraw to de-Bayer the image. (Instead, we could separate the four channels without de-Bayering. As the subject is very flat (low contrast), this shouldn't make much difference.)

The script RawLevelsRng.bat writes the statistics into a CSV file, which csv2tab.bat converts to an HTML table for this web page.

rem goto skip

call %PICTBAT%RawLevelsRng %NEFDIR%AGA_NN.NEF 3534 3553 lcr_shots.csv

call csv2tab lcr_shots
file ExpDiff minR meanR maxR minG meanG maxG minB meanB maxB
AGA_3534 +1.9 0.0971237 0.115321 0.134493 0.209354 0.236018 0.255497 0.151476 0.172334 0.191684
AGA_3535 +0.9 0.0440376 0.0555669 0.0661479 0.0994888 0.113619 0.125368 0.0705272 0.0829434 0.0942703
AGA_3536 0 0.0215152 0.0284768 0.0378424 0.0496833 0.0582614 0.066163 0.0353551 0.0426907 0.0503547
AGA_3537 -1.1 0.00875867 0.013573 0.0216068 0.0225223 0.0276617 0.0324103 0.0154269 0.0202093 0.0246586
AGA_3538 -2.1 0.00379949 0.00669889 0.0166018 0.0107423 0.0137834 0.0170901 0.006775 0.0100658 0.0138247
AGA_3539 -1.8 0.00515755 0.0084644 0.0183719 0.0139773 0.0173787 0.0213016 0.00906386 0.0126837 0.0162814
AGA_3540 -3.1 0.00146487 0.00345157 0.0130617 0.00482185 0.00700874 0.00939956 0.00282291 0.00509911 0.00747691
AGA_3541 -3.8 0.000427254 0.00226522 0.0123293 0.00299078 0.00472025 0.0065919 0.00164798 0.00343113 0.00534067
AGA_3542 +1.9 0.0964827 0.114794 0.132082 0.210452 0.23423 0.25678 0.150652 0.170609 0.189731
AGA_3543 +2.9 0.189486 0.231595 0.260442 0.418113 0.472264 0.511804 0.30135 0.345265 0.379232
AGA_3544 +3.9 0.396185 0.473591 0.51284 0.851362 0.942682 0.965195 0.627527 0.703039 0.755412
AGA_3545 +4.7 0.72221 0.826153 0.890242 0.960983 0.963097 0.965255 0.997253 0.99997 1
AGA_3546 +4.7 0.675516 0.761466 0.814069 0.960983 0.963097 0.965255 0.997253 0.99997 1
AGA_3547 +5.6 0.997314 0.999989 1 0.963057 0.965202 0.96733 0.996566 0.999251 1
AGA_3548 +6.3 0.997253 0.999989 1 0.969588 0.971793 0.973921 0.993819 0.996566 0.999314
AGA_3549 +7.4 0.997253 0.999989 1 0.969588 0.971793 0.973921 0.993819 0.996566 0.999314
AGA_3550 +6.4 0.997253 0.999989 1 0.969588 0.971793 0.973921 0.993819 0.996566 0.999314
AGA_3551 +7.4 0.997314 0.999989 1 0.9738 0.976005 0.978133 0.991714 0.994371 0.997055
AGA_3552 +9.1 0.997314 0.999989 1 0.967269 0.969414 0.971542 0.98021 0.982897 0.985579
AGA_3553 +8.1 0.997314 0.999989 1 0.967269 0.969414 0.971542 0.98021 0.982897 0.985579

Some ExpDiff values are duplicated because my photography was a bit careless.

Something strange is happening in the blue at high exposures. The sensor is saturated at +4.7 and +5.6, but then records lower values at +6.3 and above. It seems the electronic sensor exhibits solarisation, like film does. The same is true, to a lesser degree, in the other channels.

We eliminate rows that have min at zero or max at one; remove columns other than the ExpDiff and mean values; sort on ExpDiff; and group rows that have the same ExpDiff values, calculating the mean values of the means.

(I don't publish source or binaries of utility programs cSelect, chStrs, cProject or cSort.)


set TMPCSV=\temp\lcr.csv
cSelect  /ilcr_shots.csv /o%TMPCSV% /h /kminR /w0 /x
cSelect  /i%TMPCSV% /h /kminG /w0 /x
cSelect  /i%TMPCSV% /h /kminB /w0 /x
cSelect  /i%TMPCSV% /h /kmaxR /w1 /x
cSelect  /i%TMPCSV% /h /kmaxG /w1 /x
cSelect  /i%TMPCSV% /h /kmaxB /w1 /x
chStrs   /i%TMPCSV% /f"+" /t\0
cProject /i%TMPCSV% /h /kExpDiff,meanR,meanG,meanB
rem cSort    /i%TMPCSV% /olcr_shots2.csv /h /kExpDiff
cGroup   /i%TMPCSV% /olcr_shots2.csv /h /kExpDiff /u /emeanR,meanG,meanB
cPrefix  /ilcr_shots2.csv /h /tExpDiff,meanR,meanG,meanB
call csv2tab lcr_shots2
ExpDiff meanR meanG meanB
-3.8 0.0022652200000000001 0.0047202499999999996 0.0034311300000000001
-3.1 0.0034515700000000002 0.0070087400000000003 0.0050991099999999996
-2.1 0.0066988899999999999 0.013783399999999999 0.0100658
-1.8 0.0084644000000000004 0.0173787 0.012683699999999999
-1.1 0.013573 0.027661700000000001 0.020209299999999999
0 0.0284768 0.058261399999999998 0.042690699999999998
0.9 0.055566900000000002 0.113619 0.0829434
1.9 0.11505750000000001 0.235124 0.1714715
2.9 0.231595 0.47226400000000002 0.34526499999999999
3.9 0.47359099999999998 0.94268200000000002 0.70303899999999997

Some observations:

Each unit of ExpDiff is a photographic stop, a doubling of exposure. So we plot the log (base 2) of the mean values against ExpDiff:

gnuplot -c ^
  lcr_shots2.csv lcr_shots2.svg ^
  . . "2 3 4" ExpDiff LogMeanVal

We can see the log values are very close to a straight line with unity slope, and the channels are very nearly parallel. This is true within the experimental error of the camera recording ExpDiff to a resolution of 0.1.

I conclude this camera is sufficiently linear that no transformation is needed.

Correcting linearity

If the linearity was worse, we could correct it. One obvious method is to construct a clut where the input values are from the previous table, and the outputs are the ideal values. We might assume the ExpDiff values are correct, and the means at the largest ExdDiff are correct. The ideal values for each ExpDiff are then easily calculated. We would also add entries in the clut for input values zero and one, with outputs zero and one.


For convenience, .bat scripts are also available in a single zip file. See Zipped BAT files.


rem Given %1 is a template name of raw files that includes NN,
rem %2 is a start number,
rem %3 is an end number,
rem %4 is an output for CSV,
rem writes data for each raw file.

set INTMPLT=%1
set START=%2
set END=%3
set CSVFILE=%4

if "%INTMPLT%"=="." set INTMPLT=
if "%INTMPLT%"=="" set INTMPLT=abc_NN.NEF

if "%START%"=="." set START=
if "%START%"=="" set START=0

if "%END%"=="." set END=
if "%END%"=="" set END=%START%

if "%CSVFILE%"=="." set CSVFILE=
if "%CSVFILE%"=="" set CSVFILE=rlr.csv

set TMPTIFF=\temp\rlr.tiff

echo file, ExpDiff, minR, meanR, maxR,  minG, meanG, maxG,  minB, meanB, maxB >%CSVFILE%

for /l %%N in (%START%, 1, %END%) do (
  echo %%N

  set LZ=0000%%N
  set LZ=!LZ:~-4!
  echo LZ=!LZ!


  echo !NEFFILE!


  if ERRORLEVEL 1 exit /B 1


set NEFFILE=%1
set TMPTIFF=%2
set CSVFILE=%3

set NEF_SHT=%~n1

%DCRAW% -v -4 -r 1 1 1 1 -g 1 1 -o 0 -T -R 1 -O %TMPTIFF% %NEFFILE%

if ERRORLEVEL 1 exit /B 1

set EDIF=
for /F "usebackq tokens=3 delims=: " %%A in (`exiftool ^
  -ExposureDifference ^
  %NEFFILE%`) do set EDIF=%%A

if "%EDIF%"=="" exit /B 1

set sVALS=^
%NEF_SHT%, ^
%EDIF%,  ^
%%[fx:minima.r], %%[fx:mean.r], %%[fx:maxima.r],  ^
%%[fx:minima.g], %%[fx:mean.g], %%[fx:maxima.g],  ^
%%[fx:minima.b], %%[fx:mean.b], %%[fx:maxima.b]\n

%IMDEV%convert ^
  -gravity Center ^
  -crop 33x33%%+0+0 +repage ^
  -format "%sVALS%" ^
  info: >>%CSVFILE%


rem From %1.csv, creates partial %1.htm with a table.
rem %2 is parameters to table, eg "border".

set c2tTAB_PARAMS=%2
if "%c2tTAB_PARAMS%"=="." set c2tTAB_PARAMS=

chSep   /p0 /i%1.csv /o%1.htm /f"," /t\(/td\)\(td\) /T\(/th\)\(th\) /n
cPrefix /p0 /i%1.htm /l\(tr\)\(td\) /r\(/td\)\(/tr\) /t"\(table %c2tTAB_PARAMS%\)" /b\(/table\) /X
chStrs  /p0 /i%1.htm /m1 /f\(td\) /t\(th\)
chStrs  /p0 /i%1.htm /m1 /f\(/td\) /t\(/th\)

All images on this page were created by the commands shown, using:

%IM%identify -version
Version: ImageMagick 6.9.5-3 Q16 x86 2016-07-22
Copyright: Copyright (C) 1999-2015 ImageMagick Studio LLC
Visual C++: 180040629
Features: Cipher DPC Modules OpenMP 
Delegates (built-in): bzlib cairo flif freetype jng jp2 jpeg lcms lqr openexr pangocairo png ps rsvg tiff webp xml zlib

To improve internet download speeds, some images may have been automatically converted (by ImageMagick, of course) from PNG to JPG.

Source file for this web page is lincamraw.h1. To re-create this web page, execute "procH1 lincamraw".

This page, including the images, is my copyright. Anyone is permitted to use or adapt any of the code, scripts or images for any purpose, including commercial use.

Anyone is permitted to re-publish this page, but only for non-commercial use.

Anyone is permitted to link to this page, including for commercial use.

Page version v1.0 29-March-2018.

Page created 07-Apr-2018 08:12:27.

Copyright © 2018 Alan Gibson.