Distortion

ImageMagick provides several ways to distort an image by applying various transformations against user-supplied arguments. In Wand, the method Image.distort is used, and follows a basic function signature of:

with Image(...) as img:
    img.distort(method, arguments)

Where method is a string provided by DISTORTION_METHODS, and arguments is a list of doubles. Each method parses the arguments list differently. For example:

# Arc can have a minimum of 1 argument
img.distort('arc', (degree, ))
# Affine 3-point will require 6 arguments
points = (x1, y1, x2, y2,
          x3, y3, x4, y4,
          x5, y5, x6, y6)
img.distort('affine', points)

A more complete & detailed overview on distortion can be found in Distorting Images usage article by Anthony Thyssen.

Controlling Resulting Images

Virtual Pixels

When performing distortion on raster images, the resulting image often includes pixels that are outside original bounding raster. These regions are referred to as vertical pixels, and can be controlled by setting Image.virtual_pixel to any value defined in VIRTUAL_PIXEL_METHOD.

Virtual pixels set to 'transparent', 'black', or 'white' are the most common, but many users prefer use the existing background color.

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = img[70, 46]
    img.virtual_pixel = 'background'
    img.distort('arc', (60, ))
../_images/distort-arc-background.png

Other virtual_pixel values can create special effects.

Virtual Pixel

Example

dither

../_images/distort-arc-dither.png

edge

../_images/distort-arc-edge.png

mirror

../_images/distort-arc-mirror.png

random

../_images/distort-arc-random.png

tile

../_images/distort-arc-tile.png

Matte Color

Some distortion transitions can not be calculated in the virtual-pixel space. Either being invalid, or NaN (not-a-number). You can define how such a pixel should be represented by setting the Image.matte_color property.

from wand.color import Color
from wand.image import Image

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.matte_color = Color('ORANGE')
    img.virtual_pixel = 'tile'
    args = (0, 0, 30, 60, 140, 0, 110, 60,
            0, 92, 2, 90, 140, 92, 138, 90)
    img.distort('perspective', args)
../_images/distort-arc-matte.png

Rendering Size

Setting the 'distort:viewport' artifact allows you to define the size, and offset of the resulting image:

img.artifacts['distort:viewport'] = '300x200+50+50'

Setting the 'distort:scale' artifact will resizing the final image:

img.artifacts['distort:scale'] = '75%'

Affine

Affine distortion performs a shear operation. The arguments are similar to perspective, but only need a pair of 3 points, or 12 real numbers.

src1x, src1y, dst1x, dst1y,
src2x, src2y, dst2x, dst2y,
src3x, src3y, dst3x, dst3y,
...

For example:

from wand.color import Color
from wand.image import Image

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    args = (
        10, 10, 15, 15,  # Point 1: (10, 10) => (15,  15)
        139, 0, 100, 20, # Point 2: (139, 0) => (100, 20)
        0, 92, 50, 80    # Point 3: (0,  92) => (50,  80)
    )
    img.distort('affine', args)
../_images/distort-affine.png

Affine Projection

Affine projection is identical to Scale Rotate Translate, but requires exactly 6 real numbers for the distortion arguments.

Scalex, Rotatex, Rotatey, Scaley, Translatex, Translatey

For example:

from collections import namedtuple
from wand.color import Color
from wand.image import Image

Point = namedtuple('Point', ['x', 'y'])

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    rotate = Point(0.1, 0)
    scale = Point(0.7, 0.6)
    translate = Point(5, 5)
    args = (
        scale.x, rotate.x, rotate.y,
        scale.y, translate.x, translate.y
    )
    img.distort('affine_projection', args)
../_images/distort-affine-projection.png

Arc

Arc distortion curves the image around the center point of an image. The arguments are:

ArcAngle, RotateAngle, TopRadius, BottomRadius

Where ArcAngle is the only required arguments, and the rest are optional.

For example:

from wand.color import Color
from wand.image import Image

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    args = (
        270,  # ArcAngle
        45,   # RotateAngle
    )
    img.distort('arc', args)
../_images/distort-arc.png

Barrel

Barrel distortion attempts to correct spherical distortion caused by camera lenses. It operates with four constant coefficient values A, B, C, & D mapped to the images EXIF meta-data. Usually camera, lens, and zoom attributes. The equation for barrel distortion is:

\[{R}_{src} = r * \left( A * r^3 + B * r^2 + C * r + D \right)\]

Where r is the destination radius. The arguments for the distortion are:

A B C D X Y

Where X & Y are optional center coordinates.

For example:

from wand.color import Color
from wand.image import Image

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    args = (
        0.2,  # A
        0.0,  # B
        0.0,  # C
        1.0,  # D
    )
    img.distort('barrel', args)
../_images/distort-barrel.png

Barrel Inverse

The barrel inverse distortion has the same arguments as the barrel distortion, but performs a different equation.

\[{R}_{src} = { r \over \left( A * r^3 + B * r^2 + C * r + D \right) }\]

It does not reverse, or undo, the effects of the barrel distortion.

For example:

from wand.color import Color
from wand.image import Image

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    args = (
        0.0,   # A
        0.0,   # B
        -0.5,  # C
        1.5,   # D
    )
    img.distort('barrel_inverse', args)
../_images/distort-barrel-inverse.png

Bilinear

The bilinear distortion is similar to perspective distortion, but evenly distributes pixels rather than compress & reduce shrinking areas. This means that shrunk areas of the image will appear flat with bilinear as opposed to farther away. The arguments are:

src1x, src1y, dst1x, dst1y,
src2x, src2y, dst2x, dst2y,
src3x, src3y, dst3x, dst3y,
src4x, src4y, dst4x, dst4y,
...

Unlike other inverse methods, bilinear distortion effects are directional. Meaning you can undo a distortion previously applied. For example:

from itertools import chain
from wand.color import Color
from wand.image import Image

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    source_points = (
        (0, 0),
        (140, 0),
        (0, 92),
        (140, 92)
    )
    destination_points = (
        (14, 4.6),
        (126.9, 9.2),
        (0, 92),
        (140, 92)
    )
    order = chain.from_iterable(zip(source_points, destination_points))
    arguments = list(chain.from_iterable(order))
    img.distort('bilinear_forward', arguments)
../_images/distort-bilinear-forward.png

And then reverted with by swapping the destination with source coordinates, and using 'bilinear_reverse':

order = chain.from_iterable(zip(destination_points, source_points))
arguments = list(chain.from_iterable(order))
img.distort('bilinear_reverse', arguments)
../_images/distort-bilinear-reverse.png

Cylinder & Plane

Cylinder 2 plane is a radial projection to correct common field of vision (fov) distortions. The arguments are:

FovAngle, CenterX, CenterY, FovOutput, DestCenterX, DestCenterY

Where only the first argument is required, and the rest are optional. The FovAngle value can be roughly calculated by:

\[{FovAngle} = {{LensFocalLength} \over {FilmWidth}} * { 180 \over \pi }\]

The 'plane_2_cylinder' is the inverted behavior. The arguments are:

FovAngle, CenterX, CenterY

For (a rather poor) example:

import math
from wand.color import Color
from wand.image import Image

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    lens = 60
    film = 35
    args = (
        lens/film * 180/math.pi,
    )
    img.distort('plane_2_cylinder', args)
../_images/distort-plane2cylinder.png

And the inverse:

img.distort('cylinder_2_plane', args)
../_images/distort-cylinder2plane.png

Perspective

Perspective distortion requires 4 pairs of points which is a total of 16 doubles. The order of the arguments are groups of source & destination coordinate pairs.

src1x, src1y, dst1x, dst1y,
src2x, src2y, dst2x, dst2y,
src3x, src3y, dst3x, dst3y,
src4x, src4y, dst4x, dst4y,
...

For example:

from itertools import chain
from wand.color import Color
from wand.image import Image

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    source_points = (
        (0, 0),
        (140, 0),
        (0, 92),
        (140, 92)
    )
    destination_points = (
        (14, 4.6),
        (126.9, 9.2),
        (0, 92),
        (140, 92)
    )
    order = chain.from_iterable(zip(source_points, destination_points))
    arguments = list(chain.from_iterable(order))
    img.distort('perspective', arguments)
../_images/distort-perspective.png

Polar & Depolar

Polar distortion is similar to arc distort method, but does not attempt to preserve aspect ratios.

Radiusmax, Radiusmin, Centerx, Centery, Anglestart, Angleend

All the arguments are optional, and an argument of 0 will use the distance of the center to the closet edge as the default radius.

For example:

from wand.image import Image

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.virtual_pixel = 'black'
    img.distort('polar', (0,))
../_images/distort-polar.png

For depolar distortion, the arguments are the same. However to revert the image previously distorted with nearest edge (argument 0), use -1 for ImageMagick to calculate the radius distance from the middle to the farthest edge.

For example:

from wand.image import Image

with Image(filename='distort-polar.png') as img:
    img.virtual_pixel = 'horizontal_tile'
    img.distort('depolar', (-1,))
../_images/distort-depolar.png

Polynomial

Polynomial distortion requires at least four sets of coordinates & control points, and will perform a standard polynomial equation. However, the first distortion argument is reserved to define the Order, or Complexity of the two dimensional equation.

Order, src1x, src1y, dst1x, dst1y,
       src2x, src2y, dst2x, dst2y,
       src3x, src3y, dst3x, dst3y,
       src4x, src4y, dst4x, dst4y,
       ...

For example:

from collections import namedtuple
from wand.color import Color
from wand.image import Image

Point = namedtuple('Point', ['x', 'y', 'i', 'j'])

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    order = 1.5
    alpha = Point(0, 0, 26, 0)
    beta = Point(139, 0, 114, 23)
    gamma = Point(139, 91, 139, 80)
    delta = Point(0, 92, 0, 78)
    args = (
        order,
        alpha.x, alpha.y, alpha.i, alpha.j,
        beta.x, beta.y, beta.i, beta.j,
        gamma.x, gamma.y, gamma.i, gamma.j,
        delta.x, delta.y, delta.i, delta.j,
    )
    img.distort('polynomial', args)
../_images/distort-polynomial.png

Scale Rotate Translate

A more common form of distortion, the method 'scale_rotate_translate' can be controlled by the total number of arguments.

The total arguments dictate the following order.

Total Arguments

Argument Order

1

Angle

2

Scale, Angle

3

X, Y, Angle

4

X, Y, Scale, Angle

5

X, Y, ScaleX, ScaleY, Angle

6

X, Y, Scale, Angle, NewX, NewY

7

X, Y, ScaleX, ScaleY, Angle, NewX, NewY

For example…

A single argument would be treated as an angle:

from wand.color import Color
from wand.image import Image

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    angle = 90.0
    img.distort('scale_rotate_translate', (angle,))
../_images/distort-srt-angle.png

Two arguments would be treated as a scale & angle:

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    angle = 90.0
    scale = 0.5
    img.distort('scale_rotate_translate', (scale, angle,))
../_images/distort-srt-scale-angle.png

And three arguments would describe the origin of rotation:

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    x = 80
    y = 60
    angle = 90.0
    img.distort('scale_rotate_translate', (x, y, angle,))
../_images/distort-srt-xy-angle.png

… and so forth.

Shepards

Shepard’s distortion moves (or smudges) a given source point to a destination coordinate. The arguments are:

src1x, src1y, dst1x, dst1y,
src2x, src2y, dst2x, dst2y,
...

The size, or inverse weighted distance, of the source point can be controlled by defining shepards:power artifact.

For example:

from collections import namedtuple
from wand.color import Color
from wand.image import Image

Point = namedtuple('Point', ['x', 'y', 'i', 'j'])

with Image(filename='rose:') as img:
    img.resize(140, 92)
    img.background_color = Color('skyblue')
    img.virtual_pixel = 'background'
    img.artifacts['distort:viewport'] = "160x112-10+10"
    img.artifacts['shepards:power'] = "4.0"
    alpha = Point(0, 0, 30, 15)
    beta = Point(70, 46, 60, 70)
    args = (
        *alpha,
        *beta
    )
    img.distort('shepards', args)
../_images/distort-shepards.png