snibgo's ImageMagick pages

dcraw and WB

... (white balance) and clipping and contrast.

dcraw, Dave Coffin's raw image converter, is a splendid piece of work. It must count as (probably) the greatest and most influential little program of the last 20 years. It isn't a library, and it's difficult to pull apart to tweak it in the directions I want, or even to understand. But what it does, it does well.

This page shows how we can use dcraw to get accurately white-balanced images, with good contrast and brightness, without clipping, automatically.

Scripts on this page assume that the version of ImageMagick in %IM7DEV% has been built with various process modules. See Process modules.

Install dcraw

Some versions of IM come with a pre-built binary of dcraw, or you can build it yourself.

  1. Get the source. There is only one source file.
    wget http://cybercom.net/~dcoffin/dcraw/dcraw.c
    Or you can download my slightly hacked version, dcrawimg.c. Or download the official source, and apply the differences file dcrawag.diff between it and my hacked version.
  2. Patch and hack it as you want. As written by Dave Coffin, it doesn't have an option to name the output file. The ImageMagick suite includes a patch for this, adding a "-O <file>" option.
  3. Compile and link, and install. The Jasper library is only used for a type of camera I don't have, and my computer doesn't have the library. These are the Cygwin bash commands I use:
    gcc -o dcrawag.exe dcraw/dcrawimg.c -DNO_JASPER -I. \
      -Ofast -m64 -march=native -mtune=native -Wall -llcms2 -ljpeg
    
    cp dcrawag.exe $PICTBAT\dcrawag.exe
  4. Rename the executable to whatever you want, and copy it to wherever you want. My patch changes the behaviour of the program, so I don't (currently) call it dcraw.exe. I run it indirectly, via an environment variable.
  5. echo %DCRAW% 
    F:\pictures\dcrawag.exe 
  6. Check that running the program with no parameters gives something like this:
    %DCRAW% 
     Raw photo decoder "dcraw" v9.27
    by Dave Coffin, dcoffin a cybercom o net
    slightly hacked by Alan Gibson v2.0
    
    Usage:  /cygdrive/f/pictures/dcrawag [OPTION]... [FILE]...
    
    -v        Print verbose messages
    -c        Write image data to standard output
    -e        Extract embedded thumbnail image
    -i        Identify files without decoding them
    -i -v     Identify files and show metadata
    -z        Change file dates to camera timestamp
    -w        Use camera white balance, if possible
    -a        Average the whole image for white balance
    -A <x y w h> Average a grey box for white balance
    -r <r g b g> Set custom white balance
    -R n      Divisor for white balance multipliers
    +M/-M     Use/don't use an embedded color matrix
    -C <r b>  Correct chromatic aberration
    -O <file> Write output to this file
    -P <file> Fix the dead pixels listed in this file
    -K <file> Subtract dark frame (16-bit raw PGM)
    -k <num>  Set the darkness level
    -S <num>  Set the saturation level
    -n <num>  Set threshold for wavelet denoising
    -H [0-9]  Highlight mode (0=clip, 1=unclip, 2=blend, 3+=rebuild)
    -t [0-7]  Flip image (0=none, 3=180, 5=90CCW, 6=90CW)
    -o [0-6]  Output colorspace (raw,sRGB,Adobe,Wide,ProPhoto,XYZ,ACES)
    -o <file> Apply output ICC profile from file
    -p <file> Apply camera ICC profile from file or "embed"
    -d        Document mode (no color, no interpolation)
    -D        Document mode without scaling (totally raw)
    -j        Don't stretch or rotate raw pixels
    -W        Don't automatically brighten the image
    -B <num>  Clipping threshold for auto-brighten (default = 0.0001)
    -b <num>  Adjust brightness (default = 1.0)
    -g <p ts> Set custom gamma curve (default = 2.222 4.5)
    -q [0-3]  Set the interpolation quality
    -h        Half-size color image (twice as fast as "-q 0")
    -f        Interpolate RGGB as four colors
    -m <num>  Apply a 3x3 median filter to R-G and B-G
    -s [0..N-1] Select one raw image or "all" from each file
    -6        Write 16-bit instead of 8-bit
    -4        Linear 16-bit, same as "-6 -W -g 1 1"
    -T        Write TIFF instead of PPM
    
    Note the -O and -B options.

Auto brighten

dcraw has a "automatically brighten the image" operation. This stretches the pixel values by multiplying channel values. The idea is that if the maximum value is 75%, multiplying by 1.3333 will use more of the range between 0 and 100%. (dcraw won't also stretch the bottom, so if the minimum value was 10% it might become 13.333%. Why won't it stretch both ends? I don't know.) This happens by default; if we don't want it, we can disable it with "-W", and we can then use IM to "-auto-level" or whatever we want.

But this auto-brighten feature has a problem: it doesn't merely stretch to use values up to 100%, but over-stretches, clipping the top 1% of pixels. This is a lot of pixels. An area one-tenth of the image width and one-tenth of the height amounts to 1% of the image area. The whole point of using raw images (for me) is to get good pixels from the camera, giving me more flexibility than the in-camera JPEG provides, and hence better quality. Wrecking 1% of the pixels before I even begin the creative stuff is a bad start.

So I've also created a patch for dcraw. This adds a variable, BrightThr initialised to 0.0001, and an option "-B <num>" to set the variable. The variable is used in a line that used to read...

perc = width * height * 0.01;

... but I have changed to ...

perc = width * height * BrightThr;

So, by default "auto-brighten" will now clip 0.0001 of the pixels, which is 0.01% of the pixels. If that is too much, -B 0 will auto-brighten without clipping any pixels. As before, "-W" will not auto-brighten at all.

White balance

dcraw applies a white balance by simple channel multiplication in linear colorspace, before de-Bayering and conversion to sRGB or whatever. It can get the multipliers from metadata in the raw file ("camera" white balance), or from averaging a rectangle of pixels that should be neutral gray, or from the "-r" command line option. Multiplying channels can result in clipping.

dcraw's -b option, to set brightness, also multiplies in linear colorspace. See Brightness below.

We will show three methods for white balancing:

  1. Use camera WB and default dcraw processing;
  2. Use camera WB and dcraw -R n;
  3. Calculate -r n n n n multipliers from gray patches.

[Embed linear images with a profile from Elle.]

First, some basic statistics about the raw Bayer data. "-D" give us "Document", which isn't de-Bayered or scaled. We don't auto-brighten or white-balance or adjust gamma.

set SRCNEF=%PICTLIB%20171029\AGA_3437.nef

set WEBDIMS=600x600
set WEBSIZE=-resize %WEBDIMS%

%DCRAW% -v -6 -W -D -r 1 1 1 1 -g 1 1 -o 0 -T -O dcr_basic.tiff %SRCNEF%

What are the minimum, mean and maximum Bayer values?

%IMG7%magick dcr_basic.tiff ^
  -format "%%[fx:minima*QuantumRange] %%[fx:mean*QuantumRange] %%[fx:maxima*QuantumRange]\n" ^
  info: 
0 1148.18 16383

We are using IM Q16 but the camera records only 14 bits per pixel, so the maximum value possible is 2^14-1 = 16383.

How many Bayer values are at the extremes?

call %PICTBAT%propIntClip dcr_basic.tiff 
 f:\prose\PICTURES>rem From image dcr_basic.tiff, 

f:\prose\PICTURES>rem finds proportion that have 0 or 100 in any channel. 

f:\prose\PICTURES>c:\im\ImageMagick-7.1.0-42-Q16-HDRI\magick   dcr_basic.tiff   -separate   -fill White -opaque Black   -fill Black +opaque White   -background Black   -compose Lighten -layers Merge   -format "clipped=%[fx:mean]\n"   info: 
clipped=0.500001

f:\prose\PICTURES>c:\im\ImageMagick-7.1.0-42-Q16-HDRI\magick   dcr_basic.tiff   -separate   -fill White +opaque Black +write x.tiff   -negate +write x2.tiff   -background Black   -compose Lighten -layers Merge +write x3.tiff   -format "shadowClipped=%[fx:mean]\n"   info: 
shadowClipped=0.500001

A very small number of sensor pixels recorded no signal at all. A larger number recorded the maximum signal, so the sensors were saturated (the amount of light was at or beyond the capacity of the sensors). (The camera has 35 M pixels, so the proportion of pixels that are clipped, 0.05%, isn't horribly bad.) We want to avoid increasing the numbers at either end.

WB and default dcraw

We tell dcraw to make a 16-bit TIFF sRGB image, using the camera WB, with an auto brighten that doesn't clip.

First, we use -H 0. This normalises the WB channel multipliers so the smallest multiplier is 1.0, so values in other channels can increase, so this can cause clipping in those other channels. However, this clipping means that pixels recorded at or near 100% in all three channels will become exactly or nearly white after the balancing, which is normally the correct result.

%DCRAW% -v -6 -w -o 1 -H 0 -B 0 -T -O dcr_wbdef0.tiff %SRCNEF%

What are the minimum, mean and maximum channel values?

%IMG7%magick dcr_wbdef0.tiff ^
  -format "%%[fx:minima] %%[fx:mean] %%[fx:maxima]\n" ^
  info: 
0 0.186095 1

How many pixel channel values are at the extremes?

call %PICTBAT%propIntClipRGB dcr_wbdef0.tiff 
shadowClipped.0=11489
shadowClipped.1=24
shadowClipped.2=1448
highlightClipped.0=882805
highlightClipped.1=13256
highlightClipped.2=1.3762e+06

Many pixels (about 3%) have clipped in the blue channel.

The image looks like this:

%IMG7%magick ^
  dcr_wbdef0.tiff ^
  %WEBSIZE% ^
  dcr_wbdef0.jpg
dcr_wbdef0.jpg

Using a GUI editor, we can see that most of the sky has burnt out in one or more channels.

We repeat, using -H 1. Modes from this upwards normalise the WB channel multipliers so the largest is 1.0. It also does other processing that I don't understand.

%DCRAW% -v -6 -w -o 1 -H 1 -B 0 -T -O dcr_wbdef1.tiff %SRCNEF%

What are the minimum, mean and maximum channel values?

%IMG7%magick dcr_wbdef1.tiff ^
  -format "%%[fx:minima] %%[fx:mean] %%[fx:maxima]\n" ^
  info: 
0 0.119695 1

How many pixel channel values are at the extremes?

call %PICTBAT%propIntClipRGB dcr_wbdef1.tiff 
shadowClipped.0=11580
shadowClipped.1=23
shadowClipped.2=1538
highlightClipped.0=195181
highlightClipped.1=1
highlightClipped.2=1

That's better. Far fewer pixels have clipped.

The image looks like this:

%IMG7%magick ^
  dcr_wbdef1.tiff ^
  %WEBSIZE% ^
  dcr_wbdef1.jpg
dcr_wbdef1.jpg

Eek! The sky has gone stupid. Other parts of the image are darker than the previous version, and this is expected as the multiplier normalisation has reduced instead of increasing values.

We repeat, using -H 2:

%DCRAW% -v -6 -w -o 1 -H 2 -B 0 -T -O dcr_wbdef2.tiff %SRCNEF%

What are the minimum, mean and maximum channel values?

%IMG7%magick dcr_wbdef2.tiff ^
  -format "%%[fx:minima] %%[fx:mean] %%[fx:maxima]\n" ^
  info: 
0 0.14441 1

How many pixel channel values are at the extremes?

call %PICTBAT%propIntClipRGB dcr_wbdef2.tiff 
shadowClipped.0=11422
shadowClipped.1=23
shadowClipped.2=1538
highlightClipped.0=70589
highlightClipped.1=70498
highlightClipped.2=70498

That's better. Far fewer pixels have clipped, about 0.2%.

The image looks like this:

%IMG7%magick ^
  dcr_wbdef2.tiff ^
  %WEBSIZE% ^
  dcr_wbdef2.jpg
dcr_wbdef2.jpg

That looks better.

Settings of -H 3 and above look bad.

WB and dcraw -R n

%DCRAW% -v -4 -o 0 -T -H 1 -O x.tiff %SRCNEF%

%IMG7%magick identify -format %[fx:maxima] x.tiff

%DCRAW% -v -4 -o 0 -T -H 0 -R 0.25745 -O x.tiff %SRCNEF%

%IMG7%magick x.tiff -set colorspace RGB -colorspace sRGB s.tiff
set RNum=2

%DCRAW% -v -4 -w -W -o 0 -H 0 -R %RNum% -T -O dcr_wbrn0.tiff %SRCNEF%

What are the minimum, mean and maximum channel values?

%IMG7%magick dcr_wbrn0.tiff ^
  -format "%%[fx:minima] %%[fx:mean] %%[fx:maxima]\n" ^
  info: 
0 0.045132 0.958984

Calculate the maximum multiplied by the RNum:

for /F "usebackq" %%L in (`%IMG7%magick dcr_wbrn0.tiff ^
  -format "newRNum=%%[fx:maxima*%RNum%]\n" ^
  info:`) do set %%L

echo newRNum=%newRNum% 
newRNum=1.91797 

Re-run dcraw, using newRNum:

%DCRAW% -v -4 -w -W -o 0 -H 0 -R %newRNum% -T -O dcr_wbrn0.tiff %SRCNEF%

How many pixel channel values are at the extremes?

call %PICTBAT%propIntClipRGB dcr_wbrn0.tiff 
shadowClipped.0=62
shadowClipped.1=2
shadowClipped.2=12
highlightClipped.0=70446
highlightClipped.1=5
highlightClipped.2=1

Blah.

The image looks like this:

%IMG7%magick ^
  dcr_wbrn0.tiff ^
  %WEBSIZE% ^
  dcr_wbrn0.jpg
dcr_wbrn0.jpg

When using -R n, you should usually also use -W or -6.

WB and dcraw -r from gray patches

This method calculates white balance from gray patches on a colour card. During the photography session, we include a standard colour card in one of the photos. We automatically find the patches, and the lightest patch should be a neutral gray, so this gives us the channel multipliers.

First, we process the raw image in a "flat" manner, with no white balancing or conversion to sRGB. [[ The "-h" option reduces the time to one-third. ]] We allow a small proportion of pixels to clip in "auto brighten".

In the -r option, the four numbers are multipliers for the Bayer RGBG channels. Whatever numbers we give, the standard version of dcraw will normalise to make the lowest or the highest multiplier 1.0. If "-H 0" is used (the default), the lowest multiplier is made 1.0, so this will never lower values in any channels (and it can cause clipping). For higher "-H" settings, the highest multiplier is made 1.0, so this will never raise values in any channels (and it can't cause clipping). In either case, we can't use -r to change the overall lightness of the image.

(My patched version of dcraw has an extra option, -R n, which prevents normalisation of the -r white balance multipliers.)

set SRCNEF=%PICTLIB%20171029\AGA_3431.nef

set WEBSIZE=-resize 600x600

set dcrParams=-v -B 0.0001 -6 -T

%DCRAW% %dcrParams% -r 1 1 1 1 -o 0 -g 1 1 -O dcr_flat.tiff %SRCNEF%

Out of interest, we take a look at the image.
We pretend the image is sRGB, so IM won't convert it.
For the web, we show just the relevant part.

%IMG7%magick ^
  dcr_flat.tiff ^
  -set colorspace sRGB ^
  -gravity SouthWest -crop 35x35%%+0+0 ^
  %WEBSIZE% ^
  -quality 40 ^
  dcr_flat.jpg
dcr_flat.jpg

The image looks dark because the pixels values are linear but we are displaying as if they were sRGB.

Next, extract the colour card. See Finding and analysing colour charts for more detail.

call %PICTBAT%24card dcr_flat.tiff dcr_flat_XX.png

This gives a 6x4 pixel image, and a scaled-up version for viewing:

dcr_flat_scl.png

dcr_flat_scl.png

The bottom row of the 6x4 image should be neutral shades of gray. We could choose any pixel on that row to give us the multipliers for red and blue, compared to green, but the lightest patch will show a colour-cast most easily, so we choose it. We are not concerned with the absolute values in the channels (eg whether the patch is pure white or 90% white), but only with their relative values.

set FMT=^
r=%%[fx:mean.r]\n^
g=%%[fx:mean.g]\n^
b=%%[fx:mean.b]\n^
mr=%%[fx:mean.g/mean.r]\n^
mb=%%[fx:mean.g/mean.b]\n

for /F "usebackq" %%L in (`%IMG7%magick ^
  -precision 16 ^
  dcr_flat_mat.png ^
  -gravity SouthWest -crop 1x1+0+0 +repage ^
  -format "%FMT%" ^
  info:`) do set %%L

echo r=%r% g=%g% b=%b% 
echo mr=%mr% mb=%mb% 
r=0.4866712443732357 g=0.977630273899443 b=0.6918745708400091 
mr=2.008810434564495 mb=1.413016629173834 

For convenience, we put this in a script, whiteMult.bat.

Re-run dcraw with those multipliers. This time, we include the conversion to sRGB.

%DCRAW% %dcrParams% -r %mr% 1 %mb% 1 -o 1 -O dcr_real.tiff %SRCNEF%

Out of interest, we take a look at the image.
For the web, we show just the relevant part.

%IMG7%magick ^
  dcr_real.tiff ^
  -gravity SouthWest -crop 35x35%%+0+0 ^
  %WEBSIZE% ^
  -quality 40 ^
  dcr_real.jpg
dcr_real.jpg

The white balance of the result looks good. We can check it by finding the card and examining the bottom row. (If we hadn't used "-h" for the first dcraw, we could re-use the coordinates from the first 24card.bat.)

call %PICTBAT%24card dcr_real.tiff dcr_real_XX.png

This gives a 6x4 pixel image, and a scaled-up version for viewing:

dcr_real_scl.png

dcr_real_scl.png

Rather than looking at numbers, we will graph the gray patches:

%IMG7%magick ^
  dcr_real_mat.png ^
  -gravity South ^
    -crop x1+0+0 +repage ^
  -scale "600x1^!" ^
  dcr_real_mat_gr.png

call %PICTBAT%graphLineCol ^
  dcr_real_mat_gr.png
dcr_real_mat_gr_glc.png

As expected, the lightest patch is neutral. The others are fairly close, though not exact. We have ignored the camera's WB metadata, and used dcraw features to balance a stepped gray card. We can use the same settings to balance all photos (or video frames) taken under the same conditions.

If we want a closer match to gray throughout the range, we can find the polynomials that most closely transform each channel to match the grayed patches, and hence to match each other. See Colour checker charts for more detail.

set mypolyr=
for /F "usebackq tokens=1,2 delims==" %%A in (`%IM7DEV%magick ^
  dcr_real_mat.png ^
  -gravity South -crop x1+0+0 +repage ^
  ^( +clone ^
     -colorspace Gray ^
  ^) ^
  -precision 16 ^
  -process 'cols2mat method NoCrossPoly noTrans f stdout' ^
  NULL:`) do (
  if "%%A"=="PolyRed" set mypolyr=%%B
  if "%%A"=="PolyGreen" set mypolyg=%%B
  if "%%A"=="PolyBlue" set mypolyb=%%B
)
if "%mypolyr%"=="" goto error

echo mypolyr=%mypolyr% 
echo mypolyg=%mypolyg% 
echo mypolyb=%mypolyb% 
mypolyr=0.1237015048600939,0.8764969348923533,0.01229858749638352 
mypolyg=-0.01700277760391167,1.015004353649351,-0.0002309841390380485 
mypolyb=-0.1875444578742101,1.202348214008359,-0.02983195546404099 

For convenience, we put this in a script, grayPoly.bat.

We can apply these polynomials directly to the _mat image, and graph it.

%IMG7%magick ^
  dcr_real_mat.png ^
  -gravity South ^
    -crop x1+0+0 +repage ^
  -channel R -function Polynomial %mypolyr% ^
  -channel G -function Polynomial %mypolyg% ^
  -channel B -function Polynomial %mypolyb% ^
  +channel ^
  -scale "600x1^!" ^
  dcr_real_mat_gr2.png

call %PICTBAT%graphLineCol ^
  dcr_real_mat_gr2.png
dcr_real_mat_gr2_glc.png

The graph is greatly improved (though the difference in the patches is not visible).

Apply the polynomials to the output from dcraw, and also ensure the standard deviation is at least 0.166667. For the setmnsd module, see Set mean and stddev.

%IM7DEV%magick ^
  dcr_real.tiff ^
  -channel R -function Polynomial %mypolyr% ^
  -channel G -function Polynomial %mypolyg% ^
  -channel B -function Polynomial %mypolyb% ^
  +channel ^
  -auto-level ^
  -depth 32 ^
  -define "quantum:format=floating-point" ^
  +write dcr_rp.tiff ^
  ( +clone ^
    %WEBSIZE% ^
    -quality 40 ^
    +write dcr_rp_sm.jpg ^
    +delete ^
  ) ^
  -process 'setmnsd sd 0.166667 d incOnly v' ^
  +write dcr_rp_sd.tiff ^
  %WEBSIZE% ^
  -quality 40 ^
  dcr_rp_sd_sm.jpg 

We show the result before and after setting the SD.

dcr_rp_sm.jpg dcr_rp_sd_sm.jpg

The setmnsd module has sent this to stdout:

setmnsd options:  verbose  sdGoal 0.166667  tolerance 1e-05  mid mean  initCon0 1e-07  initCon2 30  direction incOnlydecOnlyboth
input: mean=0.20839228 sd=0.14062557
nIter=19
result: mean=0.25034501  sd=0.16666933
setmnsd command: -sigmoidal-contrast 2.5030329,20.839228%

Developing without clipping

A purist (like me) might say we shouldn't allow dcraw to clip at all. What happens then? We repeat the above with auto-brighten but without any clipping.

set dcrParams=-v -B 0 -6 -T

%DCRAW% %dcrParams% -r 1 1 1 1 -o 0 -g 1 1 -O dcr_flat_nc.tiff %SRCNEF%

call %PICTBAT%24card dcr_flat_nc.tiff dcr_flat_nc_XX.png

call %PICTBAT%whiteMult dcr_flat_nc_mat.png dcr_wm

set dcr_wm 
dcr_wm_b=0.4306858930342565
dcr_wm_g=0.6093537804226749
dcr_wm_mb=1.414844995571302
dcr_wm_mr=2.011383096605218
dcr_wm_r=0.3029526207370107
%DCRAW% %dcrParams% -r %dcr_wm_mr% 1 %dcr_wm_mb% 1 -o 1 -O dcr_real_nc.tiff %SRCNEF%

call %PICTBAT%24card dcr_real_nc.tiff dcr_real_nc_XX.png

call %PICTBAT%grayPoly dcr_real_nc_mat.png dcr_gp

set dcr_gp 
dcr_gp_polyb=-0.2980922587311937,1.176119490546361,-0.01102737607666592
dcr_gp_polyg=-0.02826531796778715,1.013959754170565,0.0003155726938555597
dcr_gp_polyr=0.2010445813695857,0.8889097628070177,0.003474310135753049
%IM7DEV%magick ^
  dcr_real_nc.tiff ^
  -channel R -function Polynomial %dcr_gp_polyr% ^
  -channel G -function Polynomial %dcr_gp_polyg% ^
  -channel B -function Polynomial %dcr_gp_polyb% ^
  +channel ^
  -auto-level ^
  -process 'setmnsd sd 0.166667 d incOnly v' ^
  +write dcr_rpnc_sd.tiff ^
  %WEBSIZE% ^
  -quality 40 ^
  dcr_rpnc_sd_sm.jpg 
setmnsd options:  verbose  sdGoal 0.166667  tolerance 1e-05  mid mean  initCon0 1e-07  initCon2 30  direction incOnlydecOnlyboth
input: mean=0.091012126 sd=0.073283111
nIter=20
result: mean=0.21448637  sd=0.16666103
setmnsd command: -sigmoidal-contrast 6.2427924,9.1012126%

Here is the result:

dcr_rpnc_sd_sm.jpg

dcr_rpnc_sd_sm.jpg

Because we haven't allowed dcraw to clip, the output from dcraw is darker, with lower contrast (ie lower standard deviation). So setmnsd needed a steeper curve to make the SD 0.166667, which has visibly increased the saturation. In addition, setmnsd applies a sigmoidal, "S", curve instead of the dcraw linear multiplication, so this increases the contrast around the mid-point and decreases it at the extremes. In my opinion, both results are satisfactory.

Downsizing for the web needs some sharpening. I like the halo minimization technique, although this also slightly increases saturation.

call %PICTBAT%resampHM ^
  dcr_rpnc_sd.tiff ^
  %WEBDIMS% d 100 . . ^
  dcr_web.png
dcr_web.pngjpg

Brightness

dcraw has an option, -b, for setting the brightness. This multiples all the channels by the same number, after de-Bayering, but still in linear RGB space. If we have auto-brightened, some pixel values will already be at 100%, and -b will increase them further, causing more clipping.

I don't usually want clipping, so I don't use -b.

For basic "developing" of raw images by dcraw, I do the equivalent of IM "-auto-level" but no other tonal adjustment.

Clipping

As an indicator of clipping, the script propIntClipRGB.bat counts how many pixels in each channel are at the lowest value (shadow clipping) or highest value (highlight clipping). As the script does an "-auto-level" to each channel independently, we expect a value of at least one for each clipped.

call %PICTBAT%propIntClipRGB dcr_flat.tiff 
shadowClipped.0=210352
shadowClipped.1=13709
shadowClipped.2=358816
highlightClipped.0=2
highlightClipped.1=3750
highlightClipped.2=7
call %PICTBAT%propIntClipRGB dcr_real.tiff 
shadowClipped.0=268174
shadowClipped.1=26163
shadowClipped.2=1.15202e+06
highlightClipped.0=3671
highlightClipped.1=5
highlightClipped.2=8

Why do we have over a million pixels shadow-clipped? Experiments show that dcraw's "o 1" creates this.

call %PICTBAT%propIntClipRGB dcr_rp.tiff 
shadowClipped.0=268174
shadowClipped.1=26163
shadowClipped.2=1.15202e+06
highlightClipped.0=3671
highlightClipped.1=5
highlightClipped.2=8
call %PICTBAT%propIntClipRGB dcr_rp_sd.tiff 
shadowClipped.0=268174
shadowClipped.1=26163
shadowClipped.2=1.15202e+06
highlightClipped.0=3682
highlightClipped.1=5
highlightClipped.2=8
call %PICTBAT%propIntClipRGB dcr_flat_nc.tiff 
shadowClipped.0=210352
shadowClipped.1=13709
shadowClipped.2=358816
highlightClipped.0=1
highlightClipped.1=1
highlightClipped.2=1
call %PICTBAT%propIntClipRGB dcr_real_nc.tiff 
shadowClipped.0=268050
shadowClipped.1=26187
shadowClipped.2=1.15033e+06
highlightClipped.0=1
highlightClipped.1=1
highlightClipped.2=1
call %PICTBAT%propIntClipRGB dcr_rpnc_sd.tiff 
shadowClipped.0=268050
shadowClipped.1=26187
shadowClipped.2=1.15033e+06
highlightClipped.0=1
highlightClipped.1=1
highlightClipped.2=1

Use IM for sRGB conversion

The above development works well, except for the shadow clipping. This seems to be caused by dcraw's conversion to sRGB. So, we can do that in IM instead. This changes the process from the second dcraw onwards, but we show the complete process for clarity.

set dcrParams=-v -B 0 -6 -T -o 0 -g 1 1

%DCRAW% %dcrParams% -r 1 1 1 1 -O dcr_flat_nc2.tiff %SRCNEF%

call %PICTBAT%24card dcr_flat_nc2.tiff dcr_flat_nc2_XX.png

call %PICTBAT%whiteMult dcr_flat_nc2_mat.png dcr_wm2

set dcr_wm2 
dcr_wm2_b=0.4306858930342565
dcr_wm2_g=0.6093537804226749
dcr_wm2_mb=1.414844995571302
dcr_wm2_mr=2.011383096605218
dcr_wm2_r=0.3029526207370107
%DCRAW% %dcrParams% -r %dcr_wm2_mr% 1 %dcr_wm2_mb% 1 -O dcr_real_nc2.tiff %SRCNEF%

call %PICTBAT%24card dcr_real_nc2.tiff dcr_real_nc2_XX.png

call %PICTBAT%grayPoly dcr_real_nc2_mat.png dcr_gp2

set dcr_gp2 
dcr_gp2_polyb=-0.1660481056765132,1.084244140875546,-0.001469736138722338
dcr_gp2_polyg=-0.01858332659971874,1.0087915315815,0.0001250062521560949
dcr_gp2_polyr=0.1198562568555971,0.940842675243889,0.0003073658570344404
%IM7DEV%magick ^
  dcr_real_nc2.tiff ^
  -channel R -function Polynomial %dcr_gp2_polyr% ^
  -channel G -function Polynomial %dcr_gp2_polyg% ^
  -channel B -function Polynomial %dcr_gp2_polyb% ^
  +channel ^
  -set colorspace RGB ^
  -colorspace sRGB ^
  -auto-level ^
  -process 'setmnsd sd 0.166667 d incOnly v' ^
  +write dcr_rpnc2_sd.tiff ^
  %WEBSIZE% ^
  -quality 40 ^
  dcr_rpnc2_sd_sm.jpg 
setmnsd options:  verbose  sdGoal 0.166667  tolerance 1e-05  mid mean  initCon0 1e-07  initCon2 30  direction incOnlydecOnlyboth
input: mean=0.19257008 sd=0.11061397
nIter=19
result: mean=0.28864945  sd=0.1666758
setmnsd command: -sigmoidal-contrast 4.167876,19.257008%

Here is the result:

dcr_rpnc2_sd_sm.jpg

dcr_rpnc2_sd_sm.jpg
call %PICTBAT%propIntClipRGB dcr_flat_nc2.tiff 
shadowClipped.0=210352
shadowClipped.1=13709
shadowClipped.2=358816
highlightClipped.0=1
highlightClipped.1=1
highlightClipped.2=1
call %PICTBAT%propIntClipRGB dcr_real_nc2.tiff 
shadowClipped.0=84011
shadowClipped.1=15090
shadowClipped.2=220208
highlightClipped.0=1
highlightClipped.1=2
highlightClipped.2=1
call %PICTBAT%propIntClipRGB dcr_rpnc2_sd.tiff 
shadowClipped.0=84011
shadowClipped.1=15090
shadowClipped.2=220208
highlightClipped.0=1
highlightClipped.1=2
highlightClipped.2=1

Future

The "-B 0" option works well, stretching values upwards to use the full 16-bit range without clipping. Or we can allow any proportion of pixels to clip. But what proportion is reasonable? I think 1% is far too high, but how can we determine the right amount? One possibility: any pixel that is already at maximum in at least one channel, and has high values in the other channels, can be safely clipped.

Aside: dcraw contains code, in function colorcheck(), that does some processing with pixels of a 24-colour card in an image. The coordinates of each patch (w, h, x, y) should be put in the array cut[24][4], but the only way of doing this is to modify the dcraw source code. Weird.

Scripts

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

whiteMult.bat

rem From bottom-left pixel of image %1,
rem calculates multipliers green/red and green/blue.
rem Sets environment variables prefixed by %2.
@rem
@rem Updated:
@rem   18-August-2022 for IM v7.
@rem

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 wm

set PREF=%2
if "%PREF%"=="." set PREF=
if "%PREF%"=="" set PREF=whiteMult


set r=

set FMT=^
r=%%[fx:mean.r]\n^
g=%%[fx:mean.g]\n^
b=%%[fx:mean.b]\n^
mr=%%[fx:mean.g/mean.r]\n^
mb=%%[fx:mean.g/mean.b]\n

for /F "usebackq" %%L in (`%IMG7%magick ^
  -precision 16 ^
  %INFILE% ^
  -gravity SouthWest -crop 1x1+0+0 +repage ^
  -format "%FMT%" ^
  info:`) do set %%L

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

echo %PREF%_r=%r% %PREF%_g=%g% %PREF%_b=%b% 
echo %PREF%_mr=%mr% %PREF%_mb=%mb%

call echoRestore

@endlocal & set %PREF%_r=%r%&set %PREF%_g=%g%&set %PREF%_b=%b%& set %PREF%_mr=%mr%&set %PREF%_mb=%mb%

grayPoly.bat

rem From botom row of image %1, assuming pixels should be neutral grays,
rem calculates channel polynomials.
rem Sets environment variables prefixed by %2.
@rem
@rem Updated:
@rem   20-August-2022 for IM v7.
@rem

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 gp

set PREF=%2
if "%PREF%"=="." set PREF=
if "%PREF%"=="" set PREF=grayPoly


set polyr=
for /F "usebackq tokens=1,2 delims==" %%A in (`%IM7DEV%magick ^
  %INFILE% ^
  -gravity South -crop x1+0+0 +repage ^
  ^( +clone ^
     -colorspace Gray ^
  ^) ^
  -precision 16 ^
  -process 'cols2mat method NoCrossPoly noTrans f stdout' ^
  NULL:`) do (
  if "%%A"=="PolyRed" set polyr=%%B
  if "%%A"=="PolyGreen" set polyg=%%B
  if "%%A"=="PolyBlue" set polyb=%%B
)
if "%mypolyr%"=="" exit /B 1

echo %PREF%_polyr=%polyr% 
echo %PREF%_polyg=%polyg% 
echo %PREF%_polyb=%polyb% 

call echoRestore

@endlocal & set %PREF%_polyr=%polyr%&set %PREF%_polyg=%polyg%&set %PREF%_polyb=%polyb%

propIntClipRGB.bat

@rem From image %1,
@rem after auto-levelling each channel independently,
@rem finds number of pixels that have 0 or 100% in each channel.
@rem
@rem Updated:
@rem   20-August-2022 for IM v7.
@rem

@%IMG7%magick ^
  %1 ^
  -channel RGB ^
  -auto-level ^
  -separate ^
  +channel ^
  ( -clone 0-2 ^
    -fill White +opaque Black ^
    -negate ^
    -format "shadowClipped.%%s=%%[fx:int(mean*w*h+0.5)]\n" ^
    +write info: ^
    -delete 0-2 ^
  ) ^
  -fill Black +opaque White ^
  -format "highlightClipped.%%s=%%[fx:int(mean*w*h+0.5)]\n" ^
  info:

dcrawag.diff

--- dcraw.c	2017-11-06 23:02:40.011105100 +0000
+++ dcrawimg.c	2018-05-17 10:50:13.685703600 +0100
@@ -2,6 +2,8 @@
    dcraw.c -- Dave Coffin's raw photo decoder
    Copyright 1997-2016 by Dave Coffin, dcoffin a cybercom o net
 
+   [This file has been slightly hacked by Alan Gibson.]
+
    This is a command-line ANSI C program to convert raw photos from
    any digital camera on any computer running any operating system.
 
@@ -21,6 +23,9 @@
 
    $Revision: 1.477 $
    $Date: 2016/05/10 21:30:43 $
+
+   Updates (by Alan Gibson):
+     17-March-2018 Added "-R" option.
  */
 
 #define DCRAW_VERSION "9.27"
@@ -28,6 +33,8 @@
 #ifndef _GNU_SOURCE
 #define _GNU_SOURCE
 #endif
+#define WIN32_LEAN_AND_MEAN
+#define VC_EXTRALEAN
 #define _USE_MATH_DEFINES
 #include <ctype.h>
 #include <errno.h>
@@ -42,7 +49,7 @@
 #include <time.h>
 #include <sys/types.h>
 
-#if defined(DJGPP) || defined(__MINGW32__)
+#if defined(DJGPP) || defined(__MINGW32__) || defined(WIN32) || defined(WIN64)
 #define fseeko fseek
 #define ftello ftell
 #else
@@ -68,6 +75,10 @@
 typedef unsigned long long UINT64;
 #endif
 
+#if !defined(M_PI)
+#  define M_PI 3.14159265358979323846
+#endif
+
 #ifdef NODEPS
 #define NO_JASPER
 #define NO_JPEG
@@ -104,6 +115,7 @@
 FILE *ifp, *ofp;
 short order;
 const char *ifname;
+const char *outfile = NULL;
 char *meta_data, xtrans[6][6], xtrans_abs[6][6];
 char cdesc[5], desc[512], make[64], model[64], model2[64], artist[64];
 float flash_used, canon_ev, iso_speed, shutter, aperture, focal_len;
@@ -140,6 +152,9 @@
 void (*write_thumb)(), (*write_fun)();
 void (*load_raw)(), (*thumb_load_raw)();
 jmp_buf failure;
+double BrightThr=0.0001;
+int noNormWBM=0;
+float wbmDiv=1.0;
 
 struct decode {
   struct decode *branch[2];
@@ -4247,11 +4262,18 @@
 	dmax = pre_mul[c];
   }
   if (!highlight) dmax = dmin;
+  if (noNormWBM) dmax = wbmDiv;
+  if (verbose) {
+    fprintf (stderr,
+      _("Multipliers pre-norm: ") );
+    FORCC fprintf (stderr, " %f", pre_mul[c]);
+    fputc ('\n', stderr);
+  }
   FORC4 scale_mul[c] = (pre_mul[c] /= dmax) * 65535.0 / maximum;
   if (verbose) {
     fprintf (stderr,
       _("Scaling with darkness %d, saturation %d, and\nmultipliers"), dark, sat);
-    FORC4 fprintf (stderr, " %f", pre_mul[c]);
+    FORCC fprintf (stderr, " %f", pre_mul[c]);
     fputc ('\n', stderr);
   }
   if (filters > 1000 && (cblack[4]+1)/2 == 1 && (cblack[5]+1)/2 == 1) {
@@ -9538,6 +9560,21 @@
 	for (out_cam[i][j] = k=0; k < 3; k++)
 	  out_cam[i][j] += out_rgb[output_color-1][i][k] * rgb_cam[k][j];
   }
+
+  if (verbose) {
+    fprintf (stderr, "rgb_cam: ");
+    for (i=0; i < 3; i++)
+      for (j=0; j < colors; j++)
+        fprintf (stderr, " %g", rgb_cam[i][j]);
+    fprintf (stderr, "\n");
+
+    fprintf (stderr, "out_cam: ");
+    for (i=0; i < 3; i++)
+      for (j=0; j < colors; j++)
+        fprintf (stderr, " %g", out_cam[i][j]);
+    fprintf (stderr, "\n");
+  }
+
   if (verbose)
     fprintf (stderr, raw_color ? _("Building histograms...\n") :
 	_("Converting to %s colorspace...\n"), name[output_color-1]);
@@ -9794,7 +9831,7 @@
   int c, row, col, soff, rstep, cstep;
   int perc, val, total, white=0x2000;
 
-  perc = width * height * 0.01;		/* 99th percentile white level */
+  perc = width * height * BrightThr;		/* eg 0.01 for 99th percentile white level */
   if (fuji_width) perc /= 2;
   if (!((highlight & ~2) || no_auto_bright))
     for (white=c=0; c < colors; c++) {
@@ -9802,6 +9839,12 @@
 	if ((total += histogram[c][val]) > perc) break;
       if (white < val) white = val;
     }
+
+  if (verbose)
+    fprintf (stderr,_("bright=%f, gamma_curve imax=%i\n"),
+      bright,
+      (int)floor((white << 3)/bright));
+
   gamma_curve (gamm[0], gamm[1], 2, (white << 3)/bright);
   iheight = height;
   iwidth  = width;
@@ -9862,6 +9905,7 @@
   if (argc == 1) {
     printf(_("\nRaw photo decoder \"dcraw\" v%s"), DCRAW_VERSION);
     printf(_("\nby Dave Coffin, dcoffin a cybercom o net\n"));
+    printf(_("slightly hacked by Alan Gibson v2.0\n"));
     printf(_("\nUsage:  %s [OPTION]... [FILE]...\n\n"), argv[0]);
     puts(_("-v        Print verbose messages"));
     puts(_("-c        Write image data to standard output"));
@@ -9873,8 +9917,10 @@
     puts(_("-a        Average the whole image for white balance"));
     puts(_("-A <x y w h> Average a grey box for white balance"));
     puts(_("-r <r g b g> Set custom white balance"));
+    puts(_("-R n      Divisor for white balance multipliers"));
     puts(_("+M/-M     Use/don't use an embedded color matrix"));
     puts(_("-C <r b>  Correct chromatic aberration"));
+    puts(_("-O <file> Write output to this file"));
     puts(_("-P <file> Fix the dead pixels listed in this file"));
     puts(_("-K <file> Subtract dark frame (16-bit raw PGM)"));
     puts(_("-k <num>  Set the darkness level"));
@@ -9891,6 +9937,7 @@
     puts(_("-D        Document mode without scaling (totally raw)"));
     puts(_("-j        Don't stretch or rotate raw pixels"));
     puts(_("-W        Don't automatically brighten the image"));
+    puts(_("-B <num>  Clipping threshold for auto-brighten (default = 0.0001)"));
     puts(_("-b <num>  Adjust brightness (default = 1.0)"));
     puts(_("-g <p ts> Set custom gamma curve (default = 2.222 4.5)"));
     puts(_("-q [0-3]  Set the interpolation quality"));
@@ -9918,6 +9965,10 @@
       case 'b':  bright      = atof(argv[arg++]);  break;
       case 'r':
 	   FORC4 user_mul[c] = atof(argv[arg++]);  break;
+      case 'R':
+           noNormWBM = 1;
+           wbmDiv = atof(argv[arg++]);
+           break;
       case 'C':  aber[0] = 1 / atof(argv[arg++]);
 		 aber[2] = 1 / atof(argv[arg++]);  break;
       case 'g':  gamm[0] =     atof(argv[arg++]);
@@ -9943,6 +9994,7 @@
 #endif
 	break;
       case 'P':  bpfile     = argv[arg++];  break;
+      case 'O':  outfile    = argv[arg++];  break;
       case 'K':  dark_frame = argv[arg++];  break;
       case 'z':  timestamp_only    = 1;  break;
       case 'e':  thumbnail_only    = 1;  break;
@@ -9965,6 +10017,7 @@
       case '4':  gamm[0] = gamm[1] =
 		 no_auto_bright    = 1;
       case '6':  output_bps       = 16;  break;
+      case 'B':  BrightThr        = atof(argv[arg++]);  break;
       default:
 	fprintf (stderr,_("Unknown option \"-%c\".\n"), opt);
 	return 1;
@@ -10226,7 +10279,10 @@
     if (write_to_stdout)
       strcpy (ofname,_("standard output"));
     else {
-      strcpy (ofname, ifname);
+      if (outfile)
+        strcpy (ofname,outfile);
+      else {
+        strcpy (ofname, ifname);
       if ((cp = strrchr (ofname, '.'))) *cp = 0;
       if (multi_out)
 	sprintf (ofname+strlen(ofname), "_%0*d",
@@ -10234,6 +10290,7 @@
       if (thumbnail_only)
 	strcat (ofname, ".thumb");
       strcat (ofname, write_ext);
+      }
       ofp = fopen (ofname, "wb");
       if (!ofp) {
 	status = 1;

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

%IMG7%magick -version
Version: ImageMagick 7.1.0-42 Q16-HDRI x64 396d87c:20220709 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenCL 
Delegates (built-in): bzlib cairo freetype gslib heic jng jp2 jpeg jxl lcms lqr lzma openexr pangocairo png ps raqm raw rsvg tiff webp xml zip zlib
Compiler: Visual Studio 2022 (193231332)
%DCRAW% | head -n 4 
 Raw photo decoder "dcraw" v9.27
by Dave Coffin, dcoffin a cybercom o net
slightly hacked by Alan Gibson v2.0

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

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


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 9-November-2017.

Page created 21-Aug-2022 06:54:45.

Copyright © 2022 Alan Gibson.