snibgo's ImageMagick pages

Gain and bias

We can tweak an image to match the brightness (and colour) and contrast of another.

This technique is frequently mentioned, and is described in detail (but only in the context of opaque images) in Optimized Hierarchical Block Matching for Fast and Accurate Image Registration, Changsoo Je, Hyung-Min Park, 2013.

This simple method applies linear transformations to the colour channels of an image to make the mean and standard deviation of each channel match those of a second image. It works for opaque images, and images with transparency. It is less precise, but also simpler and quicker, than alternatives such as matching histograms and blending pyramids. The change to the histogram is less extreme than we get from matching histograms, and can be more appropriate when the histograms of the two images are not really related.

The gain and bias method changes the image to a given mean and standard deviation. A simpler alternative is to change only the mean; see Setting the mean.

The method

Suppose we want to change image A to match image B. For each of the colour channels we can apply a linear transformation:

Vout = VA * gain + bias

where VA is a pixel value in image A, Vout is the resulting pixel value, and:

gain = SDB / SDA

bias = meanB - meanA * gain

SD is the standard deviation. We can use the results, gain and bias, as two arguments to "-function Polynomial".

The inverse is simple:

VA = (Vout - bias) / gain
VA = Vout/gain - bias/gain

By doing this process to three colour channels independently, we get a colour shift. Alternatively, we can apply it just to the L channel of the CIELab colorspace, or to the aggregated colour channels.

ASIDE: This is essentially the same normalisation that is performed for Normalised Cross Correlation. For NCC, pixels values are normalised by adjusting for the means and SD of the entire image:

normA = (VA - meanA) / SDA

An entire image can be normalised in this way, but values will be outside 0 to 100%. An ordinary photograph might have VA ranging from 0.0 to 1.0, with mean = 0.5 and SD = 0.16. Resulting pixel values will range from -3.125 to +3.125.

Worked example

As example images, we use:

set SRCA=dpt_lvs_sm.jpg
dpt_lvs_sm.jpg
set SRCB=toes_holed.png

This image is mostly transparent.

toes_holed.png

ImageMagick directly gives us the mean and standard deviation of opaque images:

set FMT=^
MN_R=%%[fx:mean.r]\n^
MN_G=%%[fx:mean.g]\n^
MN_B=%%[fx:mean.b]\n^
SD_R=%%[fx:standard_deviation.r]\n^
SD_G=%%[fx:standard_deviation.g]\n^
SD_B=%%[fx:standard_deviation.b]\n

%IM%convert ^
  %SRCA% ^
  -format "%FMT%" ^
  info: 
MN_R=0.477337
MN_G=0.32723
MN_B=0.194844
SD_R=0.24988
SD_G=0.195016
SD_B=0.146462

For images with transparency, it isn't so easy. We find the numbers for the second source: first for the entire image, then for just the top-left corner.

%IM%convert ^
  %SRCB% ^
  -format "entire:\n%FMT%" ^
  +write info: ^
  -crop 20x20+0+0 +repage ^
  -format "\ntop-left:\n%FMT%" ^
  info: 
entire:
MN_R=0.164059
MN_G=0.174321
MN_B=0.138335
SD_R=0.218871
SD_G=0.228798
SD_B=0.18428

top-left:
MN_R=0.375216
MN_G=0.418598
MN_B=0.317598
SD_R=0.0494502
SD_G=0.020334
SD_B=0.0516237

For the transparent pixels, IM has used black, with zero values, so the means for the entire image are lower than just the corner, and the SDs are higher.

By scaling to 1x1, IM can give us accurate means:

%IM%convert ^
  %SRCB% ^
  -scale "1x1^!" ^
  -format "entire:\n%FMT%" ^
  info: 
entire:
MN_R=0.435477
MN_G=0.462867
MN_B=0.36733
SD_R=0
SD_G=0
SD_B=0

These means are much closer to the ones for the top-left corner. Of course, scaling to 1x1 has removed information for the SD. The SD is the square root of the variance, which is the mean of the squares of the values less the square of the mean. We can do all this in IM.

%IM%convert ^
  %SRCB% ^
  ( -clone 0 ^
    -evaluate Pow 2 ^
    -scale "1x1^!" ^
  ) ^
  ( -clone 0 ^
    -scale "1x1^!" ^
    -format "mn_R=%%[fx:mean.r]\nmn_G=%%[fx:mean.g]\nmn_B=%%[fx:mean.b]\n" ^
    +write info: ^
    -evaluate Pow 2 ^
  ) ^
  -delete 0 ^
  -alpha off ^
  -compose MinusSrc -composite ^
  -evaluate Pow 0.5 ^
  -format "sd_R=%%[fx:mean.r]\nsd_G=%%[fx:mean.g]\nsd_B=%%[fx:mean.b]\n" ^
  info: 
mn_R=0.435477
mn_G=0.462867
mn_B=0.36733
sd_R=0.0935073
sd_G=0.0724498
sd_B=0.0770428

We put this in the simple script meanSdTr.bat.

call %PICTBAT%meanSdTr %SRCA% SRC_A_
set SRC_A_ 
SRC_A_mn_B=0.19484245059891661
SRC_A_mn_G=0.32722972457465477
SRC_A_mn_R=0.47733272297245749
SRC_A_sd_B=0.14647135118638896
SRC_A_sd_G=0.19504081788357366
SRC_A_sd_R=0.2498817425803006
call %PICTBAT%meanSdTr %SRCB% SRC_B_
set SRC_B_ 
SRC_B_mn_B=0.36733043411917299
SRC_B_mn_G=0.46286717021438928
SRC_B_mn_R=0.43547722590981919
SRC_B_sd_B=0.077042801556420237
SRC_B_sd_G=0.072449835965514617
SRC_B_sd_R=0.093507286182955673

These are listed in alphabetical order.

The script calcGainBias.bat then uses those environment variables to calculate gain and bias:

call %PICTBAT%calcGainBias SRC_A_ SRC_B_ A2B_
set A2B_ 
A2B_bias_B=0.26484480717116488
A2B_bias_G=0.34131446075730421
A2B_bias_R=0.25685638282388734
A2B_gain_B=0.52599229086363164
A2B_gain_G=0.37145986543576909
A2B_gain_R=0.37420615534929164

We can plug these gain and bias numbers into -function Polynomial:

%IM%convert ^
  %SRCA% ^
  -channel R ^
    -function Polynomial !A2B_gain_R!,!A2B_bias_R! ^
  -channel G ^
    -function Polynomial !A2B_gain_G!,!A2B_bias_G! ^
  -channel B ^
    -function Polynomial !A2B_gain_B!,!A2B_bias_B! ^
  +channel ^
  gb_a2b.png
gb_a2b.pngjpg

We can see the image has acquired a green cast and the contrast has lowered to roughly match the grass.

Check the statistics of the result:

call %PICTBAT%meanSdTr gb_a2b.png A2B_out_
set A2B_out_ 
A2B_out_mn_B=0.36739147020675977
A2B_out_mn_G=0.46282139314869919
A2B_out_mn_R=0.43546196688792249
A2B_out_sd_B=0.076951247425040059
A2B_out_sd_G=0.072449835965514617
A2B_out_sd_R=0.093583581292439155

These numbers agree with the desired numbers, given above as SRC_B_mn_B etc.

We glue it all together in a single script imgGainBias.bat that directly transforms an image to match another.

Make the leaves look like the grass.

call %PICTBAT%imgGainBias ^
  %SRCA% %SRCB% gb_1to2.png
gb_1to2.pngjpg

Make the grass look like the leaves.

call %PICTBAT%imgGainBias ^
  %SRCB% %SRCA% gb_2to1.png
gb_2to1.pngjpg

For direct comparison:

Composite the grass over the grassy-leaves.

%IM%convert ^
  gb_1to2.png ^
  %SRCB% ^
  -composite ^
  gb_comp_b.png

This is successful.

gb_comp_b.pngjpg

Composite the leavsey-grass over the leaves.

%IM%convert ^
  %SRCA% ^
  gb_2to1.png ^
  -composite ^
  gb_comp_a.png

This is less successful.

gb_comp_a.pngjpg

We can do the work in L*a*b* space instead of sRGB:

Make the grass look like the leaves, via Lab.

%IM%convert %SRCA% -colorspace Lab -set colorspace sRGB gb_labA.miff
%IM%convert %SRCB% -colorspace Lab -set colorspace sRGB gb_labB.miff

call %PICTBAT%imgGainBias gb_labB.miff gb_labA.miff gb_2to1lab.png

%IM%convert ^
  gb_2to1lab.png ^
  -set colorspace Lab -colorspace sRGB ^
  gb_2to1lab.png
gb_2to1lab.pngjpg

Common standard

Instead of transforming one image to match another, we can match any number of images to a common standard. For example, we set the L channel of Lab versions to a mean of 0.5 and a standard deviation of 0.1667.

call %PICTBAT%imgMnSdL ^
  %SRCA% ^
  0.5 0.1667 ^
  gb_lvs_ms.png
gb_lvs_ms.pngjpg
call %PICTBAT%imgMnSdL ^
  %SRCB% ^
  0.5 0.1667 ^
  gb_toes_ms.png
gb_toes_ms.pngjpg

Process from a crop

This technique is useful when we want to apply a linear process IM to an entire image, but the statistics for that process should come from a crop of that image.

For example, we might want to process an image such that the central 50% in both directions is normalized. IM's "-normalize" works by building a histogram, finding the cut-off points in the histogram, and stretching values accordingly. The stretching could be done with "-level" or "-function Polynomial", but IM doesn't tell us what the arguments would be. The gain-and-bias technique finds the parameters.

Here, we assume the image is fully opaque so we can use IM to calculate the mean and standard deviation directly, and we assume that we want to adjust the colour channels by the same amount so we don't get a colour shift. We find the mean and standard deviation for the cropped image, and for the normalized cropped image. From those four numbers, we calculate the gain and bias that transforms the crop into the normalized crop.

toes.png

toes.pngjpg
for /F "usebackq" %%L in (`%IM%convert ^
  toes.png ^
  -gravity Center -crop 50%%x50%%+0+0 +repage ^
  -format "MN0=%%[fx:mean]\nSD0=%%[fx:standard_deviation]\n" ^
  +write info: ^
  -normalize ^
+write gb_test.png ^
  -format "MN1=%%[fx:mean]\nSD1=%%[fx:standard_deviation]\n" ^
  info:`) do set %%L

for /F "usebackq" %%L in (`%IM%identify ^
  -format ^
    "GAIN=%%[fx:%SD1%/%SD0%]\nBIAS=%%[fx:%MN1%-%MN0%*%SD1%/%SD0%]"
  xc:`) do set %%L

%IM%convert ^
  toes.png ^
  -function Polynomial %GAIN%,%BIAS% ^
  gb_toes_norm.png
gb_toes_norm.pngjpg

Writing to gb_test.png isn't necessary, but can be useful to check the crop and normalize do what we want.

gb_test.png

gb_test.pngjpg

Gaussian standard deviation

As an aside from gain and bias, the method of deriving standard deviation from first principles can solve another problem.

The usual "-statistic StandardDeviation" generates blocky results. As the rectangular window moves over the image, individual pixels jump from taking no part in the calculation, to being as important as every other pixel in the rectangle. To show the problem we create a sample image with high contrast between one pixel and the others. We enlarge so we can see it.

A sample image.

%IM%convert ^
  xc:White ^
  -gravity Center -bordercolor Black -border 3 ^
  gb_one.png

call %PICTBAT%blockPix gb_one.png
gb_one_bp.png

We apply the usual "-statistic StandardDeviation" to that image. The maximum possible SD is 0.5 (i.e. 50%), so we multiply by 2 for clarity without clipping.

%IM%convert ^
  gb_one.png ^
  -statistic StandardDeviation 3x3 ^
  -evaluate Multiply 2 ^
  gb_one_sd.png

call %PICTBAT%blockPix gb_one_sd.png
gb_one_sd_bp.png

In the result, nine pixels have the same standard deviation.

In the first-principle SD, we use a blur instead of mean for the calculation. (Strictly speaking "-gaussian-blur" is needed for proper Gaussian, but "-blur" is quicker and almost exactly the same.) When the window moves over the window, an individual pixel gradually transitions from taking no part in the calculation to taking full part.

set sBLUR=-blur 0x0.3

%IM%convert ^
  gb_one.png ^
  ( -clone 0 ^
    -evaluate Pow 2 ^
    %sBLUR% ^
  ) ^
  ( -clone 0 ^
    %sBLUR% ^
    -evaluate Pow 2 ^
  ) ^
  -delete 0 ^
  -alpha off ^
  -compose MinusSrc -composite ^
  -evaluate Pow 0.5 ^
  -evaluate Multiply 2 ^
  gb_one_gsd.png

call %PICTBAT%blockPix gb_one_gsd.png

The SD has a clear maximum.

gb_one_gsd_bp.png

We implement this in a script gaussStdDev.bat.

In the following example, we also -auto-level for visual clarity.

call %PICTBAT%gaussStdDev ^
  toes.png gb_toes_gsd.png ^
  "-blur 0x0.3" "-auto-level"
gb_toes_gsd.pngjpg

("Gaussian standard deviation" is my name for this operation, to distinguish it from the usual IM operation.)

Scripts

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

meanSd.bat

This script is included for completness, but meanSdTr.bat is always preferred.

rem From opaque image %1,
rem calculates mean and standard deviation.
rem Prefixes output variable names with %2.
rem
rem If image has transparency, this gives the WRONG results.
rem For images with transparency, meanSdTr.bat should be used.
rem
rem meanSdTr.bat is also faster than this script.

@for /F "usebackq" %%L in (`%IM%convert ^
  %1 ^
  -precision 19 ^
  -format "%2mn_R=%%[fx:mean.r]\n%2mn_G=%%[fx:mean.g]\n%2mn_B=%%[fx:mean.b]\n" ^
  +write info: ^
  -format "%2sd_R=%%[fx:standard_deviation.r]\n%2sd_G=%%[fx:standard_deviation.g]\n%2sd_B=%%[fx:standard_deviation.b]\n" ^
  info:`) do set %%L

meanSdTr.bat

rem From image %1 with transparency,
rem calculates mean and standard deviation.
rem Prefixes output variable names with %2.

@for /F "usebackq" %%L in (`%IM%convert ^
  %1 ^
  -precision 19 ^
  ^( -clone 0 ^
    -evaluate Pow 2 ^
    -scale "1x1^!" ^
  ^) ^
  ^( -clone 0 ^
    -scale "1x1^!" ^
    -format "%2mn_R=%%[fx:mean.r]\n%2mn_G=%%[fx:mean.g]\n%2mn_B=%%[fx:mean.b]\n" ^
    +write info: ^
    -evaluate Pow 2 ^
  ^) ^
  -delete 0 ^
  -alpha off ^
  -compose MinusSrc -composite ^
  -evaluate Pow 0.5 ^
  -format "%2sd_R=%%[fx:mean.r]\n%2sd_G=%%[fx:mean.g]\n%2sd_B=%%[fx:mean.b]\n" ^
  info:`) do set %%L

calcGainBias.bat

rem From prefixes %1 and %2, calculates gain and bias to transform image 1 to be like 2.
rem Prefixes output variable names with %3.
rem If an SD==0, the script will attempt to divide by zero.
rem
rem Values for %1 and %2 can come from setEnvGainBias.bat.

rem This assumes "setlocal enabledelayedexpansion" has been set.

set cgbTEST=abc

if not "!cGBTEST!"=="abc" exit /B 1

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

for /F "usebackq" %%L in (`%IM%identify ^
  -precision 19 ^
  -format "%3gain_R=%%[fx:!%2sd_R!/!%1sd_R!]\n%3gain_G=%%[fx:!%2sd_G!/!%1sd_G!]\n%3gain_B=%%[fx:!%2sd_B!/!%1sd_B!]\n" ^
  xc:`) do set %%L

for /F "usebackq" %%L in (`%IM%identify ^
  -precision 19 ^
  -format "%3bias_R=%%[fx:!%2mn_R!-!%1mn_R!*!%3gain_R!]\n%3bias_G=%%[fx:!%2mn_G!-!%1mn_G!*!%3gain_G!]\n%3bias_B=%%[fx:!%2mn_B!-!%1mn_B!*!%3gain_B!]\n" ^
  xc:`) do set %%L

imgGainBias.bat

rem From image %1 and reference image %2
rem applies gain and bias to match means and SD of reference.
rem Output to %3.

call %PICTBAT%meanSdTr %1 igb1_
call %PICTBAT%meanSdTr %2 igb2_

call %PICTBAT%calcGainBias igb1_ igb2_ igb_gb_

%IM%convert ^
  %1 ^
  -channel R -function Polynomial !igb_gb_gain_R!,!igb_gb_bias_R! ^
  -channel G -function Polynomial !igb_gb_gain_G!,!igb_gb_bias_G! ^
  -channel B -function Polynomial !igb_gb_gain_B!,!igb_gb_bias_B! ^
  +channel ^
  %3

gaussStdDev.bat

rem Given image %1,
rem writes %2 as Gaussian standard deviation,
rem with operation %3, default "-blur 0x1".
rem %4 is post-processing, eg "-auto-level".

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

@setlocal

rem @call echoOffSave

call %PICTBAT%setInOut %1 gsd

if not "%2"=="" if not "%2"=="." set OUTFILE=%2

set sBLUR=%~3
if "%sBLUR%"=="." set sBLUR=
if "%sBLUR%"=="" set sBLUR=-blur 0x1

set sPOST=%~4
if "%sPOST%"=="." set sPOST=

%IM%convert ^
  %INFILE% ^
  ( -clone 0 ^
    -evaluate Pow 2 ^
    %sBLUR% ^
  ) ^
  ( -clone 0 ^
    %sBLUR% ^
    -evaluate Pow 2 ^
  ) ^
  -delete 0 ^
  -alpha off ^
  -compose MinusSrc -composite ^
  -evaluate Pow 0.5 ^
  %sPOST% ^
  %OUTFILE%

call echoRestore

endlocal & set gsdOUTFILE=%OUTFILE%


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

%IM%convert -version
Version: ImageMagick 6.9.5-3 Q16 x86 2016-07-22 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2015 ImageMagick Studio LLC
License: http://www.imagemagick.org/script/license.php
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

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


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.1 26-Oct-2016.

Page created 26-Oct-2016 21:41:48.

Copyright © 2016 Alan Gibson.