snibgo's ImageMagick pages

Stretch linear

When pixels don't reach 0 or 100%, how do we stretch them?

An alternative to -auto-level stretches just values that are high or low.

This method is closely related to Putting OOG back in the box.

Sample input

We use "+level" to create an example:

set SRC=sl_src.png

set FMTrgb=^
red: %%[fx:minima.r] to %%[fx:maxima.r]\n^
green: %%[fx:minima.g] to %%[fx:maxima.g]\n^
blue: %%[fx:minima.b] to %%[fx:maxima.b]\n

%IM%convert ^
  toes.png ^
  +level 10,90%% ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  -format "%FMTrgb%" ^
  +write info: ^
  %SRC% 
red: 0.173419 to 0.899992
green: 0.149508 to 0.831907
blue: 0.100008 to 0.859861
sl_src.pngjpg

Auto-level

We can "-auto-level". This applies a gain and bias that modifies all pixels, increasing contrast. The same transformation is usually applied to all the channels, though they can be processed separately (but this will create a colour cast).

%IMDEV%convert ^
  %SRC% ^
  -auto-level ^
  sl_al.jpg
sl_al.jpg
%IMDEV%convert ^
  %SRC% ^
  -channel RGB ^
  -auto-level ^
  +channel ^
  sl_al2.jpg
sl_al2.jpg

Linear modulation

Another option is to modulate all values outside certain limits, say all values above 0.9 or below 0.1. The modulation is by a linear function (the usual gain-and-bias) where a value at 0.9 is unchanged but greater values are progressively increased so the highest value is transformed to 1.0. We modulate values below 0.1 in a similar way. The overall transformation is shown in green on this diagram:

sl_diag.png

In the diagram, in-gamut colours are in the square between (0,0) and (1,1). Input values are between x0 and x1, where 0<x0<x1<1. We want the output values to be between 0 and 1. Most input values, those between P0 and P1 (where x0<P0<P1<x1), are unchanged. Input values between x0 and P0 will be transformed linearly by y=a*x+b, and values between P1 and x1 will be transformed by y=c*x+d, where a, b, c and d are calculated from x0, x1, P0 and P1:

a = -P0 / (x0 - P0)
b = P0 * x0 / (x0 - P0)
c = (1-P1) / (x1-P1)
d = P1 * (x1-1) / (x1-P1)

This is the same calculation as Putting OOG back in the box.

Highlights and shadows are processed independently. We can modulate highlights, or shadows, or both. Highlights are modified only if x1<1.0, and shadows are modified only if x0>0.0. We can use default values for P0 and P1 so that the slopes a=b=2.

This increases contrast where the input is less than P0 or more than P1, and will cause hue-shift for RGB pixels that have channels straddling P0 or P1. Setting P0 to x0 will leave shadows unchanged; setting P1 to x1 will leave highlights unchanged.

Three methods are shown. The first operates on all colour channels (usually red, green and blue), applying the same transformation to each channel. The second operates on all colour channels, applying independent transformations to each channel. The third operates on the L channel of HCLp.

All colour channels, same transformation

call %PICTBAT%strLinear ^
  "%SRC%" sl_linsam.png 
sl_linsam.pngjpg
DefP0=0.2000150271225756 DefP1=0.7999857668299195
P0=0.2000150271225756 P1=0.7999857668299195
DO_LO=1 DO_HI=1
X0=0.1000075135612878 X1=0.8999928834149598
a=2 b=-0.2000150271225756 c=2 d=-0.7999857668299195

All colour channels, independently

The script strLinear3.bat separates the image, makes three independent calls to oogLinear.bat, and combines the results.

call %PICTBAT%strLinear3 ^
  "%SRC%" sl_linind.png 
sl_linind.pngjpg
DefP0=0.3468375653836032 DefP1=0.7999857668299195
P0=0.3468375653836032 P1=0.7999857668299195
DO_LO=1 DO_HI=1
X0=0.1734187826918016 X1=0.8999928834149598
a=2 b=-0.3468375653836032 c=2 d=-0.7999857668299195
DefP0=0.2990160138111133 DefP1=0.663812436550812
P0=0.2990160138111133 P1=0.663812436550812
DO_LO=1 DO_HI=1
X0=0.1495080069055567 X1=0.831906218275406
a=2.000000000000001 b=-0.2990160138111135 c=2 d=-0.663812436550812
DefP0=0.200015492783863 DefP1=0.7197243873308703
P0=0.200015492783863 P1=0.7197243873308703
DO_LO=1 DO_HI=1
X0=0.1000077463919315 X1=0.8598621936654351
a=2 b=-0.200015492783863 c=2 d=-0.7197243873308703

Lightness only

set FMThclp=^
H: %%[fx:minima.r] to %%[fx:maxima.r]\n^
C: %%[fx:minima.g] to %%[fx:maxima.g]\n^
L: %%[fx:minima.b] to %%[fx:maxima.b]\n

%IMDEV%convert ^
  %SRC% ^
  -colorspace HCLp ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  -format "%FMThclp%" ^
  info: 
H: 0 to 0.999933
C: 0.000457771 to 0.304142
L: 0.180125 to 0.840898

[No image]

In HCLp colorspace, we separate the L channel and stretch it.

%IMDEV%convert ^
  %SRC% ^
  -colorspace HCLp ^
  -separate ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  sl_hclp.miff

call %PICTBAT%strLinear ^
  sl_hclp.miff[2] sl_hclp_lcorr.miff 

[No image]

DefP0=0.3602504731063383 DefP1=0.681796275470824
P0=0.3602504731063383 P1=0.681796275470824
DO_LO=1 DO_HI=1
X0=0.1801252365531691 X1=0.840898137735412
a=1.999999999999999 b=-0.360250473106338 c=2 d=-0.681796275470824
%IMDEV%convert ^
  sl_hclp.miff ^
  sl_hclp_lcorr.miff ^
  -swap 2,-1 ^
  +delete ^
  -combine ^
  -set colorspace HCLp ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  -format "%FMThclp%" ^
  +write info: ^
  -colorspace sRGB ^
  -format "%FMTrgb%" ^
  +write info: ^
  sl_hclp_lin.miff 
H: 0 to 0.999933
C: 0.000457771 to 0.304142
L: 1.78814e-07 to 1
red: 4.41128e-07 to 1
green: 0 to 1
blue: 0 to 1
sl_hclp_lin.miffjpg

We don't want to stretch the H or C channels, so the script strLinear3.bat is not helpful here.

The script strLinear1ch.bat takes an image typically in sRGB colorspace, converts to a given colorspace such as HCLp or Lab, stretches one channel, and converts back to the original colorspace.

call %PICTBAT%strLinear1ch ^
  %SRC% sl_str_lab.png . . Lab 0 
sl_str_lab.png
COLSP=Lab  CHNUM=0
OrigCOLSP=sRGB
DefP0=0.3668097780008823 DefP1=0.710867509644215
P0=0.3668097780008823 P1=0.710867509644215
DO_LO=1 DO_HI=1
X0=0.1834048890004412 X1=0.8554337548221075
a=2 b=-0.3668097780008824 c=2 d=-0.710867509644215

The result is virtually the same as the HCLp process, because the L channels are virtually the same.

Comparison

Most of the results have differences that are subtle. For comparison purposes, we make a GIF of the results that are similar.

%IM%convert ^
  -loop 0 ^
  -delay 100 ^
  -gravity NorthWest -fill White ^
  ( sl_linsam.png    -annotate +10+10 "LinSame" ) ^
  ( sl_linind.png    -annotate +10+10 "LinInd" ) ^
  ( sl_hclp_lin.miff -annotate +10+10 "HCLP" ) ^
  ( sl_str_lab.png   -annotate +10+10 "Lab" ) ^
  sl_all.gif
sl_all.gif

Which method is best?

I currently have no conclusion about the "best" method.

Process module

A process module would be faster, especially for the independent version, and could be incorporated into "convert" or "magick" commands. Options would include:

Future

Can we invert the process? We haven't lost any pixel data, but we have lost the knowledge of the original minima and maxima. We can't reverse the process without this knowledge.

Instead of linear transformations y=a*x+b etc, we could use spline curves, eg from P1 in the direction of (1,1), to x1 from the direction of (1,1).

Scripts

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

strLinear.bat

@rem From %1, an image that may contain values that don't reach 0 to 100%,
@rem makes output %2 with values stretched to 0 to 100%.
@rem %3 limit for lower transformation.
@rem %4 limit for upper transformation.


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

@setlocal enabledelayedexpansion

@call echoOffSave

rem call %PICTBAT%setInOut %1 slin


set INFILE=%~1

set OUTFILE=%2
if "%OUTFILE%"=="." set OUTFILE=
if "%OUTFILE%"=="" set OUTFILE=NULL:

set P0=%3
if "%P0%"=="." set P0=

set P1=%4
if "%P1%"=="." set P1=

set FMT=^
X0=%%[fx:minima]\n^
X1=%%[fx:maxima]\n^
DefP0=%%[fx:2*minima]\n^
DefP1=%%[fx:2*maxima-1]\n

set X0=
for /F "usebackq" %%L in (`%IMDEV%convert ^
  %INFILE% ^
  -precision 16 ^
  -format "%FMT%" ^
  info:`) do set %%L
if "%X0%"=="" exit /B 1

echo DefP0=%DefP0% DefP1=%DefP1%

if "%P0%"=="" set P0=%DefP0%
if "%P1%"=="" set P1=%DefP1%


:: FIXME: if X0 > -epsilon, don't calc a and b.
:: FIXME: if X1 < 1+epsilon, don't calc c and d.

set DO_LO=0
set DO_HI=0

set EPS=1e-5

set FMT=^
DO_LO=%%[fx:%X0%^>%EPS%?1:0]\n^
DO_HI=%%[fx:%X1%^<1-%EPS%?1:0]\n

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

:: FIXME: If we are doing both ends, and P0 >= P1,
:: do autolevel instead.

if %X0%==%P0% set DO_LO=0
if %X1%==%P1% set DO_HI=0

if %DO_LO%==0 goto skipAB

set FMT=^
a=%%[fx:(-%P0%)/(%X0%-(%P0%))]\n^
b=%%[fx:%P0%*(%X0%)/(%X0%-(%P0%))]\n

set a=
for /F "usebackq" %%L in (`%IM%identify ^
  -precision 16 ^
  -format "%FMT%" ^
  xc:`) do set %%L
if "%a%"=="" exit /B 1

:skipAB

if %DO_HI%==0 goto skipCD

set FMT=^
c=%%[fx:(1-%P1%)/(%X1%-%P1%)]\n^
d=%%[fx:%P1%*(%X1%-1)/(%X1%-%P1%)]\n

set c=
for /F "usebackq" %%L in (`%IM%identify ^
  -precision 16 ^
  -format "%FMT%" ^
  xc:`) do set %%L
if "%c%"=="" exit /B 1

:skipCD

echo P0=%P0% P1=%P1%
echo DO_LO=%DO_LO% DO_HI=%DO_HI%
echo X0=%X0% X1=%X1%
echo a=%a% b=%b% c=%c% d=%d%

set POLY_HI=
set POLY_LO=

if %DO_HI%==1 set POLY_HI=^
  ( -clone 0 ^
    -function Polynomial %c%,%d% ^
  ) ^
  -compose Lighten -composite

if %DO_LO%==1 set POLY_LO=^
  ( -clone 0 ^
    -function Polynomial %a%,%b% ^
  ) ^
  -compose Darken -composite

%IMDEV%convert ^
  %INFILE% ^
  -define compose:clamp=off ^
  %POLY_HI% ^
  %POLY_LO% ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  %OUTFILE%


call echoRestore

@endlocal & set slinOUTFILE=%OUTFILE%

strLinear3.bat

@rem Applies strLinear independently to three channels.

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

@setlocal enabledelayedexpansion

@call echoOffSave

rem call %PICTBAT%setInOut %1 slin


set INFILE=%~1

set OUTFILE=%2
if "%OUTFILE%"=="." set OUTFILE=
if "%OUTFILE%"=="" set OUTFILE=NULL:

set P0=%3
if "%P0%"=="." set P0=
if "%P0%"=="" set P0=.

set P1=%4
if "%P1%"=="." set P1=
if "%P1%"=="" set P1=.

set TMP_FILE=\temp\strlin3_%%d.miff
set TMP0=\temp\strlin3_0.miff
set TMP1=\temp\strlin3_1.miff
set TMP2=\temp\strlin3_2.miff


for /F "usebackq" %%L in (`%IMDEV%convert ^
  %INFILE% ^
  -format "COLSP=%%[colorspace]\n" ^
  +write info: ^
  -channel RGB ^
  -separate ^
  +channel ^
  -depth 32 ^
  -define "quantum:format=floating-point" ^
  +adjoin ^
  %TMP_FILE%`) do set %%L

call %PICTBAT%strLinear %TMP0% %TMP0% %P0% %P1%
if ERRORLEVEL 1 exit /B 1
call %PICTBAT%strLinear %TMP1% %TMP1% %P0% %P1%
if ERRORLEVEL 1 exit /B 1
call %PICTBAT%strLinear %TMP2% %TMP2% %P0% %P1%
if ERRORLEVEL 1 exit /B 1

%IMDEV%convert ^
  %TMP0% %TMP1% %TMP2% ^
  -combine ^
  -set colorspace %COLSP% ^
  -depth 32 ^
  -define "quantum:format=floating-point" ^
  %OUTFILE%

call echoRestore

@endlocal & set slinOUTFILE=%OUTFILE%

strLinear1ch.bat

@rem From %1, an image that may contain values that don't reach 0 to 100%,
@rem makes output %2 with values stretched to 0 to 100%.
@rem %3 limit for lower transformation.
@rem %4 limit for upper transformation.
@rem %5 colorspace that contains a lightness channel (eg Lab, HCL).
@rem %6 number of lightness channel (0, 1 or 2).


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

@setlocal enabledelayedexpansion

@call echoOffSave

rem call %PICTBAT%setInOut %1 slin


set INFILE=%~1

set OUTFILE=%2
if "%OUTFILE%"=="." set OUTFILE=
if "%OUTFILE%"=="" set OUTFILE=NULL:

set P0=%3
if "%P0%"=="." set P0=
if "%P0%"=="" set P0=.

set P1=%4
if "%P1%"=="." set P1=
if "%P1%"=="" set P1=.

set COLSP=%5
if "%COLSP%"=="." set COLSP=
if "%COLSP%"=="" set COLSP=Lab

set CHNUM=%6
if "%CHNUM%"=="." set CHNUM=
if "%CHNUM%"=="" (
  if /I %COLSP%==Lab set CHNUM=0
)
if "%CHNUM%"=="" exit / 1

echo COLSP=%COLSP%  CHNUM=%CHNUM%

set TMP_PREF=\temp\strlin1ch

set TMP_FILE=%TMP_PREF%_%%d.miff
set TMP0=%TMP_PREF%_0.miff
set TMP1=%TMP_PREF%_1.miff
set TMP2=%TMP_PREF%_2.miff

set TMP_CH=%TMP_PREF%_%CHNUM%.miff

for /F "usebackq" %%L in (`%IMDEV%convert ^
  %INFILE% ^
  -format "OrigCOLSP=%%[colorspace]\n" ^
  +write info: ^
  -colorspace %COLSP% ^
  -channel RGB ^
  -separate ^
  +channel ^
  -depth 32 ^
  -define "quantum:format=floating-point" ^
  +adjoin ^
  %TMP_FILE%`) do set %%L

echo OrigCOLSP=%OrigCOLSP%

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

call %PICTBAT%strLinear %TMP_CH% %TMP_CH% %P0% %P1%
if ERRORLEVEL 1 exit /B 1

%IMDEV%convert ^
  %TMP0% %TMP1% %TMP2% ^
  -combine ^
  -set colorspace %COLSP% ^
  -colorspace %OrigCOLSP% ^
  -depth 32 ^
  -define "quantum:format=floating-point" ^
  %OUTFILE%



call echoRestore

@endlocal & set slinOUTFILE=%OUTFILE%

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

%IM%identify -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

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 strlinear.h1. To re-create this web page, execute "procH1 strlinear".


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 16-November-2017.

Page created 04-Dec-2017 12:25:56.

Copyright © 2017 Alan Gibson.