snibgo's ImageMagick pages

Levels

We can do a linear transformation of levels by using either -level or +level, with a suitable change of parameters. For some situations, it isn't obvious which of these we should use.

The problem

Suppose we want to make an image brighter. We want black to become 50% gray. Any pixels currently at or above 50% gray, we want to become white. We can show this as a graph, where the x-axis represents the input and the y-axis represents the output. The normal range, 0 to 100% in both dimensions, is the heavy rectangle. The blue line on the graph represents the transformation from input level to output level.

lv_50g.png

In this diagram:

In our sample requirement, py1=50%, px2=50%, px1<0, and py2>100%. We could use either -level or +level. But -level needs px1 and px2 as parameters, and +level needs py1 and py2.

The problem is general. We might know any two of the values and want to calculate the other two.

From similar triangles:

 py1      100     100-py1   py2-py1   py2-100     py2
----- = ------- = ------- = ------- = ------- = ------- = tan(-θ)
-px1    px2-px1     px2       100     100-px2   100-px1

... where θ is the angle of the line to the horizontal, clockwise is positive.

(In this example, px1<0 so -px1>0.)

From those equalities, we get three equations for each of the four parameters:

 
      py1*px2   100*py1   10000 - py2*px2
px1 = ------- = ------- = ---------------
      py1-100   py1-py2      100 - py2


      px1*(py1-100)   100*(py1-100)   100*(100-px1) + px1*py2
px2 = ------------- = ------------- = -----------------------
          py1            py1-py2               py2

      100*px1   px1*py2   10000 - px2*py2
py1 = ------- = ------- = ---------------
      px1-px2   px1-100      100 - px2

      py1*(px1-100)   100*(px1-100)   100*(100-py1) + py1*px2
py2 = ------------- = ------------- = -----------------------
           px1           px1-px2               px2

In every equation we can interchange "x" and "y" and the equation remains valid.

A script levels4.bat is provided to calculate any two parameters from the other two, by using the appropriate equations from the 12 above. The script does not check for degenerate cases of horizontal or vertical lines.

Test the script

lv_plus.png

Reading values from the diagram, with an accuracy of about 1%:

px1 = 50%
px2 = 132.5%
py1 = -59.5%
py2 = 60%

We can check the script with these parameters. As we know two out of four values, there are six cases to test.

call %PICTBAT%levels4 50 132.5     ?  ? 
call %PICTBAT%levels4 50     ? -59.5  ? 
call %PICTBAT%levels4 50     ?     ? 60 
call %PICTBAT%levels4  ? 132.5 -59.5  ? 
call %PICTBAT%levels4  ? 132.5     ? 60 
call %PICTBAT%levels4  ?     ? -59.5 60 
50 132.5 -60.6060606060606 60.6060606060606
50 134.033613445378 -59.5 59.5
50 133.333333333333 -60 60
49.4278996865204 132.5 -59.5 60.877358490566
51.25 132.5 -63.0769230769231 60
49.7907949790795 133.47280334728 -59.5 60

Yes, the script works.

As a further check, we mirror-image the diagram left-right about the x=50 axis, so the line slopes from top-left to bottom-right, giving these values:

px1 = 50%
px2 = -32.5%
py1 = 60%
py2 = -59.5%

Testing these six cases:

call %PICTBAT%levels4 50 -32.5  ?     ? 
call %PICTBAT%levels4 50     ? 60     ? 
call %PICTBAT%levels4 50     ?  ? -59.5 
call %PICTBAT%levels4  ? -32.5 60     ? 
call %PICTBAT%levels4  ? -32.5  ? -59.5 
call %PICTBAT%levels4  ?     ? 60 -59.5 
50 -32.5 60.6060606060606 -60.6060606060606
50 -33.3333333333333 60 -60
50 -34.0336134453782 59.5 -59.5
48.75 -32.5 60 -63.0769230769231
50.5721003134796 -32.5 60.877358490566 -59.5
50.2092050209205 -33.4728033472803 60 -59.5

Again, this is correct.

We use the first pair of numbers as parameters to -level, or the second pair with +level.

%IM%convert ^
  -size 200x200 ^
  gradient: ^
  ( +clone ^
    -level 50,133.33333%% ^
    +write lv_grad1.png ^
    +delete ^
  ) ^
  ( +clone ^
    +level -60,60%% ^
    +write lv_grad2.png ^
    +delete ^
  ) ^
  NULL:
lv_grad1.png lv_grad2.png

The top-most diagram on this page has px2 = py1 = 50%. So:

call %PICTBAT%levels4 ? 50 50 ? 
-50 50 50 150
%IM%convert ^
  -size 200x200 ^
  gradient: ^
  -bordercolor #000 -border 1 ^
  ( +clone ^
    -level -50,50%% ^
    +write lv_grad3.png ^
    +delete ^
  ) ^
  ( +clone ^
    +level 50,150%% ^
    +write lv_grad4.png ^
    +delete ^
  ) ^
  NULL:
lv_grad3.png lv_grad4.png

As required, black has become mid-gray, and anything that was at or above mid-gray has become white.

Clipping

I would expect these two commands to give the same results. (-channel RGB is so v7 doesn't level the alpha channel.)

%IMDEV%convert -size 1x5 gradient: -channel RGB -level 0,50%% +channel txt: 
# ImageMagick pixel enumeration: 1,5,4294967295,srgb
0,0: (4.29497e+09,4.29497e+09,4.29497e+09)  #FFFFFFFFFFFFFFFFFFFFFFFF  white
0,1: (4.29497e+09,4.29497e+09,4.29497e+09)  #FFFFFFFFFFFFFFFFFFFFFFFF  white
0,2: (4.29497e+09,4.29497e+09,4.29497e+09)  #FFFFFFFFFFFFFFFFFFFFFFFF  white
0,3: (2.14748e+09,2.14748e+09,2.14748e+09)  #800000008000000080000000  srgb(50%,50%,50%)
0,4: (0,0,0)  #000000000000000000000000  black
%IMDEV%convert -size 1x5 gradient: -channel RGB +level 0,200%% +channel txt: 
# ImageMagick pixel enumeration: 1,5,4294967295,srgb
0,0: (8.58993e+09,8.58993e+09,8.58993e+09)  #FFFFFFFFFFFFFFFFFFFFFFFF  srgb(200%,200%,200%)
0,1: (6.44245e+09,6.44245e+09,6.44245e+09)  #FFFFFFFFFFFFFFFFFFFFFFFF  srgb(150%,150%,150%)
0,2: (4.29497e+09,4.29497e+09,4.29497e+09)  #FFFFFFFFFFFFFFFFFFFFFFFF  srgb(100%,100%,100%)
0,3: (2.14748e+09,2.14748e+09,2.14748e+09)  #800000008000000080000000  srgb(50%,50%,50%)
0,4: (0,0,0)  #000000000000000000000000  black

They don't match because the -level version clips.

I think that when using HDRI, neither -level nor +level should clip values. Currently, -level does clip values. I think this is a bug.

The version of %IMDEV% is:

%IMDEV%convert -version 
Version: ImageMagick 6.9.3-7 Q32 x86_64 2016-04-03 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2016 ImageMagick Studio LLC
License: http://www.imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenMP 
Delegates (built-in): bzlib cairo fftw fontconfig freetype fpx jbig jng jpeg lcms ltdl lzma pangocairo png rsvg tiff webp wmf x xml zlib

See the IM forum thread Process HDRI without clamping.

Polar specification

Instead of specifying the line by two points, we can describe it with polar coordinates (r,θ), where r is the distance of the line from a certain point and θ is the angle of the line to the horizontal. For convenience, we take the "certain point" to be (50%,50%). So θ has the range [-90°,+90°], with both extremes being vertical. r is always positive and, if the line is within the box, the maximum value of r is between 50% and sqrt(2)*50%.

Let (px3,py3) be the coordinates of the point on the line that is closest to (50%,50%), so the perpendicular at (px3,py3) passes through (50%,50%), and makes an angle of θ with the vertical, and the distance of (px3,py3) to (50%,50%) is r.

50-px3 = r.sin(θ)

py3-50 = r.cos(θ)

Hence we know px3 and py3.

py3-py1
------- = tan(θ)
  px3

Hence:

py1 = py3 - px3.tan(θ)

For θ near +90° or -90° we can use:

 px3
------- = tan(90°-θ)
py3-py1

Hence:

py1 = py3 - px3 / tan(90°-θ)  [[That's no help.]]

Nope. Above is garbage. For a given (r,θ) with the above definition, two lines satisfy the conditions. We can change the definition: θ is the direction (from 0 to 360°) of the perpendicular line from, say, due north (or east or any direction). Positive clockwise seems more natural. So first diagram has θ = -45°. Second example has θ = +135°.

px3 = 50 + dx
py3 = 50 + dy
dx = r.sin(θ)
dy = r.cos(θ)

Hence:

dx   px3-50
-- = ------ = tan(θ)
dy   py3-50

Making the diagrams

I would normally make this type of diagram with Inkscape, or my own system that generates SVG from a higher-level language. But it is simple enough to code in IM:

lv_teaser.png
%IM%convert ^
  -size 500x500 xc:#eef ^
  -fill none ^
  -stroke #000 ^
  -strokewidth 1 ^
  -draw "line 0,150 499,150" ^
  -draw "line 0,349 499,349" ^
  -draw "line 150,0 150,499" ^
  -draw "line 349,0 349,499" ^
  -strokewidth 3 ^
  -draw "rectangle 150,150 349,349" ^
  -fill #000 ^
  -stroke none ^
  -pointsize 15 ^
  -draw "text 480,345 x" ^
  -draw "text 135,15 y" ^
  -draw "text 120,370 '0,0'" ^
  -draw "text 355,370 '100,0'" ^
  -draw "text 105,140 '0,100'" ^
  lv_base.png

%IM%convert ^
  lv_base.png ^
  -strokewidth 3 ^
  -stroke #00f ^
  -draw "line 0,400 400,0" ^
  -strokewidth 1 ^
  -draw "line 50,340 50,360" ^
  -draw "line 250,140 250,160" ^
  -draw "line 140,250 160,250" ^
  -draw "line 340,50 360,50" ^
  -stroke none ^
  -fill #00f ^
  -pointsize 20 ^
  -draw "text 50,375 px1" ^
  -draw "text 220,135 px2" ^
  -draw "text 165,255 py1" ^
  -draw "text 365,55 py2" ^
  -bordercolor #fff -border 20 ^
  lv_50g.png

%IM%convert ^
  lv_base.png ^
  -strokewidth 3 ^
  -stroke #00f ^
  -draw "line 125,499 499,50" ^
  -strokewidth 1 ^
  -draw "line 250,340 250,360" ^
  -draw "line 415,140 415,160" ^
  -draw "line 140,469 160,469" ^
  -draw "line 340,230 360,230" ^
  -stroke none ^
  -fill #00f ^
  -pointsize 20 ^
  -draw "text 250,375 px1" ^
  -draw "text 385,135 px2" ^
  -draw "text 165,474 py1" ^
  -draw "text 365,235 py2" ^
  -bordercolor #fff -border 20 ^
  +write lv_plus.png ^
  -resize 50%% ^
  lv_teaser.png

Scripts

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

levels4.bat

The script does not check for degenerate cases.

@rem Given 4 level parameters,
@rem px1, px2, py1, py2,
@rem two of which are question marks,
@rem calculates the missing two.
@rem The script does _not_ check for degenerate cases.

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

@setlocal

@call echoOffSave


set PX1=%1
set PX2=%2
set PY1=%3
set PY2=%4

set FLAGS=0

if "%PX1%"=="?" set /A FLAGS=1
if "%PX2%"=="?" set /A FLAGS ^|=2
if "%PY1%"=="?" set /A FLAGS ^|=4
if "%PY2%"=="?" set /A FLAGS ^|=8

if %FLAGS%==3 (
  for /F "usebackq" %%L in (`%IM%identify
    -precision 15 ^
    -format "PX1=%%[fx:100*(%PY1%)/((%PY1%)-(%PY2%))]\nPX2=%%[fx:100*(%PY1%-100)/((%PY1%)-(%PY2%))]" ^
    xc:`) do set %%L
) else if %FLAGS%==5 (
  for /F "usebackq" %%L in (`%IM%identify
    -precision 15 ^
    -format "PX1=%%[fx:(10000-(%PY2%)*(%PX2%))/(100-(%PY2%))]\nPY1=%%[fx:(10000-(%PY2%)*(%PX2%))/(100-(%PX2%))]" ^
    xc:`) do set %%L
) else if %FLAGS%==9 (
  for /F "usebackq" %%L in (`%IM%identify
    -precision 15 ^
    -format "PX1=%%[fx:((%PY1%)*(%PX2%))/((%PY1%)-100)]\nPY2=%%[fx:(100*(100-(%PY1%))+(%PY1%)*(%PX2%))/(%PX2%)]" ^
    xc:`) do set %%L
) else if %FLAGS%==6 (
  for /F "usebackq" %%L in (`%IM%identify
    -precision 15 ^
    -format "PX2=%%[fx:(100*(100-(%PX1%))+(%PX1%)*(%PY2%))/(%PY2%)]\nPY1=%%[fx:((%PX1%)*(%PY2%))/((%PX1%)-100)]" ^
    xc:`) do set %%L
) else if %FLAGS%==10 (
  for /F "usebackq" %%L in (`%IM%identify
    -precision 15 ^
    -format "PX2=%%[fx:(%PX1%)*((%PY1%)-100)/(%PY1%)]\nPY2=%%[fx:(%PY1%)*((%PX1%)-100)/(%PX1%)]" ^
    xc:`) do set %%L
) else if %FLAGS%==12 (
  for /F "usebackq" %%L in (`%IM%identify
    -precision 15 ^
    -format "PY1=%%[fx:100*(%PX1%)/((%PX1%)-(%PX2%))]\nPY2=%%[fx:100*((%PX1%)-100)/((%PX1%)-(%PX2%))]" ^
    xc:`) do set %%L
) else (
  echo Need two numbers and two "?" question marks.
  exit /B 1
)


echo %PX1% %PX2% %PY1% %PY2%

call echoRestore

@endlocal

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

%IM%identify -version
Version: ImageMagick 6.9.2-5 Q16 x64 2015-10-31 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2015 ImageMagick Studio LLC
License: http://www.imagemagick.org/script/license.php
Visual C++: 180031101
Features: Cipher DPC Modules OpenMP 
Delegates (built-in): bzlib cairo freetype jng jp2 jpeg lcms lqr openexr pangocairo png ps rsvg tiff webp xml zlib

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

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


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 11-November-2014.

Page created 26-May-2016 16:12:39.

Copyright © 2016 Alan Gibson.