snibgo's ImageMagick pages

Maximise local contrast

... without clipping.

In IM, we can increase global contrast with -level or a number of other operations, and we can increase local contrast with -unsharp. If we increase contrast by too much, we push values out of gamut, so they are clipped and we lose detail. How can we increase local contrast so it is maximised, without clipping?

The problem is closely related to Adaptive blur and sharpen, but the method given on this page has a different goal.

The method

We will work in Q32 HDRI:

if "%IM32f%"=="" call %PICTBAT%setim8

set sHDRI=^
  -depth 32 ^
  -define "compose:clamp=off" ^
  -define "quantum:format=floating-point"

We start with an auto-levelled gray image, from the L channel of CIELab:

set SRC=toes.png
set SRC_GR=mlc_srcgr.miff

%IM32f%convert ^
  %SRC% ^
  %sHDRI% ^
  -colorspace Lab ^
  -channel R -separate +channel ^
  -set colorspace sRGB ^
  -auto-level ^
  %SRC_GR%
mlc_srcgr.miffjpg

Subtract the local mean. Values are close to zero, so we need floating-point to avoid clipping. The process will expand the range of these values.

set SIG1=2

%IM32f%convert ^
  %SRC_GR% ^
  %sHDRI% ^
  ( +clone ^
    -blur 0x%SIG1% ^
    +write mlc_blr.miff ^
  ) ^
  -compose MinusSrc -composite ^
  mlc_minblr.miff
mlc_blr.miffjpg mlc_minblr.miffjpg

mlc_minblr.miff contains high-frequency data (positive and negative). If we add it back to the blur (low-frequency data, positive only), this will reconstruct the original source image.

mlc_minblr.miff isn't easy to see, so we "-auto-level" to put pixels in the 0 to 100% range, just for viewing:

%IM32f%convert ^
  mlc_minblr.miff ^
  %sHDRI% ^
  -auto-level ^
  mlc_minblr_p.png
mlc_minblr_p.pngjpg

If we double the values in mlc_minblr.miff before adding it back to the blur, this will increase the local contrast:

%IM32f%convert ^
  mlc_minblr.miff ^
  %sHDRI% ^
  -evaluate Multiply 2 ^
  mlc_blr.miff ^
  -compose Plus -composite ^
  mlc_uns1.miff
mlc_uns1.miffjpg

This operation will usually cause some clipping, as it has done here:

%IM32f%convert ^
  mlc_uns1.miff ^
  -format "min=%%[fx:minima]  max=%%[fx:maxima]\n" ^
  info: 
min=-0.0840412  max=1.12593

From the image we see that most areas have not clipped. In those areas, we could multiply mlc_minblr.miff by a larger number before adding it back to the blur. To maximise local contrast everywhere without clipping, we will multiply by a number that varies across the image. This is most conveniently done by dividing by the reciprocal, something like this ...

%IM32f%convert ^
  mlc_minblr.miff ^
  %sHDRI% ^
  mlc_numbers.miff ^
  -compose Divide -composite ^
  mlc_blr.miff ^
  -compose Plus -composite ^
  mlc_max_cont.miff

... where mlc_numbers.miff contains the divider at each pixel. So now we need to calculate mlc_numbers.miff. Ideally, this would be the local peaks in mlc_minblr.miff, but this wouldn't be a smoothly changing function. mlc_minblr.miff contains positive and negative values, so we take the absolute value, and blur it. This smooths the peaks, so we multiply it by the maximum absolute value divided by the maximum blurred absolute value. (It would be easier to -auto-level the blurred absolute, but outliers would squish the values too much.)

First, take the absolute and blur it, and calculate the multiplier...

set SIG2=%SIG1%

for /F "usebackq" %%L in (`%IM32f%convert ^
  mlc_minblr.miff ^
  %sHDRI% ^
  -evaluate Abs 0 ^
  +write mlc_minblr_abs.miff ^
  -format "maxA=%%[fx:maxima]\n" ^
  +write info: ^
  -blur 0x%SIG2% ^
  +write mlc_minblr_avg.miff ^
  -format "maxB=%%[fx:maxima]\n" ^
  info:`) do set %%L

for /F "usebackq" %%L in (`%IM32f%identify ^
  -format "mult=%%[fx:%maxA%/%maxB%]\n"
  xc:`) do set %%L

echo maxA=%maxA% maxB=%maxB% mult=%mult% 
maxA=0.241885 maxB=0.081344 mult=2.97361 
mlc_minblr_abs.miffjpg mlc_minblr_avg.miffjpg

... and apply the multiplier:

%IM32f%convert ^
  mlc_minblr_avg.miff ^
  %sHDRI% ^
  -evaluate Multiply %mult% ^
  mlc_minblr_avg2.miff
mlc_minblr_avg2.miffjpg

Now we can divide mlc_minblr.miff by the numbers in mlc_minblr_avg2.miff to locally maximise the high frequency component.

%IM32f%convert ^
  mlc_minblr.miff ^
  %sHDRI% ^
  mlc_minblr_avg2.miff ^
  -compose DivideSrc -composite ^
  mlc_minblr_div.miff
mlc_minblr_div.miffjpg

mlc_minblr_div.miff ranges from -100% to +100%, more or less. -auto-level would reduce this to exactly 0 to 100%, but outliers would squish this by too much, so we use oogPower.bat (see Putting OOG back in the box: Power modulation).

%IM32f%convert ^
  mlc_minblr_div.miff ^
  %sHDRI% ^
  -evaluate Divide 2 ^
  -evaluate Add 50%% ^
  mlc_minblr_div2.miff
mlc_minblr_div2.miffjpg
call %PICTBAT%oogPower ^
  mlc_minblr_div2.miff ^
  mlc_minblr_div3.miff
mlc_minblr_div3.miffjpg

This is the high frequency component of the maximum-contrast image. For some purposes, this is what we want. Or we can maximise further, by thresholding:

%IM%convert ^
  mlc_minblr_div3.miff ^
  -threshold 50%% ^
  mlc_minblr_div3_t.miff
mlc_minblr_div3_t.miffjpg

However, it is more correct to add the high frequency component back to the blur:

%IM32f%convert ^
  mlc_blr.miff ^
  %sHDRI% ^
  mlc_minblr_div3.miff ^
  -compose Mathematics ^
    -define compose:args=0,1.0,1.0,-0.5 ^
  -composite ^
  mlc_maxc.miff
mlc_maxc.miffjpg

This addition can cause clipping, so we do another oogPower:

call %PICTBAT%oogPower ^
  mlc_maxc.miff ^
  mlc_maxc2.miff
mlc_maxc2.miffjpg

Restore the colour:

%IM32f%convert ^
  %SRC% ^
  %sHDRI% ^
  -colorspace Lab ^
  -separate ^
  mlc_maxc2.miff ^
  -swap 0,3 ^
  +delete ^
  -combine ^
  -set colorspace Lab ^
  -colorspace sRGB ^
  mlc_maxc3.miff
mlc_maxc3.miffjpg

The script

We put the above into a script, maxLocCont.bat.

Option Description
%1 Input image
%2 Output file
%3 Sigma to find the local mean. Default: 2.
%4 Sigma to find local maxima. Default: %3.
%5 Blend percentage Default: 100.
%6 Col or Gray. Default: Col

For example:

call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_sc1.png 0.5
mlc_sc1.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_sc2.png 1
mlc_sc2.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_sc3.png 2
mlc_sc3.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_sc4.png 3
mlc_sc4.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_sc5.png 10
mlc_sc5.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_sc6.png 20
mlc_sc6.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_sc7.png 50
mlc_sc7.pngjpg

Generally, the two sigmas should be equal. If the second is larger than the first, the contrast is reduced. If the second is smaller than the first, contrast is increased.

call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_sc13.png 1 3
mlc_sc13.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_sc31.png 3 1
mlc_sc31.pngjpg

We can use the blend parameter from 0 to 100 to control the degree of the effect, or use a value beyond that range.

call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_b1.png 0.5 . 50
mlc_b1.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_b2.png 2 . 50
mlc_b2.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_b3.png 2 . 150
mlc_b3.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_b4.png 2 . -150
mlc_b4.pngjpg

By default, the process modifies the L channel of Lab. The Gray option is slightly faster, returning just the L channel.

call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_m1.png 0.5 . 50 Gray

call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_m1b.png 2 . 50 Gray
mlc_m1.pngjpg mlc_m1b.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_m2.png 0.5 . 100 Gray

call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_m2b.png 2 . 100 Gray
mlc_m2.pngjpg mlc_m2b.pngjpg
call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_m3.png 0.5 . 150 Gray

call %PICTBAT%maxLocCont ^
  %SRC% ^
  mlc_m3b.png 2 . 150 Gray
mlc_m3.pngjpg mlc_m3b.pngjpg

The script also returns the name of a temporary file in mlcAVG. This image is black where there is no detail and lightest where there is most detail, so we can use it as a mask for the effect. (See also Adaptive blur and sharpen.) Working with the last example above:

Greatest effect in areas with detail

%IMDEV%convert ^
  %SRC% ^
  -colorspace Lab ^
  -channel R -separate +channel ^
  -set colorspace sRGB ^
  mlc_m3b.png ^
  ( %mlcAVG% -auto-level ) ^
  -compose Over -composite ^
  mlc_mskd.png
mlc_mskd.pngjpg

Greatest effect in areas with no detail

%IMDEV%convert ^
  %SRC% ^
  -colorspace Lab ^
  -channel R -separate +channel ^
  -set colorspace sRGB ^
  mlc_m3b.png ^
  ( %mlcAVG% -auto-level -negate ) ^
  -compose Over -composite ^
  mlc_mskdn.png
mlc_mskdn.pngjpg

JPEG source

For the source, we can use a JPEG image. However, the detail that is revealed may be JPEG artefacts:

Make a JPEG image

%IM%convert ^
  %SRC% ^
  -quality 50 ^
  mlc_src_jpg.jpg
mlc_src_jpg.jpg

Maximise local contrast

call %PICTBAT%maxLocCont ^
  mlc_src_jpg.jpg ^
  mlc_sj.png 0.5 . 100 Gray
mlc_sj.pngjpg

Increasing grizzle

The script makes grizzled old men even more so.

set SKIN_SRC=%PICTLIB%20130201\AGA_1201_sRGB.tiff

for /F "usebackq" %%L in (`%IM%identify ^
  -format "ss_size=%%wx%%h" ^
  %SKIN_SRC%`) do set %%L

echo ss_size = %ss_size% 
ss_size = 4924x7378 

As usual, we operate on the full-size image but also small versions for the web.

set WEBSIZE=-resize 600x600
%IM%convert ^
  %SKIN_SRC% ^
  -strip ^
  -colorspace Gray ^
  +write mlc_face.miff ^
  %WEBSIZE% ^
  mlc_face_sm.miff
mlc_face_sm.miffjpg
call %PICTBAT%maxLocCont ^
  mlc_face.miff ^
  mlc_face_mlc.miff 20

%IM%convert ^
  mlc_face_mlc.miff ^
  %WEBSIZE% ^
  mlc_face_mlc_sm.miff
mlc_face_mlc_sm.miffjpg

Show a 1:1 crop:

%IM%convert ^
  mlc_face_mlc.miff ^
  -crop 600x400+2800+2830 +repage ^
  mlc_eye_mlc.jpg
mlc_eye_mlc.jpg

As before, we can use %mlcAVG% as a mask to modulate the effect. We equalize the mask to get an even spread of the effect.

%IM%convert ^
  mlc_face.miff ^
  mlc_face_mlc.miff ^
  ( %mlcAVG% -equalize ) ^
  -compose Over -composite ^
  %WEBSIZE% ^
  mlc_mod1.jpg
mlc_mod1.jpg

From %SKIN_SRC%, an unpublished tool finds the bounding boxes for the eyes. This gives:

set leftEye=792x419+262+3157
set rightEye=1148x486+2585+2901

The script selCoordsBB.bat makes a mask, using the bounding boxes as gradient ellipses.

call %PICTBAT%selCoordsBB ^
  mlc_eyes_msk.miff ^
  %ss_size% ^
  "%leftEye% %rightEye%"

%IM%convert ^
  mlc_eyes_msk.miff ^
  %WEBSIZE% ^
  mlc_eyes_msk_sm.miff
mlc_eyes_msk_sm.miffjpg

We can then use this mask to apply the effect to just the eyes:

%IM%convert ^
  mlc_face.miff ^
  mlc_face_mlc.miff ^
  mlc_eyes_msk.miff ^
  -compose Over -composite ^
  +write mlc_eyes_comp_sm.miff ^
  %WEBSIZE% ^
  mlc_eyes_comp_sm.miff
mlc_eyes_comp_sm.miffjpg

Scripts

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

maxLocCont.bat

rem From image %1,
rem writes %2 with maximised local contrast.
rem %3 sigma to find the local mean.
rem %4 sigma to find local mamxima.
rem %5 blend percentage
rem %6 Col or Gray [default Col]
rem %7 Post-process, eg -auto-level
@rem
@rem Updated:
@rem   23-December-2017 Replaced oogPower.bat with "-process oogbox".
@rem

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 mlc


set INFILE=%~1

if not "%2"=="" if not "%2"=="." set OUTFILE=%2

set SIG1=%3
if "%SIG1%"=="." set SIG1=
if "%SIG1%"=="" set SIG1=2

set SIG2=%4
if "%SIG2%"=="." set SIG2=
if "%SIG2%"=="" set SIG2=%SIG1%

set BLENDPC=%5
if "%BLENDPC%"=="." set BLENDPC=
if "%BLENDPC%"=="" set BLENDPC=100

set COLGR=%6
if "%COLGR%"=="." set COLGR=
if "%COLGR%"=="" set COLGR=Col

set POST_PROC=%7
if "%POST_PROC%"=="." set POST_PROC=

set TMPDIR=\temp\
set GRAY=%TMPDIR%mlc_gray.miff
set BLR=%TMPDIR%mlc_blr.miff
set MINBLR=%TMPDIR%mlc_minblr.miff
set AVG=%TMPDIR%mlc_minblr_avg.miff
set DIV=%TMPDIR%mlc_minblr_div.miff
set MAXC=%TMPDIR%mlc_maxc.miff


if "%IM32f%"=="" call %PICTBAT%setim8

set "sHDRI=-depth 32 -define compose:clamp^=off -define quantum:format^=floating-point"

if %BLENDPC%==100 (
  set WR_GRAY=
) else (
  set WR_GRAY=+write %GRAY%
)

%IM32f%convert ^
  %INFILE% ^
  %sHDRI% ^
  -colorspace Lab ^
  -channel R -separate +channel ^
  -set colorspace sRGB ^
  %WR_GRAY% ^
  -auto-level ^
  ( +clone ^
    -blur 0x%SIG1% ^
    +write %BLR% ^
  ) ^
  -compose MinusSrc -composite ^
  %MINBLR%

for /F "usebackq" %%L in (`%IM32f%convert ^
  %MINBLR% ^
  %sHDRI% ^
  -precision 19 ^
  -evaluate Abs 0 ^
  -format "maxA=%%[fx:maxima]\n" ^
  +write info: ^
  -blur 0x%SIG2% ^
  +write %AVG% ^
  -format "maxB=%%[fx:maxima]\n" ^
  info:`) do set %%L

for /F "usebackq" %%L in (`%IM32f%identify ^
  -precision 19 ^
  -format "mult=%%[fx:%maxA%/%maxB%]\n"
  xc:`) do set %%L

echo maxA=%maxA% maxB=%maxB% mult=%mult%

%IMDEV%convert ^
  %MINBLR% ^
  %sHDRI% ^
  ( %AVG% ^
    -evaluate Multiply %mult% ^
  ) ^
  -compose DivideSrc -composite ^
  -evaluate Divide 2 ^
  -evaluate Add 50%% ^
  -process 'oogbox' ^
  %DIV%

%IMDEV%convert ^
  %BLR% ^
  %sHDRI% ^
  %DIV% ^
  -compose Mathematics ^
    -define compose:args=0,1.0,1.0,-0.5 ^
  -composite ^
  -process 'oogbox' ^
  %MAXC%

if %BLENDPC%==100 (
  set CHAN_L=%MAXC%
) else (
  set CHAN_L=%GRAY% %MAXC% -compose blend -define compose:args=%BLENDPC% -composite
)

if /I %COLGR%==Col (

  %IM32f%convert ^
    %INFILE% ^
    %sHDRI% ^
    -colorspace Lab ^
    -separate ^
    ^( %CHAN_L% ^) ^
    -swap 0,3 ^
    +delete ^
    -combine ^
    -set colorspace Lab ^
    -colorspace sRGB ^
    %POST_PROC% ^
    %OUTFILE%

) else if /I %COLGR%==Gray (

  %IM32f%convert ^
    %sHDRI% ^
    %CHAN_L% ^
    %POST_PROC% ^
    %OUTFILE%

) else (
  echo Bad COLGR [%COLGR%]
  exit /B 1
)


call echoRestore

@endlocal & set mlcOUTFILE=%OUTFILE%& set mlcAVG=%AVG%

selCoords.bat

rem Makes a mask, white around selected coordinates, otherwise black.
rem %1 output name.
rem %2 dimensions, WxH. [default 600x400]
rem %3 quoted list of coordinate quadlets. Use "c" for percent.
rem   Each quadlet is:
rem     x, y    coordinate [each defaults to 50c]
rem     rx, ry  radii of effect [each defaults to 5c]
rem   Each quad becomes a radial gradient.

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 do


set OUTFILE=%1
if "%OUTFILE%"=="." set OUTFILE=
if "%OUTFILE%"=="" set OUTFILE=sc_out.png

set DIMS=%~2
if "%DIMS%"=="." set DIMS=
if "%DIMS%"=="" set DIMS=600x400

set COORDLIST=%~3
if "%COORDLIST%"=="." set COORDLIST=
if "%COORDLIST%"=="" set COORDLIST=50c,60c,.,.,11,12,4,.

call parseXxY2 600 400 dim %DIMS%

rem set dim


:: Parse COORDLIST into gx, gy, grx, gry
::   and use these for radial gradients.

call parseCommaList "%COORDLIST%" nCoordList CoordList

rem echo nCoordList=%nCoordList%
rem set CoordList

set /A remCoord=%nCoordList%%%4

if not %remCoord%==0 (
  echo %~0: COORDLIST should have multiple of 4 entries.
  exit /B 1
)

set /A numCoord=%nCoordList%/4

%IM%convert ^
  -size %dim_X%x%dim_Y% ^
  xc:Black ^
  %OUTFILE%

for /L %%i in (1,1,%numCoord%) do (
  call :getQuad %%i

  if "!coord.x!"=="." set coord.x=
  if "!coord.x!"=="" set coord.x=50c

  if "!coord.y!"=="." set coord.y=
  if "!coord.y!"=="" set coord.y=50c

  if "!coord.rx!"=="." set coord.rx=
  if "!coord.rx!"=="" set coord.rx=5c

  call parseNum !coord.x! %dim_X% gx
  call parseNum !coord.y! %dim_Y% gy
  call parseNum !coord.rx! %dim_X% grx
  call parseNum !coord.ry! %dim_Y% gry

  %IM%convert ^
    -size %dim_X%x%dim_Y% ^
    -define gradient:center=!gx!,!gy! ^
    -define gradient:radii=!grx!,!gry! ^
    radial-gradient: ^
    %OUTFILE% ^
    -compose Lighten -composite ^
    %OUTFILE%
)

rem set coord

call echoRestore

@endlocal scOUTFILE=%OUTFILE%

@exit /B 0

::----------------------------------
:: Subroutines

:getQuad

set /A n0=(%1-1)*4
set /A n1=(%1-1)*4+1
set /A n2=(%1-1)*4+2
set /A n3=(%1-1)*4+3

set coord.x=!CoordList[%n0%]!
set coord.y=!CoordList[%n1%]!
set coord.rx=!CoordList[%n2%]!
set coord.ry=!CoordList[%n3%]!

exit /B 0

selCoordsBB.bat

rem Makes a mask, white around selected coordinates, otherwise black.
rem %1 output name.
rem %2 dimensions, WxH. [default 600x400]
rem %3 quoted list of bounding boxes.

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 do


set OUTFILE=%1
if "%OUTFILE%"=="." set OUTFILE=
if "%OUTFILE%"=="" set OUTFILE=sc_out.png

set DIMS=%~2
if "%DIMS%"=="." set DIMS=
if "%DIMS%"=="" set DIMS=600x400

set BBLIST=%~3
if "%BBLIST%"=="." set BBLIST=
if "%BBLIST%"=="" set BBLIST=10x10+40+50

call parseCommaList "%BBLIST%" nBbList BbList

set /A nBbm1=%nBbList%-1


set sQuads=
for /L %%i in (0,1,%nBbm1%) do (
  echo %%i  !BbList[%%i]!

  for /F "usebackq tokens=1-4 delims=x+" %%A in ('!BbList[%%i]!') do (
    echo %%A %%B %%C %%D

    for /F "usebackq" %%L in (`%IM%identify ^
      -format "%%[fx:%%C+%%A/2],%%[fx:%%D+%%B/2],%%[fx:%%A/2],%%[fx:%%B/2]" ^
      xc:`) do set sQuads=!sQuads! %%L
  )
)

echo sQuads=[%sQuads%]

call %PICTBAT%selCoords %OUTFILE% %DIMS% "%sQuads%"

call echoRestore

@endlocal scOUTFILE=%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
%IM32f%identify -version
Version: ImageMagick 6.9.0-0 Q32 x86_64 2015-02-28 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2014 ImageMagick Studio LLC
Features: DPC HDRI Modules OpenMP
Delegates (built-in): bzlib fontconfig freetype fpx jbig jng jpeg lcms ltdl lzma png tiff x 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 maxloccont.h1. To re-create this web page, execute "procH1 maxloccont".


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 4-December-2017.

Page created 26-Mar-2018 11:52:33.

Copyright © 2018 Alan Gibson.