snibgo's ImageMagick pages

Clut cookbook

ImageMagick uses a clut (Colour Look Up Table) to transform the colours of an image, channel by channel. From the input image, a red channel pixel value is used as an index into the clut image. The value in the red channel of the clut image is used as the new value for the red channel of the output image. The process is repeated for green and blue.

A clut can also be thought of as variation of an absolute displacement map.

ImageMagick accesses a clut file along its diagonal, from top left to bottom right, so it can be two-dimensional. As a clut file represents a single dimension, this is a waste of space and processing power. So a clut file is generally either vertical (read from the top down, width=1) or horizontal (read from left to right, height=1).

My clut files have one dimension, usually horizontal. They are often monochrome (gray, with no colour).

For convenience in the cookbook, sizes are small, eg 1x100. In practice, they are often larger such as 1x1000 or 1x65536. (Currently, there is no point in using larger cluts.)

As they are only one pixel wide or high, they are difficult to see and I don't bother to include them as images on this page. Instead I put them through a script that makes an image that shows both the tonal values and the numerical values as a graph, or shows a graph of the three colour channels plus a sample. See Scripts below.

Cluts from "-fx"

An "-fx" expression is interpreted for every pixel, so it is slow. But a clut has few pixels so speed is usually not an issue. An "-fx" is easy to understand.

If we start wth a gradient, u will range from zero to one.

Linear

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "u" ^
  cl_fx1.png
call %PICTBAT%graph1d cl_fx1.png
cl_fx1_g1d.png

Half a cycle of sine, sin(x) where 0<x<pi (radians), 0<x<180°

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "sin(pi*u)" ^
  cl_fx2.png
call %PICTBAT%graph1d cl_fx2.png
cl_fx2_g1d.png

Four cycles of sine, sin(x) where 0<x<8.pi (radians)

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "sin(8*pi*u)/2+0.5" ^
  cl_fx3.png
call %PICTBAT%graph1d cl_fx3.png
cl_fx3_g1d.png

Squish function; squish(x)=1.0/(1.0+exp(-x))

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "squish(u)" ^
  cl_fx4.png
call %PICTBAT%graph1d cl_fx4.png
cl_fx4_g1d.png

Flattened ends

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "min(max(u,0.2),0.7)" ^
  cl_fx5.png
call %PICTBAT%graph1d cl_fx5.png
cl_fx5_g1d.png

Quarter circle

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "sqrt(u*(2-u))" ^
  cl_fx6.png
call %PICTBAT%graph1d cl_fx6.png
cl_fx6_g1d.png

Half circle

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "sqrt(u*(4-u*4))" ^
  cl_fx7.png
call %PICTBAT%graph1d cl_fx7.png
cl_fx7_g1d.png

Arctan. See De-barrel distortion.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "atan(u*Pi)/atan(Pi)" ^
  cl_atan.png
call %PICTBAT%graph1d cl_atan.png
cl_atan_g1d.png

Arctan2.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "atan2(u*Pi,1)/atan(Pi,1)" ^
  cl_atan2.png
call %PICTBAT%graph1d cl_atan2.png
cl_atan_g1d.png

Reverse the slope after it peaks at white.

Initial slope has gradient 1.5, so peak is at 1/1.5 = 2/3.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "V=u*1.5;V<1?V:2-V" ^
  cl_fx8.png
call %PICTBAT%graph1d cl_fx8.png
cl_fx8_g1d.png

Linear transformation such that X1 becomes Y1.

set X1=0.2
set Y1=0.6

%IM%convert ^
  -size 1x100 gradient: -rotate 90 ^
  -fx "u<%X1%?u*%Y1%/%X1%:%Y1%+(u-%X1%)*(1-%Y1%)/(1-%X1%)" ^
  cl_linxy.png

call %PICTBAT%graph1d cl_linxy.png
cl_linxy_g1d.png

Staircase with 6 steps. One is black but none is white.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "int(u*6)/6" ^
  cl_fx9.png
call %PICTBAT%graph1d cl_fx9.png
cl_fx9_g1d.png

Staircase with 6 steps, including black and white.

For this, we reduce the numerator by one.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "int(u*6)/5" ^
  cl_fx10.png
call %PICTBAT%graph1d cl_fx10.png
cl_fx10_g1d.png

Staircase by posterize with 6 steps.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -posterize 6 ^
  cl_post1.png
call %PICTBAT%graph1d cl_post1.png
cl_post1_g1d.png

Staircase by posterize with 6 steps, default dithering.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  ( +clone ^
  -posterize 6 -write mpr:STEPS +delete ) ^
  -remap mpr:STEPS ^
  cl_post2.png
call %PICTBAT%graph1d cl_post2.png
cl_post2_g1d.png

Staircase by posterize with 6 steps, Riemersma dithering.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  ( +clone ^
  -posterize 6 -write mpr:STEPS +delete ) ^
  -dither Riemersma ^
  -remap mpr:STEPS ^
  cl_post3.png
call %PICTBAT%graph1d cl_post3.png
cl_post3_g1d.png

Staircase by posterize with 6 steps, Floyd-Steinberg dithering.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  ( +clone ^
  -posterize 6 -write mpr:STEPS +delete ) ^
  -dither FloydSteinberg ^
  -remap mpr:STEPS ^
  cl_post4.png
call %PICTBAT%graph1d cl_post4.png
cl_post4_g1d.png

Random threshold.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -seed 1234 ^
  -random-threshold 25%%,75%% ^
  cl_rt1.png
call %PICTBAT%graph1d cl_rt1.png
cl_rt1_g1d.png

A sigmoidal curve with inflection at (a,b),
slope s at ends, and contrast strength E.

See Luminous Landscape forum: equation for a contrast curve.

set a=0.4
set b=0.5
set s=0.9
set E=3

set a1=(1-%a%)
set s1=(1-%s%)
set sa=%s%*%a%
set sa1=%s%*%a1%
set LR=log(%b%)/log(%a%)
set LR1=log(1-%b%)/log(1-%a%)

for /F "usebackq" %%L in (`%IM%identify
  -precision 15 ^
  -format "a1=%%[fx:%a1%]\ns1=%%[fx:%s1%]\nsa=%%[fx:%sa%]\nsa1=%%[fx:%sa1%]" ^
  xc:`) do set %%L

for /F "usebackq" %%L in (`%IM%identify
  -precision 15 ^
  -format "LR=%%[fx:%LR%]\nLR1=%%[fx:%LR1%]" ^
  xc:`) do set %%L

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "u<%a%?pow(%s1%*u+%sa%*pow(u/%a%,%E%),%LR%):1-pow(%s1%*(1-u)+%sa1%*pow((1-u)/%a1%,%E%),%LR1%)" ^
  cl_fxsig.png
call %PICTBAT%graph1d cl_fxsig.png
cl_fxsig_g1d.png

For the first half of the curve, where u<=a, the formula is:

y = ( (1-s)*u + s*a* (u/a)E )log(b)/log(a)

The second half is similar. When s==1:

y = ( a * (u/a)E )log(b)/log(a)

This method may be useful, so we put it in a script, mSigClut.bat.

call %PICTBAT%mSigClut cl_fxsig2.png 256 0.4 0.5 0.9 3

call %PICTBAT%graph1d cl_fxsig2.png
cl_fxsig2_g1d.png
call %PICTBAT%mSigClut cl_fxsig3.png 256 0.5 0.5 1 3

call %PICTBAT%graph1d cl_fxsig3.png
cl_fxsig3_g1d.png
call %PICTBAT%mSigClut cl_fxsig4.png 256 0.5 0.5 1 0.33333

call %PICTBAT%graph1d cl_fxsig4.png
cl_fxsig4_g1d.png
call %PICTBAT%mSigClut cl_fxsig5.png 256 0.3 0.3 1 0.33333

call %PICTBAT%graph1d cl_fxsig5.png
cl_fxsig5_g1d.png
call %PICTBAT%mSigClut cl_fxsig6.png 256 0.3 0.7 1 0.33333

call %PICTBAT%graph1d cl_fxsig6.png
cl_fxsig6_g1d.png
call %PICTBAT%mSigClut cl_fxsig7.png 256 0.5 0.5 1 4

call %PICTBAT%graph1d cl_fxsig7.png
cl_fxsig7_g1d.png
call %PICTBAT%mSigClut cl_fxsig8.png 256 0.5 0.5 0.5 4

call %PICTBAT%graph1d cl_fxsig8.png
cl_fxsig8_g1d.png
call %PICTBAT%mSigClut cl_fxsig9.png 256 0.5 0.5 0.5 10

call %PICTBAT%graph1d cl_fxsig9.png
cl_fxsig9_g1d.png
call %PICTBAT%mSigClut cl_fxsig10.png 256 0.5 0.5 0.1 10

call %PICTBAT%graph1d cl_fxsig10.png
cl_fxsig10_g1d.png
call %PICTBAT%mSigClut cl_fxsig11.png 256 0.5 0.5 0.1 100

call %PICTBAT%graph1d cl_fxsig11.png
cl_fxsig11_g1d.png

Cluts from evaluate, function etc

Linear

%IM%convert -size 1x100 gradient: -rotate 90 ^
  cl_g.png
call %PICTBAT%graph1d cl_g.png
cl_g_g1d.png

Sigmoid

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -sigmoidal-contrast 5,50%% ^
  cl_sig1.png
call %PICTBAT%graph1d cl_sig1.png
cl_sig1_g1d.png

Sigmoid with greatest slope at x=25%.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -sigmoidal-contrast 5,25%% ^
  cl_sig2.png
call %PICTBAT%graph1d cl_sig2.png
cl_sig2_g1d.png

Sigmoid with greatest slope at x=75%.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -sigmoidal-contrast 5,75%% ^
  cl_sig2a.png
call %PICTBAT%graph1d cl_sig2a.png
cl_sig2a_g1d.png

Inverse sigmoid, with least slope at x=25%.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  +sigmoidal-contrast 5,25%% ^
  cl_sig3.png
call %PICTBAT%graph1d cl_sig3.png
cl_sig3_g1d.png

Inverse sigmoid, with least slope at x=75%.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  +sigmoidal-contrast 5,75%% ^
  cl_sig3a.png
call %PICTBAT%graph1d cl_sig3a.png
cl_sig3a_g1d.png

-Level. The arguments specify the x-values where the line intersects y=0 and y=100%.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -level 10%%,70%% ^
  cl_lev1.png
call %PICTBAT%graph1d cl_lev1.png
cl_lev1_g1d.png

+Level. The arguments specify the y-values where the line intersects x=0 and x=100%.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  +level 10%%,70%% ^
  cl_lev2.png
call %PICTBAT%graph1d cl_lev2.png
cl_lev2_g1d.png

-Level with arguments outside 0 to 100%.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -level -50%%,150%% ^
  cl_lev3.png
call %PICTBAT%graph1d cl_lev3.png
cl_lev3_g1d.png

Flattened top.
Cap each channel at 70%.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -channel RGB ^
  -evaluate Min 70%% ^
  +channel ^
  cl_flattop.png
call %PICTBAT%graph1d cl_flattop.png
cl_flattop_g1d.png

Flattened middle.
For the region centred on 50% plus or minus 20% (ie from 30% to 70%), make it 15% gray.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fuzz 20%% -fill gray(15%%) -opaque gray(50%%) ^
  cl_flat1.png
call %PICTBAT%graph1d cl_flat1.png
cl_flat1_g1d.png

Flattened outer. As above but set everything outside the region.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fuzz 20%% -fill gray(15%%) +opaque gray(50%%) ^
  cl_flat2.png
call %PICTBAT%graph1d cl_flat2.png
cl_flat2_g1d.png

Flattened middle using fx.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fx "u>0.3&&u<0.7?0.15:u" ^
  cl_flat_fx.png
call %PICTBAT%graph1d cl_flat_fx.png
cl_flat_fx_g1d.png

Threshold black.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -black-threshold 40%% ^
  cl_thb.png
call %PICTBAT%graph1d cl_thb.png
cl_thb_g1d.png

Threshold white.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -white-threshold 40%% ^
  cl_thw.png
call %PICTBAT%graph1d cl_thw.png
cl_thw_g1d.png

Threshold a middle range. Turn 70% plus or minus 20%, ie 50% to 90%, to white.

By turning pixels outside the range to black, we avoid difficulties with input white pixels.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -fuzz 20%% ^
  -fill Black +opaque gray(70%%) ^
  -fill White +opaque Black ^
  cl_thm.png
call %PICTBAT%graph1d cl_thm.png
cl_thm_g1d.png

Threshold a middle range, using transparency.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -alpha Opaque ^
  -fuzz 20%% -transparent gray(70%%) ^
  -channel RGB -evaluate set 0 +channel ^
  -background White -layers Flatten ^
  cl_thm2.png
call %PICTBAT%graph1d cl_thm2.png

This is slower than the previous method ...

cl_thm2_g1d.png

... but is readily expanded for multiple ranges.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -alpha Opaque ^
  -fuzz 5%% -transparent gray(25%%) ^
  -fuzz 20%% -transparent gray(70%%) ^
  -channel RGB -evaluate set 0 +channel ^
  -background White -layers Flatten ^
  cl_thm3.png
call %PICTBAT%graph1d cl_thm3.png
cl_thm3_g1d.png

One cycle of sine.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -evaluate sin 1 ^
  cl_sin1.png
call %PICTBAT%graph1d cl_sin1.png
cl_sin1_g1d.png

One cycle of cosine.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -evaluate cos 1 ^
  cl_cos1.png
call %PICTBAT%graph1d cl_cos1.png
cl_cos1_g1d.png

Half a cycle of cosine.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -evaluate cos 0.5 ^
  cl_cos2.png
call %PICTBAT%graph1d cl_cos2.png
cl_cos2_g1d.png

Four cycles of sine.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -evaluate sin 4 ^
  cl_sin4.png
call %PICTBAT%graph1d cl_sin4.png
cl_sin4_g1d.png

Half a cycle of sine.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -evaluate sin 0.5 ^
  cl_sin05.png
call %PICTBAT%graph1d cl_sin05.png
cl_sin05_g1d.png

Half a cycle of sine, offset by -90°.
Gradient is +1 at centre (provided w = pi * h) and zero at ends.

y = amp * sin(2*pi* (freq * x + phase / 360)) + bias

set FREQ=0.5
set PHASE=-90
set AMP=0.5
set BIAS=0.5
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function sinusoid %FREQ%,%PHASE%,%AMP%,%BIAS% ^
  cl_sinus1.png
call %PICTBAT%graph1d cl_sinus1.png
cl_sinus1_g1d.png

Two cycles of sine, offset by +90°.

set FREQ=2
set PHASE=90
set AMP=0.5
set BIAS=0.5
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function sinusoid %FREQ%,%PHASE%,%AMP%,%BIAS% ^
  cl_sinus2.png
call %PICTBAT%graph1d cl_sinus2.png
cl_sinus2_g1d.png

Arcsine.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function arcsin 1 ^
  cl_asin.png
call %PICTBAT%graph1d cl_asin.png
cl_asin_g1d.png

Arcsine. y = range/pi * asin ( 2/width * ( x - center ) ) + bias

Default values.

set WIDTH=1.0
set CENTER=0.5
set RANGE=1.0
set BIAS=0.5
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function arcsin %WIDTH%,%CENTER%,%RANGE%,%BIAS% ^
  cl_asin2.png
call %PICTBAT%graph1d cl_asin2.png
cl_asin2_g1d.png

Arcsine.

Using half the input values.

set WIDTH=0.5
set CENTER=0.5
set RANGE=1.0
set BIAS=0.5
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function arcsin %WIDTH%,%CENTER%,%RANGE%,%BIAS% ^
  cl_asin3.png
call %PICTBAT%graph1d cl_asin3.png
cl_asin3_g1d.png

Arcsine.

Using half the output values.

set WIDTH=1.0
set CENTER=0.5
set RANGE=0.5
set BIAS=0.5
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function arcsin %WIDTH%,%CENTER%,%RANGE%,%BIAS% ^
  cl_asin4.png
call %PICTBAT%graph1d cl_asin4.png
cl_asin4_g1d.png

Arcsine.

set WIDTH=2.0
set CENTER=0.0
set RANGE=2.0
set BIAS=0.0
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function arcsin %WIDTH%,%CENTER%,%RANGE%,%BIAS% ^
  cl_asin5.png
call %PICTBAT%graph1d cl_asin5.png
cl_asin5_g1d.png

Arcsine.

set WIDTH=2.0
set CENTER=1.0
set RANGE=2.0
set BIAS=1.0
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function arcsin %WIDTH%,%CENTER%,%RANGE%,%BIAS% ^
  cl_asin6.png
call %PICTBAT%graph1d cl_asin6.png
cl_asin6_g1d.png

Arctan.

y = range/pi * atan (slope * pi * ( u - center ) ) + bias

set SLOPE=1.0
set CENTER=0.5
set RANGE=1.0
set BIAS=0.5
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function arctan %SLOPE% ^
  cl_atan1.png
call %PICTBAT%graph1d cl_atan1.png
cl_atan1_g1d.png

Quarter of a circle. y = sqrt (-x2 + 2x).

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function Polynomial -1,2,0 -evaluate Pow 0.5 ^
  cl_qtrCircTL.png
call %PICTBAT%graph1d cl_qtrCircTL.png
cl_qtrCircTL_g1d.png

Quarter of a circle. y = sqrt (1-x2).

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function Polynomial -1,0,1 -evaluate Pow 0.5 ^
  cl_qtrCircTR.png
call %PICTBAT%graph1d cl_qtrCircTR.png
cl_qtrCircTR_g1d.png

Quarter of a circle. y = 1-sqrt (-x2 + 2x).

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function Polynomial -1,2,0 -evaluate Pow 0.5 ^
  -negate ^
  cl_qtrCircBL.png
call %PICTBAT%graph1d cl_qtrCircBL.png
cl_qtrCircBL_g1d.png

Quarter of a circle. y = 1-sqrt (1-x2).

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function Polynomial -1,0,1 -evaluate Pow 0.5 ^
  -negate ^
  cl_qtrCircBR.png
call %PICTBAT%graph1d cl_qtrCircBR.png
cl_qtrCircBR_g1d.png

Two quarters of a circle. Gradients at each end are zero. Slope in centre is vertical.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function Polynomial -1,2,0 -evaluate Pow 0.5 ^
  ( -clone 0 -negate -flop -evaluate divide 2 ) ^
  ( -clone 0 -evaluate divide 2 -evaluate add 50%% ) ^
  -delete 0 ^
  +append ^
  cl_qtrCirc2.png
call %PICTBAT%graph1d cl_qtrCirc2.png
cl_qtrCirc2_g1d.png

The same two quarters of a circle, arranged differently.
Slopes at each end are vertical. Slope in centre is horizontal.

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function Polynomial -1,2,0 -evaluate Pow 0.5 ^
  ( -clone 0 -evaluate divide 2 ) ^
  ( -clone 0 -negate -flop ^
    -evaluate divide 2 ^
    -evaluate add 50%% ^
  ) ^
  -delete 0 ^
  +append ^
  cl_qtrCirc3.png
call %PICTBAT%graph1d cl_qtrCirc3.png
cl_qtrCirc3_g1d.png

Parabolic. y = 2.(-x2 + x).

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function Polynomial -4,4,0 ^
  cl_poly1.png
call %PICTBAT%graph1d cl_poly1.png
cl_poly1_g1d.png

Half a circle. Slopes at each end are vertical. y = 2.sqrt (-x2 + x).

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -function Polynomial -4,4,0 -evaluate Pow 0.5 ^
  cl_semiCirc.png
call %PICTBAT%graph1d cl_semiCirc.png
cl_semiCirc_g1d.png

Exponent. y = e(value * x)

%IM%convert -size 1x100 gradient: -rotate 90 ^
  -evaluate Exponential -0.5 ^
  cl_exp1.png
call %PICTBAT%graph1d cl_exp1.png
cl_exp1_g1d.png
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -evaluate Exponential -1.0 ^
  cl_exp2.png
call %PICTBAT%graph1d cl_exp2.png
cl_exp2_g1d.png
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -evaluate Exponential -2.0 ^
  cl_exp3.png
call %PICTBAT%graph1d cl_exp3.png
cl_exp3_g1d.png
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -evaluate Exponential -5.0 ^
  cl_exp4.png
call %PICTBAT%graph1d cl_exp4.png
cl_exp4_g1d.png
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -solarize 50%% ^
  -evaluate Multiply 2 ^
  cl_sol1.png
call %PICTBAT%graph1d cl_sol1.png
cl_sol1_g1d.png
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -solarize 75%% ^
  cl_sol2.png
call %PICTBAT%graph1d cl_sol2.png
cl_sol2_g1d.png
%IM%convert -size 1x100 gradient: -rotate 90 ^
  -negate ^
  -solarize 75%% ^
  -negate ^
  cl_sol3.png
call %PICTBAT%graph1d cl_sol3.png
cl_sol3_g1d.png
%IM%convert -size 1x256 gradient: -rotate 90 ^
  -evaluate cos 1 -evaluate divide 2 ^
  cl_cosamp.png

call %PICTBAT%graph1d cl_cosamp.png
cl_cosamp_g1d.png

Amplitude modulation.

%IM%convert -size 1x256 ^
  ( gradient: -evaluate sine 4 ) ^
  ( gradient: -evaluate cos 1 -evaluate divide 2 ) ^
  -compose Exclusion -composite ^
  -rotate 90 ^
  cl_ampmod.png

call %PICTBAT%graph1d cl_ampmod.png
cl_ampmod_g1d.png

Frequency modulation.

%IM%convert -size 1x256 gradient: -rotate 90 ^
  -evaluate sin 0.5 -auto-level ^
  -evaluate cos 4 ^
  cl_freqmod.png

call %PICTBAT%graph1d cl_freqmod.png
cl_freqmod_g1d.png

Ramps.

%IM%convert -size 1x64 gradient: -rotate 90 ^
  -duplicate 3 ^
  +append ^
  cl_ramp.png

call %PICTBAT%graph1d cl_ramp.png
cl_ramp_g1d.png

y = x ^ (1/2.2).

%IM%convert -size 1x256 gradient: -rotate 90 ^
  -evaluate Pow 0.454545 ^
  cl_pow454545.png

call %PICTBAT%graph1d cl_pow454545.png
cl_pow454545_g1d.png

The RGB => sRGB transformation, which is approximately y = x ^ (1/2.2).

See also Greyscale Gamma: RGB/sRGB.

%IM%convert -size 1x256 gradient: -rotate 90 ^
  -set colorspace RGB ^
  -colorspace sRGB ^
  cl_rgbsrgb.png

call %PICTBAT%graph1d cl_rgbsrgb.png
cl_rgbsrgb_g1d.png

Power (gamma) curve such that X1 becomes Y1, where 0 < X1,Y1, < 1.

We raise to the power k where Y1 = X1^k, thus k = log(Y1)/log(X1).

With IM v7, we can do the calculation within the command.

set X1=0.4
set Y1=0.6

%IMG7%magick ^
  -size 1x100 gradient: -rotate 90 ^
  -evaluate pow %%[fx:log(%Y1%)/log(%X1%)] ^
  cl_x1y1.png

call %PICTBAT%graph1d cl_x1y1.png
cl_x1y1_g1d.png

Quadratic curve that passes through (0,0): y = a.x2 + b.x + 0.

To also pass through (1,1), we need b = 1-a.

So: dy/dx = 2ax + b = 2ax + 1 - a.

At x=0, we have: y = 0 and dy/dx = 1-a.

At x=1, we have: y = 1 and dy/dx = 1+a.

If (gradient at 1) = R * (gradient at 0),
then a = (R-1)/(R+1) and b = 2/(R+1).

With IM v7, we can do the calculation within the command.

:skip

set R=(1/5)

%IMG7%magick ^
  -size 1x256 gradient: -rotate 90 ^
  -function Polynomial %%[fx:(%R%-1)/(%R%+1)],%%[fx:2/(%R%+1)],0 ^
  cl_qc1.png

call %PICTBAT%graph1d cl_qc1.png
cl_qc1_g1d.png

As previous, but resize to get gradient 1.0 at origin.

%IMG7%magick ^
  -size 1x256 gradient: -rotate 90 ^
  -resize "%%[fx:2*w/(%R%+1)]x%%[fx:h]^!" ^
  -function Polynomial %%[fx:(%R%-1)/(%R%+1)],%%[fx:2/(%R%+1)],0 ^
  cl_qc2.png

call %PICTBAT%graph1d cl_qc2.png
cl_qc2_g1d.png

Cluts from filters

We can create cluts by enlarging a single pixel with various filters. The source image is five pixels wide, all black except for a central white pixel. Hence the average value is 0.2. The resulting cluts also average 0.2, except where they are truncated at the ends or by going negative.

%IM%convert ^
  xc: ^
  -bordercolor Black -border 2x0 ^
  cl_onepix.png

%IM%convert ^
  cl_onepix.png ^
  -filter triangle ^
  -resize "300x1^!" ^
  cl_filttri.png

call %PICTBAT%graph1d cl_filttri.png
cl_filttri_g1d.png

Gaussian with undefined (default) sigma.

%IM%convert ^
  cl_onepix.png ^
  -filter gaussian ^
  -resize "300x1^!" ^
  cl_filtgauss.png

call %PICTBAT%graph1d cl_filtgauss.png

Process modules: mkgauss describes a "-process" module in C for the same job.

cl_filtgauss_g1d.png

Gaussian with defined sigma.

%IM%convert ^
  cl_onepix.png ^
  -filter gaussian -define filter:sigma=1 ^
  -resize "300x1^!" ^
  cl_filtgauss1.png

call %PICTBAT%graph1d cl_filtgauss1.png
cl_filtgauss1_g1d.png

Gaussian with defined sigma.

%IM%convert ^
  cl_onepix.png ^
  -filter gaussian -define filter:sigma=2 ^
  -resize "300x1^!" ^
  cl_filtgauss2.png

call %PICTBAT%graph1d cl_filtgauss2.png
cl_filtgauss2_g1d.png

Asymmetrical Gaussian.

%IM%convert ^
  xc: ^
  -size 2x1 xc:Black +append ^
  cl_onepixL.png

%IM%convert ^
  cl_onepixL.png ^
  -filter gaussian -define filter:sigma=1 ^
  -resize "300x1^!" ^
  cl_filtgauss_as.png

call %PICTBAT%graph1d cl_filtgauss_as.png
cl_filtgauss_as_g1d.png
%IM%convert ^
  cl_onepix.png ^
  -filter catrom ^
  -resize "300x1^!" ^
  cl_filtcat.png

call %PICTBAT%graph1d cl_filtcat.png
cl_filtcat_g1d.png
%IM%convert ^
  cl_onepix.png ^
  -filter spline ^
  -resize "300x1^!" ^
  cl_filtspl.png

call %PICTBAT%graph1d cl_filtspl.png
cl_filtspl_g1d.png
%IM%convert ^
  cl_onepix.png ^
  -filter lanczos ^
  -resize "300x1^!" ^
  cl_filtlanc.png

call %PICTBAT%graph1d cl_filtlanc.png
cl_filtlanc_g1d.png
%IM%convert ^
  cl_onepix.png ^
  -filter hamming ^
  -resize "300x1^!" ^
  cl_filtham.png

call %PICTBAT%graph1d cl_filtham.png
cl_filtham_g1d.png

By using a wider source and mid-gray pixels each side of the white,
we see more of the Hamming curve.

%IM%convert ^
  -size 5x1 xc:gray50 ^
  -size 1x1 xc:white ^
  -size 5x1 xc:gray50 ^
  +append ^
  cl_grayWhitePix.png

%IM%convert ^
  cl_grayWhitePix.png ^
  -filter hamming ^
  -resize "300x1^!" ^
  cl_filtham2.png

call %PICTBAT%graph1d cl_filtham2.png
cl_filtham2_g1d.png
%IM%convert ^
  cl_onepix.png ^
  -filter mitchell ^
  -resize "300x1^!" ^
  cl_filtmit.png

call %PICTBAT%graph1d cl_filtmit.png
cl_filtmit_g1d.png

Cluts from append

Some cluts are easily built in sections, appended together.

%IM%convert ^
  -size 100x1 xc:gray(25%%) ^
  -size 100x1 gradient:gray(25%%)-gray(75%%) ^
  -size 100x1 gradient:gray(75%%)-gray(0%%) ^
  +append ^
  cl_app1.png

call %PICTBAT%graph1d cl_app1.png
cl_app1_g1d.png
%IM%convert ^
  -size 100x1 gradient:black-white ^
  -size 100x1 gradient:white-black ^
  +append ^
  cl_app2.png

call %PICTBAT%graph1d cl_app2.png
cl_app2_g1d.png
%IM%convert ^
  -size 50x1 gradient:black-white ^
  -size 50x1 gradient:white-black ^
  -size 50x1 gradient:black-white ^
  +append ^
  cl_app3.png

call %PICTBAT%graph1d cl_app3.png
cl_app3_g1d.png
%IM%convert ^
  -size 50x1 gradient:black-white ^
  -size 50x1 gradient:white-black ^
  -size 50x1 gradient:black-white ^
  -size 50x1 gradient:white-black ^
  -size 50x1 gradient:black-white ^
  +append ^
  cl_app4.png

call %PICTBAT%graph1d cl_app4.png
cl_app4_g1d.png
%IM%convert ^
  -size 50x1 gradient:black-white ^
  -size 100x1 xc:white ^
  -size 100x1 gradient:white-gray(50%%) ^
  +append ^
  cl_app5.png

call %PICTBAT%graph1d cl_app5.png
cl_app5_g1d.png
%IM%convert ^
  -size 50x1 gradient:white-gray(10%%) ^
  -size 100x1 xc:gray(10%%) ^
  -size 100x1 gradient:gray(10%%)-gray(50%%) ^
  +append ^
  cl_app6.png

call %PICTBAT%graph1d cl_app6.png
cl_app6_g1d.png

The script mClutUpDn.bat makes a clut with a peak centred on a percentage of the width, with a given width of peak, and ramp up and down at each side of the peak.

In these examples, the width is set to 256, merely for illustration.

Default settings

call %PICTBAT%mClutUpDn ^
  256 . . . . cl_mcud1.png
cl_mcud1_g1d.png

Centre peak at 30%, peak width 20%, ramp width 10%.

call %PICTBAT%mClutUpDn ^
  256 30 20 10 . cl_mcud2.png
cl_mcud2_g1d.png

Centre peak at 30%, peak width 0, ramp width 10%.

call %PICTBAT%mClutUpDn ^
  256 30 0 10 . cl_mcud2a.png
cl_mcud2a_g1d.png

Centre peak at 30%, peak width 20%, ramp width 0.

call %PICTBAT%mClutUpDn ^
  256 30 20 0 . cl_mcud2b.png
cl_mcud2b_g1d.png

Centre peak at 30%, peak width 20%, ramp width 35%.

call %PICTBAT%mClutUpDn ^
  256 30 20 35 . cl_mcud3.png
cl_mcud3_g1d.png

As above, but with wrap.

call %PICTBAT%mClutUpDn ^
  256 30 20 35 1 cl_mcud4.png
cl_mcud4_g1d.png

Centre peak at 70%, peak width 20%, ramp width 35%.

call %PICTBAT%mClutUpDn ^
  256 70 20 35 . cl_mcud5.png
cl_mcud5_g1d.png

As above, but with wrap.

call %PICTBAT%mClutUpDn ^
  256 70 20 35 1 cl_mcud6.png
cl_mcud6_g1d.png

The wrap facility is useful when making masks for hues, when hue=0% is almost the same as hue=100%.

The environment variable mcudTRANS will transform the ramps, or perform any other transformation.

Default settings

set mcudTRANS=-sigmoidal-contrast 5x50%%
set mcudTRANS=-function sinusoid 0.5,-90,0.5,0.5

call %PICTBAT%mClutUpDn ^
  256 . . . . cl_mcud1t.png
cl_mcud1t_g1d.png

Centre peak at 30%, peak width 20%, ramp width 10%.

call %PICTBAT%mClutUpDn ^
  256 30 20 10 . cl_mcud2t.png
cl_mcud2t_g1d.png

Centre peak at 30%, peak width 20%, ramp width 35%.

call %PICTBAT%mClutUpDn ^
  256 30 20 35 . cl_mcud3t.png
cl_mcud3t_g1d.png

As above, but with wrap.

call %PICTBAT%mClutUpDn ^
  256 30 20 35 1 cl_mcud4t.png
cl_mcud4t_g1d.png

Centre peak at 70%, peak width 20%, ramp width 35%.

call %PICTBAT%mClutUpDn ^
  256 70 20 35 . cl_mcud5t.png
cl_mcud5t_g1d.png

As above, but with wrap.

call %PICTBAT%mClutUpDn ^
  256 70 20 35 1 cl_mcud6t.png

set mcudTRANS=
cl_mcud6t_g1d.png

Cluts from cumulation

Cumulating (integrating) a clut makes another clut. This uses a process module from Process modules: cumulate histogram.

%IMDEV%convert ^
  cl_app1.png ^
  -process 'cumulhisto norm' ^
  cl_app1_ch.png

call %PICTBAT%graph1d cl_app1_ch.png
cl_app1_ch_g1d.png
%IMDEV%convert ^
  cl_app2.png ^
  -process 'cumulhisto norm' ^
  cl_app2_ch.png

call %PICTBAT%graph1d cl_app2_ch.png
cl_app2_ch_g1d.png
%IMDEV%convert ^
  cl_app3.png ^
  -process 'cumulhisto norm' ^
  cl_app3_ch.png

call %PICTBAT%graph1d cl_app3_ch.png
cl_app3_ch_g1d.png
%IMDEV%convert ^
  cl_app4.png ^
  -process 'cumulhisto norm' ^
  cl_app4_ch.png

call %PICTBAT%graph1d cl_app4_ch.png
cl_app4_ch_g1d.png
%IMDEV%convert ^
  cl_app5.png ^
  -process 'cumulhisto norm' ^
  cl_app5_ch.png

call %PICTBAT%graph1d cl_app5_ch.png
cl_app5_ch_g1d.png
%IMDEV%convert ^
  cl_app6.png ^
  -process 'cumulhisto norm' ^
  cl_app6_ch.png

call %PICTBAT%graph1d cl_app6_ch.png
cl_app6_ch_g1d.png

Cluts from Gimp

We can create cluts (or hald-cluts) in Gimp. For example, we might want a complex tone curve that processes a photograph in a particular way. If we apply that same tone curve to a gradient, we have created a clut.

In Gimp, open the photograph.
Menu "Color, Curves".
The curves panel shows a histogram and the curve,
which initially is a straight diagonal line.

cl_gimp1.png

Adjust the curve as desired.

cl_gimp2.png

Save the curve as a preset, with any name you want.

At the command prompt, create a gradient.

%IM%convert -size 1x256 gradient: -rotate 90 cl_grad.png

In Gimp, open this gradient file cl_grad.png. If you want to see it, zoom to 800% or whatever. Menu "Color, Curves". Open the saved preset curve. The lastest saved is at the bottom of the list. "OK" it. This applies the curve to the gradient. Export the result as a PNG, eg named cl_gimp_clut.png. (In the "Export image as..." window, the box "Save gamma" should generally be ticked.) This result is a 256x1 clut, so we can visualise it in the usual way.

call %PICTBAT%graph1d cl_gimp_clut.png
cl_gimp_clut_g1d.png

The curve shown is the same as the one we created in Gimp.

Aside: I first discovered the joys of digital images through Gimp, and especially the Curves facility. Playing with Curves is a great way of learning about tonal shifts, and discovering the effects of IM operations "-gamma", "level", "-sigmoidal-contrast", etc. The opposite is also true: showing a clut as a graph is a useful way of identifying the effect of a clut.

Further side: The process described above of creating a gradient, applying a saved curve in Gimp, and saving it as a clut is somewhat tedious and error-prone. Gimp saves curves in a simple text format, so I wrote gimpCurve.exe to read the file and create a clut. I don't curently publish this program. I suppose the task would be quite simple as a Unix script.

Cluts from graphics

Given a black line on a white background that represents a gradient, we can convert it to a clut. We write and show intermediate results (cl_*_tmp.png) purely for illustration.

An example source file, clutLine.png.

clutLine.png

Turn black the white pixels above the black pixels and resize to one line.

"-scale" (instead of "-resize") gives the arithmetically average result.

We write and show the intermediate result (cl_clutLine_tmp.png) purely for illustration.

%IM%convert clutLine.png ^
  -morphology Distance "1x2+0+1:0,1" ^
  -write cl_clutLine_tmp.png ^
  -scale "100x1^!" ^
  +depth ^
  cl_clutLine.png

call %PICTBAT%graph1d cl_clutLine.png
cl_clutLine_tmp.png
cl_clutLine_g1d.png

How close is the result?

The input and output have the same dimensions, so can be directly compared.

%IM%convert clutLine.png ^
  cl_clutLine_g1d.png ^
  ( clutLine.png -transparent White ) ^
  -gravity Center ^
  -composite ^
  cl_clutLineComp.png
cl_clutLineComp.png

This clut can be used as a vertical-only displacement map that would make the line horizontal. See Displacement maps.

If the line has a thickness, we can find the top, bottom or middle. We write and show intermediate results (*_tmp.png) purely for illustration.

An example source file, clutLineThk.png.

clutLineThk.png

Find the top of the line.

Turn black all the white pixels below the line, resize and negate.

Note that the resulting black includes all of the line.

%IM%convert clutLineThk.png ^
  -morphology Distance "1x2+0+0:0,1" ^
  -write cl_clutLineThkT_tmp.png ^
  -scale "100x1^!" ^
  -negate ^
  +depth ^
  cl_clutLineThkT.png

call %PICTBAT%graph1d cl_clutLineThkT.png
cl_clutLineThkT_tmp.png
cl_clutLineThkT_g1d.png

Find the bottom of the line.

Turn black the white pixels above the black pixels and resize to one line.

%IM%convert clutLineThk.png ^
  -morphology Distance "1x2+0+1:0,1" ^
  -write cl_clutLineThkB_tmp.png ^
  -scale "100x1^!" ^
  +depth ^
  cl_clutLineThkB.png

call %PICTBAT%graph1d cl_clutLineThkB.png
cl_clutLineThkB_tmp.png
cl_clutLineThkB_g1d.png

Find the middle of the line. (strLine6)

We find the top of the black blob and make all pixels beneath it black.

Then paint a mid-gray version of the blob over it.

Thus the resize to a single line will use the height plus half the height of the thick line.

%IM%convert ^
  clutLineThk.png ^
  ( -clone 0 ^
    -virtual-pixel white ^
    -morphology EdgeOut rectangle:1x2 ^
    -negate ^
    -morphology Distance "1x2+0+0:0,1" ^
    -threshold 50%% ^
  ) ^
  ( -clone 0 -function Polynomial -1,0.5 ) ^
  -delete 0 ^
  -compose Lighten -composite ^
  -write cl_clutLineThkM_tmp.png ^
  -scale "100x1^!" ^
  -negate ^
  cl_clutLineThkM.png

call %PICTBAT%graph1d cl_clutLineThkM.png
cl_clutLineThkM_tmp.png
cl_clutLineThkM_g1d.png

Alternate Find the middle of the line.

Turn black all the white pixels below the line.

Then paint a mid-gray version of the blob over it.

Thus the resize to a single line will use the height plus half the height of the thick line.

%IM%convert ^
  clutLineThk.png ^
  ( -clone 0 ^
    -morphology Distance "1x2+0+0:0,1" ^
  ) ^
  ( -clone 0 -function Polynomial -1,0.5 ) ^
  -delete 0 ^
  -compose Lighten -composite ^
  -write cl_clutLineThkM2_tmp.png ^
  -scale "100x1^!" ^
  -negate ^
  cl_clutLineThkM2.png

call %PICTBAT%graph1d cl_clutLineThkM2.png
cl_clutLineThkM2_tmp.png
cl_clutLineThkM2_g1d.png

Make a blink-comparator using animated gif.

%IM%convert ^
  -delay 50 ^
  clutLineThk.png ^
  cl_clutLineThkT_tmp.png ^
  clutLineThk.png ^
  cl_clutLineThkM_tmp.png ^
  clutLineThk.png ^
  cl_clutLineThkB_tmp.png ^
  cl_clutLineThkComp.gif
cl_clutLineThkComp.gif

As mentioned above, we can use cluts as displacement maps. The cluts we have generated are light where the line is above the centre. Hence we need to invert the displacement to move the line to the centre. We do this with a negative argument which is roughly half the height because we want black (or white) in the clut to move a pixel from the bottom (or top) of the source image to the image center.

The image is 100 pixels high. The centre of the image is at 49.5.

The line is at least one pixel high. Without an "-evaluate" offset, alignment with the three methods would place the top of the top-most pixel at the same location as the bottom of the bottom-most pixel, at the same location as the middle of the middle-most pixel. If we want to line up pixels instead of boundaries, we need to move the top-aligned image down by half a pixel and the bottom-aligned image up half a pixel. We do this by subtracting or adding an offset to the clut. As the height is 100 pixels, half a pixel is 0.5% of quantum.

Align the top of the thick line.

%IM%convert ^
  clutLineThk.png ^
  ( cl_clutLineThkT.png -evaluate subtract 0.5%% -scale "100x100^!" ) ^
  -compose Displace -set option:compose:args 0x-50 -composite ^
  -compose Over -bordercolor khaki -border 1 ^
  cl_clutLineThk_dispT.png
cl_clutLineThk_dispT.png

Align the bottom of the thick line.

%IM%convert ^
  clutLineThk.png ^
  ( cl_clutLineThkB.png -evaluate add 0.5%% -scale "100x100^!" ) ^
  -compose Displace -set option:compose:args 0x-50 -composite ^
  -compose Over -bordercolor khaki -border 1 ^
  cl_clutLineThk_dispB.png
cl_clutLineThk_dispB.png

Align the middle of the thick line.

The 50% crop is for later use.

%IM%convert ^
  clutLineThk.png ^
  +depth ^
  ( cl_clutLineThkM.png -scale "100x100^!" ) ^
  -compose Displace -set option:compose:args 0x-50 -composite ^
  ( +clone ^
    -gravity North -crop 100%%x50%%+0+0 ^
    +repage ^
    -write cl_halfThk.png +delete ^
  ) ^
  -compose Over -bordercolor khaki -border 1 ^
  cl_clutLineThk_dispM.png
cl_clutLineThk_dispM.png

Blink comparator.

%IM%convert ^
  -delay 50 ^
  cl_clutLineThk_dispT.png ^
  cl_clutLineThk_dispB.png ^
  cl_clutLineThk_dispM.png ^
  cl_clutLineThk_disp_comp.gif
cl_clutLineThk_disp_comp.gif

When a source image is displaced in this way, some of the image will be displaced out of the resulting image. The source may be given an "-extent" first.

This technique could be used to make text flow parallel with the edge of a containing shape. See SVG text: Rotating text in areas.

A different displacement is also of interest. Take the top half of the middle-aligned thick line, created above.

Half the middle-aligned thick line.

cl_halfThk.png

cl_halfThk.png

We can spread this vertically, by different amounts along the horizontal line, to fit the rectangle.

Values in the top row of the displacement map (whether absolute or relative), will be the average lightness of the column. Values in the bottom row will be 1.0 for the absolute map, or 0.5 for the relative map. Values in intermediate rows will vary linearly. So we can create a relative map quite easily.

This relative map has to be multipled by the height (not half the height) to obtain the full displacement.

Make the relative map.

FOR /F "usebackq" %%L ^
IN (`%IM%identify ^
  -format "htWW=%%w\nhtHH=%%h\nhtHHm1=%%[fx:h-1]\nhtHHp1=%%[fx:h+1]" ^
  cl_halfThk.png`) ^
DO set %%L

%IM%convert ^
  cl_halfThk.png ^
  -gravity South -chop 0x1 ^
  -scale "%htWW%x1^!" -scale "%htWW%x%htHH%^!" ^
  -size %htWW%x%htHH% gradient: ^
  -compose Mathematics ^
    -define compose:args=0.5,0,0,0.5 ^
    -composite ^
  +depth ^
  cl_halfThkMap.png
cl_halfThkMap.png

Apply the relative map.

%IM%convert ^
  cl_halfThk.png ^
  cl_halfThkMap.png ^
  -background Red -virtual-pixel background ^
  -compose Displace ^
    -set option:compose:args 0x%htHHm1% ^
    -composite ^
  cl_halfThkMapped.png

Grey bands occur where the bottom line
of cl_halfThk.png is grey.

cl_halfThkMapped.png

For the inverse displacement, converting a black rectangle into the black shape cl_halfThk.png, the absolute map should be anything negative (ie darker than mid-grey) where we want white (so it fetches a virtual pixel), and the bottom line is 1.0, white, so pixels come from the bottom line of the black rectangle. Map pixels at the top of the black shape should be 0.0, black, so pixels come from the top line of the black rectangle.

Taking relative values as -1.0 .. 0.0 .. +1.0: For the inverse relative map, pixels at the bottom will be 0. For the forwards displacement, pixels are displaced fom the top of the blob to the top of the image. The inverse displacement needs to do the opposite, so pixels for the inverse displacement at the top of the blob will be -(pixels at top of forwards displacement). So pixels at top of inverse map will be -f*(pixels at top of forwards displacement) where f is (height / number of black pixels in column). [[the average lightness of negated cl_halfThk.png.]]

[[and anything positive (ie lighter than mid-grey) where we want black. An obvious choice for the absolute map is cl_halfThk.png negated.]]

From the relative map, make the absolute map, and rotate it so the displacements are horizontal.

%IM%convert ^
  cl_halfThkMap.png ^
  -size %htWW%x%htHH% gradient:black-white ^
  -compose Mathematics ^
    -define compose:args=0,1,1,-0.5 ^
    -composite ^
  -rotate -90 ^
  +repage ^
  -function polynomial 2,-1 ^
  +depth ^
  cl_halfThkAbs.png
cl_halfThkAbs.png

Invert the absolute map.

call %PICTBAT%inv2dAbsDisp cl_halfThkAbs.png
cl_halfThkAbs_iad.png

Rotate the absolute map back, and convert it to relative.

%IM%convert ^
  cl_halfThkAbs_iad.png ^
  -rotate 90 ^
  -size %htWW%x%htHH% ^
  gradient:black-white ^
  -compose Mathematics ^
    -define compose:args=0,-0.5,1,0.5 ^
    -composite ^
  cl_halfThkAbs_iad_rel.png
cl_halfThkAbs_iad_rel.png

Apply the relative map to a black rectangle.

%IM%convert ^
  -size %htWW%x%htHH% xc:Black ^
  cl_halfThkAbs_iad_rel.png ^
  -background Red -virtual-pixel background ^
  -compose Displace ^
    -set option:compose:args 0x50 ^
    -composite ^
  cl_halfThkAbs_iad_rel_b.png
cl_halfThkAbs_iad_rel_b.png

Colour cluts

A clut may have different values in the three channels. This could, for example, be used to apply a false colour to a monochrome image.

We can use any of the above techniques on individual channels, for example:

%IM%convert -size 1x256 gradient: -rotate 90 ^
  -channel R -evaluate sin 1 ^
  -channel G -evaluate cos 1 ^
  +channel ^
  cl_chan1.png

call %PICTBAT%graphLineCol cl_chan1.png
cl_chan1_glc.png

Rainbow cluts

See also the closely-related Colour wheels.

In HSL and related colorspaces, vary the first channel from zero to 100% as the x-dimension. Set the second and third channels to 100%.

set WW=256


for %%H in (HSL,HSB,HSI,HSV,HCL,HCLp,HWB) do (%IM%convert ^
  -size 1x%WW% gradient: -rotate 90 ^
  -size %WW%x1 xc:#fff ^
  -size %WW%x1 xc:#fff ^
  -combine -set colorspace %%H ^
  -colorspace sRGB ^
  cl_rb_%%H.png

  call %PICTBAT%graphLineCol cl_rb_%%H.png

  %IM%convert ^
    cl_rb_%%H_glc.png ^
    -gravity Center label:"%%H x,100%%,100%%" -append ^
    cl_rb_%%H_glc.png
)
cl_rb_HSL_glc.png cl_rb_HSB_glc.png cl_rb_HSI_glc.png cl_rb_HSV_glc.png cl_rb_HCL_glc.png cl_rb_HCLp_glc.png cl_rb_HWB_glc.png

As above but set the second channel to 50%.

for %%H in (HSL,HSB,HSI,HSV,HCL,HCLp,HWB) do (%IM%convert ^
  -size 1x%WW% gradient: -rotate 90 ^
  -size %WW%x1 xc:#fff ^
  -size %WW%x1 xc:gray^(50%%^) ^
  -combine -set colorspace %%H ^
  -colorspace sRGB ^
  cl_rb_%%H_2.png

  call %PICTBAT%graphLineCol cl_rb_%%H_2.png

  %IM%convert ^
    cl_rb_%%H_2_glc.png ^
    -gravity Center label:"%%H x,100%%,50%%" -append ^
    cl_rb_%%H_2_glc.png
)
cl_rb_HSL_2_glc.png cl_rb_HSB_2_glc.png cl_rb_HSI_2_glc.png cl_rb_HSV_2_glc.png cl_rb_HCL_2_glc.png cl_rb_HCLp_2_glc.png cl_rb_HWB_2_glc.png

Or we can use a technique that lists explicit colours, such as one of those below.

%IM%convert ^
  -size 1x256 gradient: -rotate 90 ^
  ( -size 1x1 ^
    xc:#f00 xc:#ff0 xc:#0f0 xc:#0ff xc:#00f xc:#f0f xc:#f00 ^
    +append ^
  ) ^
  -clut ^
  cl_rb_fc.png

call %PICTBAT%graphLineCol cl_rb_fc.png
cl_rb_fc_glc.png

Cold-warm cluts

%IM%convert ^
  xc:#004 xc:#404 xc:#800 xc:#f00 xc:#f80 xc:#ff0 xc:#ffa ^
  +append ^
  -resize "256x1^!" ^
  cl_falseCol.png

call %PICTBAT%graphLineCol cl_falseCol.png
cl_falseCol_glc.png

The graph shows the channels change at a variable rate; they are curves, not linear. The clut is nearly flat at each end, so distinguishing values by colour is difficult.

The "triangle" filter for resize gives a linear response, but it leaves flat portions at each end.

%IM%convert ^
  xc:#004 xc:#404 xc:#800 xc:#f00 xc:#f80 xc:#ff0 xc:#ffa ^
  +append ^
  -filter Triangle ^
  -resize "256x1^!" ^
  cl_falseCol2.png

call %PICTBAT%graphLineCol cl_falseCol2.png
cl_falseCol2_glc.png

We know the width and the colours at each end, so we can fairly easily remove the repetition at the ends.

%IM%convert cl_falseCol2.png -flop %TEMP%\cl_flop.png

for /F "usebackq tokens=3 delims=(),@ " %%F in (`%IM%compare ^
  -subimage-search -metric RMSE ^
  -similarity-threshold 0 -dissimilarity-threshold 1 ^
  %TEMP%\cl_flop.png ^
  xc:#004 ^
  NULL: 2^>^&1`) DO set /A cl_START=256-%%F

for /F "usebackq tokens=3 delims=(),@ " %%F in (`%IM%compare ^
  -subimage-search -metric RMSE ^
  -similarity-threshold 0 -dissimilarity-threshold 1 ^
  cl_falseCol2.png ^
  xc:#ffa ^
  NULL: 2^>^&1`) DO set /A cl_WIDTH=%%F-%cl_START%+2

echo cl_START=%cl_START% cl_WIDTH=%cl_WIDTH%

%IM%convert ^
  cl_falseCol2.png ^
  -crop %cl_WIDTH%x1+%cl_START%+0 +repage ^
  cl_falseCol2cr.png

call %PICTBAT%graphLineCol cl_falseCol2cr.png
cl_falseCol2cr_glc.png

We can also get a linear response by using "gradient:", though a value is repeated at every transition. This method enables discontinuities in the clut.

%IM%convert ^
  -size 1x42 ^
  gradient:#004-#404 ^
  gradient:#404-#800 ^
  gradient:#800-#f00 ^
  gradient:#f00-#f80 ^
  gradient:#f80-#ff0 ^
  gradient:#ff0-#ffa ^
  -rotate -90 ^
  +append ^
  cl_falseCol3.png

call %PICTBAT%graphLineCol cl_falseCol3.png
cl_falseCol3_glc.png

A clut doesn't need to be large. Here we create a clut of just seven pixels. When used on a larger image, IM interpolates between the seven values. In this example, the larger image is one pixel high. It, too, is a clut. This method doesn't have the repeated values, so no kinks. But we can't create individual discontinuities. TODO: When is a small clut good enough? Interpolation issues. Apply false colour clut to gradient, with fc clut very small or large.

%IM%convert ^
  -size 1x256 gradient: -rotate 90 ^
  ( -size 1x1 ^
    xc:#004 xc:#404 xc:#800 xc:#f00 xc:#f80 xc:#ff0 xc:#ffa ^
    +append ^
  ) ^
  -clut ^
  cl_cl_fc.png

call %PICTBAT%graphLineCol cl_cl_fc.png
cl_cl_fc_glc.png

We can tell "-clut" how to interpolate between values. The "-interpolate filter" options are noticably slow.

%IM%convert ^
  -size 1x256 gradient: -rotate 90 ^
  ( -size 1x1 ^
    xc:#004 xc:#404 xc:#800 xc:#f00 xc:#f80 xc:#ff0 xc:#ffa ^
    +append ^
  ) ^
  -interpolate integer ^
  -clut ^
  cl_cl_fc2.png

call %PICTBAT%graphLineCol cl_cl_fc2.png
cl_cl_fc2_glc.png
%IM%convert ^
  -size 1x256 gradient: -rotate 90 ^
  ( -size 1x1 ^
    xc:#004 xc:#404 xc:#800 xc:#f00 xc:#f80 xc:#ff0 xc:#ffa ^
    +append ^
  ) ^
  -interpolate spline ^
  -clut ^
  cl_cl_fc3.png

call %PICTBAT%graphLineCol cl_cl_fc3.png

The end colours are not quite present.

cl_cl_fc3_glc.png

We can repeat the end colours.

%IM%convert ^
  -size 1x256 gradient: -rotate 90 ^
  ( -size 1x1 ^
    xc:#004 xc:#004 ^
    xc:#404 xc:#800 xc:#f00 xc:#f80 xc:#ff0 ^
    xc:#ffa xc:#ffa ^
    +append ^
  ) ^
  -interpolate spline ^
  -clut ^
  cl_cl_fc4.png

call %PICTBAT%graphLineCol cl_cl_fc4.png

The end colours are present.

cl_cl_fc4_glc.png
%IMO%convert ^
  -size 1x256 gradient: -rotate 90 ^
  ( -size 1x1 ^
    xc:#004 xc:#404 xc:#800 xc:#f00 xc:#f80 xc:#ff0 xc:#ffa ^
    +append ^
  ) ^
  -interpolate filter ^
  -clut ^
  cl_cl_fc5.png

call %PICTBAT%graphLineCol cl_cl_fc5.png
cl_cl_fc5_glc.png
%IMO%convert ^
  -size 1x256 gradient: -rotate 90 ^
  ( -size 1x1 ^
    xc:#004 xc:#404 xc:#800 xc:#f00 xc:#f80 xc:#ff0 xc:#ffa ^
    +append ^
  ) ^
  -interpolate filter ^
  -filter Mitchell ^
  -clut ^
  cl_cl_fc6.png

call %PICTBAT%graphLineCol cl_cl_fc6.png
cl_cl_fc6_glc.png
%IMO%convert ^
  -size 1x256 gradient: -rotate 90 ^
  ( -size 1x1 ^
    xc:#004 xc:#404 xc:#800 xc:#f00 xc:#f80 xc:#ff0 xc:#ffa ^
    +append ^
  ) ^
  -interpolate filter ^
  -filter Spline ^
  -clut ^
  cl_cl_fc7.png

call %PICTBAT%graphLineCol cl_cl_fc7.png
cl_cl_fc7_glc.png

Colour shift (white balance) cluts

We can process cluts. For example, we might know a few required transformations for colour changes for an image. Perhaps we want black to remain black, white to remain white, and two other values to become rgb(10%,10%,20%) and rgb(85%,80%,55%). We can make a clut of just these four values (note the repeated ends). The transformations need to be sorted. We can "-equalize" the clut to spread tones evenly. When the clut is used, tones in the image won't change.

The basic clut.

%IM%convert ^
  -size 1x256 gradient: -rotate 90 ^
  ( -size 1x1 ^
    xc:black xc:black ^
    xc:rgb(10%%,10%%,20%%) xc:rgb(85%%,80%%,55%%) ^
    xc:white xc:white ^
    +append ^
  ) ^
  -interpolate spline ^
  -clut ^
  cl_wb1.png

call %PICTBAT%graphLineCol cl_wb1.png
cl_wb1_glc.png

The equalised clut.
Note the slight glitch at the top.
I suppose this comes from the processing of equalize at maximum values.

%IM%convert ^
  -size 1x256 gradient: -rotate 90 ^
  ( -size 1x1 ^
    xc:black xc:black ^
    xc:rgb(10%%,10%%,20%%) xc:rgb(85%%,80%%,55%%) ^
    xc:white xc:white ^
    +append ^
  ) ^
  -interpolate spline ^
  -clut ^
  -equalize ^
  cl_wb2.png

call %PICTBAT%graphLineCol cl_wb2.png
cl_wb2_glc.png

The colours could be specified as the required outputs (instead of inputs), then inserting ...

( +clone -colorspace gray ) ^
-compose Mathematics -define compose:args=0,2,-1,0 -composite

... after "-equalize", or before it, or after the "+append".

This technique may be accurate enough for small colour-shifts, much smaller than shown here. However, a required transformation will be placed at an x-value equal to the average of the three channels, but when the clut is used, three different x-values corresponding to the three channels will be looked up.

A more accurate colour transformation clut can be constructed.

Suppose we want rgb(45%,33%,70%) to become gray(60%).

If black stays black, white stays white, and there is only one other transformation, the clut can be a power function: y = x k, so k = log(y)/log(x).

for /F "usebackq" %%L ^
in (`%IM%identify ^
  -format "kR=%%[fx:log(0.6)/log(0.45)]\nkG=%%[fx:log(0.6)/log(0.33)]\nkB=%%[fx:log(0.6)/log(0.70)]" ^
  xc:`) ^
do set %%L

%IM%convert ^
  -size 1x250 gradient: -rotate 90 ^
  -channel R -evaluate Pow %kR% ^
  -channel G -evaluate Pow %kG% ^
  -channel B -evaluate Pow %kB% ^
  cl_colt1.png

call %PICTBAT%graphLineCol cl_colt1.png
cl_colt1_glc.png

Compare the above power method with the channel polynomial from cBezCurve:

for /F "usebackq tokens=*" %%F ^
in (`@cBezCurve /p0 /C ^
  /I"0,0,0 0,0,0;  0.45,0.33,0.70 0.6,0.6,0.6;  1,1,1 1,1,1" ^
  /y-`) ^
do %IM%convert ^
  -size 1x250 gradient: -rotate 90 ^
  %%F ^
  cl_colt1a.png

call %PICTBAT%graphLineCol cl_colt1a.png
cl_colt1a_glc.png

We can adapt this method for a simple (and fairly useless) displacement, with clipping at both ends.

for /F "usebackq" %%L ^
in (`%IM%identify ^
  -format "dR=%%[fx:100*(0.6-0.45)]\ndG=%%[fx:100*(0.6-0.33)]\ndB=%%[fx:100*(0.6-0.70)]" ^
  xc:`) ^
do set %%L

%IM%convert ^
  -size 1x250 gradient: -rotate 90 ^
  -channel R -evaluate Add %dR%%% ^
  -channel G -evaluate Add %dG%%% ^
  -channel B -evaluate Add %dB%%% ^
  cl_colt2.png

call %PICTBAT%graphLineCol cl_colt2.png
cl_colt2_glc.png

Here is a linear transformation. I multiply each x-coordinate by ten to reduce the effect of the doubling. Conceptually simple but messy to code.

The resize is only for display on the web page.

%IM%convert ^
  ( -size 450x1 gradient:black-gray(60%%) ^
    -size 550x1 gradient:gray(60%%)-white ^
    +append ^
  ) ^
  ( -size 330x1 gradient:black-gray(60%%) ^
    -size 670x1 gradient:gray(60%%)-white ^
    +append ^
  ) ^
  ( -size 700x1 gradient:black-gray(60%%) ^
    -size 300x1 gradient:gray(60%%)-white ^
    +append ^
  ) ^
  -combine ^
  -set colorspace sRGB ^
  -resize "250x1^!" ^
  cl_colt3.png

call %PICTBAT%graphLineCol cl_colt3.png
cl_colt3_glc.png

This append/combine technique can be extended to any number of transformation points, but creating it by hand would be awkward.

If we are correcting a highlight and we don't care about any lighter tones, the clut can be simpler. This works best when the "gray point" is near white because we get continued diversion at lighter tones, resulting in heavy clipping.

%IM%convert ^
  -size 1x250 gradient: -rotate 90 ^
  -evaluate Multiply 0.6 ^
  ( +clone -fill rgb(45%%,33%%,70%%) -colorize 100 ) ^
  +swap ^
  -compose Divide -composite ^
  cl_colt4.png

call %PICTBAT%graphLineCol cl_colt4.png
cl_colt4_glc.png

A clut can be smoothed.

cl_falseCol3.png

cl_falseCol3_glc.png

%IM%convert ^
  cl_falseCol3.png ^
  -blur 0x10 ^
  cl_falseCol4.png

call %PICTBAT%graphLineCol cl_falseCol4.png
cl_falseCol4_glc.png

Cluts from cumulative histogram

This section describes a script for creating histograms. Process modules: mkhisto describes a much faster "-process" module in C for the same job.

A cumulative histogram is a clut. Mathematically, the cumulative is the integration of the ordinary histogram. (The ordinary histogram is the differential of the cumulative.)

toes.png

toes.pngjpg

Make a histogram the conventional way.

%IM%convert ^
  toes.png ^
  -define histogram:unique-colors=false ^
  histogram:cl_toes_hist.png
cl_toes_hist.png

Make a histogram with a script.

set c2hDO_CUMUL=0
set c2hINBITS=8
call %PICTBAT%col2Histo toes.png

call %PICTBAT%graphLineCol %c2hOUTFILE%
toes_c2h_glc.png

Make a cumulative histogram with the same script.

set c2hDO_CUMUL=1
call %PICTBAT%col2Histo toes.png cl_toes_cumul.png
set c2hDO_CUMUL=

call %PICTBAT%graphLineCol cl_toes_cumul.png

The cumulative histogram cl_toes_cumul.png is a clut.

cl_toes_cumul_glc.png

Cluts from cBezCurve.exe

We can use a custom program. This might require us to give both the input and output colours. cBezCurve.exe can create a polynomial for each channel directly usable by ImageMagick. The polynomials could be used on the image, or on a clut which is then used to modift the image. The clut technique will generally be faster.

(I don't publish the source or executable of cBezCurve.exe.)

for /F "usebackq tokens=*" %%F ^
in (`@cBezCurve /p0 /C ^
  /I"0,0,0 0,0,0;  0.15,0.15,0.15 0.1,0.1,0.2;  0.7,0.7,0.7 0.85,0.80,0.55;  1,1,1 1,1,1" ^
  /y-`) ^
do %IM%convert ^
  -size 1x250 gradient: -rotate 90 ^
  %%F ^
  cl_cbc1.png

call %PICTBAT%graphLineCol cl_cbc1.png
cl_cbc1_glc.png
for /F "usebackq tokens=*" %%F ^
in (`@cBezCurve /p0 /C ^
  /I"0,0,0 0,0,0;  gray 0.1,0.1,0.2;  gray 0.85,0.80,0.55;  1,1,1 1,1,1" ^
  /y-`) ^
do %IM%convert ^
  -size 1x250 gradient: -rotate 90 ^
  %%F ^
  cl_cbc2.png

call %PICTBAT%graphLineCol cl_cbc2.png
cl_cbc2_glc.png

cBezCurve.exe can directly make clut files using a variety of methods.

Passing through all the points, with linear interpolation along segments between points.

cBezCurve /p0 /C ^
  /I"0,0,0 0,0,0;  gray 0.1,0.1,0.2;  gray 0.85,0.80,0.55;  1,1,1 1,1,1" ^
  /T0 /Lcl_cbcc0.ppm

call %PICTBAT%graphLineCol cl_cbcc0.ppm
cl_cbcc0_glc.png

Bezier spline, passing through all the points.

cBezCurve /p0 /C ^
  /I"0,0,0 0,0,0;  gray 0.1,0.1,0.2;  gray 0.85,0.80,0.55;  1,1,1 1,1,1" ^
  /T1 /Lcl_cbcc1.ppm

call %PICTBAT%graphLineCol cl_cbcc1.ppm
cl_cbcc1_glc.png

Polynomial, passing through all the points.

cBezCurve /p0 /C ^
  /I"0,0,0 0,0,0;  gray 0.1,0.1,0.2;  gray 0.85,0.80,0.55;  1,1,1 1,1,1" ^
  /T2 /Lcl_cbcc2.ppm

call %PICTBAT%graphLineCol cl_cbcc2.ppm
cl_cbcc2_glc.png

Linear regression, the straight line that fits best between points.

cBezCurve /p0 /C ^
  /I"0,0,0 0,0,0;  gray 0.1,0.1,0.2;  gray 0.85,0.80,0.55;  1,1,1 1,1,1" ^
  /T3 /Lcl_cbcc3.ppm

call %PICTBAT%graphLineCol cl_cbcc3.ppm
cl_cbcc3_glc.png

Another clut, for use later. The red channel increases; green decreases, blue does both.

Bezier spline, passing through all the points.

cBezCurve /p0 /C ^
  /I"0,0,0 0,0.8,0;  0.13,0.13,0.13 0.1,0.60,0.55;  0.66,0.66,0.66 0.85,0.25,0.2;  1,1,1 1,0.1,1" ^
  /T1 /Lcl_cbcc4.ppm

call %PICTBAT%graphLineCol cl_cbcc4.ppm
cl_cbcc4_glc.png

Cluts for hue ranges

Sometimes we want a mask that is white where the image is a certain hue (centHue), and black otherwise. This would be a binary mask: every pixel is either black or white.

We will use HSL colorspace, so centHue=0 or 100% is red, centHue=33.3% is green, centHue=66.6% is blue, etc. Hue wraps around at 100%, and the clut must behave properly there.

We might want the mask to be white where the hue is within a certain range rangeHue. This is the percentage of hues that will become white. (Those hues between centHue-rangeHue/2 and centHue+rangeHue/2, with wraparound at 0 and 100%.) Other hues will become black. When rangeHue is zero, the mask is white only when the hue is exactly centHue.

Instead of a binary mask, we might want a transition over a certain range of hues (transHue), so hues just outside centHue-rangeHue/2 and centHue+rangeHue/2 are a shade of gray. When transHue is zero, we get a binary mask. However, IM disregards -level when the levels are equal, so use a small number such as 0.01 instead of zero. A more intelligent script would check for zero, and use -threshold instead.

The code below gives a linear transition. Of course, this can be modified by other methods shown on this page to give a cosine or any other rolloff.

Generally, rangeHue+2*transHue should not exceed 100.

set centHue=33.3
set rangeHue=10
set transHue=20

With these numbers, hues between 28.3 and 38.3% will be white, hues below 8.3% or above 58.3% will be black, and others will be shades of gray.

set sFMT=^
ADD_PC=%%[fx:50-%centHue%]\n^
LEV_HI=%%[fx:100-%rangeHue%]\n^
LEV_LO=%%[fx:100-%rangeHue%-2*%transHue%]

for /F "usebackq" %%L in (`%IM%identify ^
  -format "%sFMT%" ^
  xc:`) do set %%L

echo ADD_PC=%ADD_PC% LEV_HI=%LEV_HI% LEV_LO=%LEV_LO% 
ADD_PC=16.7 LEV_HI=90 LEV_LO=50 
%IM%convert ^
  -size 1x256 gradient: -rotate 90 ^
  -evaluate AddModulus %ADD_PC%%% ^
  -solarize 50%% ^
  -evaluate Multiply 2 ^
  -level %LEV_LO%,%LEV_HI%%% ^
  cl_hr1.png

call %PICTBAT%graph1d cl_hr1.png
cl_hr1_g1d.png

LEV_HI is the percentage of hues that will be white, and LEV_LO is the percentage of hues that will not be black.

In v7, we can do the arithmetic within a "magick" command. We must not use a "%" after the first number in "-level".

%IMG7%magick ^
  -size 1x256 gradient: -rotate 90 ^
  -evaluate AddModulus %%[fx:50-%centHue%]%% ^
  -solarize 50%% ^
  -evaluate Multiply 2 ^
  -level %%[fx:100-%rangeHue%-2*%transHue%],%%[fx:100-%rangeHue%]%% ^
  cl_hr1_7.png

call %PICTBAT%graph1d cl_hr1_7.png
cl_hr1_7_g1d.png

Widen the transition:

set centHue=33.3
set rangeHue=10
set transHue=40

%IMG7%magick ^
  -size 1x256 gradient: -rotate 90 ^
  -evaluate AddModulus %%[fx:50-%centHue%]%% ^
  -solarize 50%% ^
  -evaluate Multiply 2 ^
  -level %%[fx:100-%rangeHue%-2*%transHue%],%%[fx:100-%rangeHue%]%% ^
  cl_hr2_7.png

call %PICTBAT%graph1d cl_hr2_7.png
cl_hr2_7_g1d.png

Test at red hue (0%):

set centHue=0
set rangeHue=10
set transHue=40

%IMG7%magick ^
  -size 1x256 gradient: -rotate 90 ^
  -evaluate AddModulus %%[fx:50-%centHue%]%% ^
  -solarize 50%% ^
  -evaluate Multiply 2 ^
  -level %%[fx:100-%rangeHue%-2*%transHue%],%%[fx:100-%rangeHue%]%% ^
  cl_hr3_7.png

call %PICTBAT%graph1d cl_hr3_7.png
cl_hr3_7_g1d.png

The clut can then be applied to the hue channel of an image to make a mask. Alternatively, make the mask by applying the same procedure directly to the hue channel. For example, to lower the saturation of red pixels, (and pixels within plus or minus 5% of red), with a 10% transition range:

set centHue=0
set rangeHue=10
set transHue=10

%IMG7%magick ^
  rose: ^
  ( -clone 0 -modulate 100,0,100 ) ^
  ( -clone 0 ^
    -colorspace HSL ^
    -channel H -separate +channel ^
    -evaluate AddModulus %%[fx:50-%centHue%]%% ^
    -solarize 50%% ^
    -evaluate Multiply 2 ^
    -level %%[fx:100-%rangeHue%-2*%transHue%],%%[fx:100-%rangeHue%]%% ^
  ) ^
  -compose Over -composite ^
  cl_hrrose_7.png
cl_hrrose_7.png

Blurring cluts

Cluts can be blurred to create or expand transitions. As they have only one dimension, we use a one-dimensional blur for better performance than the two-dimensional "-blur".

cl_thm.png

cl_thm_g1d.png
%IM%convert ^
  cl_thm.png ^
  -morphology Convolve Blur:0x5 ^
  cl_thm_b1.png

call %PICTBAT%graph1d cl_thm_b1.png
cl_thm_b1_g1d.png

cl_flat2.png

cl_flat2_g1d.png
%IM%convert ^
  cl_flat2.png ^
  -morphology Convolve Blur:0x5 ^
  cl_flat2_b1.png

call %PICTBAT%graph1d cl_flat2_b1.png
cl_flat2_b1_g1d.png

Inverting cluts

See also Process modules: invclut, which describes a much faster "-process" module in C for the same job.

A clut transforms an image X into another image X'. Under certain conditions there will exist another clut that will transform X' into X.

Graphically, the clut is the transformation y = f(x). The inverse is x = f'(y).

Inverting a monochrome clut, so the effect is reversed, is fairly simple. We scale it vertically to a square, and compare it to (subtract it from) a vertical gradient. Pixels greater than the gradient are made white; others are made black. This is scaled horizontally to make a vertical clut. We rotate it to the horizontal orientation, just like the original clut.

cl_sig2.png, created above:

cl_sig2_g1d.png

Invert it.

FOR /F "usebackq" %%L ^
IN (`%IM%identify -format "WW=%%w" cl_sig2.png`) ^
DO set %%L

%IM%convert ^
  cl_sig2.png ^
  -scale "%WW%x%WW%^!" ^
  -size %WW%x%WW% gradient: ^
  -compose MinusDst -composite ^
  -fill #fff +opaque #000 ^
  -scale "1x%WW%^!" ^
  -rotate 90 ^
  cl_sig2_icl.png

call %PICTBAT%graph1d cl_sig2_icl.png
cl_sig2_icl_g1d.png

We can test the inversion by reading an image, applying the clut, then applying the inverse clut and we should be back where we started. The comparison should be near zero, ie less than 0.01.

%IM%convert ^
  rose: ^
  ( +clone ^
    cl_sig2.png -clut ^
    cl_sig2_icl.png -clut ^
  ) ^
  -metric RMSE ^
  -format "%%[distortion]" ^
  -compare info:
0.0037959

The first clut can be thought of as a gradient: that has been displaced, so the clut is an absolute displacement map, and the inverse clut gives a reverse displacement. So applying the inverse clut to the first clut should give a gradient:.

%IM%convert ^
  cl_sig2.png ^
  cl_sig2_icl.png -clut ^
  ( -size 1x%WW% gradient: -rotate 90 ) ^
  -metric RMSE ^
  -format "%%[distortion]" ^
  -compare info:
0.0037535

This simple method works for cluts that increase monotonically, but fails when the clut decreases, ie has a negative slope.

cl_exp3.png, created above:

cl_exp3_g1d.png

Invert it.

FOR /F "usebackq" %%L ^
IN (`%IM%identify -format "WW=%%w" cl_exp3.png`) ^
DO set %%L

%IM%convert ^
  cl_exp3.png ^
  -scale "%WW%x%WW%^!" ^
  -size %WW%x%WW% gradient: ^
  -compose MinusDst -composite ^
  -fill #fff +opaque #000 ^
  -scale "1x%WW%^!" ^
  -rotate 90 ^
  cl_exp3_icl.png

call %PICTBAT%graph1d cl_exp3_icl.png

The inversion is wrong.

cl_exp3_icl_g1d.png

We cure the problem with a "-negate".

%IM%convert ^
  cl_exp3.png ^
  -scale "%WW%x%WW%^!" ^
  -size %WW%x%WW% gradient: ^
  -compose MinusDst -composite ^
  -fill #fff +opaque #000 ^
  -scale "1x%WW%^!" ^
  -negate ^
  -rotate 90 ^
  cl_exp3_icl2.png

call %PICTBAT%graph1d cl_exp3_icl2.png

The inversion is correct.

cl_exp3_icl2_g1d.png

Test the inversion:

%IM%convert ^
  rose: ^
  ( +clone ^
    cl_exp3.png -clut ^
    cl_exp3_icl2.png -clut ^
  ) ^
  -metric RMSE ^
  -format "%%[distortion]" ^
  -compare info:
0.00411614

So we have a solution for cluts that increase and a slightly different soltion for ones that decrease. Where a clut both increases and decreases, multiple x-values yield the same y-value, so no solution is possible.

Inverting a colour clut that increase monotonically is slightly more complicated. We need to "un-sync" the colour channels, so the threshold is applied to each channel independently of the others. We should also (but don't here) test whether each of the three slopes increases or decreases.

cl_cbcc2.ppm, created above:

cl_cbcc2_glc.png

Invert it.

FOR /F "usebackq" %%L ^
IN (`%IM%identify -format "WW=%%w" cl_cbcc2.ppm`) ^
DO set %%L

%IM%convert ^
  cl_cbcc2.ppm ^
  -scale "%WW%x%WW%^!" ^
  -size %WW%x%WW% gradient: ^
  -compose MinusDst -composite ^
  -channel RGB -threshold 0 +channel ^
  -scale "1x%WW%^!" ^
  -rotate 90 ^
  cl_cbcc2_icl.png

call %PICTBAT%graphLineCol cl_cbcc2_icl.png
cl_cbcc2_icl_glc.png

Test the inversion:

%IM%convert ^
  rose: ^
  ( +clone ^
    cl_cbcc2.ppm -clut ^
    cl_cbcc2_icl.png -clut ^
  ) ^
  -metric RMSE ^
  -format "%%[distortion]" ^
  -compare info:
0.00148846

Inverting the awkward clut:

cl_cbcc4.ppm, created above:

cl_cbcc4_glc.png

The green channel decreases, so we have a "-negate" just for this.

FOR /F "usebackq" %%L ^
IN (`%IM%identify -format "WW=%%w" cl_cbcc4.ppm`) ^
DO set %%L

%IM%convert ^
  cl_cbcc4.ppm ^
  -scale "%WW%x%WW%^!" ^
  -size %WW%x%WW% gradient: ^
  -compose MinusDst -composite ^
  -channel RGB -threshold 0 ^
  -channel G -negate ^
  +channel ^
  -scale "1x%WW%^!" ^
  -rotate 90 ^
  cl_cbcc4_icx.png

call %PICTBAT%graphLineCol cl_cbcc4_icx.png
cl_cbcc4_icx_glc.png

Apply the inverse clut to the clut.

FOR /F "usebackq" %%L ^
IN (`%IM%identify -format "WW=%%w" cl_cbcc4.ppm`) ^
DO set %%L

%IM%convert ^
  cl_cbcc4.ppm ^
  cl_cbcc4_icx.png -clut ^
  cl_cbcc4_comp.png

call %PICTBAT%graphLineCol cl_cbcc4_comp.png

We can see the red and green channels have inverted successfully.
Blue has inverted successfully at the ends but not elsewhere.
The blue channel of the clut can't be inverted.

cl_cbcc4_comp_glc.png

For convenience, a script determines whether each channel increases or decreases.

cl_cbcc4.ppm, created above:

cl_cbcc4_glc.png

Calculate the inverse clut with a script.

call %PICTBAT%invClut cl_cbcc4.ppm

call %PICTBAT%graphLineCol cl_cbcc4_icl.png
cl_cbcc4_icl_glc.png

The command needs memory for two images that are n*n pixels square. For 8-bit cluts, n=256, and the method works well. For 16-bit cluts, n=65536 so each image needs 34 GB and the processing (on disk) takes 90 minutes.

This covers the inversion of cluts, which can also be thought of as absolute 1-D displacement maps. For the inversion of relative 1-D displacement maps, see Displacement maps: Inverse displacements.

Another method for inverting cluts uses a process module. See Process modules: invclut. This works correctly only when the clut increases monotonically.

Calculate the inverse clut with a process module.

%IMDEV%convert ^
  cl_cbcc4.ppm ^
  -process invclut ^
  cl_pm_icl.png

call %PICTBAT%graphLineCol cl_pm_icl.png
cl_pm_icl_glc.png

The inversion is correct for the red channel, but not for the green or blur channels because they do not increase monotonically.

Clut algebra

-clut behaves like the arithmetic operator multiply, and grad.png is the identity, the number 1. invClutA corresponds to 1/clutA.

The following two commands are equivalent:

convert in.png clutA.png -clut clutB.png -clut out.png

convert in.png ( clutA.png clutB.png -clut ) -clut out.png

In words: taking an image, applying a clut, than applying a second clut, is the same as applying the second clut to the first and applying the resulting clut to the image.
(x * A) * B = x * (A * B)

In general, the order is significant (blah is it?). That is, the two commands ...

convert in.png clutA.png -clut clutB.png -clut out1.png
convert in.png clutB.png -clut clutA.png -clut out2.png

... generally do not create the same results. (But they will if clutA.png and clutB.png are inverses of each other.)

If invClutA.png is the inverse of clutA.png (see Inverting cluts above), then ...

convert in.png clutA.png -clut invClutA.png -clut out.png

... makes an output that is the same as the input.
x * A * 1/A = x

If invClutA.png is the inverse of clutA.png, it does not necessarily follow that clutA.png is the inverse of invClutA.png. If clutA.png has a horizontal portion, then multiple inputs define the same output, and the inverse of clutA.png is not invertible.

If grad.png is a horizontal gradient from black to white, the same size as the clut image, then ...

convert grad.png clutA.png -clut out.png

... makes an output that is the same as clutA.png,
1 * A = A

And ....

convert clutA.png grad.png -clut out.png

... also makes an output that is the same as clutA.png.
A * 1 = A

Provided clutA.png always increases (there are no flat portions), then ...

convert clutA.png invClutA.png -clut out.png

... makes an output that is the same as grad.png.
A * 1/A = 1

Histogram algebra

I will denote the command fragment ...

imageA -process 'mkhisto cumul norm'

... as ...

CH(imageA)

CH(clutA) creates the inverse clut.

CH(clutA) = invclut(clutA)

Provided clutA is invertible, and its inverse is invertible, then:

invclut(invclut(clutA)) = clutA

... so:

invclut(invclut(invclut(clutA))) = invclut(clutA)

For general images, noting that cumulative histograms are always invertible:

CH(invclut(CH(imageA))) = CH(imageA)

In words: if we know the cumulative histogram of an image CH(imageA) then we can takes its inverse, and the cumulative histogram of this inverse is the same as the cumulative histogram of the original image. The inverse CH of an image is a proxy for that image.

Application: Matching histograms

If an image undergoes channel curve manipulations to form a second image, the cumulative histograms (CHs) can be used to transform either image to the other. More usefully, the same transformation can be applied to other images in order to obtain the same effect.

toes.png manipulated with Gimp to make toes_x.jpg.

toes.pngjpg toes_x.jpg

Calculate the cumulative histograms (CHs).

set c2hDO_CUMUL=1
call %PICTBAT%col2Histo toes.png cl_toes_chist.png
call %PICTBAT%col2Histo toes_x.jpg cl_toes_x_chist.png
set c2hDO_CUMUL=

call %PICTBAT%graphLineCol cl_toes_chist.png
call %PICTBAT%graphLineCol cl_toes_x_chist.png
cl_toes_chist_glc.png cl_toes_x_chist_glc.png

Calculate the inverse CHs.

call %PICTBAT%invClut cl_toes_chist.png
call %PICTBAT%invClut cl_toes_x_chist.png

call %PICTBAT%graphLineCol cl_toes_chist_icl.png
call %PICTBAT%graphLineCol cl_toes_x_chist_icl.png
cl_toes_chist_icl_glc.png cl_toes_x_chist_icl_glc.png

Apply the inverse of toes CH to toes_x CH ...

%IM%convert ^
  cl_toes_x_chist.png ^
  cl_toes_chist_icl.png ^
  -clut ^
  cl_tx_it.png

call %PICTBAT%graphLineCol cl_tx_it.png
cl_tx_it_glc.png

... and the inverse of toes_x CH to toes CH.

 %IM%convert ^
  cl_toes_chist.png ^
  cl_toes_x_chist_icl.png ^
  -clut ^
  cl_t_ixt.png

call %PICTBAT%graphLineCol cl_t_ixt.png
cl_t_ixt_glc.png

We have created two cluts. One will transform from toes.png to toes_x.jpg. The other will transform in the opposite direction.

Apply these to toes and toes_x.

 %IM%convert ^
  toes.png ^
  cl_t_ixt.png ^
  -clut ^
  cl_toes_txit.png

%IM%convert ^
  toes_x.jpg ^
  cl_tx_it.png ^
  -clut ^
  cl_toes_x_txit.png
cl_toes_txit.png cl_toes_x_txit.png

Check the results:

%IM%compare -metric RMSE toes.png cl_toes_x_txit.png NULL: 
echo. 
%IM%compare -metric RMSE toes_x.jpg cl_toes_txit.png NULL: 

cmd /c exit /B 0
489.949 (0.00747614) 
309.884 (0.00472852)

Both comparisons are less than 1%. We have created a clut that replicates the transformation we made in Gimp. As a bonus, we also have the inverse transformation.

From the cumulative histograms of two images, we can easily create a clut to transform from one image to the other. Creating these cumulative histograms in a script is a pain. Perhaps it will eventually be built in to ImageMagick.

I think my method is equivalent to Fred Weinhaus's histmatch script. See Fred's ImageMagick Scripts. My method uses IM tools to make a clut by applying the inverse of one clut to another, where Fred's method uses (in genLutImage()) Unix tools to walk through one CH until the count exceeds the other.

Instead of my scripts above, a faster and more accurate method uses process modules. See Process modules: Matching histograms.

Application: Using cluts for animation

A clut can be used as a temporal transformation for animation, eg to transform a linear motion into a smooth start/stop motion. The x-dimension of a horizontal clut represents time. The pixel value represents a transformed time, eg for a smooth start and stop.

Alternatively, the clut may be a spatial transformation, so the pixel value represents an x-coordinate, or the values in two channels represent x- and y-coordinates.

Given a clut, we want to look up entry pr (where 0 <= pr <= 1). If the animation has n frames excluding the final frame, for frame i (where 0 <= i <= n to include the final frame or 0 <= i <= n-1 to exclude it), pr = i/n. We lookup pixel x-coordinate = pr * width.

for /F "usebackq" %%L ^
in (`%IM%convert %1 -format "PRT=%%[fx:p{pr*w,pr*h}.r]" info:`) ^
do set %%L

As an example, we make a clut with a sine function and cosine function, which define movement in a circle. But we distort the gradient before calculating sine and cos, and this warps time. We then clone, flop and append to repeat the movement backwards.

%IM%convert ^
  -size 1x256 gradient: -rotate 90 ^
  -sigmoidal-contrast 10x50%% ^
  -channel R -evaluate sin 1 ^
  -channel G -evaluate cos 1 ^
  +channel ^
  ( +clone -flop ) ^
  +append ^
  cl_sincos.png

call %PICTBAT%graphLineCol cl_sincos.png
cl_sincos_glc.png

The red channel defines the horizontal motion; the green channel defines the vertical motion. The blue channel is not used.

If NUM_FRAMES is the number of frames in a motion excluding the final frame:

set NUM_FRAMES=200
set DIAM=200

set /A nfm1=%NUM_FRAMES%-1

set ANIM_LIST=cl_anim.lis
set FRAME_LIST=cl_anim_frames.lis
del %ANIM_LIST% 2>nul
del %FRAME_LIST% 2>nul

for /L %%i in (0,1,%nfm1%) do (

  set sFRAME=00000%%i
  set sFRAME=!sFRAME:~-6!

  for /F "usebackq" %%L in (`%IM%convert ^
    cl_sincos.png ^
    -format "X=%%[fx:%DIAM%*p{w*%%i/%NUM_FRAMES%,h*%%i/%NUM_FRAMES%}.r]\nY=%%[fx:%DIAM%*p{w*%%i/%NUM_FRAMES%,h*%%i/%NUM_FRAMES%}.g]" ^
    info:`) do set %%L

  echo %%i !sFRAME! !X! !Y! >>%ANIM_LIST%

  set FNAME=%TEMP%\cl_frame_!sFRAME!.png

  %IM%convert -size 200x200 xc:blue ^
    -fill Yellow -draw "translate !X!,!Y! circle 0,0 0,10" ^
    !FNAME!

  echo !FNAME! >>%FRAME_LIST%
)

rem %IM%convert -loop 0 @%FRAME_LIST% cl_anim.gif

call %PICTBAT%blendFrames cl_anim_frames.lis 3 cl_anim_frames2.lis

%IM%convert -loop 0 @cl_anim_frames2.lis cl_anim2.gif
0 000000 100.002 200 
1 000001 100.447 200 
2 000002 100.944 199.997 
3 000003 101.482 199.989 
4 000004 102.082 199.978 
5 000005 102.746 199.962 
6 000006 103.48 199.939 
7 000007 104.288 199.909 
8 000008 105.178 199.866 
9 000009 106.149 199.81 
10 000010 107.229 199.738 
11 000011 108.42 199.645 
12 000012 109.721 199.526 
13 000013 111.159 199.374 
14 000014 112.731 199.186 
15 000015 114.465 198.948 
16 000016 116.356 198.653 
17 000017 118.433 198.285 
18 000018 120.701 197.832 
19 000019 123.175 197.277 
20 000020 125.875 196.592 
21 000021 128.803 195.762 
22 000022 131.975 194.748 
23 000023 135.405 193.522 
24 000024 139.087 192.041 
25 000025 143.032 190.268 
26 000026 147.247 188.129 
27 000027 151.701 185.594 
28 000028 156.379 182.585 
29 000029 161.252 179.038 
30 000030 166.27 174.88 
31 000031 171.37 170.03 
32 000032 176.464 164.441 
33 000033 181.447 157.997 
34 000034 186.191 150.699 
35 000035 190.526 142.446 
36 000036 194.289 133.275 
37 000037 197.262 123.167 
38 000038 199.232 112.193 
39 000039 199.984 100.457 
40 000040 199.261 88.1172 
41 000041 196.924 75.4057 
42 000042 192.701 62.5936 
43 000043 186.595 50.0088 
44 000044 178.448 38.0465 
45 000045 168.394 27.0842 
46 000046 156.508 17.536 
47 000047 143.06 9.78994 
48 000048 128.36 4.12726 
49 000049 112.812 0.873548 
50 000050 96.8795 0.0488289 
51 000051 81.0447 1.86233 
52 000052 65.7703 6.06186 
53 000053 51.5214 12.5836 
54 000054 38.6488 21.0712 
55 000055 27.4522 31.2157 
56 000056 18.1427 42.6228 
57 000057 10.7767 54.8696 
58 000058 5.43587 67.5859 
59 000059 1.94266 80.4061 
60 000060 0.274662 92.9952 
61 000061 0.146487 105.12 
62 000062 1.40383 116.577 
63 000063 3.79461 127.223 
64 000064 7.09667 136.97 
65 000065 11.111 145.775 
66 000066 15.6199 153.662 
67 000067 20.4783 160.613 
68 000068 25.5197 166.723 
69 000069 30.6266 172.01 
70 000070 35.7024 176.582 
71 000071 40.6715 180.493 
72 000072 45.474 183.821 
73 000073 50.0722 186.642 
74 000074 54.4287 189.007 
75 000075 58.5397 191 
76 000076 62.3833 192.651 
77 000077 65.9665 194.03 
78 000078 69.2948 195.166 
79 000079 72.3696 196.105 
80 000080 75.2068 196.877 
81 000081 77.8133 197.506 
82 000082 80.2111 198.023 
83 000083 82.4021 198.438 
84 000084 84.4085 198.778 
85 000085 86.23 199.047 
86 000086 87.8965 199.264 
87 000087 89.419 199.439 
88 000088 90.8013 199.576 
89 000089 92.0604 199.685 
90 000090 93.2027 199.768 
91 000091 94.2453 199.834 
92 000092 95.1807 199.884 
93 000093 96.0382 199.922 
94 000094 96.8157 199.948 
95 000095 97.5204 199.971 
96 000096 98.1591 199.983 
97 000097 98.7353 199.992 
98 000098 99.2571 199.997 
99 000099 99.7302 200 
100 000100 100.002 200 
101 000101 99.5528 200 
102 000102 99.0557 199.997 
103 000103 98.5185 199.989 
104 000104 97.9181 199.978 
105 000105 97.2537 199.962 
106 000106 96.5197 199.939 
107 000107 95.7124 199.909 
108 000108 94.8223 199.866 
109 000109 93.8509 199.81 
110 000110 92.7706 199.738 
111 000111 91.5803 199.645 
112 000112 90.279 199.526 
113 000113 88.841 199.374 
114 000114 87.2693 199.186 
115 000115 85.5354 198.948 
116 000116 83.644 198.653 
117 000117 81.5669 198.285 
118 000118 79.2992 197.832 
119 000119 76.8253 197.277 
120 000120 74.1253 196.592 
121 000121 71.1975 195.762 
122 000122 68.0247 194.748 
123 000123 64.5949 193.522 
124 000124 60.9125 192.041 
125 000125 56.968 190.268 
126 000126 52.7528 188.129 
127 000127 48.2988 185.594 
128 000128 43.621 182.585 
129 000129 38.7475 179.038 
130 000130 33.7298 174.88 
131 000131 28.6296 170.03 
132 000132 23.5355 164.441 
133 000133 18.5534 157.997 
134 000134 13.8093 150.699 
135 000135 9.47402 142.446 
136 000136 5.71139 133.275 
137 000137 2.7382 123.167 
138 000138 0.76759 112.193 
139 000139 0.0156252 100.457 
140 000140 0.738537 88.1172 
141 000141 3.07622 75.4057 
142 000142 7.29882 62.5936 
143 000143 13.4047 50.0088 
144 000144 21.5516 38.0465 
145 000145 31.6057 27.0842 
146 000146 43.4922 17.536 
147 000147 56.9397 9.78994 
148 000148 71.6396 4.12726 
149 000149 87.1883 0.873548 
150 000150 103.12 0.0488289 
151 000151 118.955 1.86233 
152 000152 134.23 6.06186 
153 000153 148.479 12.5836 
154 000154 161.351 21.0712 
155 000155 172.548 31.2157 
156 000156 181.857 42.6228 
157 000157 189.223 54.8696 
158 000158 194.564 67.5859 
159 000159 198.057 80.4061 
160 000160 199.725 92.9952 
161 000161 199.854 105.12 
162 000162 198.596 116.577 
163 000163 196.205 127.223 
164 000164 192.903 136.97 
165 000165 188.889 145.775 
166 000166 184.38 153.662 
167 000167 179.522 160.613 
168 000168 174.48 166.723 
169 000169 169.373 172.01 
170 000170 164.298 176.582 
171 000171 159.328 180.493 
172 000172 154.526 183.821 
173 000173 149.928 186.642 
174 000174 145.571 189.007 
175 000175 141.46 191 
176 000176 137.617 192.651 
177 000177 134.034 194.03 
178 000178 130.705 195.166 
179 000179 127.63 196.105 
180 000180 124.793 196.877 
181 000181 122.187 197.506 
182 000182 119.789 198.023 
183 000183 117.602 198.438 
184 000184 115.591 198.778 
185 000185 113.77 199.047 
186 000186 112.104 199.264 
187 000187 110.581 199.439 
188 000188 109.199 199.576 
189 000189 107.94 199.685 
190 000190 106.797 199.768 
191 000191 105.755 199.834 
192 000192 104.819 199.884 
193 000193 103.962 199.922 
194 000194 103.184 199.948 
195 000195 102.48 199.971 
196 000196 101.841 199.983 
197 000197 101.265 199.992 
198 000198 100.743 199.997 
199 000199 100.27 200 
cl_anim2.gif

The motion blur comes from temporal supersampling.

Scripts

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

I visualise cluts with scripts graph1d.bat and graphLineCol.bat.

graph1d.bat

rem From image %1 with height=1,
rem makes a graph with same width but max height=256.
@rem
@rem Optional:
@rem   %2 is background colour (default Khaki).
@rem   %3 is grid (default 0 = no grid).
@rem   %4 is output filename.
@rem   %5 border colour ("none" = no border) [blue]
@rem
@rem Updated:
@rem   11 March 2017 Simplified.
@rem   20 June 2017 Restored gradient in output.


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

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 g1d


set BACKCOL=%2
if "%BACKCOL%"=="." set BACKCOL=
if "%BACKCOL%"=="" set BACKCOL=Khaki

set GRID_SPEC=%3
if "%GRID_SPEC%"=="." set GRID_SPEC=
if "%GRID_SPEC%"=="" set GRID_SPEC=0

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

set BORDCOL=%5
if "%BORDCOL%"=="." set BORDCOL=
if "%BORDCOL%"=="" set BORDCOL=blue


set TXTFILE=%TEMP%\sp.txt

for /F "usebackq" %%L ^
in (`%IM%identify -format "WW=%%w\nHH=%%h" %INFILE%`) ^
do set %%L

set S_RESIZE=
if %WW% GTR 1000 (
  set S_RESIZE=-resize 1000x%HH%
  set WW=1000
)

set newH=256

if %WW% LSS 256 (
  set newH=%WW%
)

set S_GRID=
if not %GRID_SPEC%==0 (
  call %PICTBAT%grid %WW% %newH% 4 3 1
  set S_GRID=grid.png -compose Exclusion -composite
)

rem echo %0: WW=%WW% newH=%newH%

set /A newH4=4*%newH%

set BRDR=
if /I not "%BORDCOL%"=="none" set BRDR=-bordercolor %BORDCOL% -compose Over -border 1x1

goto skip

%IM%convert ^
  %INFILE% ^
  -scale "%WW%x1^!" ^
  -scale "%WW%x%newH4%^!" ^
  -size %WW%x%newH4% gradient: ^
  -compose MinusDst -composite ^
  -threshold 0 ^
  -resize "%WW%x%newH%^!" ^
  +level-colors Black,%BACKCOL% ^
  %S_GRID% ^
  %BRDR% ^
  %OUTFILE%

:skip

%IM%convert ^
  %INFILE% ^
  -scale "%WW%x1^!" ^
  -scale "%WW%x%newH4%^!" ^
  +write mpr:SCLE ^
  -size %WW%x%newH4% gradient: ^
  -compose MinusDst -composite ^
  -threshold 0 ^
  -negate ^
+write g.png ^
  mpr:SCLE ^
  +swap ^
  -compose CopyOpacity -composite ^
  -resize "%WW%x%newH%^!" ^
  -background %BACKCOL% ^
  -compose Over -layers Flatten ^
  %S_GRID% ^
  %BRDR% ^
  %OUTFILE%

call echoRestore

graphLineCol.bat

rem From image %1 with height=1,
rem makes a colour line graph with same width but max height=256.
@rem
@rem Optional:
@rem   %2 is background colour, can be "none" (default Black).
@rem   %3 is grid (default 0 = no grid).
@rem   %4 is whether to show clut below graph (default 1 = do show).
@rem   %5 output file
@rem   %6 limit output width [1000]
@rem
@rem Updated:
@rem   15-May-2016 to use "-layers flatten".
@rem   13-October-2016 Fixed bug in OUTFILE.


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

@setlocal disabledelayedexpansion
@rem Otherwise ! in LINE_IMG causes IM to misread.

@call echoOffSave

rem Can't use setInOut.
rem call %PICTBAT%setInOut %1 glc
set INFILE=%1
set OUTFILE=%~dpn1_glc.png


set BACKCOL=%2
if "%BACKCOL%"=="." set BACKCOL=
if "%BACKCOL%"=="" set BACKCOL=#000

set GRID_SPEC=%3
if "%GRID_SPEC%"=="." set GRID_SPEC=
if "%GRID_SPEC%"=="" set GRID_SPEC=0

set SHOW_CLUT=%4
if "%SHOW_CLUT%"=="." set SHOW_CLUT=
if "%SHOW_CLUT%"=="" set SHOW_CLUT=1

set sOUT=%5
if "%sOUT%"=="." set sOUT=
if "%sOUT%"=="" set sOUT=%OUTFILE%
set OUTFILE=%sOUT%

set LIM_WW=%6
if "%LIM_WW%"=="." set LIM_WW=
if "%LIM_WW%"=="" set LIM_WW=1000


FOR /F "usebackq" %%L IN (`%IM%identify ^
  -format "WW=%%w\nHH=%%h" ^
  %INFILE%`) DO set %%L

if %WW% GTR %LIM_WW% (
  set WW=%LIM_WW%
)

set newH=256

if %WW% LSS 256 (
  set newH=%WW%
)

if %GRID_SPEC%==0 (
  set S_GRID=
) else (
  call %PICTBAT%grid %WW% %newH% 20 4 . gray50
  set S_GRID=grid.png -compose Exclusion -composite
)

set /A newH4=4*%newH%

if /I %BACKCOL%==none (
  set sBACK=
) else (
  set sBACK=-background %BACKCOL% -compose Over -flatten
)

set sBACK=-background %BACKCOL%


set sBORD=-bordercolor Black -border 1x1
set sBORD=

set LINE_IMG=^
  -separate +channel -scale "%WW%x%newH4%^!" ^
  -size %WW%x%newH4% gradient: ^
  -compose MinusDst -composite ^
  -fill #fff +opaque #000 ^
  -resize "%WW%x%newH%^!" ^
  -morphology edgeout diamond:1

set /A SHOW_CLUT_HT=%WW%/10
if %SHOW_CLUT_HT% LSS 25 set SHOW_CLUT_HT=25

if %SHOW_CLUT%==0 (
  set sCLUT=
) else (
  set sCLUT= ^( %INFILE% -scale "%WW%x%SHOW_CLUT_HT%^!" ^) -append
)

rem %IMDEV%convert %INFILE% -depth 16 %TEMP%\glc.png
rem set INFILE=%TEMP%\glc.png

%IM%convert ^
  "%INFILE%" ^
  -colorspace sRGB ^
  -scale "%WW%x1^!" ^
  -scale "%WW%x%newH%^!" ^
  ( -clone 0 ^
    ( -clone 0 ^
      -fill #f00 -colorize 100 ^
    ) ^
    ( -clone 0 -channel R %LINE_IMG% ) ^
    -delete 0 ^
    -alpha off ^
    -compose CopyOpacity -composite ^
  ) ^
  ( -clone 0 ^
    ( -clone 0 ^
      -fill #0f0 -colorize 100 ^
    ) ^
    ( -clone 0 -channel G %LINE_IMG% ) ^
    -delete 0 ^
    -alpha off ^
    -compose CopyOpacity -composite ^
  ) ^
  ( -clone 0 ^
    ( -clone 0 ^
      -fill #00f -colorize 100 ^
    ) ^
    ( -clone 0 -channel B %LINE_IMG% ) ^
    -delete 0 ^
    -alpha off ^
    -compose CopyOpacity -composite ^
  ) ^
  -delete 0 ^
  %sBACK% ^
  -compose Add -layers Flatten ^
  %S_GRID% ^
  %sBORD% ^
  %sCLUT% ^
  %OUTFILE%

if ERRORLEVEL 1 (
  echo %0: convert failed
  exit /B 1
)

call echoRestore & set glcOUTFILE=%OUTFILE%

mSigClut.bat

rem %1 output filename
rem %2 output width, pixels [4096]
rem %3 a, an input fraction [0.5]
rem %4 b, an output fraction [0.5]
rem %5 s, slope at both ends
rem %6 E, contrast strength, > 0
@rem       1 for straight line,
@rem      >1 increases contrast at mid point
@rem      <1 decreases contrast at mid point
@rem      Values v and 1/v are symmetrical.
@rem
@rem   Inflection is at (a,b).
@rem
@rem
@rem

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 msc

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

set WW=%2
if "%WW%"=="." set WW=
if "%WW%"=="" set WW=4096

set a=%3
if "%a%"=="." set a=
if "%a%"=="" set a=0.5

set b=%4
if "%b%"=="." set b=
if "%b%"=="" set b=0.5

set s=%5
if "%s%"=="." set s=
if "%s%"=="" set s=1

set E=%6
if "%E%"=="." set E=
if "%E%"=="" set E=3

set a1=(1-%a%)
set s1=(1-%s%)
set sa=%s%*%a%
set sa1=%s%*%a1%
set LR=log(%b%)/log(%a%)
set LR1=log(1-%b%)/log(1-%a%)

for /F "usebackq" %%L in (`%IM%identify
  -precision 15 ^
  -format "a1=%%[fx:%a1%]\ns1=%%[fx:%s1%]\nsa=%%[fx:%sa%]\nsa1=%%[fx:%sa1%]" ^
  xc:`) do set %%L

for /F "usebackq" %%L in (`%IM%identify
  -precision 15 ^
  -format "LR=%%[fx:%LR%]\nLR1=%%[fx:%LR1%]" ^
  xc:`) do set %%L

%IM%convert -size 1x%WW% gradient: -rotate 90 ^
  -fx "u<%a%?pow(%s1%*u+%sa%*pow(u/%a%,%E%),%LR%):1-pow(%s1%*(1-u)+%sa1%*pow((1-u)/%a1%,%E%),%LR1%)" ^
  %OUTFILE%

rem call %PICTBAT%graph1d %OUTFILE%

call echoRestore

@endlocal

invClut.bat

rem From %1, a clut image, height=1, make the inverse clut.
rem Optional %2 is output filename.

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

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 icl

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


FOR /F "usebackq" %%L ^
IN (`%IM%identify -format "WW=%%w\nHH=%%h\nWm1=%%[fx:w-1]" %INFILE%`) ^
DO set %%L

if not %HH%==1 (
  echo %0: Expected height of 1 in %*
  exit /B 1
)

FOR /F "usebackq" %%L ^
IN (`%IM%convert ^
  %INFILE% ^
  -format "negR=%%[fx:p{0,0}.r>p{%Wm1%,0}.r]\nnegG=%%[fx:p{0,0}.g>p{%Wm1%,0}.g]\nnegB=%%[fx:p{0,0}.b>p{%Wm1%,0}.b]" ^
  info:`) ^
DO set %%L

set sNegR=
set sNegG=
set sNegB=
if %negR%==1 set sNegR=-channel R -negate
if %negG%==1 set sNegG=-channel G -negate
if %negB%==1 set sNegB=-channel B -negate

%IM%convert ^
  %INFILE% ^
  -scale "%WW%x%WW%^!" ^
  -size %WW%x%WW% gradient: ^
  -compose MinusDst -composite ^
  -channel RGB -threshold 0 ^
  %sNegR% %sNegG% %sNegB% ^
  +channel ^
  -scale "1x%WW%^!" ^
  -rotate 90 ^
  %OUTFILE%

call echoRestore

@endlocal & set iclOUTFILE=%OUTFILE%

blendFrames.bat

rem Blend frames, temporally subsampling after supersampling.
rem Given %1 is a list of frames
rem and %2 is an integer >= 2,
rem makes frames in groups, each the mean of %2 frames,
rem writing list of new frames to %3.

rem FIXME: what do we do if any remainder?

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

@setlocal enabledelayedexpansion

@call echoOffSave


set INNUM=0
set OUTNUM=0

set sFRAMES=

del %3 2>nul

for /F %%F in (%1) do (
  set /A COUNT=!INNUM!%%%2
  rem echo COUNT=!COUNT!
  rem echo %%F
  set FNAME=%%F

  if !COUNT!==0 (
    rem If got some, output them.

    if not "!sFRAMES!"=="" (
      set sOUTNUM=00000!OUTNUM!
      set sOUTNUM=!sOUTNUM:~-6!

      set sFOUT=%TEMP%\%%~nF_bf_!sOUTNUM!%%~xF
      rem echo sFOUT=!sFOUT!
      echo !sFOUT! >>%3

      %IM%convert !sFRAMES! -evaluate-sequence Mean !sFOUT!

      rem echo sFRAMES=!sFRAMES!

      set sFRAMES=!FNAME!
      set /A OUTNUM+=1
    )
  ) else (
    set sFRAMES=!sFRAMES! !FNAME!
  )

  set /A INNUM+=1
)

echo INNUM=%INNUM% OUTNUM=%OUTNUM%

@call echoRestore

@endlocal

col2Histo.bat

rem From colour image %1,
rem makes 3-channel clut of the cumulative histogram.
rem
rem If %2 is given, this will be the output file.
@rem
@rem Also uses:
@rem   c2hINBITS optional, 8, 16. If not set, uses depth of input.
@rem     8 is quicker but less precise.
@rem   c2hDO_CUMUL if 1, makes cumulative histogram.
@rem


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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 c2h

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

set TMPEXT=.miff
set TMPEXT=%EXT%

if "%c2hDO_CUMUL%"=="" set c2hDO_CUMUL=0

set g2hDO_CUMUL=%c2hDO_CUMUL%

if %c2hDO_CUMUL%==0 (
  set AUTO=-auto-level
) else (
  set AUTO=
)

if "%c2hINBITS%"=="" (
  for /F "usebackq" %%L in (`%IM%identify ^
    -format "c2hINBITS=%%z" %INFILE%`) do set %%L
  set SETDEPTH=
) else (
  set SETDEPTH=-depth %c2hINBITS%
)

if "%c2hINBITS%"=="" (
  echo %0: Failed to find INBITS for INFILE [%INFILE%]
  exit /B 1
)

set g2hINBITS=%c2hINBITS%


set TEMPDIR=%TEMP%

%IM%convert ^
  %INFILE% ^
  %SETDEPTH% ^
  -separate ^
  %TEMPDIR%\%~n1_%sioCODE%_%%d%TMPEXT%


call %PICTBAT%gray2Histo %TEMPDIR%\%~n1_%sioCODE%_0%TMPEXT%
if ERRORLEVEL 1 exit /B 1
set c0=%g2hOUTFILE%

call %PICTBAT%gray2Histo %TEMPDIR%\%~n1_%sioCODE%_1%TMPEXT%
if ERRORLEVEL 1 exit /B 1
set c1=%g2hOUTFILE%

call %PICTBAT%gray2Histo %TEMPDIR%\%~n1_%sioCODE%_2%TMPEXT%
if ERRORLEVEL 1 exit /B 1
set c2=%g2hOUTFILE%

%IM%convert ^
  %c0% ^
  %c1% ^
  %c2% ^
  -combine ^
  -set colorspace sRGB ^
  %AUTO% ^
  %OUTFILE%


call echoRestore

endlocal & set c2hOUTFILE=%OUTFILE%

gray2Histo.bat

rem From image %1, assumed grayscale, makes a histogram (clut) image.
@rem
@rem Also uses:
@rem   g2hINBITS optional, 8, 16. If not set, uses depth of input.
@rem     8 is much quicker but less precise.
@rem   g2hDO_CUMUL if 1, makes cumulative histogram.
@rem


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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 g2h


rem Output is normalised so maximum count is white.
rem Or cumulative, from 0.0 to 1.0.

if "%g2hDO_CUMUL%"=="" set g2hDO_CUMUL=0

set OUTTXT=%TEMP%\%~n1_%sioCODE%_out.txt
set TXTFILE=%TEMP%\%~n1_%sioCODE%.txt

del %TXTFILE% 2>nul



set INBITS=%g2hINBITS%

if "%INBITS%"=="" (
  for /F "usebackq" %%L in (`%IM%identify ^
    -format "INBITS=%%z" %INFILE%`) do set %%L
)

if "%INBITS%"=="" (
  echo %0: Failed to find INBITS for INFILE [%INFILE%]
  exit /B 1
)

set OUTBITS=16

%IM%convert %INFILE% -depth %INBITS% -format %%c histogram:info:%TXTFILE%

if %INBITS%==8 (
  set OUT_W=256
) else if %INBITS%==16 (
  set OUT_W=65536
) else (
  echo %0: Bad INBITS=%INBITS%
  exit /B 1
)

set /A OUT_Wm1=%OUT_W%-1

if %OUTBITS%==8 (
  set OUT_MAX=255
) else if %OUTBITS%==16 (
  set OUT_MAX=65535
) else (
  echo %0: Bad OUTBITS=%OUTBITS%
  exit /B 1
)



echo # ImageMagick pixel enumeration: %OUT_W%,1,%OUT_MAX%,srgb>%OUTTXT%

set MAX_CNT=0
set CUMUL_CNT=0

for /F "tokens=1 delims=:(), " %%L in (%TXTFILE%) do (
  if !MAX_CNT! LSS %%L set MAX_CNT=%%L
)

set MAX_CNT=65535

if %g2hDO_CUMUL%==1 (
  for /F "usebackq" %%L in (`%IM%convert ^
    %INFILE% -format "MAX_CNT=%%[fx:w*h]" ^
    info:`) do set %%L
)

set NEXT_X=0
set CNTint=0

for /F "usebackq" %%L in (`%IM%identify ^
  -precision 9 ^
  -format "FACT=%%[fx:%OUT_MAX%/%MAX_CNT%]" ^
  xc:`) do set %%L

for /F "tokens=1,2 eol=# delims=:(), " %%L in (%TXTFILE%) do (
  set CNT=%%L
  set VAL=%%M
  set /A CUMUL_CNT+=%%L

  if %g2hDO_CUMUL%==1 (

    set CNT=!CUMUL_CNT!

    if !VAL! GTR !NEXT_X! (
      set /A LAST_X=!VAL!-1

      for /L %%x in (!NEXT_X!,1,!LAST_X!) do echo %%x,0: ^(!CNTint!,!CNTint!,!CNTint!^)>>%OUTTXT%
    )
  )

  for /F "usebackq" %%L in (`%IM%identify ^
    -format "CNTint=%%[fx:int(%FACT%*!CNT!+0.5)]" ^
    xc:`) do set %%L

  echo !VAL!,0: ^(!CNTint!,!CNTint!,!CNTint!^)>>%OUTTXT%

  set /A NEXT_X=!VAL!+1
)

if %NEXT_X% LEQ %OUT_Wm1% (
  for /L %%x in (%NEXT_X%,1,%OUT_Wm1%) do echo %%x,0: ^(!CNTint!,!CNTint!,!CNTint!^)>>%OUTTXT%
)

%IM%convert -background Black %OUTTXT% %OUTFILE%
if ERRORLEVEL 1 (
  echo %0: error processing %*
)

call echoRestore

endlocal & set g2hOUTFILE=%OUTFILE%

mClutUpDn.bat

rem Makes an up-down clut.
rem
rem %1 is width of clut [default 10000]
rem %2 is centre of peak (% of width) [50]
rem %3 is width of peak (% of width) [20]
rem %4 is width of each ramp (% of width) [10]
rem %5 is 0 or 1, whether to wrap. If 0, there may be only one ramp (up or down). For hue, use 1.
rem %6 is output. Default mcud.png.
@rem
@rem Can also use:
@rem   mcudTRANS a transformation such as "-sigmoidal-contrast 5,50%".

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

@setlocal

rem @call echoOffSave

rem call %PICTBAT%setInOut %1 mcud


set WW=%1
if "%WW%"=="." set WW=
if "%WW%"=="" set WW=10000

set PKpc=%2
if "%PKpc%"=="." set PKpc=
if "%PKpc%"=="" set PKpc=50

set PKWpc=%3
if "%PKWpc%"=="." set PKWpc=
if "%PKWpc%"=="" set PKWpc=20

set RWpc=%4
if "%RWpc%"=="." set RWpc=
if "%RWpc%"=="" set RWpc=10

set DO_WRAP=%5
if "%DO_WRAP%"=="." set DO_WRAP=
if "%DO_WRAP%"=="" set DO_WRAP=0

set OUTFILE=%6
if "%OUTFILE%"=="" set OUTFILE=mcud.png


set /A PK=%PKpc%*%WW%/100
set /A PKW=%PKWpc%*%WW%/100
set /A RW=%RWpc%*%WW%/100

set /A FLAT_L=%WW%/2-%PKW%/2-%RW%
set /A FLAT_R=%WW%-%FLAT_L%-%PKW%-2*%RW%
set /A ROLL=(%PKpc%-50)*%WW%/100

set sROLL=%ROLL%
set absROLL=%ROLL%
if %ROLL% GEQ 0 (
  set sROLL=+%ROLL%
) else (
  set /A absROLL=-%ROLL%
)

rem FIXME: wrong. If no wrap, we want new part to be zero.
set strROLL=
set strBLANK=
if "%PKpc%" NEQ "50" (
  set strROLL=-roll %sROLL%+0
  if "%DO_WRAP%"=="0" (
    if %ROLL% LSS 0 (
      set strBLANK=-size %absROLL%x1 xc:#000 -gravity West -compose Over -composite
    ) else (
      set strBLANK=-size %absROLL%x1 xc:#000 -gravity East -compose Over -composite
    )
  )
)

echo FLAT_L=%FLAT_L% PK=%PK% PKW=%PKW% RW=%RW% FLAT_R=%FLAT_R% absROLL=%absROLL% strROLL=%strROLL%

%IM%convert ^
  -size %FLAT_L%x1 xc:#000 ^
  -size %RW%x1 gradient:#000-#fff ^
  -size %PKW%x1 xc:#fff ^
  -size %RW%x1 gradient:#fff-#000 ^
  -size %FLAT_R%x1 xc:#000 ^
  +append ^
  %strBLANK% ^
  %mcudTRANS% ^
  %strROLL% ^
  %OUTFILE%

%IM%identify %OUTFILE%

call %PICTBAT%graph1d %OUTFILE%


call echoRestore

endlocal & set mcudOUTFILE=%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 ckbkClut.h1. To re-create this web page, execute "procH1 ckbkClut" or cookbook.bat.


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 v2.2 6-June-2014.

Page created 18-Jul-2017 13:52:33.

Copyright © 2017 Alan Gibson.