Morphology¶
Morphology modifies an image by evaluating the pixel values surrounding each pixel. The basic Wand method signature is:
img.morphology(method, kernel, iterations)
Where method
is the operation to apply, and is defined by
MORPHOLOGY_METHODS
. The kernel
can include predefined
built-in shapes, or user-defined shapes.
Shapes¶
Shapes, also known as “kernels”, are small matrices that control morphology method operations. The kernels define the size, and targeted pixels to modify.
To demonstrate a kernel’s shape; let’s generate a simple black canvas around a white pixel.
from wand.image import Image
with Image(width=1, height=1, pseudo='xc:white') as img:
img.border('black', 6, 6, compose='copy')
img.save(filename='morph-dot.png')
![../../_images/morph-dot.png](../../_images/morph-dot.png)
Built-In Kernels¶
ImageMagick contains about three dozen pre-built kernels that cover most common morphology uses, as well as a few specific ones leveraged for internal operations. To use built-in kernels, the following string format is required.
label[:arg1,arg2,arg3,..]
Where label is a string defined in KERNEL_INFO_TYPES
.
Each label can have additional optional arguments, which are defined
by a comma separated list of doubles. A colon ':'
symbol should separate
the label & argument list. For example:
disk:2.5,3,5
Below is a small list of examples for the most common kernel shapes.
Cross¶
with Image(filename='morph-dot.png') as img:
img.morphology(method='dilate', kernel='cross:3')
img.sample(width=60, height=60)
img.save(filename='morph-kernel-cross3.png')
![../../_images/morph-kernel-cross3.png](../../_images/morph-kernel-cross3.png)
Diamond¶
with Image(filename='morph-dot.png') as img:
img.morphology(method='dilate', kernel='diamond:3')
img.sample(width=60, height=60)
img.save(filename='morph-kernel-diamond3.png')
![../../_images/morph-kernel-diamond3.png](../../_images/morph-kernel-diamond3.png)
Disk¶
with Image(filename='morph-dot.png') as img:
img.morphology(method='dilate', kernel='disk:5')
img.sample(width=60, height=60)
img.save(filename='morph-kernel-disk5.png')
![../../_images/morph-kernel-disk5.png](../../_images/morph-kernel-disk5.png)
Octagon¶
with Image(filename='morph-dot.png') as img:
img.morphology(method='dilate', kernel='octagon:5')
img.sample(width=60, height=60)
img.save(filename='morph-kernel-octagon5.png')
![../../_images/morph-kernel-octagon5.png](../../_images/morph-kernel-octagon5.png)
Plus¶
with Image(filename='morph-dot.png') as img:
img.morphology(method='dilate', kernel='plus:3')
img.sample(width=60, height=60)
img.save(filename='morph-kernel-plus3.png')
![../../_images/morph-kernel-plus3.png](../../_images/morph-kernel-plus3.png)
Ring¶
with Image(filename='morph-dot.png') as img:
img.morphology(method='dilate', kernel='ring:5,4')
img.sample(width=60, height=60)
img.save(filename='morph-kernel-ring5.png')
![../../_images/morph-kernel-ring5.png](../../_images/morph-kernel-ring5.png)
Square¶
with Image(filename='morph-dot.png') as img:
img.morphology(method='dilate', kernel='square:3')
img.sample(width=60, height=60)
img.save(filename='morph-kernel-square3.png')
![../../_images/morph-kernel-square3.png](../../_images/morph-kernel-square3.png)
Custom Kernels¶
Users can define their own kernel shape by building a string that follows the format:
geometry:pix1,pix2,pix3,...
Where geometry is defined as WIDTHxHEIGHT of the kernel, followed by a colon, and then a comma separated list of float values. For example:
custom_kernel = """
5x5:
-,-,1,-,-
-,1,1,1,-
-,-,-,-,-
-,1,-,1,-
1,1,1,1,1
"""
with Image(filename='morph-dot.png') as img:
img.morphology(method='dilate', kernel=custom_kernel)
img.sample(width=60, height=60)
img.save(filename='morph-kernel-custom.png')
![../../_images/morph-kernel-custom.png](../../_images/morph-kernel-custom.png)
By default, the kernel’s “origin” is calculated to be at the center of the kernel. Users can set the kernel origin by defining ±X±Y as part of the geometry. For example:
top_left_origin = """
3x3+0+0:
1,1,-
1,0,0
-,0,-
"""
bottom_right_origin = """
3x3+2+2:
1,1,-
1,0,0
-,0,-
"""
Methods¶
Morphology methods are broken into three general groups. Basic methods (such as Erode, Dilate, Open, & Close) are used to increase or reduce foreground shapes. Difference methods (such as Edge In, Edge Out, Top Hat & Bottom Hat) draw pixels around foreground edges. Pattern matching methods (such as Hit and Miss, Thinning & Thicken) add pixels when a kernel is matched.
Morphology is intended for images with a black background, and a white foreground. To demonstrate morphology methods, let’s create a basic binary image. We can quickly generate a PBM image from bytes-string literal.
pbm = b"""P1
10 10
1 1 1 1 1 1 1 1 1 1
1 1 0 1 1 0 0 0 0 1
1 0 0 0 1 0 0 1 1 1
1 1 0 1 1 0 0 0 0 1
1 1 1 1 1 0 0 1 1 1
1 1 1 1 0 0 0 0 0 1
1 0 0 0 0 0 0 1 1 1
1 0 1 0 1 0 1 1 1 1
1 0 1 0 1 0 1 1 0 1
1 1 1 1 1 1 1 1 1 1
"""
with Image(blob=pbm, format="PBM") as img:
img.sample(100, 100)
img.save(filename="morph-src.png")
![../../_images/morph-src.png](../../_images/morph-src.png)
The morphology examples below will all use 'morph-src.png'
source image.
Erode¶
Erode reduces matching white pixels, and expands black spaces.
with Image(filename='morph-src.png') as img:
img.morphology(method='erode', kernel='octagon')
img.save(filename='morph-erode.png')
![../../_images/morph-erode.png](../../_images/morph-erode.png)
Dilate¶
Dilate increases matching white pixels, and reduces black spaces.
with Image(filename='morph-src.png') as img:
img.morphology(method='dilate', kernel='octagon')
img.save(filename='morph-dilate.png')
![../../_images/morph-dilate.png](../../_images/morph-dilate.png)
Open¶
Open rounds the white edges, but preserves “holes”, or black corners.
with Image(filename='morph-src.png') as img:
img.morphology(method='open', kernel='octagon')
img.save(filename='morph-open.png')
![../../_images/morph-open.png](../../_images/morph-open.png)
Notices the black “inner” corners remain sharp, but the white “outer” corners are rounded.
Close¶
Close rounds the black edges, and removes any “holes”.
with Image(filename='morph-src.png') as img:
img.morphology(method='close', kernel='octagon')
img.save(filename='morph-close.png')
![../../_images/morph-close.png](../../_images/morph-close.png)
Notices the white “outer” corners remain sharp, but the black “inner” corners are rounded.
Smooth¶
Smooth applies both Open & Close methods. This will remove small objects & holes, and smooth both white & black corners.
with Image(filename='morph-src.png') as img:
img.morphology(method='smooth', kernel='octagon')
img.save(filename='morph-smooth.png')
![../../_images/morph-smooth.png](../../_images/morph-smooth.png)
Edge In¶
Edge In method performs a Erode, but only keeps the targeted pixel next to a shape. This means the edge is drawn just inside the white of a object.
with Image(filename='morph-src.png') as img:
img.morphology(method='edgein', kernel='octagon')
img.save(filename='morph-edgein.png')
![../../_images/morph-edgein.png](../../_images/morph-edgein.png)
Edge Out¶
Edge Out performs similar to Edge In, but uses the results of Dilate to draw a edge border just outside of an object.
with Image(filename='morph-src.png') as img:
img.morphology(method='edgeout', kernel='octagon')
img.save(filename='morph-edgeout.png')
![../../_images/morph-edgeout.png](../../_images/morph-edgeout.png)
Edge¶
The Edge method performs both Erode & Dilate methods, but only keeps differences between them as the resulting image. The result is border drawn on the edge of the objects within the image.
with Image(filename='morph-src.png') as img:
img.morphology(method='edge', kernel='octagon')
img.save(filename='morph-edge.png')
![../../_images/morph-edge.png](../../_images/morph-edge.png)
Top Hat¶
The Top Hat method performs the Open morphology method, but only returns the pixels matched by the kernel.
with Image(filename='morph-src.png') as img:
img.morphology(method='tophat', kernel='octagon')
img.save(filename='morph-tophat.png')
![../../_images/morph-tophat.png](../../_images/morph-tophat.png)
Bottom Hat¶
The Bottom Hat method performs the Close morphology method, but only returns the pixels matched by the kernel.
with Image(filename='morph-src.png') as img:
img.morphology(method='bottom_hat', kernel='octagon')
img.save(filename='morph-bottom_hat.png')
![../../_images/morph-bottom_hat.png](../../_images/morph-bottom_hat.png)
Hit and Miss¶
The hit-and-miss (a.k.a. HMT) method will remove all pixels from the image, unless a kernel pattern is matched; in which case, the pixel under the matched kernel will be set to white.
with Image(filename='morph-src.png') as img:
corners = """
3x3:
1,1,-
1,0,0
-,0,-
"""
img.morphology(method='hit_and_miss', kernel=corners)
img.save(filename='morph-hit_and_miss.png')
![../../_images/morph-hit_and_miss.png](../../_images/morph-hit_and_miss.png)
Thinning¶
The thinning method removes a pixel when the kernel matches neighboring pixels. When using custom kernels, you can control which pixel should be targeted by setting the X/Y offset of the kernel’s geometry.
with Image(filename='morph-src.png') as img:
img.morphology(method='thinning',
kernel='3x1-0-0:1,1,0',
iterations=3)
img.save(filename='morph-thinning.png')
![../../_images/morph-thinning.png](../../_images/morph-thinning.png)
There’s also a special 'skeleton'
built-in kernel, paired with -1
iterations to continue to reduce all pixels down to a minimum line.
with Image(filename='morph-src.png') as img:
img.morphology(method='thinning',
kernel='skeleton',
iterations=-1)
img.save(filename='morph-thinning-skeleton.png')
![../../_images/morph-thinning-skeleton.png](../../_images/morph-thinning-skeleton.png)
Thicken¶
The thicken method adds a pixel whenever a kernel matches neighboring pixels. You can control the targeted pixel by defining the offset geometry on custom kernels.
with Image(filename='morph-src.png') as img:
K = """
3x3+0+0:
0,-,-
-,0,-
-,-,1
"""
img.morphology(method='thicken',
kernel=K,
iterations=4)
img.save(filename='morph-thicken.png')
![../../_images/morph-thicken.png](../../_images/morph-thicken.png)
Distance¶
Distance method is a unique, and very special morphology. Given a binary black & white image, each white pixel will be replace with a color value corresponding to the distance to the nearest edge.
with Image(filename='morph-src.png') as img:
img.morphology(method='distance',
kernel='euclidean',
iterations=4)
img.save(filename='morph-distance-raw.png')
![../../_images/morph-distance-raw.png](../../_images/morph-distance-raw.png)
The resulting image is not that special. The pixel values are so low that they
appear black. However, if we use auto_level()
method, we should be able to shift the values across the full grayscale.
with Image(filename='morph-src.png') as img:
img.morphology(method='distance',
kernel='euclidean',
iterations=4)
img.auto_level()
img.save(filename='morph-distance-auto.png')
![../../_images/morph-distance-auto.png](../../_images/morph-distance-auto.png)
Other kernels used for distance morphology are 'chebyshev'
, 'manhattan'
,
'octagonal'
, and 'euclidean'
. The basic kernel string format is:
distance_kernel[:radius[,scale]]
For example:
manhattan:5,400