snibgo's ImageMagick pages

Straightening two lines

When an image has two wobbly lines that are roughly horizontal, we can distort the image such that both lines become straight and horizontal.

The distortion is in the vertical direction only. It shrinks columns so that both lines become straight and horizontal. Within each column the shrinkage is constant, so pixels above and below the input lines will also be distorted.

Scripts on this page assume that "magick" is HDRI.

This page was developed in response to a forum question: how to remove vertical and horizontal line in this image.

Sample image

We use an image with a grid so we can see what is happening.

set SRC=s2lsrc.png

call %PICTBAT%gridOver ^
  toes.png %SRC% 8 8 1 yellow
s2lsrc.pngjpg

Markup lines to be straightened

We draw two non-intersecting aliased blue lines across the input image. The lines are aliased, so the pixels on the line are exactly blue. The lines can be straight or curved, but mustn't double-back on themselves. The goal is to distort the image to make those lines straight and horizontal. The lines divide the image onto three areas: top, middle and bottom.

%IMG7%magick ^
  %SRC% ^
  -stroke Blue +antialias ^
  -draw "line 0,%%[fx:h*0.1],%%[fx:w-1],%%[fx:h*0.4]" ^
  -draw "line 0,%%[fx:h*0.9],%%[fx:w-1],%%[fx:h*0.8]" ^
  s2l_bl1.png

%IMG7%magick ^
  %SRC% ^
  -stroke Blue +antialias ^
  -draw "line 0,%%[fx:h*0.1],%%[fx:w-1],%%[fx:h*0.4]" ^
  -draw "line 0,%%[fx:h*0.8],%%[fx:w/2],%%[fx:h*0.6]" ^
  -draw "line %%[fx:w/2],%%[fx:h*0.6],%%[fx:w-1],%%[fx:h*0.9]" ^
  s2l_bl1.png
s2l_bl1.pngjpg

The image will be distorted to make the inner edges of both blue lines straight and horizontal.

The lines can be any thickness, or even fill the entire top and bottom areas. Only the inner edges (the bottom of the top line, and the top of the bottom line) matter.

Shrinking method

This method distorts by shrinking columns.

Pixels in the top line that are higher then the lowest part of that line will be lowered.

Pixels in the bottom line that are lower then the highest part of that line will be raised.

Step 1: Make two Nx1 cluts

The script blue2cluts.bat takes the marked image and makes two grayscale images the same width but only one pixel high. They represent the proportion of the image that is in the top and bottom areas. The script works by flood-filling from the top-left and bottom-left corners, so it assumes the lines do not pass through those pixels.

We need high precision, so we write to floating-point miff files.

The main outputs are two Nx1 images:

call %PICTBAT%blue2cluts ^
  s2l_bl1.png s2l_blc_XX.miff

[No image]

Convert to PNG so we can see the images.

%IMG7%magick s2l_blc_1.miff s2l_blc_1.png
%IMG7%magick s2l_blc_2.miff s2l_blc_2.png
s2l_blc_1.png s2l_blc_2.png

These images are the relative displacement maps for the rows that will contain the top and bottom blue lines.

For the top line, pixels are generally pulled down, so the relative displacement values are 50% or lower. If P is the proportion of a column that is in the top area, 0<=P<=1, and max(P) is the maximal P across all columns, the resulting value V(0<=V<=1) in that column is V = P - max(P) + 0.5.

For the bottom line, pixels are generally pulled up, so the relative displacement values are 50% or higher. The resulting value is V = max(P) - P + 0.5.

If b2cDEBUG is set, the script will also create debug images like these:

set b2cDEBUG=s2l_dbg_XX.png

call %PICTBAT%blue2cluts ^
  s2l_bl1.png s2l_blc_XX.miff

set b2cDEBUG=
s2l_dbg_1.png s2l_dbg_2.png

The first debug image shows the top area (including the blue line) as white. The second debug image is for the bottom area.

The script also sets environment variables:

set b2c 
b2cHH=233
b2cMAXBOT=39.91416204127565
b2cMAXGAP=161
b2cMAXTOP=40.34334885461967
b2cMINGAP=80
b2cNEWHH=46
b2cNEWY=94
b2cXTRABOT=302.1739094336239
b2cXTRATOP=-204.3478339630024

Step 2: Make the full displacement map

The script interpClut.bat takes those two Nx1 images as input, and makes an Nx%3 output interpolating linearly in the middle area between these lines, and also extrapolates for the pixels in the top and bottom areas. The output is a relative displacement map.

call %PICTBAT%interpClut ^
  s2l_blc_1.miff s2l_blc_2.miff ^
  %b2cHH% ^
  s2l_lt1.miff ^
  %b2cXTRATOP% %b2cXTRABOT%

%IMG7%magick s2l_lt1.miff s2l_lt1.png
s2l_lt1.pngjpg

Step 3: Distort the image with the map

We use the displacement map to distort an image. We would normally use the source image but for demonstration we use the version with blue lines so we can see what has happened.

%IMG7%magick ^
  s2l_bl1.png ^
  s2l_lt1.miff ^
  -virtual-pixel None ^
  -compose Displace ^
    -set option:compose:args 0x100%% ^
    -composite ^
  s2l_d1.png
s2l_d1.pngjpg

We can see the effects of the transformation:

The environment variables give the height and y-offset for a crop of the middle area:

%IMG7%magick ^
  s2l_d1.png ^
  -crop x%b2cNEWHH%+0+%b2cNEWY% +repage ^
  s2l_d1_crop.png
s2l_d1_crop.pngjpg

Supersampling improves the results for graphics (including grid lines), making edges less aliased, but makes little difference to ordinary photos:

%IMG7%magick ^
  s2l_bl1.png ^
  s2l_lt1.miff ^
  -resize 200%% ^
  -virtual-pixel None ^
  -compose Displace ^
    -set option:compose:args 0x100%% ^
    -composite ^
  -resize 50%% ^
  s2l_d1s.png
s2l_d1s.pngjpg

Combining the three steps

For convenience, the script str2lines3.bat runs the three steps.

Make a source image.

%IMG7%magick ^
  %SRC% ^
  -fill Blue ^
  -draw "circle 100,-300,100,50" ^
  -draw "circle 150,500,150,150" ^
  s2l_src2.png
s2l_src2.pngjpg

Process and distort it.

call %PICTBAT%str2lines3 ^
  s2l_src2.png . s2l_src2_out.png
s2l_src2_out.pngjpg

Stretching vertically

The method shown above shrinks vertically. The distance between the blue lines will generally reduce. The script blue2cluts.bat has set environment variables:

echo %b2cNEWHH% %b2cMINGAP% %b2cMAXGAP% 
46 80 161 

b2cMINGAP is the smallest gap between the blue lines in the input image, and b2cMAXGAP is the maximum gap.

b2cNEWHH is the gap between the parallel lines in the output image.

So we could stretch the output image vertically by some factor between b2cMINGAP/b2cNEWHH and b2cMAXGAP/b2cNEWHH. The factor we choose will determine which part of the image will be overall unchanged in the vertical dimension. But shrinking an image then stretching it gives poor quality; stretching first gives better quality. We could stretch vertically then re-run the entire process.

Instead, we will re-run just step 3, stretching so the lines are %b2cMINGAP% pixels apart.

%IMG7%magick ^
  s2l_bl1.png ^
  s2l_lt1.miff ^
  -resize "x%%[fx:%b2cHH%*%b2cMINGAP%/%b2cNEWHH%]^!" ^
  -virtual-pixel None ^
  -compose Displace ^
    -set option:compose:args 0x100%% ^
    -composite ^
  s2l_d2.png
s2l_d2.pngjpg

Variations: shrink, stretch or mean

The method shown above shrinks the gap in each column between the blue lines to make them parallel and horizontal. The lowest part of the edge of the top blue line and the highest part of the edge of the bottom blue line do not move.

Two variations are provided:

call %PICTBAT%str2lines3 ^
  s2l_bl1.png . ^
  s2l_var1.png . shrink
s2l_var1.pngjpg
call %PICTBAT%str2lines3 ^
  s2l_bl1.png . ^
  s2l_var2.png . stretch
s2l_var2.pngjpg
call %PICTBAT%str2lines3 ^
  s2l_bl1.png . ^
  s2l_var3.png . mean
s2l_var3.pngjpg

In all methods, the output is the same size as the input.

Shrink is the only method that copies all of the input image into the output (provided tt>=0). The other two methods will generally truncate the input.

In principle, we could combine these methods, eg shrink for the top line and stretch for the bottom line, but the script has no facility for this.

Straightening vertical lines

The scripts shown here will straighten two lines that span the image width, and make them horizontal. If we want a similar action for vertical lines, first rotate the image by 90°, and rotate it back afterwards.

More than two lines

If we have more than two lines that we want to make straight and horizontal, these scripts won't do the job. The scripts work by creating a displacement map that varies in a linear manner from top to bottom. This linear gradient can be made to pass through any two points; it can't be made to pass through any three or more general points.

The ideas could be extended so the displacement curve was non-linear, such as a Bezier spline or polynomial.

Instead of moving to the inner edges of the blue lines, we might move to the average of the blue lines.

Two dimensions

If we want to make one pair of lines straight and horizontal, and make another pair of lines straight and vertical, we can run the process twice. The first process will distort the lines required for the second process.

The lines can be drawn in different channels such as blue for the horizontal lines and green for the vertical, so the intersections are cyan.

Intermediate results

We can make any intermediate result by blending the full displacement map with 50%. However, we can do the job with less work by blending pixels in just the two Nx1 maps with 50%. We use a blending parameter tt where 0<=tt<=1.

When tt=0, we have no effect: both Nx1 maps are entirely 50%, so the full interpolated and extrapolated map is also entirely 50%.

When tt=1, we have the full effect.

In the Nx1 images we change each value V to V' where:

V' = 0.5 + (V - 0.5) * tt
   = 0.5 + V*tt - 0.5*tt
   = V*tt + 0.5*(1-tt)

We can use -function Polynomial for this.

The script str2lines3.bat has a parameter for tt, but we show the individual steps. For example, we interpolate three-quarters towards the full effect:

Step 1 is run in the usual way:

call %PICTBAT%blue2cluts ^
  s2l_bl1.png s2l_blc_XX.miff . . s2l_34_
set tt=0.75

%IMG7%magick ^
  s2l_blc_1.miff ^
  -function Polynomial "%tt%,%%[fx:0.5*(1-%tt%)]" ^
  s2l_blct_1.miff

%IMG7%magick ^
  s2l_blc_2.miff ^
  -function Polynomial "%tt%,%%[fx:0.5*(1-%tt%)]" ^
  s2l_blct_2.miff

We then follow steps 2 and 3 as before, using these new Nx1 maps:

call %PICTBAT%interpClut ^
  s2l_blct_1.miff s2l_blct_2.miff ^
  %s2l_34_HH% ^
  s2l_lt1t.miff ^
  %s2l_34_XTRATOP% %s2l_34_XTRABOT%

%IMG7%magick s2l_lt1t.miff s2l_lt1t.png
s2l_lt1t.pngjpg
%IMG7%magick ^
  s2l_bl1.png ^
  s2l_lt1t.miff ^
  -virtual-pixel None ^
  -compose Displace ^
    -set option:compose:args 0x100%% ^
    -composite ^
  s2l_d1t.png
s2l_d1t.pngjpg

We can use t<0 or t>1 for extrapolated distortion:

call %PICTBAT%str2lines3 ^
  s2l_bl1.png . s2l_dex.png -0.1
s2l_dex.pngjpg
call %PICTBAT%str2lines3 ^
  s2l_bl1.png . s2l_dex2.png 1.4
s2l_dex2.pngjpg

Animation

We can animate by varying tt between 0.0 and 1.0, or any values we want. For convenience, we use %%I/100 where 0<=%%I<=100. Step one does not need to be repeated within the loop.

for /L %%I in (0,1,100) do (
  set /A LZ=%%I+1000
  set LZ=000000!LZ!
  set LZ=!LZ:~-6!
  echo LZ=!LZ!

  %IMG7%magick ^
    s2l_blc_1.miff ^
    -function Polynomial "%%[fx:%%I/100],%%[fx:0.5*(1-%%I/100)]" ^
    s2l_blca_1.miff

  %IMG7%magick ^
    s2l_blc_2.miff ^
    -function Polynomial "%%[fx:%%I/100],%%[fx:0.5*(1-%%I/100)]" ^
    s2l_blca_2.miff

  call %PICTBAT%interpClut ^
    s2l_blca_1.miff s2l_blca_2.miff ^
    %b2cHH% ^
    s2l_lt1a.miff ^
    %b2cXTRATOP% %b2cXTRABOT%

  %IMG7%magick ^
    s2l_bl1.png ^
    s2l_lt1a.miff ^
    -virtual-pixel None ^
    -compose Displace ^
      -set option:compose:args 0x100%% ^
      -composite ^
    s2l_frm_!LZ!.tiff
)

Make a GIF from the frames:

%IMG7%magick ^
  s2l_frm_*.tiff ^
  -duplicate 10 ^
  -set dispose previous ^
  s2l_frms.gif
s2l_frms.gif

In the course of the animation, the lowest part of the top blue line and the highest part of the bottom blue line do not move.

Cleanup: delete the frames.

del s2l_frm_*.tiff

Scripts

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

blue2cluts.bat

rem %1 is image with two non-intersecting aliased blue lines.
rem %2 is template name for the two output files. Must contain XX.
rem %3 channel for separation: R or G or B [default B].
rem %4 method shrink or stretch or mean [shrink]
rem %5 prefix for environment variables [b2c]
@rem
@rem Also uses:
@rem   b2cDEBUG name for debugging outputs. Must contain XX.
@rem

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

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 b2c

set OUTTMPLT=%2
if "%OUTTMPLT%"=="." set OUTTMPLT=
if "%OUTTMPLT%"=="" set OUTTMPLT=b2c_XX.miff

set OUT1=%OUTTMPLT:XX=1%
set OUT2=%OUTTMPLT:XX=2%

set chSEP=%3
if "%chSEP%"=="." set chSEP=
if "%chSEP%"=="" set chSEP=B

set ShrStr=%4
if "%ShrStr%"=="." set ShrStr=
if "%ShrStr%"=="" set ShrStr=shrink

set ENVPREF=%5
if "%ENVPREF%"=="." set ENVPREF=
if "%ENVPREF%"=="" set ENVPREF=b2c

if /I %ShrStr%==shrink (
  set MINMAX=maxima
) else if /I %ShrStr%==stretch (
  set MINMAX=minima
) else if /I %ShrStr%==mean (
  set MINMAX=mean
) else (
  echo %0: ShrStr is neither shrink nor stretch
  exit /B 1
)

if "%b2cDEBUG%"=="" (
  set WRDBG1=
  set WRDBG2=
) else (
  set WRDBG1=+write %b2cDEBUG:XX=1%
  set WRDBG2=+write %b2cDEBUG:XX=2%
)

for /F "usebackq" %%L in (`%IMG7%magick ^
  %INFILE% ^
  -format "HH=%%[h]\n" ^
  +write info: ^
  -define quantum:format^=floating-point ^
  -depth 32 ^
  -precision 16 ^
  -channel %chSEP% -separate +channel ^
  -fill Black +opaque White ^
  ^( -clone 0 ^
     -fill White -draw "color 0,0 floodfill" ^
     -fill Red -draw "color 0,0 floodfill" ^
     -fill Black -opaque White ^
     -fill White -draw "color 0,0 floodfill" ^
     +write mpr:WtTOP ^
     %WRDBG1% ^
     -scale "x1^!" ^
     -channel RGB ^
     -format "MAXTOP=%%[fx:%MINMAX%*100]\n" +write info: ^
     -evaluate Subtract %%[%MINMAX%] ^
     -function polynomial "1,0.5" ^
     +channel ^
     -write %OUT1% ^
     +delete ^
  ^) ^
  -fill White -draw "color 0,%%[fx:h-1] floodfill" ^
  -fill Red -draw "color 0,%%[fx:h-1] floodfill" ^
  -fill Black -opaque White ^
  -fill White -draw "color 0,%%[fx:h-1] floodfill" ^
  +write mpr:WtBOT ^
  %WRDBG2% ^
  -scale "x1^!" ^
  -channel RGB ^
  -format "MAXBOT=%%[fx:%MINMAX%*100]\n" +write info: ^
  -evaluate Subtract %%[%MINMAX%] ^
  -function polynomial "-1,0.5" ^
  +channel ^
  -write %OUT2% ^
  +delete ^
  mpr:WtTOP ^
  mpr:WtBOT ^
  -compose Lighten -composite ^
  -channel RGB -negate +channel ^
  -scale "x1^!" ^
  -format "MINGAP=%%[fx:minima]\n" +write info: ^
  -format "MAXGAP=%%[fx:maxima]\n" +write info: ^
  NULL:`) do set %%L

set sFMT=^
XTRATOP=%%[fx:0-100/(100-%MAXTOP%-%MAXBOT%)*%MAXTOP%]\n^
XTRABOT=%%[fx:100+100/(100-%MAXTOP%-%MAXBOT%)*%MAXBOT%]\n^
NEWHH=%%[fx:int(%HH%*(100-%MAXTOP%-%MAXBOT%)/100+0.5)]\n^
NEWY=%%[fx:int(%HH%*%MAXTOP%/100+0.5)]\n^
MINGAP=%%[fx:int(%HH%*%MINGAP%+0.5)]\n^
MAXGAP=%%[fx:int(%HH%*%MAXGAP%+0.5)]\n

for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -precision 16 ^
  -format "%sFMT%" ^
  xc:`) do set %%L

call echoRestore

@endlocal & set %ENVPREF%HH=%HH%& ^
set %ENVPREF%NEWHH=%NEWHH%& set %ENVPREF%NEWY=%NEWY%& ^
set %ENVPREF%MAXTOP=%MAXTOP%& set %ENVPREF%MAXBOT=%MAXBOT%& ^
set %ENVPREF%XTRATOP=%XTRATOP%& set %ENVPREF%XTRABOT=%XTRABOT%& ^
set %ENVPREF%MINGAP=%MINGAP%& set %ENVPREF%MAXGAP=%MAXGAP%

interpClut.bat

rem Given %1 and %2 are Nx1 images,
rem makes interpolated image Nx%3 pixels,
rem named %4.
rem %5 is percentage at top of %1 [0].
rem %6 is percentage at top of %1 [100].
rem
rem Assumes magick is HDRI.

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

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 ic

set IMG1=%INFILE%
set IMG2=%2

set HH=%3
if "%HH%"=="." set HH=
if "%HH%"=="" set HH=10

if not "%4"=="" set OUTFILE=%4

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

set PCBOT=%6
if "%PCBOT%"=="." set PCBOT=
if "%PCBOT%"=="" set PCBOT=100

rem IM v7.0.7-28 can't handle negatives in "gradient:".
rem See https://www.imagemagick.org/discourse-server/viewtopic.php?f=3&t=36160

if "%PCTOP:~0,1%"=="-" (

  set sFMT=^
POS=%%[fx:-^(%PCTOP%^)]\n^
PCBOT=%%[fx:%PCBOT%-^(%PCTOP%^)]

  for /F "usebackq" %%L in (`%IMG7%magick identify ^
    -precision 16 ^
    -format "!sFMT!" ^
    %IMG1%`) do set %%L

  set SUB=-evaluate subtract !POS!%%
  set PCTOP=0
) else (
  set SUB=
)

echo top=%PCTOP% bot=%PCBOT% sub=%SUB%

%IMG7%magick ^
  %IMG1% ^
  -set option:MYSIZE %%[fx:w]x%HH% ^
  ( +clone ^
    -scale "%%[MYSIZE]^!" ^
    -write mpr:IMG1 ^
    +delete ^
  ) ^
  %IMG2% ^
  -define compose:clamp=off ^
  -compose MinusDst -composite ^
  -scale "%%[MYSIZE]^!" ^
  -size "%%[MYSIZE]" ^
  ( gradient:gray(%PCTOP%%%)-gray(%PCBOT%%%) ^
    %SUB% ^
  ) ^
  -compose Multiply -composite ^
  mpr:IMG1 ^
  -compose Plus -composite ^
  -define quantum:format=floating-point ^
  -depth 32 ^
  %OUTFILE%

if ERRORLEVEL 1 exit /B 1

call echoRestore

@endlocal & set icOUTFILE=%OUTFILE%

str2lines3.bat

rem Straighten two lines: three steps combined.

rem %1 is image with two non-intersecting aliased blue lines.
rem %2 image to be distorted.
rem %3 is output.
rem %4 proportion of effect (typically 0.0 to 1.0) [1.0]
rem %5 shrink or stretch or mean
rem %6 prefix for environment variables [s2l3]

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

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 s2l3

set OUTTMPLT=b2c_XX.miff

set chSEP=B

set INDIST=%2
if "%INDIST%"=="." set INDIST=
if "%INDIST%"=="" set INDIST=%INFILE%

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

set FCTPROP=%4
if "%FCTPROP%"=="." set FCTPROP=
if "%FCTPROP%"=="1.0" set FCTPROP=1
if "%FCTPROP%"=="" set FCTPROP=1

set ShrStr=%5
if "%ShrStr%"=="." set ShrStr=
if "%ShrStr%"=="" set ShrStr=shrink

set ENVPREF=%6
if "%ENVPREF%"=="." set ENVPREF=
if "%ENVPREF%"=="" set ENVPREF=s2l3

set TMPDIR=\temp

call %PICTBAT%blue2cluts ^
  %INFILE% ^
  %TMPDIR%\s2l3_blc_XX.miff ^
  %chSEP% ^
  %ShrStr% ^
  %ENVPREF%

if ERRORLEVEL 1 exit /B 1

if not %FCTPROP%==1 (
  %IMG7%magick ^
    %TMPDIR%\s2l3_blc_1.miff ^
    -function Polynomial "%FCTPROP%,%%[fx:0.5*(1-%FCTPROP%)]" ^
    %TMPDIR%\s2l3_blc_1.miff

  %IMG7%magick ^
    %TMPDIR%\s2l3_blc_2.miff ^
    -function Polynomial "%FCTPROP%,%%[fx:0.5*(1-%FCTPROP%)]" ^
    %TMPDIR%\s2l3_blc_2.miff
)

call %PICTBAT%interpClut ^
  %TMPDIR%\s2l3_blc_1.miff %TMPDIR%\s2l3_blc_2.miff ^
  !%ENVPREF%HH! ^
  %TMPDIR%\s2l3_lt1.miff ^
  !%ENVPREF%XTRATOP! !%ENVPREF%XTRABOT!

if ERRORLEVEL 1 exit /B 1

%IMG7%magick ^
  %INFILE% ^
  %TMPDIR%\s2l3_lt1.miff ^
  -virtual-pixel None ^
  -compose Displace ^
    -set option:compose:args 0x100%% ^
    -composite ^
  %OUTFILE%

if ERRORLEVEL 1 exit /B 1


call echoRestore

@endlocal & set s2l3OUTFILE=%OUTFILE%

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

%IMG7%magick identify -version
Version: ImageMagick 7.0.7-28 Q16 x64 2018-03-25 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 HDRI Modules OpenMP 
Delegates (built-in): bzlib cairo flif freetype gslib heic jng jp2 jpeg lcms lqr 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 str2lines.h1. To re-create this web page, run "procH1 str2lines".


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 14-June-2019.

Page created 03-Jul-2019 22:44:54.

Copyright © 2019 Alan Gibson.