snibgo's ImageMagick pages

Direct polar distortion

Distortion using polar coordinates directly.

This page derives, describes, verifies and demonstrates a number of polar transformations.

This page is mostly about distorting between an ellipse and a shape. For the common requirement of distorting between the rectangular boundary of an image and a shape, see Radial distortions: Rect to shape.

See also the Shape centres page.

There is currently a bug in "-function polynomial" when used with OpenCL. See ImageMagick/issues/2404. Pending the bug fix, we disable OpenCL when running commands on this page.

set MAGICK_OCL_DEVICE=false

References

Types of distortion maps

A Cartesian map defines displacements in terms of x and y, which are recorded in the red and green channels of the map image. There are two types of Cartesian maps:

We can convert between absolute and relative maps by adding or subtracting an identity absolute Cartesian map.

A polar map defines displacements in terms of ρ and θ (rho and theta) with respect to a given centre and direction. These maps can also be relative or absolute. We can record ρ and θ in the red and green channels (called a rhoTheta encoding), but it can be beneficial to use three colour channels (a rhoSinCos encoding).

We can summarise the map types, and the conversions between them, in a diagram:

pdd_diag.png

Polar distortion maps

The usual IM distortion/displacement is Cartesian: the distortion map defines the X and Y coordinates to be used to lookup the colour to be used in the output image. Sometimes polar distortion is more suitable. Polar distortions shows how we can do this indirectly, by transforming polar images to Cartesian, distorting that, and converting back to polar. When that page was written, the "-fx" operation was slow.

As "-fx" performance has improved, it is sensible to do the polar distortion directly, using a polar distortion map.

As usual, these are pull maps: each pixel in the map stores coordinates in the source image from which the colour should be copied to this location.

The maps store polar coordinates ρ and θ (rho and theta). ρ represents a relative distance from a centre. ρ is 0.0 at the centre, increasing to 1.0 at the edge of the map. (As usual, "1.0" means "1.0 times QuantumRange".) The map edge is usually an ellipse, or part of an ellipse. ρ can be greater than 1.0, because the image may extend beyond the map edge. θ is a polar angle, between 0° and 360°. My usual convention is that 0° and 360° are at the top, in "north" position, where y is smallest (like a magnetic compass, or a clock), and θ increases clockwise (ditto). (Beware: some writers use different conventions, eg that 0° is in the east position, where y is 0.0.)

θ could be defined in one of two ways:

  1. θ = arctan2 (dx, dy).
  2. θ = arctan2 (dx, dy * ASP) where ASP is the aspect ratio of the ellipse, width/height. Under this definition, the angle between north and the nort-east corner of a rectangle is 45°, even when the rectangle is not square.

We will use the second definition.

We store ρ in the red channel, and θ in the green channel. The blue channel is irrelevant, but we will set it to zero. I call this a rhoTheta encoding. It has discontinuity at 0° and 360°, so it can't be supersampled or undergo a transformation that would mix pixels.

Instead, we can store:

I call this 3-channel encoding a rhoSinCos map. This map has no inherent discontinuity so can be rescaled etc without problems. We can recover θ from this map, if we need to. Trigonometric functions can be negative, so the processing needs to use HDRI, and files must be capable of storing negative numbers. We could scale and shift the trig functions so the output was never negative, but that would add layers of complexity. We generally want high precision for displacements, so 32-bit floats are already indicated.

The rhoSinCos encoding does the sin(θ) and -cos(θ) functions and multiplies each by ρ when the map is built, so this is not needed when applying the map. Hence if one map is to be used on many images, this will save on processing.

Relative polar distortion maps

An absolute map records, at each destination pixel, the polar coordinates of the source colour. A relative map records the the polar coordinates of the source colour relative to the coordinates of the destination pixel.

pdd_rel_diag.png

An absolute map Aa, and the corresponding relative map Ra, and the identity absolute map Ia, that are all encoded as rhoTheta, with the same sizes and same centres, are related by:

Ra = Aa - Ia 

Aa = Ra + Ia 

Arithmetic on θ, but not ρ, should be modulus 360°.

The identity relative polar displacement map Ir has ρ=0 and θ=0 for all pixels. This can be conveniently built as a black image.

Relative maps, like absolute maps, should be stored in floating-point formats.

Identity polar distortion map

We can set ρ=1.0 either at the centre of the edges or at the corners.

The script identEllipseAbsDispMap.bat creates an image that is an ellipsoidal polar displacement map.

Parameter Description
%1 WxH, width and height, with a "x" between them.
Default 150x100.
%2 Output filename.
Default: ieadm.png.
%3 Centre CXxCY.
Default (W-1)/2,(H-1)/2.
%4 Radius RXxRY, or one of: nearCorner, farCorner, nearEdge, farEdge.
Default: nearEdge.
%5 Map type: rhoTheta or rhoSinCos.
Default: rhoSinCos.

The default has the ellipse centre at the image centre, with the ellipse touching all four image edges ("nearEdge"), so ρ is 1.0 at the centre of each edge, and the data is stored in the "rhoSinCos" encoding.

Default identity polar displacement map.

call %PICTBAT%identEllipseAbsDispMap ^
  267x233 pdd_ident1.miff
pdd_ident1.miffpng

We show a PNG version of the map, so pixel values are clamped. We can show the range of values:

%IMG7%magick ^
  pdd_ident1.miff ^
  -format "min=%%[fx:minima] mean=%%[fx:mean] max=%%[fx:maxima] SD=%%[fx:standard_deviation]" ^
  info: 
min=-1 mean=0.256093 max=1.41421 SD=0.481787

We see that values exceed the nominal range of 0.0 to 1.0:

Distorting with maps

We distort an image with a map using a %[fx:...] expression. We put the expressions into environment variables for convenience. One expression is for rhoTheta encodings, and the other is for rhoSinTheta. They are both for absolute (not relative) maps.

set FxRhoTheta=^
  ANG = v.g * %%[fx:2*pi]; ^
  p { ^
      %%[fx:(u.w-1)/2] * (1 + sin(ANG) * v.r), ^
      %%[fx:(u.h-1)/2] * (1 - cos(ANG) * v.r)  ^
    }

set FxRhoSinCos=^
  p { ^
      %%[fx:(u.w-1)/2] * (1 + v.g), ^
      %%[fx:(u.h-1)/2] * (1 + v.b)  ^
    }

Expressions "2*pi" and "(u.w-1)/2" and "(u.h-1)/2" are constant within each image. By putting them in nested %[fx:...] expressions, they are evaluated only once per image, instead of once per pixel.

The first image, u, is the image to be distorted. The second image, v, is the distortion map.

"(u.w-1)/2" is the semi-width, in image (not pixel) units. The expressions do an absolute look-up p{X,Y} where:

We could further improve the performance of distortions by pre-calculating the semi-width and semi-height, so the rhoSinCos expression was simply p{v.g,v.b}, but that would reduce flexibility.

We verify that distorting an image with an identity map doesn't change it:

Distort toes.png with default identity map.

 %IMG7%magick ^
  toes.png ^
  pdd_ident1.miff ^
  -fx "%FxRhoSinCos%" ^
  -define quantum:format=floating-point -depth 32 ^
  pdd_ident_toes.miff
pdd_ident_toes.miffpng

How much has the image distorted?

%IMG7%magick compare -metric RMSE toes.png pdd_ident_toes.miff NULL: 
0.478341 (7.29902e-06)

There is practically no distortion.

We can instead use the simpler rhoTheta map. This map doesn't include the sin and cos calculations, so we need to do them when using the map.

Default identity polar displacement map, rhoTheta.

call %PICTBAT%identEllipseAbsDispMap ^
  267x233 pdd_ident_rt.miff . . rhoTheta
pdd_ident_rt.miffpng

Distort toes.png with identity map.

%IMG7%magick ^
  toes.png ^
  pdd_ident_rt.miff ^
  -fx "%FxRhoTheta%" ^
  -define quantum:format=floating-point -depth 32 ^
  pdd_ident_rt_toes.miff
pdd_ident_rt_toes.miffpng

How much has the image distorted?

%IMG7%magick compare -metric RMSE toes.png pdd_ident_rt_toes.miff NULL: 
0.478327 (7.2988e-06)

Again, there is practically no distortion.

Default identity polar displacement map, rhoSinCos2.

call %PICTBAT%identEllipseAbsDispMap ^
  267x233 pdd_ident_rsc2.miff . . rhoSinCos2
pdd_ident_rsc2.miffpng

Distort toes.png with identity map.

%IMG7%magick ^
  toes.png ^
  pdd_ident_rsc2.miff ^
  -fx "%FxRhoSinCos%" ^
  -define quantum:format=floating-point -depth 32 ^
  pdd_ident_rsc2_toes.miff
pdd_ident_rsc2_toes.miffpng

How much has the image distorted?

%IMG7%magick compare -metric RMSE toes.png pdd_ident_rsc2_toes.miff NULL: 
0.131456 (2.0059e-06)

Again, there is practically no distortion. The three methods have essentially the same, virtually zero, distortion. rhoSinCos2 involves no trig functions, so is likely to be the fastest.

We could also create the inverse of a polar displacement map, to convert from an arbitrary shape to an ellipse.

The identity map is built using an ellipsoidal gradient that is large enough that if the centre was in one corner of the image, the ellipse would pass through the opposite corner. The ellipse also has the same aspect ratio as the image. To make both these conditions true, the script sets the radii to (W*sqrt(2), H*sqrt(2)) for the elliptical gradient. (As we use image coordinates, these are actually ((W-1)*sqrt(2), (H-1)*sqrt(2)).) Then it divides the gradient by the value that it finds at the required location: the nearest edge, or whatever.

ASIDE: Constraining an ellipse to pass through a given point.

Where does ((W-1)*sqrt(2), (H-1)*sqrt(2)) come from?

The general equation for an ellipse centred at the origin is:

x2 + y2 = 1
a2   b2

... where a and b are the axes parallel to the x and y axes. a and b are used directly in:

-define gradient:radii=A,B

... where A and B are numbers. They cannot be %[fx:...] expressions.

For an ellipse to go through a corner point (w/2,h/2) what should a and b be?

An infinite number of ellipses will do this. We have a further condition: the aspect ratios of the ellipse and the image should be the same: a/b = w/h. So a = b*w/h.

So:

 (w/2)2   + (h/2)2 = 1
(b*w/h)2      b2 

Multiply by b2:

(w/2)2 + (h/2)2 = b2 
(w/h)2 

(1/2)2 + (h/2)2 = b2 
(1/h)2 

(h)2 + (h/2)2 = b2 
(2)2 

b2 = 2*(h/2)2 
b2 = h2/2 
b = h / sqrt(2) 

But:

a = b*w/h

So:

a = w / sqrt(2)

For the case where the origin is at (0,0) and we want the ellipse to pass through the opposite corner, we need to double these values, so:

a = w * sqrt(2)
b = h * sqrt(2)

QED.

Converting between rhoTheta and rhoSinCos

pdd_tsc_diag.png

We define two transformations as environment variables that can be used in IM commands.

set rhoTheta2rhoSinCos= ^
  -channel B -fx "-cos (g * %%[fx:2*pi] ) * r" ^
  -channel G -fx "sin (g * %%[fx:2*pi] ) * r" ^
  +channel 

set rhoSinCos2rhoTheta= ^
  -channel G -fx "0.5 - atan2(g,b) / %%[fx:2*pi]" ^
  -channel B -evaluate set 0 ^
  +channel

Below, we will verify the encoding round-trip.

Converting between polar and Cartesian maps

We can convert between a rhoSinCos polar map and the usual Cartesian map. The polar map may contain values greater than 100%, so file formats should be floating-point.

pdd_cart_diag.png

We want conversions to make equivalent maps, so an image distorted by a polar map gives the same result as the image distorted by the equivalent Cartesian map.

We convert with these formulae, which are derived from the -fx expressions in Distorting with maps:

Cx = (1 + Ps) / 2
Cy = (1 + Pc) / 2

Hence:

Ps = 2 * Cx - 1
Pc = 2 * Cy - 1
ρ = hypot (Ps, Pc)

... where:

set rhoSinCos2Cartesian=^
  -function Polynomial 0.5,0.5 ^
  -channel-fx "green=>red blue=>green" ^
  -channel B -evaluate set 0 ^
  +channel

set Cartesian2rhoSinCos=^
  -channel-fx "green=>blue red=>green" ^
  -channel GB -function Polynomial 2,-1 ^
  -channel R -fx "hypot(g,b)" ^
  +channel

Below, we will verify the polar to Cartesian round-trip.

Application: distorting from ellipse to a shape

Generate an arbitrary curved shape using the acwise program.

(
  echo 50,30
  echo 50,100
  echo 250,110
  echo 75,175
  echo 150,125
) | %IM7DEV%acwise ^
  -i - -o - ^
  --startat N -fmt curve | %IMG7%magick ^
  -size 267x233 xc:Black ^
  -fill White -stroke None ^
  -draw @- ^
  -alpha off ^
  pdd_wonb.png
pdd_wonb.png

Generate a gradient.

call %PICTBAT%shp2grad ^
  pdd_wonb.png ^
  pdd_wonb_grad.miff

echo shp2gCX=%shp2gCX% shp2gCY=%shp2gCY% 
shp2gCX=101 shp2gCY=94 

The gradient is black at the centre, white at the shape's boundary, and lighter than white outside the boundary.

We also show the location of the centre.

%IMG7%magick ^
  pdd_wonb_grad.miff ^
  -fill Red ^
  -draw ^
    "translate +%shp2gCX%+%shp2gCY% circle 0,0,5,0" ^
  pdd_wonb_grad.png
pdd_wonb_grad.png

Generate a sweep, using the same centre.

call %PICTBAT%ellipseSweepSinCos ^
  267x233 ^
  pdd_wonb_swp.miff ^
  %shp2gCX%x%shp2gCY%
pdd_wonb_swp.miffpng

Combine the gradient and sweep to make a rhoSinCos map.

 %IMG7%magick ^
    -define compose:clamp=off ^
    ( pdd_wonb_grad.miff -write mpr:RHO ) ^
    pdd_wonb_swp.miff ^
    -compose Multiply -composite ^
    -separate -delete 2 ^
    mpr:RHO -insert 0 ^
    -colorspace Gray ^
    -combine ^
    -define quantum:format=floating-point -depth 32 ^
    -set polarCX %shp2gCX% ^
    -set polarCY %shp2gCY% ^
    pdd_wonb_map.miff
pdd_wonb_map.miffpng

Distort with the rhoSinCos map.

%IMG7%magick ^
  toes.png ^
  pdd_wonb_map.miff ^
  -virtual-pixel None ^
  -fx "%FxRhoSinCos%" ^
  pdd_wonb_toes.png
pdd_wonb_toes.png

To help with understanding, create an image of the edge of the shape.

%IMG7%magick ^
  pdd_wonb.png ^
  -edge 1 -alpha off -clamp ^
  -alpha shape ^
  -channel A ^
  -evaluate Multiply 0.5 ^
  +channel ^
  pdd_edge.png
pdd_edge.png

Composite the shape edge over the distortion.

%IMG7%magick ^
  pdd_wonb_toes.png ^
  pdd_edge.png ^
  -compose Over -composite ^
  pdd_wonb_comp.png

The ellipse of the toes image has been transformed to the shape boundary.
The corners of the toes image that were outside the ellipse
are present in this image, outside the shape boundary.

pdd_wonb_comp.png

Application: distorting from a shape to ellipse

From shape to ellipse: convert to Cartesian, invert that, fill the holes, convert to polar. Use that as map for distortion.

Convert the map to Cartesian.

%IMG7%magick ^
  pdd_wonb_map.miff ^
  -virtual-pixel None ^
  %rhoSinCos2Cartesian% ^
  pdd_conv_cart.miff
pdd_conv_cart.miffpng

Invert the Cartesian map, and fill the holes.

See Process modules: invert displacement map.

Filling holes could be done with relaxFill.bat, but instead we use seamless-blend.

%IM7DEV%magick ^
  pdd_conv_cart.miff ^
  -process 'invdispmap' ^
  ( +clone -alpha extract -negate +write mpr:ALP +delete ) ^
  ( -clone 0 -alpha off -fill Black -colorize 100 ) ^
  mpr:ALP ^
  -verbose ^
  -define compose:args=10000x1e-7+1000 ^
  -compose seamless_blend -composite ^
  +verbose ^
  -alpha off ^
  pdd_conv_cart_inv.miff
pdd_conv_cart_inv.miffpng

Convert the inverted Cartesian map to polar.

%IMG7%magick ^
  pdd_conv_cart_inv.miff ^
  %Cartesian2rhoSinCos% ^
  pdd_wonb_map_inv.miff
pdd_wonb_map_inv.miffpng

Distort the distorted toes with the inverted polar map.

%IMG7%magick ^
  pdd_wonb_toes.png ^
  pdd_wonb_map_inv.miff ^
  -fx "%FxRhoSinCos%" ^
  pdd_wmi_toes.miff

if ERRORLEVEL 1 exit /B 1

%IMG7%magick compare ^
  -metric RMSE ^
  toes.png pdd_wmi_toes.miff ^
  NULL: 
1295.7 (0.0197711)
pdd_wmi_toes.miffpng

The round-trip, distorting the toes image to the shape then distorting back, is imperfect because the initial distortion has shrunk parts of the image by a linear factor around four. Shrinking an image loses detail that cannot be recovered by enlarging.

For convenience, we could write a script that takes two inputs, an image and a white-on-black mask that defines a shape, and makes an output that distorts the image so what was on the shape boundary becomes an ellipse.

Check that:

Distort a displacement map with its own inverse, and compare to identity map.

%IMG7%magick ^
  pdd_wonb_map.miff ^
  pdd_wonb_map_inv.miff ^
  -fx "%FxRhoSinCos%" ^
  -define quantum:format=floating-point -depth 32 ^
  pdd_disp_own_inv.miff

if ERRORLEVEL 1 exit /B 1

%IMG7%magick compare ^
  -metric RMSE ^
  pdd_ident1.miff pdd_disp_own_inv.miff ^
  NULL: 
117.608 (0.00179459)
pdd_disp_own_inv.miffpng

Yes, this is fairly accurate.

Verify the encoding round-trip

Above, we showed environment variables for converting between rhoTheta and rhoSinCos. We can use those environment variables like this:

Convert rhoSinCos to rhoTheta:

%IMG7%magick ^
  pdd_wonb_map.miff ^
  %rhoSinCos2rhoTheta% ^
  -define quantum:format=floating-point -depth 32 ^
  pdd_rt.miff

Convert that back to rhoSinCos:

%IMG7%magick ^
  pdd_rt.miff ^
  %rhoTheta2rhoSinCos% ^
  -define quantum:format=floating-point -depth 32 ^
  pdd_rsc.miff

Check the round-trip:

%IMG7%magick compare -metric RMSE pdd_wonb_map.miff pdd_rsc.miff NULL: 
0.589541 (8.99582e-06)

The round-trip between rhoTheta and rhoSinCos is accurate.

Verify conversion between polar and Cartesian maps

From a polar rhoSinCos map, make an absolute Cartesian map.

set SRC_POL_MAP=pdd_wonb_map.miff

%IMG7%magick ^
  %SRC_POL_MAP% ^
  %rhoSinCos2Cartesian% ^
  pdd_p_cart_map.miff
pdd_p_cart_map.miffpng

Distort an image with the absolute Cartesian map. Compare with distortion made with polar map.

%IMG7%magick ^
  toes.png ^
  pdd_p_cart_map.miff ^
  -virtual-pixel None ^
  -compose Distort -composite ^
  -define quantum:format=floating-point -depth 32 ^
  pdd_pcm_toes.miff

%IMG7%magick ^
  compare -metric RMSE ^
  pdd_wonb_toes.png ^
  pdd_pcm_toes.miff NULL: 
10.8056 (0.000164882)
pdd_pcm_toes.miffpng

From the Cartesian map, make a rhoSinCos polar map. Compare this new polar map with the original rhoSinCos map.

%IMG7%magick ^
  pdd_p_cart_map.miff ^
  %Cartesian2rhoSinCos% ^
  pdd_p_cart_pol_map.miff

%IMG7%magick ^
  compare -metric RMSE ^
  pdd_wonb_map.miff ^
  pdd_p_cart_pol_map.miff NULL: 
0.570506 (8.70536e-06)

The round-trip from polar to Cartesian and back to polar is virtually exact.

pdd_p_cart_pol_map.miffpng

Distort the image with new polar map. Compare with distortion made with original polar map.

%IMG7%magick ^
  toes.png ^
  pdd_p_cart_pol_map.miff ^
  -virtual-pixel None ^
  -fx "%FxRhoSinCos%" ^
  -define quantum:format=floating-point -depth 32 ^
  pdd_pcpm_toes.miff

%IMG7%magick ^
  compare -metric RMSE ^
  pdd_wonb_toes.png ^
  pdd_pcpm_toes.miff NULL: 
0.146428 (2.23435e-06)

The polar maps are virtually identical, so the distortions from each are also virtually identical.

pdd_pcpm_toes.miffpng

Manipulate the distortion map

If we want to manipulate the distortion, we could start with a map encoded as rhoTheta. But for this example we will convert the rhoSinCos encoding to rhoTheta, manipulate that, and distort with that map. (An alternative would be to convert the map back to rhoSinCos and distort with that.)

We will transform ρ with a power function that decreases values beween 0.0 and 1.0, with greatest effect near 0.0, so colours are not pulled so far towards the centre. (Colours outside the shape boundary will not be pulled out as far.) We also transform theta. A power curve would create second-order discontinuity because the slope of a power curve at 0.0 and 1.0 is different.

%IMG7%magick ^
  pdd_wonb_map.miff ^
  %rhoSinCos2rhoTheta% ^
  -channel 0 -evaluate Pow 1.1 ^
  -channel 1 -sigmoidal-contrast 2,75%% ^
  +channel ^
  toes.png ^
  +swap ^
  -virtual-pixel None ^
  -fx "%FxRhoTheta%" ^
  pdd_edge.png ^
  -compose Over -composite ^
  -define quantum:format=floating-point -depth 32 ^
  pdd_dist_2.miff
pdd_dist_2.miffpng

For another variation, a sin function duplicates the inside of the boundary on the outside. As above, we composite the result with the edge image.

%IMG7%magick ^
  pdd_wonb_map.miff ^
  %rhoSinCos2rhoTheta% ^
  -channel 0 -fx "sin(u*%%[fx:pi/2])" ^
  +channel ^
  toes.png ^
  +swap ^
  -virtual-pixel None ^
  -fx "%FxRhoTheta%" ^
  pdd_edge.png ^
  -compose Over -composite ^
  -define quantum:format=floating-point -depth 32 ^
  pdd_dist_3.miff
pdd_dist_3.miffpng

The duplication repeats at increasing distances from the centre.

Animation

For each frame, we can blend the identity and full-effect polar distortion maps.

setlocal enabledelayedexpansion

call %PICTBAT%PolDistVars

del %TEMP%\anim_*.tiff 2>nul

for /L %%I in (0,2,99) do (
  set LZ=000000%%I
  set LZ=!LZ:~-6!

  %IMG7%magick ^
    C:\prose\pictures\pdd_ident1.miff C:\prose\pictures\pdd_wonb_map.miff ^
    -virtual-pixel None ^
    -define compose:clamp=off ^
    -compose blend -define compose:args=%%I -composite ^
    -compose Over ^
    toes.png ^
    +swap ^
    -fx "%FxRhoSinCos%" ^
    -background Black -layers Flatten ^
    %TEMP%\anim_!LZ!.tiff
)

%IMG7%magick ^
  -loop 0 ^
  %TEMP%\anim_*.tiff ^
  ( -clone 0--1 -reverse ) ^
  -layers optimize ^
  pdd_anim.gif
pdd_anim.gif

Environment variables

This page has shown a number of environment variables that can be used for conversions. For convenience, we collect them together into a script, PolDistVars.bat.

(
  echo rem Set environment variables for polar distortions.
  echo rem This file is automatically generated when building the page poldistdir.htm.
  echo rem
) >%PICTBAT%PolDistVars.bat

cgrep ^
  /p0 /ipoldistdir.h1 /o- /s"\(code\)" /t"\(/code\)" /X | chBin ^
  /p0 /i- /o- /f"^\n" /t"^" |cGrep ^
  /p0 /i- /o- /s"set *" |chBin ^
  /p0 /i- /o- /f\n /t\r\n\r\n |chStrs ^
  /p0 /i- /o- /sltgt.dat |cGrep ^
  /p0 /i- /o- /sMAGICK_OCL_DEVICE /x |cGrep ^
  /p0 /i- /o- /sSRC_POL_MAP /x |chBin ^
  /p0 /i- /O%PICTBAT%PolDistVars.bat /f"^" /t"^\r\n"

Future

For a full polar distortion from shape to shape, we need corresponding points on the boundaries of two shapes. If we have a list of coords for input and output, this is a list of (rho,theta) for each. So we have a list of pairs of (fromTheta,toTheta). We can interpolate (eg relax-fill) these to give a full horizontal displacement map in the polar domain.

For that, we need a process that finds corresponding points in the boundaries of two masks. Perhaps corresponding peaks and troughs in the unrolled masks?

Scripts

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

ellipseGrid.bat

rem Make elliptical (or circular) grid.
rem %1,%2 are width and height.
rem %3 is output file (default ellgrid.png)
rem %4 is number of ellipses, >=0 (default 4)
rem %5 is number of radii (default 8)
rem %6 is grid colour (default white)
rem %7 is background colour, can be "none" (default black)
@rem
@rem Also uses:
@rem   gridSTROKE_WIDTH default 1

@call echoOffSave

setlocal enabledelayedexpansion

set WW=%1
if "%WW%"=="." set WW=
if "%WW%"=="" set WW=150

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

set OUTFILE=%3
if "%OUTFILE%"=="." set OUTFILE=
if "%OUTFILE%"=="" set OUTFILE=ellgrid.png

set nELLIPSES=%4
if "%nELLIPSES%"=="." set nELLIPSES=
if "%nELLIPSES%"=="" set nELLIPSES=4

set nRADII=%5
if "%nRADII%"=="." set nRADII=
if "%nRADII%"=="" set nRADII=8

set GRID_COL=%6
if "%GRID_COL%"=="." set GRID_COL=
if "%GRID_COL%"=="" set GRID_COL=White

set BACK_COL=%7
if "%BACK_COL%"=="." set BACK_COL=
if "%BACK_COL%"=="" set BACK_COL=Black

if "%gridSTROKE_WIDTH%"=="" set gridSTROKE_WIDTH=0

if %gridSTROKE_WIDTH%==1 (
  set sSTROKEW=
) else (
  set sSTROKEW=-strokewidth %gridSTROKE_WIDTH%
)

for /F "usebackq" %%L in (`%IMG7%magick ^
  xc: ^
  -format "CX=%%[fx:(%WW%-1)/2]\nCY=%%[fx:(%HH%-1)/2]\nWFRAC=%%[fx:%WW%/2/%nELLIPSES%]\nHFRAC=%%[fx:%HH%/2/%nELLIPSES%]\nAngFRAC=%%[fx:2*PI/%nRADII%]\n" ^
  info:`) do set %%L

set sDraw=

for /L %%I in (%nELLIPSES%,-1,1) do (
  set sDraw=!sDraw! ellipse %CX%,%CY%,%%[fx:%WFRAC%*%%I],%%[fx:%HFRAC%*%%I],0,360
)

for /L %%I in (0,1,%nRADII%) do (
  set sDraw=!sDraw! line "%CX%,%CY%,%%[fx:%CX%*(1+sin(%AngFrac%*%%I))],%%[fx:%CY%*(1-cos(%AngFrac%*%%I))]"
)

rem echo sDraw=%sDraw%


%IMG7%magick ^
  -size %WW%x%HH% xc:%BACK_COL% ^
  -fill None -stroke %GRID_COL% ^
  %sSTROKEW% ^
  -draw "%sDraw%" ^
  %OUTFILE%

identEllipseAbsDispMap.bat

rem Makes identity ellipse absolute displacement map.

rem %1 WxH
rem %2 output file. This can be negative, or greater than 100%.
rem %3 CXxCY centre. Default (W-1)/2,(H-1)/2
rem %4 radius. "nearCorner" or "farCorner" or "meanCorner" or "nearEdge" or "farEdge" or "meanEdge" or RXxRY
rem %5 encoding: rhoTheta or rhoSinCos
@rem
@rem Makes a radial gradient with large radius, and divides appropriately.

@rem Option for sin and cos.

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 ieadm

set sWH=%1
if "%sWH%"=="." set sWH=
if "%sWH%"=="" set sWH=150x100

set OUTFILE=%2
if "%OUTFILE%"=="." set OUTFILE=
if "%OUTFILE%"=="" set OUTFILE=ieadm.png

set sCent=%3
if "%sCent%"=="." set sCent=
if "%sCent%"=="" set sCent=50cx50c

set sRad=%4
if "%sRad%"=="." set sRad=
if "%sRad%"=="" set sRad=meanEdge

set mapType=%5
if "%mapType%"=="." set mapType=
if "%mapType%"=="" set mapType=rhoSinCos

call parseXxY2 150 100 WH %sWH%

set WW=%WH_X%
set HH=%WH_Y%

set TMP_FILE1=%TEMP%\iead_tmp1.miff
set TMP_FILE2=%TEMP%\iead_tmp2.miff

call %PICTBAT%ellipseGrad %sWH% %TMP_FILE1% %sCent% %sRad%
if ERRORLEVEL 1 exit /B 1

if /I %mapType%==rhoTheta (
  call %PICTBAT%ellipseSweep %egWW%x%egHH% %TMP_FILE2% %egCX%x%egCY%
  if ERRORLEVEL 1 exit /B 1

  %IMG7%magick ^
    %TMP_FILE1% ^
    %TMP_FILE2% ^
    ^( +clone -fill Black -colorize 100 ^) ^
    -combine ^
    -set polarCX %egCX% ^
    -set polarCY %egCY% ^
    -define quantum:format=floating-point -depth 32 ^
    %OUTFILE%

) else if /I %mapType%==rhoSinCos (
  call %PICTBAT%ellipseSweep %egWW%x%egHH% %TMP_FILE2% %egCX%x%egCY%
  if ERRORLEVEL 1 exit /B 1

  %IMG7%magick ^
    ^( %TMP_FILE1% -write mpr:RHO ^) ^
    ^( %TMP_FILE2% -write mpr:THETA ^) ^
    -fx "TWOPI=%%[fx:2*pi]; u*sin(v*TWOPI)" ^
    ^( mpr:RHO mpr:THETA ^
       -fx "TWOPI=%%[fx:2*pi]; -u*cos(v*TWOPI)" ^
    ^) ^
    mpr:RHO -insert 0 ^
    -colorspace Gray ^
    -combine ^
    -set polarCX %egCX% ^
    -set polarCY %egCY% ^
    -define quantum:format=floating-point -depth 32 ^
    %OUTFILE%

) else if /I %mapType%==rhoSinCos2 (
  call %PICTBAT%ellipseSweepSinCos %egWW%x%egHH% %TMP_FILE2% %egCX%x%egCY%
  if ERRORLEVEL 1 exit /B 1

  %IMG7%magick ^
    -define compose:clamp=off ^
    ^( %TMP_FILE1% -write mpr:RHO ^) ^
    %TMP_FILE2% ^
    -compose Multiply -composite ^
    -separate -delete 2 ^
    mpr:RHO -insert 0 ^
    -colorspace Gray ^
    -combine ^
    -set polarCX %egCX% ^
    -set polarCY %egCY% ^
    -define quantum:format=floating-point -depth 32 ^
    %OUTFILE%

) else (
  echo %0: Bad mapType [%mapType%]
  exit /B 1
)

if ERRORLEVEL 1 exit /B 1

call echoRestore

@endlocal & set ieadmOUTFILE=%OUTFILE%& set ieadmWW=%egWW%& set ieadmHH=%egHH%& set ieadmCX=%egCX%& set ieadmCY=%egCY%

ellipseGrad.bat

rem Makes ellipsoidal gradient.

rem %1 WxH
rem %2 output file: grayscale, can be greater than 100%.
rem %3 CXxCY centre. Default (W-1)/2,(H-1)/2
rem %4 radius. "nearCorner" or "farCorner" or "meanCorner" or "nearEdge" or "farEdge" or "meanEdge" or RXxRY
@rem
@rem  Maybe option to do fan composite with gradient from the edge.
@rem
@rem Makes a radial gradient with large radius, and divides appropriately.


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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 eg

set sWH=%1
if "%sWH%"=="." set sWH=
if "%sWH%"=="" set sWH=150x100

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

set sCent=%3
if "%sCent%"=="." set sCent=
if "%sCent%"=="" set sCent=50cx50c

set sRad=%4
if "%sRad%"=="." set sRad=
if "%sRad%"=="" set sRad=50cx50c

call parseXxY2 150 100 WH %sWH%

set WW=%WH_X%
set HH=%WH_Y%

set /A Wm1=%WH_X%-1
set /A Hm1=%WH_Y%-1

call parseXxY2 %Wm1% %Hm1% CENT %sCent%

@rem "nearEdge" means the darkest pixel that has same X or Y ordinate as the centre.
@rem "farEdge" means the lightest pixel that has same X or Y ordinate as the centre.
@rem Similarly corners.

for /F "usebackq" %%L in (`%IMG7%magick xc: ^
  -format "RAD_X=%%[fx:%Wm1%*sqrt(2)]\nRAD_Y=%%[fx:%Hm1%*sqrt(2)]\n" ^
  info:`) do set %%L

if /I %sRad%==nearCorner (
  set sADJ=^
    ^( +clone ^
      ^( -clone 0 -crop 1x1+0+0 ^) ^
      ^( -clone 0 -crop 1x1+%Wm1%+0 ^) ^
      ^( -clone 0 -crop 1x1+0+%Hm1% ^) ^
      ^( -clone 0 -crop 1x1+%Wm1%+%Hm1% ^) ^
      -delete 0 ^
      -evaluate-sequence Min ^
      -scale "%WW%x%HH%^^!" ^
    ^) ^
    -compose DivideSrc -composite 
) else if /I %sRad%==farCorner (
  set sADJ=^
    ^( +clone ^
      ^( -clone 0 -crop 1x1+0+0 ^) ^
      ^( -clone 0 -crop 1x1+%Wm1%+0 ^) ^
      ^( -clone 0 -crop 1x1+0+%Hm1% ^) ^
      ^( -clone 0 -crop 1x1+%Wm1%+%Hm1% ^) ^
      -delete 0 ^
      -evaluate-sequence Max ^
      -scale "%WW%x%HH%^^!" ^
    ^) ^
    -compose DivideSrc -composite 
) else if /I %sRad%==meanCorner (
  set sADJ=^
    ^( +clone ^
      ^( -clone 0 -crop 1x1+0+0 ^) ^
      ^( -clone 0 -crop 1x1+%Wm1%+0 ^) ^
      ^( -clone 0 -crop 1x1+0+%Hm1% ^) ^
      ^( -clone 0 -crop 1x1+%Wm1%+%Hm1% ^) ^
      -delete 0 ^
      -evaluate-sequence Mean ^
      -scale "%WW%x%HH%^^!" ^
    ^) ^
    -compose DivideSrc -composite 
) else if /I %sRad%==nearEdge (
  set sADJ=^
    ^( +clone ^
      ^( -clone 0 -crop 1x1+%CENT_X%+0 ^) ^
      ^( -clone 0 -crop 1x1+0+%CENT_Y% ^) ^
      ^( -clone 0 -crop 1x1+%CENT_X%+%Hm1% ^) ^
      ^( -clone 0 -crop 1x1+%Wm1%+%CENT_Y% ^) ^
      -delete 0 ^
      -evaluate-sequence Min ^
      -scale "%WW%x%HH%^^!" ^
    ^) ^
    -compose DivideSrc -composite 
) else if /I %sRad%==farEdge (
  set sADJ=^
    ^( +clone ^
      ^( -clone 0 -crop 1x1+%CENT_X%+0 ^) ^
      ^( -clone 0 -crop 1x1+0+%CENT_Y% ^) ^
      ^( -clone 0 -crop 1x1+%CENT_X%+%Hm1% ^) ^
      ^( -clone 0 -crop 1x1+%Wm1%+%CENT_Y% ^) ^
      -delete 0 ^
      -evaluate-sequence Max ^
      -scale "%WW%x%HH%^^!" ^
    ^) ^
    -compose DivideSrc -composite 
) else if /I %sRad%==meanEdge (
  set sADJ=^
    ^( +clone ^
      ^( -clone 0 -crop 1x1+%CENT_X%+0 ^) ^
      ^( -clone 0 -crop 1x1+0+%CENT_Y% ^) ^
      ^( -clone 0 -crop 1x1+%CENT_X%+%Hm1% ^) ^
      ^( -clone 0 -crop 1x1+%Wm1%+%CENT_Y% ^) ^
      -delete 0 ^
      -evaluate-sequence Mean ^
      -scale "%WW%x%HH%^^!" ^
    ^) ^
    -compose DivideSrc -composite 
) else (
  set sADJ=
  call parseXxY2 %Wm1% %Hm1% RAD %sRad%
)

%IMG7%magick ^
  -size %WW%x%HH% ^
  -define gradient:center=%CENT_X%,%CENT_Y% ^
  -define gradient:radii=%RAD_X%,%RAD_Y% ^
  radial-gradient:black-white ^
  -define compose:clamp=off ^
  %sADJ% ^
  -define quantum:format=floating-point -depth 32 ^
  %OUTFILE%

if ERRORLEVEL 1 exit /B 1

call echoRestore

@endlocal & set egOUTFILE=%OUTFILE%& set egWW=%WW%& set egHH=%HH%& set egCX=%CENT_X%& set egCY=%CENT_Y%

ellipseSweep.bat

rem Sweeps an ellipse with same aspect ratio as image, creating a gradient from north increasing clockwise.
rem %1  WxH: width and height
rem %2  output file
rem %3  centre XxY, each possibly suffixed
@rem
@rem Similar to circGrad.bat

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 cg

set sWH=%1
if "%sWH%"=="." set sWH=
if "%sWH%"=="" set sWH=150x100

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

set sCent=%3
if "%sCent%"=="." set sCent=
if "%sCent%"=="" set sCent=50cx50c

call parseXxY2 150 100 WH %sWH%

call parseXxY2 %WH_X%-1 %WH_Y%-1 cent %sCent%

%IMG7%magick ^
  -size %WH_X%x%WH_Y% xc: ^
  -colorspace Gray ^
  -fx "TWOPI=%%[fx:2*pi]; ASP=%%[fx:(w-1)/(h-1)]; atan2 (%cent_X%-i,(j-%cent_Y%)*ASP) / TWOPI + 0.5" ^
  -colorspace Gray ^
  -define quantum:format=floating-point -depth 32 ^
  %OUTFILE%

call echoRestore

endlocal & set esCX=%cent_X%& set esCY=%cent_Y%

ellipseSweepSinCos.bat

rem Sweeps an ellipse with same aspect ratio as image.
rem   Stores sin(theta) in the red channel and -cos(theta) in the green channel.
rem %1  WxH: width and height
rem %2  output file
rem %3  centre XxY, each possibly suffixed
@rem
@rem Similar to ellipseSweep.bat and circGrad.bat

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 cg

set sWH=%1
if "%sWH%"=="." set sWH=
if "%sWH%"=="" set sWH=150x100

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

set sCent=%3
if "%sCent%"=="." set sCent=
if "%sCent%"=="" set sCent=50cx50c

call parseXxY2 150 100 WH %sWH%

call parseXxY2 %WH_X%-1 %WH_Y%-1 cent %sCent%

rem We go directly to sin and cos: sin=dx/hypot(dx,dy) etc.

%IMG7%magick ^
  -size %WH_X%x%WH_Y% xc:Black ^
  -channel R ^
  -fx "ASP=%%[fx:(w-1)/(h-1)]; dx = i-%cent_X%; dy = (j-%cent_Y%)*ASP; dx / hypot(dx,dy)" ^
  -channel G ^
  -fx "ASP=%%[fx:(w-1)/(h-1)]; dx = i-%cent_X%; dy = (j-%cent_Y%)*ASP; dy / hypot(dx,dy)" ^
  +channel ^
  -define quantum:format=floating-point -depth 32 ^
  %OUTFILE%

call echoRestore

endlocal & set esCX=%cent_X%& set esCY=%cent_Y%

PolDistVars.bat

rem Set environment variables for polar distortions.
rem This file is automatically generated when building the page poldistdir.htm.
rem

set FxRhoTheta=^
  ANG = v.g * %%[fx:2*pi]; ^
  p { ^
      %%[fx:(u.w-1)/2] * (1 + sin(ANG) * v.r), ^
      %%[fx:(u.h-1)/2] * (1 - cos(ANG) * v.r)  ^
    }

set FxRhoSinCos=^
  p { ^
      %%[fx:(u.w-1)/2] * (1 + v.g), ^
      %%[fx:(u.h-1)/2] * (1 + v.b)  ^
    }

set rhoTheta2rhoSinCos= ^
  -channel B -fx "-cos (g * %%[fx:2*pi] ) * r" ^
  -channel G -fx "sin (g * %%[fx:2*pi] ) * r" ^
  +channel 

set rhoSinCos2rhoTheta= ^
  -channel G -fx "0.5 - atan2(g,b) / %%[fx:2*pi]" ^
  -channel B -evaluate set 0 ^
  +channel

set rhoSinCos2Cartesian=^
  -function Polynomial 0.5,0.5 ^
  -channel-fx "green=>red blue=>green" ^
  -channel B -evaluate set 0 ^
  +channel

set Cartesian2rhoSinCos=^
  -channel-fx "green=>blue red=>green" ^
  -channel GB -function Polynomial 2,-1 ^
  -channel R -fx "hypot(g,b)" ^
  +channel


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

%IMG7%magick -version
Version: ImageMagick 7.1.1-20 Q16-HDRI x86 98bb1d4:20231008 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenCL OpenMP(2.0) 
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 (193532217)
%IM7DEV%magick -version
Version: ImageMagick 7.1.1-20 (Beta) Q32-HDRI x86_64 66c30fc22:20231002 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenCL OpenMP(4.5) 
Delegates (built-in): bzlib cairo fftw fontconfig freetype heic jbig jng jpeg lcms ltdl lzma pangocairo png raqm raw rsvg tiff webp wmf x xml zip zlib
Compiler: gcc (11.3)

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 poldistdir.h1. To re-create this web page, run "procH1 poldistdir".


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 18-September-2023.

Page created 02-Nov-2023 13:15:56.

Copyright © 2023 Alan Gibson.