﻿

# 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``` ## 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``` ```%IMDEV%convert ^ %SRC% ^ -channel RGB ^ -auto-level ^ +channel ^ 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: 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 ``` ```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 ``` ```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 %IMDEV%convert ^ %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.

 ```%IMDEV%convert ^ %SRC% ^ -colorspace HCLp ^ -separate ^ -depth 32 ^ -define quantum:format=floating-point ^ sl_hclp.miff call %PICTBAT%strLinear ^ sl_hclp.miff 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```
 ```%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.999934 C: 0.000457771 to 0.304143 L: -5.55112e-17 to 1 red: -1.36944e-16 to 1 green: 0 to 1 blue: -1.27563e-16 to 1``` 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 ``` ```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``` Which method is best?

• Applying the process in sRGB space, whether by a single transformation or three independent transformations, can change hues slightly.
• Applying the process to the L channel of HCLp, Lab etc won't change hues.
• In this example, the sRGB transformations darken highlights somewhat.

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:

• Process which end: HiOnly, LoOnly.
• Method: linear, other
• P0, P1.
• Channels together or independent.

## 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,

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" ^
%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" ^
%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.0-49 Q16-HDRI x64 7a3f3f1:20220924 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
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 (193331630)```

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.