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

%IMG7%magick ^
  toes.png ^
  +level 10,90%% ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  -format "%FMTrgb%" ^
  +write info: ^
  %SRC% 
red: 0.173414 to 0.9
green: 0.1495 to 0.831908
blue: 0.1 to 0.859863
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).

%IMG7%magick ^
  %SRC% ^
  -auto-level ^
  sl_al.jpg
sl_al.jpg
%IMG7%magick ^
  %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.2000152590218967 DefP1=0.8000152590218967
P0=0.2000152590218967 P1=0.8000152590218967
DO_LO=1 DO_HI=1
X0=0.1000076295109484 X1=0.9000076295109484
a=2.000000000000001 b=-0.2000152590218968 c=2 d=-0.8000152590218969

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.3468375677119097 DefP1=0.8000152590218967
P0=0.3468375677119097 P1=0.8000152590218967
DO_LO=1 DO_HI=1
X0=0.1734187838559548 X1=0.9000076295109484
a=1.999999999999999 b=-0.3468375677119095 c=2 d=-0.8000152590218969
DefP0=0.2990157930876631 DefP1=0.6638132295719845
P0=0.2990157930876631 P1=0.6638132295719845
DO_LO=1 DO_HI=1
X0=0.1495078965438315 X1=0.8319066147859923
a=2 b=-0.299015793087663 c=2 d=-0.6638132295719845
DefP0=0.2000152590218967 DefP1=0.7197222858014802
P0=0.2000152590218967 P1=0.7197222858014802
DO_LO=1 DO_HI=1
X0=0.1000076295109484 X1=0.8598611429007401
a=2.000000000000001 b=-0.2000152590218968 c=2 d=-0.7197222858014802

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

%IMG7%magick ^
  %SRC% ^
  -colorspace HCLp ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  -format "%FMThclp%" ^
  info: 
H: 0 to 0.999934
C: 0.000457771 to 0.304143
L: 0.180125 to 0.840898

[No image]

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

%IMG7%magick ^
  %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.3602506651979858 DefP1=0.6817962252994583
P0=0.3602506651979858 P1=0.6817962252994583
DO_LO=1 DO_HI=1
X0=0.1801253325989929 X1=0.8408981126497291
a=2 b=-0.3602506651979859 c=2 d=-0.6817962252994583
%IMG7%magick ^
  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.999934
C: 0.000457771 to 0.304143
L: 0 to 0.971451
red: 0 to 1
green: 0 to 0.96426
blue: 0 to 0.970197
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.3668098094052796 DefP1=0.7108665693904022
P0=0.3668098094052796 P1=0.7108665693904022
DO_LO=1 DO_HI=1
X0=0.1834049047026398 X1=0.8554332846952011
a=2 b=-0.3668098094052795 c=2 d=-0.7108665693904022

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.

%IMG7%magick ^
  -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.
@rem
@rem Updated:
@rem   8-August-2022 for IM v7.
@rem

@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 (`%IMG7%magick ^
  %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 (`%IMG7%magick 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 (`%IMG7%magick 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 (`%IMG7%magick 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

:: Following needs HDRI.
::
%IMG7%magick ^
  %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.
@rem
@rem Updated:
@rem   8-August-2022 for IM v7.
@rem


@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 (`%IMG7%magick ^
  %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

%IMG7%magick ^
  %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).
@rem
@rem Updated:
@rem   8-August-2022 for IM v7.
@rem


@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 (`%IMG7%magick ^
  %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

%IMG7%magick ^
  %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:

%IMG7%magick -version
Version: ImageMagick 7.1.1-15 Q16-HDRI x64 a0a5f3d:20230730 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenCL OpenMP(2.0) 
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 (193532217)

To improve internet download speeds, some images may have been automatically converted (by ImageMagick, of course) from PNG or TIFF or MIFF 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 29-Sep-2023 08:14:50.

Copyright © 2023 Alan Gibson.