snibgo's ImageMagick pages

Non-absolute cluts

Cluts can be relative or proportional, and can have log scales.

This page logically follows from Clut cookbook.

See also Log cluts.

Absolute cluts

The ImageMagick "-clut" operation reads each input pixel value Vin, uses that as an index into the Nx1 clut image, and uses the value at that position in the clut for the output pixel value Vout. We can say the clut is absolute, in the sense that the clut value is used directly for the output pixel.

Vout = CLUTabs[Vin]

This page explores two alternatives to absolute cluts:

We make a sample absolute clut:

call %PICTBAT%mSigClut ^
  nacl_abs0.png 256 0.5 0.5 0.5 4

call %PICTBAT%graph1d nacl_abs0.png . . . none
nacl_abs0_g1d.png

Relative cluts

We can define relative cluts, where clut values are added to input pixel values, and the result is used for output pixel values.

Vout = CLUTrel[Vin] + Vin 

If clut values are floating-point, a clut value of zero means "add zero". However, we may want to restrict clut values to the range [0.0,1.0]. For that, we would subtract 0.5 from the result.

Vout = CLUTrel[Vin] + Vin - 0.5

Substituting the absolute equation and solving for CLUTrel[Vin], we get the transformation from CLUTabs[Vin] to CLUTrel[Vin]:

CLUTrel[Vin] = CLUTabs[Vin] - Vin + 0.5

For Vin, we use a gradient. The inverse transformation is:

CLUTabs[Vin] = CLUTrel[Vin] + Vin - 0.5

We show the conversion between absolute and relative cluts:

Convert the sample absolute clut to a relative clut, with 50% offset:

%IMG7%magick ^
  nacl_abs0.png ^
  -set option:MYSIZE %%[fx:w]x%%[fx:h] ^
  ( -size %%[MYSIZE] ^
    gradient:Black-White ^
  ) ^
  -compose Mathematics ^
    -define compose:args=0,-1,1,0.5 ^
  -composite ^
  nacl_rel0.png

call %PICTBAT%graphLine nacl_rel0.png

%IMG7%magick ^
  nacl_rel0_gl.png ^
  -stroke #0f08 ^
  -draw "line 0,%%[fx:h/2],%%[fx:w-1],%%[fx:h/2]" ^
  nacl_rel0_gl.png

call %PICTBAT%graph4labels ^
  nacl_rel0_gl.png nacl_rel0_gl.png ^
  0.0 1.0 "-50%%%%" "+50%%%%"

The y-axis shows the amount added, as a percentage of QuantumLevel.

nacl_rel0_gl.png

Convert it back to an absolute clut:

%IMG7%magick ^
  nacl_rel0.png ^
  -set option:MYSIZE %%[fx:w]x%%[fx:h] ^
  ( -size %%[MYSIZE] ^
    gradient:Black-White ^
  ) ^
  -compose Mathematics ^
    -define compose:args=0,1,1,-0.5 ^
  -composite ^
  nacl_abs1.png

call %PICTBAT%graph1d nacl_abs1.png
nacl_abs1_g1d.png

How accurate is the round-trip?

%IMG7%magick compare ^
  -metric RMSE ^
  nacl_abs0.png nacl_abs1.png ^
  NULL: 
0.998045 (1.52292e-05)

The round-trip is accurate.

Proportional cluts

We can also define proportional cluts, where clut values are used to multiply input pixel values to make output pixel values.

Vout = CLUTprop[Vin] * Vin 

If clut values are floating-point, in the range nearly [0,∞], we can use 100% of QuantumLevel to mean "multiply by one", and almost all multipliers in the range [0,∞] are available. However, we may want to restrict clut values to the range [0,100%]. For that, we define a constant ClutFact that is used to multiply the clut value.

Vout = CLUTprop[Vin] * Vin * ClutFact

For example, when ClutFact is 4, and 0 <= CLUTprop[Vin] <= 100%, then Vin will be multiplied by some value in the range [0,4].

Substituting the absolute equation and solving for CLUTrel[Vin], we get the transformation from CLUTabs[Vin] to CLUTprop[Vin]:

CLUTprop[Vin] = CLUTabs[Vin] / Vin / ClutFact

For Vin, we use a gradient. The inverse transformation is:

CLUTabs[Vin] = CLUTprop[Vin] * Vin * ClutFact

We show the conversion between absolute and proportional cluts:

Convert the absolute to a proportional clut, and draw horizontal line at multiplier *1:

set ClutFact=4

set SLINE=^
line ^
0,%%[fx:h*(1-1/%ClutFact%)],^
%%[fx:w-1],%%[fx:h*(1-1/%ClutFact%)]

%IMG7%magick ^
  nacl_abs0.png ^
  -set option:MYSIZE %%[fx:w]x%%[fx:h] ^
  ( -size %%[MYSIZE] ^
    gradient:Black-White ^
  ) ^
  -define compose:clamp=off ^
  -compose DivideSrc -composite ^
  -evaluate Divide %ClutFact% ^
  nacl_prop0.png

call %PICTBAT%graphLine nacl_prop0.png

%IMG7%magick ^
  nacl_prop0_gl.png ^
  -stroke #0f08 ^
  -draw "%SLINE%" ^
  nacl_prop0_gl.png

call %PICTBAT%graph4labels ^
  nacl_prop0_gl.png nacl_prop0_gl.png ^
  0.0 1.0 "*0" "*4"

The y-axis is the multiplier, from zero to 4, labelled "*0" and "*4".
The green line is at multiplier=1.

We have divide by zero at x=0.

nacl_prop0_gl.png

Convert it back to an absolute clut:

%IMG7%magick ^
  nacl_prop0.png ^
  -evaluate Multiply %ClutFact% ^
  -set option:MYSIZE %%[fx:w]x%%[fx:h] ^
  ( -size %%[MYSIZE] ^
    gradient:Black-White ^
  ) ^
  -define compose:clamp=off ^
  -compose Multiply -composite ^
  nacl_abs1p.png

call %PICTBAT%graph1d nacl_abs1p.png
nacl_abs1_g1d.png

How accurate is the round-trip?

%IMG7%magick compare ^
  -metric RMSE ^
  nacl_abs0.png nacl_abs1p.png ^
  NULL: 
0.684653 (1.04471e-05)

The round-trip is accurate.

Log-proportional cluts

We can apply a log scale to the proportional clut shown above. The result has an equal spacing in the y-axis for a given factor. We can annotate with whatever factor we wish, such as a factor of 2, so values double or halve at every interval. We can call each interval a photographic stop, also known as an Exposure Value or EV.

Here comes the maths for just the log parts of the log-proportional transformations. In the forwards direction we apply this after the forwards Proportional transformations shown above; in the reverse direction we apply it before the reverse proportional transformation.

When the domain of x is [x0,x1] where 0 < x0 < x1, the range of log(x) would be [log(x0),log(x1)]. The usual subtract and divide transforms the range to [0,1].

y = log(x)-log(x0) 
    log(x1)-log(x0)

  = log(x/x0) 
    log(x1/x0)

For the reverse:

log(x)-log(x0) = y * log(x1/x0)

log(x) = y * log(x1/x0) + log(x0)
       = log(x1^y/x0^y) + log(x0)
       = log(x0 * x1^y/x0^y)

x = x0 * x1^y / x0^y
  = x0 * (x1/x0)^y
  = x1^y / x0^(y-1)

Any logarithm base can be used; the result is the same.

For some purposes we want a symmetrical range, where x0 = 1/x1. This gives a result with the same number of stops above or below unity.

Forwards:

y = log(x/(1/x1)) 
    log(x1/(1/x1))

  = log(x*x1) 
    log(x1^2)

Reverse:

x = x1^y * x1^(y-1)
  = x1^(2*y-1)

ASIDE: For raising one image to the power of another, we use:

magick ^
  dst.png src.png ^
  -evaluate-sequence Pow ^
  out.png

... which implements:

out = srcdst

... where IM normalises src to [0,1] but does not normalise dst. If we want dst=100% to have the effect of raising to the power of one, we need to divide dst by QuantumRange.

%IMG7%magick ^
  ( xc:rgb(5%%,50%%,130%%) ^
    -evaluate Divide %%[fx:QuantumRange] ^
  ) ^
  xc:rgb(10%%,20%%,30%%) ^
  -evaluate-sequence Pow ^
  txt: 
# ImageMagick pixel enumeration: 1,1,0,65535,srgb
0,0: (58408,29308,13700)  #E428727C3584  srgb(89.1251%,44.7214%,20.9054%)

Note that 0.10.05=0.891251, 0.20.5=0.447214 and 0.31.3=0.209054.

We can also use %[fx:], which is simpler and slower, but fast enough for making cluts:

%IMG7%magick ^
  xc:rgb(5%%,50%%,130%%) ^
  xc:rgb(10%%,20%%,30%%) ^
  -fx pow(v,u) ^
  txt: 
# ImageMagick pixel enumeration: 1,1,0,65535,srgb
0,0: (58408,29308,13700)  #E428727C3584  srgb(89.1251%,44.7214%,20.9054%)

We use the forwards transformation to modify the output of the clut, shown on the y-axis of the graph. We show output values between yLo and yHi, setting these to the reciprocals of each other. When yHi is a power of two, this results in an exact number of stops. As yLo is the reciprocal of yHi, we have an equal number of stops above and below the neutral line.

set yLo=0.25
set yHi=4.0

set SLINE=^
line ^
0,%%[fx:h*0.5],^
%%[fx:w-1],%%[fx:h*0.5]

%IMG7%magick ^
  nacl_abs0.png ^
  -set option:MYSIZE %%[fx:w]x%%[fx:h] ^
  ( -size %%[MYSIZE] ^
    gradient:Black-White ^
  ) ^
  -define compose:clamp=off ^
  -compose DivideSrc -composite ^
  -fx "log(u/%yLo%)/log(%yHi%/%yLo%)" ^
  nacl_log0.png

call %PICTBAT%graphLine nacl_log0.png

%IMG7%magick ^
  nacl_log0_gl.png ^
  -stroke #0f08 ^
  -draw "%SLINE%" ^
  nacl_log0_gl.png

call %PICTBAT%graph4labels ^
  nacl_log0_gl.png nacl_log0_gl.png ^
  "0.0" "1.0" "-2" "+2"

The y-axis is the multiplier, from -2 to +2 stops (factor of 0.25 to 4).
The green line is at +0 stops.

We have divide by zero at x=0.

nacl_log0_gl.png

Convert it back to an absolute clut:

%IMG7%magick ^
  nacl_log0.png ^
  -evaluate Divide %%[fx:QuantumRange] ^
  ( +clone ^
    -evaluate set %%[fx:QuantumRange*%yHi%/%yLo%] ^
  ) ^
  -evaluate-sequence Pow ^
  -evaluate Multiply %yLo% ^
  -set option:MYSIZE %%[fx:w]x%%[fx:h] ^
  ( -size %%[MYSIZE] ^
    gradient:Black-White ^
  ) ^
  -define compose:clamp=off ^
  -compose Multiply -composite ^
  nacl_logabs.png

call %PICTBAT%graph1d nacl_logabs.png
nacl_logabs_g1d.png

How accurate is the round-trip?

%IMG7%magick compare ^
  -metric RMSE ^
  nacl_abs0.png nacl_logabs.png ^
  NULL: 
0.526634 (8.03593e-06)

The round-trip is accurate.

We can also apply a log transform to the clut input, pushing the graph towards the right. We use the log cluts: skip epsilon method, showing the top six stops. The choice of six stops is just an example; we might choose 10 stops for digital cameras, or even 16. We need to apply the same log transform to the gradient used for the division.

set E=(1/64)

set mscPREPROC=-fx "pow(%E%,(1-u))"

call %PICTBAT%mSigClut ^
  nacl_abs2.png 256 0.5 0.5 0.5 4

%IMG7%magick ^
  nacl_abs2.png ^
  -set option:MYSIZE %%[fx:w]x%%[fx:h] ^
  ( -size %%[MYSIZE] ^
    gradient:Black-White ^
    %mscPREPROC% ^
  ) ^
  -define compose:clamp=off ^
  -compose DivideSrc -composite ^
  -fx "log(u/%yLo%)/log(%yHi%/%yLo%)" ^
  nacl_log2.png

set mscPREPROC=

call %PICTBAT%graphLine nacl_log2.png

%IMG7%magick ^
  nacl_log2_gl.png ^
  -stroke #0f08 ^
  -draw "%SLINE%" ^
  nacl_log2_gl.png

call %PICTBAT%gridHigh ^
  nacl_log2_gl.png nacl_log2_gl.png x w/logtwo(1/%E%)

for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -precision 2 ^
  -format "LabLeft=%%[fx:-logtwo(1/%E%)]" ^
  xc:`) do set %%L

call %PICTBAT%graph4labels ^
  nacl_log2_gl.png nacl_log2_gl.png ^
  %LabLeft% 0 -2 +2
nacl_log2_gl.png

Convert it back to an absolute clut:

%IMG7%magick ^
  nacl_log2.png ^
  -evaluate Divide %%[fx:QuantumRange] ^
  ( +clone ^
    -evaluate set %%[fx:QuantumRange*%yHi%/%yLo%] ^
  ) ^
  -evaluate-sequence Pow ^
  -evaluate Multiply %yLo% ^
  -set option:MYSIZE %%[fx:w]x%%[fx:h] ^
  ( -size %%[MYSIZE] ^
    gradient:Black-White ^
    -fx "pow(%E%,(1-u))" ^
  ) ^
  -define compose:clamp=off ^
  -compose Multiply -composite ^
  nacl_log2abs.png

rem   -fx "log(u/%E%)/log(1/%E%)"

call %PICTBAT%graph1d nacl_log2abs.png
nacl_log2abs_g1d.png

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.
@rem   8-August-2022 for IM v7.
@rem


@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


for /F "usebackq" %%L ^
in (`%IMG7%magick 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
)

echo %0: WW=%WW% HH=%HH% newH=%newH% OUTFILE=%OUTFILE%

set /A newH4=4*%newH%

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


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

call echoRestore

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

%IMG7%magick -version
Version: ImageMagick 7.1.0-42 Q16-HDRI x64 396d87c:20220709 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenCL 
Delegates (built-in): bzlib cairo freetype gslib heic jng jp2 jpeg jxl lcms lqr lzma openexr pangocairo png ps raqm raw rsvg tiff webp xml zip zlib
Compiler: Visual Studio 2022 (193231332)

Source file for this web page is nacluts.h1. To re-create this web page, execute "procH1 nacluts" 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 v1.0 28-April-2020.

Page created 09-Aug-2022 00:25:23.

Copyright © 2022 Alan Gibson.