snibgo's ImageMagick pages

Resampling with halo minimization

Resizing an image can create halo artifacts. These can be reduced by blending two results.

The essential idea is that two resizes are done, both in RGB colorspace. One of these is performed in a further gamma-shifted colorspace. These two resizes are blended according to lightness.

Reference:

This is experimental work by other people, especially Bart van der Wolf and Nicolas Robidoux. This page is merely a snapshot of their work, and I take no credit for their ideas. They know far more than I do about resizing filters.

My script differs from theirs, in that mine does all the work in a single convert command. This save time and loss of precison in converting to sRGB, writing a file, reading it, and converting to RGB.

The folks on the Luminous Landscape thread suggest HDRI (floating point) for this process, so results on this page use IM with HDRI (Q32).

If you want to evaluate the results on this page, you should ensure your browser doesn't enlarge or reduce them. For example, Firefox on my computer is set to View -> Zoom -> Zoom Text Only, so text is easily readable to my old eyes without disturbing the images.

CAUTION: I have slight cataracts in both eyes. This smears highlights into shadows; shadows appear lighter to me than they should. Doubtless, this affects my judgement of which images appear "best". It probably makes me prefer images with high contrast and reduces my sensitivity to halos.

CAUTION 2: My interest is in photography and video. My conclusions may not be valid for computer graphics, cartoons, line drawings and so on.

Quality depends on factors that can (in principle) be measured, but also on aesthetics which are more subjective. To me, the quality of an image depends on how well it fulfills its purpose, its message. Sharp detail may be important to an image, or it might detract from the message. High acutance may be useful or another distraction.

Script arguments

The script takes two required arguments and five optional arguments. Each optional argument that is not given, or is given with "." as a placeholder, takes a default value.

Argument Description
%1 Filename of input image
%2 Resize amount.
Any valid argument to "-distort resize", eg 50% or 800x800.
%3 d = use downsample method; u = use upsample method; a = automatic.
Default: a.
%4 Sharpen amount, eg 0 or 100.
Default: 50.
%5 Deconvolve radius. 0 = no deconvolution.
Default: 0.
%6 Deconvolve sharpness, eg 0 or 100.
Default: 50.
%7 Output filename.
Default: input filename, with _rhm suffix to name.

The downsample or upsample method may be selected, regardless of the actual dimensions. If "a" for automatic is chosen, the script will use the downsample method only if both dimensions will decrease; otherwise it will use the upsample method.

The script returns environment variable rhmOUTFILE, set to the name of the output file.

Sample image

For these trials, I use this image:

set SRC=\pictures\20140523\AGA_1837_g.tiff

if "%IM32f%"=="" call %PICTBAT%setIm8

%IM32f%convert %SRC% info: 
\pictures\20140523\AGA_1837_g.tiff TIFF 7378x4924 7378x4924+0+0 16-bit TrueColor sRGB 179.7MB 2.360u 0:04.530

Larger versions of this image can be seen at Simple alignment by matching areas.

Basic Downsampling

Compare three methods: a conventional "-resize", a "-resize" in RGB space, and the halo minimization script.

#1: Default resize:

%IM32f%convert %SRC% -resize 300x200 rhm_d1.png
rhm_d1.png

#2: Resize in RGB colorspace:

%IM32f%convert %SRC% ^
  -colorspace RGB -resize 300x200 -colorspace sRGB ^
  rhm_d2.png
rhm_d2.png

#3: Resize with halo minimization

call %PICTBAT%resampHM %SRC% 300x200 d . . . rhm_d3.png
rhm_d3.png

To reduce the effect of vertical viewing angle on LCD screens, here they are side by side:

rhm_d1.png rhm_d2.png rhm_d3.png

I can see no difference between these images. With a blink comparison, I can see differences between #1 and #3, but no differences between #1 and #2, or #2 and #3. A blink comparison of scaled-up images shows differences more clearly. For some aspects (eg edge definition) I prefer #1; for other aspects (eg contrast between small adjacent areas) I prefer #3.

Basic Upsampling

Near the centre of the image is the lettering "BAR & RESTAURANT". For upsampling trials, we crop the top of the letter "B".

set CROP=-gravity NorthWest -crop 75x50+5018+3309 +repage

We enlarge with three methods: a conventional "-resize", a "-resize" in RGB space, and the halo minimization script.

#1: Default resize:

%IM32f%convert %SRC% %CROP% -resize 300x200 rhm_u1.png
rhm_u1.png

#2: Resize in RGB colorspace:

%IM32f%convert %SRC% ^
  -colorspace RGB %CROP% -resize 300x200 -colorspace sRGB ^
  rhm_u2.png
rhm_u2.png

#3: Resize with halo minimization

%IM32f%convert %SRC% %CROP% rhm_crop.png

call %PICTBAT%resampHM rhm_crop.png 300x200 u . . . rhm_u3.png
rhm_u3.png

Again, side by side:

rhm_u1.png rhm_u2.png rhm_u3.png

I can see no difference, and have no preference.

Downsampling with sharpening

The script can sharpen the image, with a strength determined by a number.

for /L %%S in (0,50,200) do (
  call %PICTBAT%resampHM %SRC% 300x200 d %%S . . rhm_d_%%S.png
)
0
rhm_d_0.png
50
rhm_d_50.png
100
rhm_d_100.png
150
rhm_d_150.png
200
rhm_d_200.png

The 100, 150 and 200 images look good to me. Detail is sharpened without being obtrusive. Maybe 200 is over-sharpened.

Let's take it to extremes, to see what happens:

for /L %%S in (300,100,700) do (
  call %PICTBAT%resampHM %SRC% 300x200 d %%S . . rhm_d_%%S.png
)
300
rhm_d_300.png
400
rhm_d_400.png
500
rhm_d_500.png
600
rhm_d_600.png
700
rhm_d_700.png

To my eyes, 400 onwards are horrible. 300 might be acceptable for flat input images.

For downsampling, sharpening of 100 to 200 seems to work well.

Let's compare this script with the results from a "-resize" followed by "-unsharp":

-resize -unsharp resampHM.bat
%IM32f%convert ^
  %SRC% ^
  -resize 300x200 ^
  -unsharp 0x1+1.0+0 ^
  rhm_us1.png
 
rhm_us1.png
100
rhm_d_100.png
%IM32f%convert ^
  %SRC% ^
  -resize 300x200 ^
  -unsharp 0x2+1.0+0 ^
  rhm_us2.png
 
rhm_us2.png
200
rhm_d_200.png
%IM32f%convert ^
  %SRC% ^
  -resize 300x200 ^
  -unsharp 0x3+1.0+0 ^
  rhm_us3.png
 
rhm_us3.png
300
rhm_d_300.png

All the results from "resize unsharp" show heavy halos, clearly evident around the lettering "ENTRANCE" and "NAWAB LOUNGE", and in the sky at the roof line. The white window frames also show well, almost glowing. The halos degrade the image, creating spurious detail that distracts from the real detail, and creating an artificial appearence.

The results from resampHM.bat show no halos (that I can see). But the detail in the brickwork, and at the lettering, is much clearer (especially at 200 and 300). I love these results.

Upsampling with sharpening

The same parameter can sharpen upsampled images.

for /L %%S in (0,50,200) do (
  call %PICTBAT%resampHM rhm_crop.png 300x200 u %%S . . rhm_u_%%S.png
)
0
rhm_u_0.png
50
rhm_u_50.png
100
rhm_u_100.png
150
rhm_u_150.png
200
rhm_u_200.png

Staircasing is visible at the white/black boundary at image top-left from 50 onwards. In the low-contrast areas, the image is perhaps slightly improved, and for images without long continuous lines values up to 200 might be used.

for /L %%S in (300,100,700) do (
  call %PICTBAT%resampHM rhm_crop.png 300x200 u %%S . . rhm_u_%%S.png
)
300
rhm_u_300.png
400
rhm_u_400.png
500
rhm_u_500.png
600
rhm_u_600.png
700
rhm_u_700.png

300 is horrible everywhere. 400 is just plain weird. Then the effect seems to fade down.

For upsampling, sharpening may be useful for images without "man-made" boundaries, but for most images I doubt that I would like any sharpening at all.

Downsampling with deconvolution

Downsampling can be followed by a deconvolution, to sharpen the result. This takes two parameters: a radius and amount.

for /L %%R in (1,1,4) do (
  for /L %%S in (100,100,400) do call %PICTBAT%resampHM ^
    %SRC% 300x200 d 0 %%R %%S rhm_d_%%R_%%S.png
)
1,100
rhm_d_1_100.png
1,200
rhm_d_1_200.png
1,300
rhm_d_1_300.png
1,400
rhm_d_1_400.png
2,100
rhm_d_2_100.png
2,200
rhm_d_2_200.png
2,300
rhm_d_2_300.png
2,400 Bad
rhm_d_2_400.png
3,100
rhm_d_3_100.png
3,200
rhm_d_3_200.png
3,300 Bad
rhm_d_3_300.png
3,400 Bad
rhm_d_3_400.png
4,100
rhm_d_4_100.png
4,200 Bad
rhm_d_4_200.png
4,300 Bad
rhm_d_4_300.png
4,400 Bad
rhm_d_4_300.png

I have marked as "Bad" the versions I consider unacceptable due to halos. Of the lines with radius greater than one, all the acceptable results are less sharp than 1,400. Thus, I would say there is never any point in using a radius larger than one.

With radius of one, I can't see difference between a deconvolved image and non-deconvolved with the same sharpness amount.

Thus, for me, deconvolution after sharpening is pointless.

Upsampling with deconvolution

Similarly, upsampling can be followed by the same sharpening deconvolution.

for /L %%R in (1,1,4) do (
  for /L %%S in (100,100,400) do call %PICTBAT%resampHM ^
    rhm_crop.png 300x200 u 0 %%R %%S rhm_u_%%R_%%S.png
)
1,100
rhm_u_1_100.png
1,200
rhm_u_1_200.png
1,300
rhm_u_1_300.png
1,400 Bad
rhm_u_1_400.png
2,100
rhm_u_2_100.png
2,200
rhm_u_2_200.png
2,300 Bad
rhm_u_2_300.png
2,400 Bad
rhm_u_2_400.png
3,100
rhm_u_3_100.png
3,200 Bad
rhm_u_3_200.png
3,300 Bad
rhm_u_3_300.png
3,400 Bad
rhm_u_3_400.png
4,100 Bad
rhm_u_4_100.png
4,200 Bad
rhm_u_4_200.png
4,300 Bad
rhm_u_4_300.png
4,400 Bad
rhm_u_4_300.png

Again, I have marked as "Bad" the versions I consider unacceptable, this time due to noticable ringing, staircasing, and an over-emphasis of detail (or noise) in the dark area. The decision is more arbitrary: badness increases evenly with both radius and sharpness.

2,300 is pretty much identical to 4,100.

As above, I would be very cautious about sharpening upsampled images.

Round-trip

We can enlarge ten-fold, then shrink ten-fold, and compare the result with the original. I also use a crude stopwatch to show how long each process takes.

Create a source for round-trip tests

%IM32f%convert ^
  %SRC% ^
  -crop 300x200+5018+3309 +repage ^
  rhm_rt_src.png
rhm_rt_src.png

Conventional up-down.

call StopWatch

%IM32f%convert ^
  rhm_rt_src.png ^
  ( +clone ^
    -resize 1000%% ^
    -resize 10%% ^
    +write rhm_rt_ud1.png ^
  ) ^
  -metric RMSE -format "%%[distortion]   " ^
  -compare info: 

call StopWatch 
0.00434616   0 00:00:00
rhm_rt_ud1.png

Conventional up-down, via linear RGB.

%IM32f%convert ^
  rhm_rt_src.png ^
  ( +clone ^
    -colorspace RGB ^
    -resize 1000%% ^
    -resize 10%% ^
    -colorspace sRGB ^
    +write rhm_rt_ud2.png ^
  ) ^
  -metric RMSE -format "%%[distortion]   " ^
  -compare info: 

call StopWatch 
0.00484559   0 00:00:00
rhm_rt_ud2.png

Conventional up-down, via the script.

call %PICTBAT%resampHM rhm_rt_src.png 1000%%%% u . . . rhm_rt_tmp.png

call %PICTBAT%resampHM rhm_rt_tmp.png 10%%%% d . . . rhm_rt_uds1.png

call StopWatch 

del rhm_rt_tmp.png

%IM32f%convert ^
  rhm_rt_src.png ^
  rhm_rt_uds1.png ^
  -metric RMSE -format %%[distortion] ^
  -compare info: 
0 00:00:39
0.00415262
rhm_rt_uds1.png

The numbers say the script is best, followed by a plain conventional, followed by conventional via linear RGB. However, the RMSE differences are small. I can see no difference between any round-trip result and the source, even when scaled up.

The script is far slower than the conventional methods.

Colorspaces

I usually work in sRGB colorspace so this script, like most of my scripts, assumes the image is encoded in sRGB. If the image is encoded as something else the results may be inaccurate.

If the input image is encoded in another colorspace known to ImageMagick, such as CMY, CMYK, Lab etc, the script should work fine, but the output will be sRGB.

AdobeRGB1998 is very similar to sRGB, but is defined (see Adobe RGB (1998) Color Image Encoding section 4.3.1.2) to have a simpler relationship between pixel values and radiometrically linear values. It is a power law, where the constant is aproximately 2.2. (It is exactly 563/256.) So converting to linear and back is very easy. For this, the script will need changing; see instructions in resampHM.bat below.

For images encoded in other colorspaces (probably with embedded profiles), I suggest these are converted to sRGB by a "-profile" command, then converted back afterwards. Again, see instructions in resampHM.bat.

Conclusions

For me, the script gives great benefit for combined downsampling and sharpening, a common operation for web preparation.

I have experimented with this technique for demosaicing Bayer images. See Demosaicing. So far, I wouldn't say it brings any improvement.

I repeat the cautions from the top of this page: I have slight cataracts, and this will bias my preferences; my interest is in photographs, rather than solid-colour graphics, etc.

Scripts

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

resampHM.bat

rem Resampling with halo minimization.
rem   %1 is input image.
rem   %2 is resize, eg 50% or 800x800, etc.
@rem
@rem Optional:
@rem   %3 is d=downsample, u = upsample, a = automatic
@rem   %4 is sharpen amount (0=none, 50=default)
@rem   %5 is deconvolve radius (0=no deconvolution)
@rem   %6 is deconvolve sharpness
@rem   %7 output filename

@rem See http://www.luminous-landscape.com/forum/index.php?topic=91754.msg763245#msg763245
@rem and http://im.snibgo.com/resamphm.htm


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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 rhm


set mSize=%2
if "%mSize%"=="." set mSize=
if "%mSize%"=="" set mSize=100%

set method=%3
if "%method%"=="." set method=
if "%method%"=="" set method=a

if /I "%method%" NEQ "a" if /I "%method%" NEQ "d" if /I "%method%" NEQ "u" (
  echo Method must be A, D or U.
  exit /B 1
)

set shpAmount=%4
if "%shpAmount%"=="." set shpAmount=
if "%shpAmount%"=="" set shpAmount=50

set deconRad=%5
if "%deconRad%"=="." set deconRad=
if "%deconRad%"=="" set deconRad=0

set deconShp=%6
if "%deconShp%"=="." set deconShp=
if "%deconShp%"=="" set deconShp=50

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


::echo mSize=%mSize% method=%method%  shpAmount=%shpAmount%


@rem If working in AdobeRGB1998, replace the next two lines with:
@rem
@rem   set colspIn=-evaluate Pow 2.19921875
@rem   set colspOut=-evaluate Pow 0.4547069271758437
@rem
@rem For images with embedded profiles, use:
@rem
@rem   set colspIn=-write IMprofile.icc -profile sRGB.icc -set colorspace sRGB -colorspace RGB
@rem   set colspOut=-set colorspace RGB -colorspace sRGB -profile IMprofile.icc

set colspIn=-colorspace RGB
set colspOut=-set colorspace RGB -colorspace sRGB


set nGam=3

for /f "usebackq" %%L in (`%IM%identify ^
  -precision 16 ^
  -format "invGam=%%[fx:1/%nGam%]" ^
  xc:`) do set %%L

set gamIn=-gamma %nGam%
set gamOut=-gamma %invGam%

if /I "%method%"=="a" (
  for /F "usebackq" %%L in (`%IM%identify ^
    -ping ^
    -format "W_OLD=%%w\nH_OLD=%%h" ^
    %INFILE%`) do set %%L

  for /F "usebackq" %%L in (`%IM%convert ^
    -size %W_OLD%x%H_OLD% ^
    xc: ^
    -scale %2 ^
    -format "W_NEW=%%w\nH_NEW=%%h" ^
    info:`) do set %%L

  set method=u
  if !W_NEW! LSS !W_OLD! if !H_NEW! LSS !H_OLD! set method=d

  rem echo From auto, method=!method!
)

if /I "%method%"=="d" (

  if %shpAmount%==0 (
    set REAL_WORK=-filter Quadratic -distort Resize %mSize%
  ) else (

    set PREPROC=-define filter:c=0.1601886205085204 -filter Cubic -distort Resize %mSize%

    set FILT=-define convolve:scale=%shpAmount%%%^^,100 -morphology Convolve DoG:3,0,0.4981063336734057

    set REAL_WORK= ^
      !PREPROC! ^
      ^( -clone 0 %gamIn% !FILT! %gamOut% ^) ^
      ^( -clone 0 !FILT! ^) ^
      -delete 0 ^
      ^( -clone 1 -colorspace gray -auto-level ^) ^
      -compose over -composite
  )
) else (
  for /f "usebackq" %%L in (`%IM%identify ^
    -precision 16 ^
    -format "deblurValue=%%[fx:1-0.0011450938298236*%shpAmount%]" ^
    xc:`) do set %%L

  set FILT=-define filter:blur=!deblurValue! -filter Lanczos -distort Resize %mSize%

  set REAL_WORK= ^
    ^( -clone 0 %gamIn% !FILT! %gamOut% ^) ^
    ^( -clone 0 !FILT! ^) ^
    -delete 0 ^
    ^( -clone 1 -colorspace gray -auto-level ^) ^
    -compose over -composite

)

rem echo REAL_WORK=%REAL_WORK%

set DECONV=

if not "%deconRad%"=="0" (
  for /f "usebackq" %%L in (`%IM%identify ^
    -precision 16 ^
    -format "blurSigma=%%[fx:max(0.4,0.3902*%deconRad%)]" ^
    xc:`) do set %%L

  set DECONFILT=-define convolve:scale=%deconShp%%%^^,100 -morphology Convolve DoG:0,0,!blurSigma!

  set DECONV= ^
    ^( -clone 0 %gamIn% !DECONFILT! %gamOut% ^) ^
    ^( -clone 0 !DECONFILT! ^) ^
    -delete 0 ^
    ^( -clone 1 -colorspace gray -auto-level ^) ^
    -compose over -composite
)

if "%IM32f%"=="" (
  call %PICTBAT%setIm8.bat
  if "%IM32f%"=="" (
    echo Can't set IM32f
    exit /B 1
  )
)

%IM32f%convert ^
  %INFILE% ^
  %colspIn% ^
  %REAL_WORK% ^
  %DECONV% ^
  %colspOut% ^
  %OUTFILE%

rem  ( -clone 0 %gamIn% %FILT% %gamOut% )
rem  ( -clone 0 %FILT% )


call echoRestore

@endlocal & set rhmOUTFILE=%OUTFILE%

setInOut.bat

@setlocal DisableDelayedExpansion
@set INFILE=%~f1
@if not exist "%INFILE%" set INFILE=%1
@endlocal & set INFILE=%INFILE%
@set INNAME=%~n1
@set INNOBR=!INFILE:[%INFILE:*[=%=!
set BASENAME=%~dpn1
if not exist %INNOBR% (
  set BASENAME=%1
  set BASENAME=!BASENAME::=_!
)
set BASENAME=!BASENAME:[%BASENAME:*[=%=!

set EXT=%~x1
@set EXT=!EXT:[%EXT:*[=%=!
@if "%EXT%"=="" set EXT=.png
@set ISJPEG=0
@set ISTIFF=0
@set sioCODE=%2
@if /I "%EXT%"==".jpg" set ISJPEG=1
@if /I "%EXT%"==".jpeg" set ISJPEG=1
@if /I "%EXT%"==".txt" set EXT=.png
@if /I "%EXT%"==".ppm" set EXT=.png
@rem if /I "%EXT%"==".miff" set EXT=.png

@set COMPR=
@if %ISJPEG%==1 (
  @set EXT=%sioJPEG_EXT%
  @if "!EXT!"=="" @set EXT=.tiff
  @if !EXT!==.tiff @set COMPR=-compress Zip
)

@set OUTBASE=%BASENAME%_%sioCODE%
@set OUTFILE=%OUTBASE%%EXT%

@setlocal DisableDelayedExpansion
@if "%sioDEBUG%"=="1" @(
  echo INFILE=%INFILE%
  echo INFILEBANG=%INFILE:!=*%
  echo OUTFILE=%OUTFILE%
  echo INNAME=%INNAME%
  echo BASENAME=%BASENAME%
  echo EXT=%EXT%
)
@endlocal

echoOffSave.bat

@echo>%TEMP%\echo_%1.txt
@for /f "tokens=3 delims=. " %%E in (%TEMP%\echo_%1.txt) do @set ECHO_SAVE=%%E
@echo off

echoRestore.bat

@echo %ECHO_SAVE%

StopWatch.bat

@set STOP_WATCH_SUB=
@if not "%STOP_WATCH_TIME%"=="" set STOP_WATCH_SUB=/S "%STOP_WATCH_TIME%"

@cDate %STOP_WATCH_SUB% /f"\q \H:\M:\s"

@for /F "usebackq tokens=*" %%L IN (`cDate /f"\d-\b-\Y \H:\M:\s"`) do @set STOP_WATCH_TIME=%%L
@rem echo %STOP_WATCH_TIME%

@if not "%1"=="" @echo %STOP_WATCH_TIME% %1 %2 %3 %4 %5 %6 %7 %8 %9>>stopwatch.lis

My usual version of IM is:

%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

This customised development version is:

%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

Unlike most of my pages, these images have not been converted to JPG. They are loss-lessy compressed PNG files.

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


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 21-September-2014.

Page created 15-Jul-2017 13:44:31.

Copyright © 2017 Alan Gibson.