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, HyungMin 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.
See also Set mean and stddev, which transforms with sigmoidalcontrast and evaluate Pow and therefore cannot cause clipping.
These techniques work for ordinary photographs. For graphics images that are mostly areas of flat colour, we want to apply most weight to those colours, and ordinary cluts may not give satisfactory results. For graphics, a "haldclut" solution may be better. See Sparse hald cluts.
Suppose we want to change image A to match image B. For each of the colour channels we want to apply a linear transformation:
V_{out} = V_{A} * gain + bias
where V_{A} is a pixel value in image A, V_{out} is the resulting pixel value, and:
gain = SD_{B} / SD_{A}
bias = mean_{B}  mean_{A} * gain
SD is the standard deviation. We can use the results, gain and bias, as two arguments to "function Polynomial".
The inverse is simple:
V_{A} = (V_{out}  bias) / gain V_{A} = V_{out}/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:
norm_{A} = (V_{A}  mean_{A}) / SD_{A}
An entire image can be normalised in this way, but values will be outside 0 to 100%. An ordinary photograph might have V_{A} 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.
BEWARE: this method can clip pixels. Values near 0 or 100% can be pushed beyond those limits, so they are all clamped at 0 or 100%. If the input has been autolevelled, any gainbias change can make a clipped output. A more sophisticated method uses sigmoidalcontrast to adjust standard deviation (contrast) and evaluate Pow to adjust mean (lightness); see the Set mean and stddev page.
As example images, we use:
set SRCA=dpt_lvs_sm.jpg 

set SRCB=toes_holed.png This image is mostly transparent. 
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 topleft corner.
%IM%convert ^ %SRCB% ^ format "entire:\n%FMT%" ^ +write info: ^ crop 20x20+0+0 +repage ^ format "\ntopleft:\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 topleft: 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 topleft 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 
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 

Make the grass look like the leaves. call %PICTBAT%imgGainBias ^ %SRCB% %SRCA% gb_2to1.png 
For direct comparison:
Composite the grass over the grassyleaves. %IM%convert ^ gb_1to2.png ^ %SRCB% ^ composite ^ gb_comp_b.png This is successful. 

Composite the leavseygrass over the leaves. %IM%convert ^ %SRCA% ^ gb_2to1.png ^ composite ^ gb_comp_a.png This is less successful. 
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 
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 

call %PICTBAT%imgMnSdL ^ %SRCB% ^ 0.5 0.1667 ^ gb_toes_ms.png 
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 cutoff 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 gainandbias 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 

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 
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 
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 
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 
In the result, nine pixels have the same standard deviation.
In the firstprinciple SD, we use a blur instead of mean for the calculation. (Strictly speaking "gaussianblur" is needed for proper Gaussian, but "blur" is quicker and almost exactly the same.) When the window moves over the image, 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. 
We implement this in a script gaussStdDev.bat.
In the following example, we also autolevel for visual clarity.
call %PICTBAT%gaussStdDev ^ toes.png gb_toes_gsd.png ^ "blur 0x0.3" "autolevel" 
("Gaussian standard deviation" is my name for this operation, to distinguish it from the usual IM operation.)
For convenience, .bat scripts are also available in a single zip file. See Zipped BAT files.
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
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
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
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
rem Given image %1, rem writes %2 as Gaussian standard deviation, rem with operation %3, default "blur 0x1". rem %4 is postprocessing, eg "autolevel". @if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal @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.53 Q16 x86 20160722 http://www.imagemagick.org Copyright: Copyright (C) 19992015 ImageMagick Studio LLC License: http://www.imagemagick.org/script/license.php Visual C++: 180040629 Features: Cipher DPC Modules OpenMP Delegates (builtin): 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 recreate 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 republish this page, but only for noncommercial use.
Anyone is permitted to link to this page, including for commercial use.
Page version v1.1 26Oct2016.
Page created 02Aug2017 22:05:41.
Copyright © 2017 Alan Gibson.