snibgo's ImageMagick pages

Sparse displacement maps with gnuplot

A method for filling holes in very sparse displacement map.

Given a very sparse image, gnuplot can guess the unknown values.

Scripts on this page use gnuplot, sed, grep and sort, which are commonly available for Unix and Windows.

Sample input

For the source, we use an image with a grid so we can see what is happening.

set SRC=dgp_src.png

call %PICTBAT%gridOver ^
  toes.png %SRC% 10 10 1 yellow

%IMG7%magick ^
  %SRC% ^
  -fill None -stroke White ^
  -draw "translate %%[fx:w*0.15],%%[fx:h*0.40] circle 0,0,0,10" ^
  -draw "translate %%[fx:w*0.85],%%[fx:h*0.50] circle 0,0,0,10" ^
  -draw "translate %%[fx:w*0.50],%%[fx:h*0.85] circle 0,0,0,10" ^
  %SRC%

for /F "usebackq" %%L in (`%IMG7%magick ^
  %SRC% ^
  -format "WW=%%w\nHH=%%h\n" ^
  info:`) do set %%L

echo WW=%WW% HH=%HH% 
WW=267 HH=233 
dgp_src.pngjpg

Worked example

Make sparse displacement map

We make a relative displacement map. Values of 50% in the red and green channels will cause zero displacement. Higher values will pull colours from the right or down; lower values will pull colours from the left or up. The pixel at (25%,30%) will take its color from 10% to the left and 15% down, and so on.

%IMG7%magick ^
  -size %WW%x%HH% xc:None ^
  -fill rgb(40%%,65%%,50%%) -draw "point %%[fx:w*0.25],%%[fx:h*0.30]" ^
  -fill rgb(60%%,40%%,50%%) -draw "point %%[fx:w*0.75],%%[fx:h*0.60]" ^
  -fill rgb(45%%,55%%,50%%) -draw "point %%[fx:w*0.55],%%[fx:h*0.80]" ^
  dgp_sps_map.miff
-fill gray(50%%) -draw "point 0,0" ^ -fill gray(50%%) -draw "point 0,0" ^ -fill gray(50%%) -draw "point 0,1" ^ -fill gray(50%%) -draw "point 0,2" ^ -fill gray(50%%) -draw "point 1,0" ^ -fill gray(50%%) -draw "point 2,0" ^ -fill gray(50%%) -draw "point %%[fx:w-1],0" ^ -fill gray(50%%) -draw "point 0,%%[fx:h-1]" ^ -fill gray(50%%) -draw "point %%[fx:w-1],%%[fx:h-1]" ^ -shave 1x1 ^ -compose SrcIn -bordercolor gray(50%%) -border 1 ^ -compose Over ^

Fill the holes with gnuplot

The script clut2txt3c.bat reads an image and writes a text file of pixels that are not fully transparent. Each line contains space-separated values: x y r g b. The coordinates are integers. The colour values are floating-point, typically between zero and QuantumRange. The input text file can list the pixels in any order, but must not contain duplicate (x,y) values, even if they have the same colours. Any duplicate (x,y) values will cause problems for gnuplot, which will then output garbage values.

The script is slow, about three pixels/second, so should not generally be used for hundreds of opaque pixels.

call %PICTBAT%clut2txt3c dgp_sps_map.miff dgp_sps.lis
if ERRORLEVEL 1 exit /B 1

The text file dgp_sps.lis is:

67 70 26207.60009765625 42591.35009765625 32767.5
200 140 39327.3974023819 26207.60009765625 32767.5
147 186 29487.55004882813 36031.4501953125 32767.5

[

Set border pixels in the text file to 50% gray:

for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "MID=%%[fx:QuantumRange/2]\n" ^
  xc:`) do set %%L

rem call %PICTBAT%mSparseBord %WW%x%HH% dgp_sps.lis "%MID% %MID% %MID%"

rem sort -g --key=1 dgp_sps.lis |sort -g --key=2 -s |sort --merge --unique -o dgp_sps2.lis

rem del dgp_sps.lis
rem ren dgp_sps2.lis dgp_sps.lis

gnuplot should be given unique values of (x,y). If dumplicate values are given, blah.

]

The script smoothSparse3c.bat reads the text file from the previous scale and writes up to three text files (one per channel) with missing values filled in. It does this by creating a script for gnuplot, and running gnuplot with that script. We will read the values with PGM, so we tell gnuplot not to write fractions. In this application, we process only the first two channels.

call %PICTBAT%smoothSparse3c dgp_sps.lis dgp_fld.lis 2
if ERRORLEVEL 1 exit /B 1

The script txt2clut3c.bat reads the three text files, using IM's PGM loader for each channel, and combines the three images. The PGM loader goes wrong wth negative or fractional numbers because the "-" and "." characters are interpreted as end-of-field.

call %PICTBAT%txt2clut3c dgp_fld.lis dgp_map.miff %WW%x%HH% extend 2
if ERRORLEVEL 1 exit /B 1

The filled map looks like this:

dgp_map.miff

dgp_map.miffjpg

Apply the filled displacement map

We apply the map in the usual way:

%IMG7%magick ^
  %SRC% ^
  dgp_map.miff ^
  -compose Displace ^
  -set option:compose:args 100%%x100%% ^
  -composite ^
  dgp_disp.png
dgp_disp.pngjpg

Graduate by mask

We use a similar method to that shown in Displacement by masked SRT. But this page uses relative displacement maps, so we mask-blend with an identity relative displacement map which is simply a 50% gray image.

First we make a mask that is white where we want the full effect, black where we want no effect, and gray inbetween.

Make a black/white/other image.

%IMG7%magick ^
  %SRC% ^
  -fill #88f -colorize 100 ^
  -shave 1x1 ^
  -bordercolor Black -border 1 ^
  -fill White ^
  -draw "point %%[fx:w*0.25],%%[fx:h*0.30]" ^
  -draw "point %%[fx:w*0.75],%%[fx:h*0.60]" ^
  -draw "point %%[fx:w*0.55],%%[fx:h*0.80]" ^
  dgp_mask.png
dgp_mask.pngjpg

Apply fanBW.

call %PICTBAT%fanBW ^
  dgp_mask.png ^
  dgp_mask_f.png
dgp_mask_f.pngjpg

Blend the map with an identity map, using the mask dgp_mask_f.png:

%IMG7%magick ^
  dgp_map.miff ^
  ( +clone ^
    -fill gray(50%%) -colorize 100 ^
  ) ^
  +swap ^
  dgp_mask_f.png ^
  -compose Over -composite ^
  dgp_map_b.miff
dgp_map_b.miffjpg

Apply the blended map:

%IMG7%magick ^
  %SRC% ^
  dgp_map_b.miff ^
  -compose Displace ^
  -set option:compose:args 10%%x10%% ^
  -composite ^
  dgp_disp2.png
dgp_disp2.pngjpg

An alternative is to make the mask using a white polygon from the points. Then the area within that polygon will be fully distorted.

Make a black/white/other image.

set POLYG=^
%%[fx:w*0.25],%%[fx:h*0.30],^
%%[fx:w*0.75],%%[fx:h*0.60],^
%%[fx:w*0.55],%%[fx:h*0.80]

%IMG7%magick ^
  %SRC% ^
  -fill #88f -colorize 100 ^
  -shave 1x1 ^
  -bordercolor Black -border 1 ^
  -fill White ^
  -draw "polygon %POLYG%" ^
  dgp_maskp.png
dgp_maskp.pngjpg

Apply fanBW.

call %PICTBAT%fanBW ^
  dgp_maskp.png ^
  dgp_maskp_f.png
dgp_maskp_f.pngjpg

Blend the map with an identity map.

%IMG7%magick ^
  dgp_map.miff ^
  ( +clone ^
    -fill gray(50%%) -colorize 100 ^
  ) ^
  +swap ^
  dgp_maskp_f.png ^
  -compose Over -composite ^
  dgp_map_bp.miff
dgp_map_bp.miffjpg

Apply the blended map:

%IMG7%magick ^
  %SRC% ^
  dgp_map_bp.miff ^
  -compose Displace ^
  -set option:compose:args 100%%x100%% ^
  -composite ^
  dgp_disp2p.png
dgp_disp2p.pngjpg

To see the differences more clearly, we make a GIF from the source and results:

%IMG7%magick ^
  -pointsize 30 ^
  -fill White ^
  -delay 50 ^
  -gravity NorthWest ^
  ( %SRC% -annotate 0,0 "SRC" ) ^
  ( dgp_disp.png -annotate 0,0 "disp" ) ^
  ( dgp_disp2.png -annotate 0,0 "disp2" ) ^
  ( dgp_disp2p.png -annotate 0,0 "disp2p" ) ^
  dgp_disp.gif
dgp_disp.gif

Animation

Future

Scripts

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

clut2txt3c.bat

rem Given %1 is 3-channel image Nx1 with alpha,
rem writes %2 text file of non-transparent pixels.
rem   Each output line has five numbers:
rem   x-coordinate, y-coordinate, and number generally 0 to Quantum for R, G and B.
rem See also txt2clut3c.bat

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 c2t3

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


for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -precision 16 ^
  -format "QPC=%%[fx:QuantumRange/100]\nQ255=%%[fx:QuantumRange/255]\n" ^
  xc:`) do set %%L


:: Colour may have % suffix, or be integer in scale 0-255.

(
  for /F "usebackq tokens=1,2,4,5,6 delims=,() " %%A in (`%IMG7%magick ^
    %INFILE% ^
    -precision 16 ^
    sparse-color:- ^| sed -e 's/ /\n/g' -e 's/%%/c/g' `) do (

    call :calcVal "%%C"
    set VALR=!VAL!
    call :calcVal "%%D"
    set VALG=!VAL!
    call :calcVal "%%E"
    set VALB=!VAL!

    echo %%A %%B !VALR! !VALG! !VALB!
  )
) >%OUTFILE%

call echoRestore

@endlocal & set c2t3OUTFILE=%OUTFILE%

@exit /B 0

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

:calcVal
    set VAL=%~1
    set LASTCH=!VAL:~-1!
    if !LASTCH!==c (
      set VAL=!VAL:~0,-1!
      for /F "usebackq" %%L in (`%IMG7%magick identify ^
        -precision 16 ^
        -format "%%[fx:!VAL!*%QPC%]" ^
        xc:`) do set VAL=%%L
    ) else (
      for /F "usebackq" %%L in (`%IMG7%magick identify ^
        -precision 16 ^
        -format "%%[fx:!VAL!*%Q255%]" ^
        xc:`) do set VAL=%%L
    )

exit /B 0

smoothSparse3c.bat

rem %1 input text file of x, y, v for sparse integer values of x and y.
rem %2 output text file of x, y, v for all x and y from first to last.
rem %3 number of channels, 1 to 3 [default 3]

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

@setlocal enabledelayedexpansion

@call echoOffSave

set INFILE=%1

set NumChan=%3
if "%NumChan%"=="." set NumChan=
if "%NumChan%"=="" set NumChan=3

:: Note: gnuplot can't handle F: etc in file path.
set OUTFILE0=%~n2_0%~x2
set OUTFILE1=%~n2_1%~x2
set OUTFILE2=%~n2_2%~x2

echo %0: %OUTFILE0% %OUTFILE1% %OUTFILE2%

set TMPSCR=\temp\ss3c_gp_script.gp

rem type %INFILE%

set nFIRSTx=
set nLASTx=
set nFIRSTy=
set nLASTy=

for /F "tokens=1,2 eol=# delims=, " %%A in (%INFILE%) do (
  if "!nFIRSTx!"=="" set nFIRSTx=%%A
  if "!nLASTx!"=="" set nLASTx=%%A
  if !nFIRSTx! GTR %%A set nFIRSTx=%%A
  if !nLASTx! LSS %%A set nLASTx=%%A
  if "!nFIRSTy!"=="" set nFIRSTy=%%B
  if "!nLASTy!"=="" set nLASTy=%%B
  if !nFIRSTy! GTR %%B set nFIRSTy=%%B
  if !nLASTy! LSS %%B set nLASTy=%%B
)

echo %0: X from %nFIRSTx% to %nLASTx%
echo %0: Y from %nFIRSTy% to %nLASTy%

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

set /A nSAMPSx=%nLASTx%-%nFIRSTx%+1
set /A nSAMPSy=%nLASTy%-%nFIRSTy%+1

echo %0: samps: %nSAMPSx% %nSAMPSy%

:: We tell gnuplot not to use decimals.
:: FIXME: gnuplot can create negative numbers, which barf IM's PNM reader.

(
  echo set format "%%.0f"
  echo set dgrid3d %nSAMPSy%,%nSAMPSx% splines
  echo set table "%OUTFILE0%"
  echo splot '%INFILE%' using 1:2:3

  if %NumChan% GTR 1 (
    echo set table "%OUTFILE1%"
    echo splot '%INFILE%' using 1:2:4

    if %NumChan% GTR 2 (
      echo set table "%OUTFILE2%"
      echo splot '%INFILE%' using 1:2:5
    )
  )
  echo unset table
  echo unset dgrid3d

) >%TMPSCR%

type %TMPSCR%

gnuplot %TMPSCR%

if ERRORLEVEL 1 (
  echo %0: gnuplot failed
  exit /B 1
)


call echoRestore

endlocal & set ss3cOUTFILE=%OUTFILE%& ^
set ss3cFirstX=%nFIRSTx%& ^
set ss3cLastX=%nLASTx%& ^
set ss3cFirstY=%nFIRSTy%& ^
set ss3cLastY=%nLASTy%

txt2clut3c.bat

rem Given %1 is three text files,
rem  each with x, y, v
rem    x,y are integers
rem    v is floating point, typically 0 to QuantumRange
rem makes three grayscale images,
rem and combines them into %2 output image.
rem %3 desired width "x" height of output, eg 600x400
rem %4 "extend" or "noextend".
rem %5 number of channels, 1 to 3 [default 3]


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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 c2x

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

set WxH=%3
if "%WxH%"=="." set WxH=

set doExt=%4
if "%doExt%"=="." set doExt=
if "%doExt%"=="" set doExt=noextend

set NumChan=%5
if "%NumChan%"=="." set NumChan=
if "%NumChan%"=="" set NumChan=3

call parseXxY 100 100 %WxH%
set reqW=%pxxyX%
set reqH=%pxxyY%

set INFILE0=%~n1_0%~x1
set INFILE1=%~n1_1%~x1
set INFILE2=%~n1_2%~x1

set TMPDIR=\temp\

set TMPTEXT=\temp\t2c3c.txt

echo %0: ss3cFirstX=%ss3cFirstX%

set /A WW=%ss3cLastX%-%ss3cFirstX%+1
set /A HH=%ss3cLastY%-%ss3cFirstY%+1

if %doExt%==extend (
  set EXTDIST=-virtual-pixel Edge ^
-set option:distort:viewport %reqW%x%reqH%-%ss3cFirstX%-%ss3cFirstY% ^
-distort SRT 1,0 +repage
) else (
  set EXTDIST=
)

for /F "usebackq" %%A in (`%IMG7%magick identify ^
    -precision 19 ^
    -format "%%[fx:QuantumRange]" ^
    xc:`) do set QR=%%A

call :mkGrayImg %INFILE0% x0.miff
if ERRORLEVEL 1 exit /B 1

set IN1=( +clone -fill gray(50%%) -colorize 100 )
set IN2=( +clone -fill gray(50%%) -colorize 100 )

if %NumChan% GTR 1 (
  set IN1=x1.miff
  call :mkGrayImg %INFILE1% !IN1!
  if ERRORLEVEL 1 exit /B 1

  if %NumChan% GTR 2 (
    set IN2=x2.miff
    call :mkGrayImg %INFILE2% !IN2!
    if ERRORLEVEL 1 exit /B 1
  )
)

%IMG7%magick ^
  x0.miff ^
  %IN1% ^
  %IN2% ^
  -combine ^
  %OUTFILE%
if ERRORLEVEL 1 exit /B 1

echo %0: OUTFILE=%OUTFILE%

call echoRestore

@endlocal & set c2xOUTFILE=%OUTFILE%

@exit /B 0

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

:mkGrayImg
set InText=%1
set OutImg=%2

echo %0: InText %InText%

set SORTED=\temp\t2c_%~n1.srt
set SORTED2=\temp\t2c2_%~n1.srt
set SORTED3=\temp\t2c3_%~n1.srt

(
  echo P2
  echo %WW% %HH%
  echo %QR%
) >%SORTED2%

:: Strip comments and empty lines; sort on y then x.
:: grep: we double the ^ for Windows.

grep -v -e '^^[[:space:]]*$' -e '^^#' %InText% |sort -g --key=1 |sort -g --key=2 -s -o %SORTED%

:: Get the third number. Also change "-0" to "0".
sed -e 's/  / /g' -e 's/-0/0/g' %SORTED% |cut -d ' ' -f 3 >>%SORTED2%

:: Clamp negative values to 0.
(
  for /F "tokens=*" %%L in (%SORTED2%) do if %%L LSS 0 (echo 0) else echo %%L
) >%SORTED3%

goto skipSlow

:: Next is slow: we call identify to calculate for every pixel.

(
  echo # ImageMagick pixel enumeration: %WW%,%HH%,%QR%,graya

  for /F "eol=# tokens=1-3 delims=, " %%A in (%SORTED%) do (
    for /F "usebackq" %%L in (`%IMG7%magick identify ^
      -precision 19 ^
      -format "VAL=%%[fx:%%C*QuantumRange/100]" ^
      xc:`) do set %%L

    echo %%A,%%B: ^(!VAL!,%QR%^)
  )
) >%TMPTEXT%

:skipSlow

rem echo %0: TMPTEXT %TMPTEXT%

rem type %TMPTEXT%

rem Don't read from   TXT:%TMPTEXT% ^

%IMG7%magick ^
  PNM:%SORTED3% ^
  -crop %WW%x%HH%+%ss3cFirstX%+%ss3cFirstY% +repage ^
  %EXTDIST% ^
  %OutImg%

if ERRORLEVEL 1 exit /B 1

exit /B 0

fanBW.bat

rem %1 input with back, white and other.
rem %2 output, replacing "other" with graduated gray.
rem Method: fan composition.

set INFILE=%1
set OUTFILE=%2

goto skip

%IMG7%magick ^
  %INFILE% ^
  -alpha off ^
  ( -clone 0 ^
    -channel RGB -negate +channel ^
    -fill White +opaque Black ^
    -morphology Distance Euclidean:4 ^
    -channel RGB -negate +channel ^
    -auto-level ^
  ) ^
  ( -clone 0 ^
    -fill White +opaque Black ^
    -morphology Distance Euclidean:4 ^
    -auto-level ^
  ) ^
  -delete 0 ^
  ( -clone 1 -evaluate Divide 2 ) ^
  ( -clone 0-1 ^
    -compose Mathematics ^
      -define compose:args=0,0.5,-0.5,0.5 ^
      -composite ^
  ) ^
  -delete 0-1 ^
  -compose DivideSrc -composite ^
  %OUTFILE%

:skip

%IMG7%magick ^
  %INFILE% ^
  -alpha off ^
  ( -clone 0 ^
    -channel RGB -negate +channel ^
    -fill White +opaque Black ^
    -morphology Distance Euclidean:4 ^
    -channel RGB -negate +channel ^
    -auto-level ^
  ) ^
  ( -clone 0 ^
    -fill White +opaque Black ^
    -morphology Distance Euclidean:4 ^
    -auto-level ^
    -evaluate Divide 2 ^
  ) ^
  -delete 0 ^
  ( -clone 0-1 ^
    -compose Mathematics ^
      -define compose:args=0,1,-0.5,0.5 ^
      -composite ^
  ) ^
  -delete 0 ^
  -compose DivideSrc -composite ^
  %OUTFILE%

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

%IM%identify -version
Version: ImageMagick 6.9.9-50 Q16 x64 2018-06-02 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 gslib heic jng jp2 jpeg lcms lqr lzma openexr pangocairo png ps raw 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 dispplot.h1. To re-create this web page, execute "procH1 dispplot".


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 2-August-2019.

Page created 04-Aug-2019 20:49:51.

Copyright © 2019 Alan Gibson.