ImageMorph module

The ImageMorph module provides morphological operations for binary and grayscale images. Morphology is a family of image-processing techniques based on the shape and structure of regions in an image. Basic uses are dilation, erosion, edge detection, and hit or miss pattern matching.

ImageMorph works by applying a lookup table (LUT) to a binary representation of the input image. Patterns used for these operations are defined using small ASCII masks, which are converted into LUTs through LutBuilder. The resulting LUTs can then be applied to an image using MorphOp.

This module is useful for tasks such as noise cleanup, detecting specific pixel shapes, extracting boundaries, thinning, or locating features defined by small structuring elements.

Supported image modes

Morphological operations in Pillow operate on images in mode "L" (8-bit grayscale). A nonzero pixel is treated as “on”, and a zero-valued pixel as “off”. To apply morphology to a binary image, ensure that the image is first converted to mode "L":

im = im.convert("L")

Defining structuring element patterns

A structuring pattern is defined using a small ASCII mask consisting of the characters:

  • 1 — pixel must be “on”

  • 0 — pixel must be “off”

  • . or X — “don’t care” value (ignored during matching)

For example, this mask detects a 2×2 corner shape:

pattern = [
    "10",
    "11",
]

Multiple patterns can be combined into a single LUT. Patterns must all be the same size, and Pillow builds a lookup table from them using LutBuilder.

Using LutBuilder

The LutBuilder class constructs a LUT that defines how a morphological operation should behave. A LUT maps every possible 3×3 neighborhood around a pixel to an output pixel value (either “on” or “off”).

Basic uses like dilation and erosion can be achieved by specifying preset operation names ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"), or you may define custom patterns.

For example, creating a LUT for a 2×2 corner detector:

from PIL import ImageMorph

patterns = [
    "10",
    "11",
]

lb = ImageMorph.LutBuilder(op_name="corner")
lb.add_patterns(patterns)
lut = lb.build_lut()

You can inspect, save, or reuse the LUT with LutBuilder.get_lut(), MorphOp.load_lut(), or MorphOp.save_lut().

Applying morphology with MorphOp

Once a LUT is created, the MorphOp class applies it to an image. The MorphOp.apply() method performs the morphological operation and returns a tuple (count, out_image) where:

  • count is the number of pixels that changed, and

  • out_image is the resulting processed image.

Example: applying a simple dilation operation:

from PIL import Image, ImageMorph

with Image.open("input.png") as im:
    im = im.convert("L")

# Built-in 8-connected dilation
op = ImageMorph.MorphOp(op_name="dilation8")

count, out = op.apply(im)
out.save("dilated.png")

You could also use the method MorphOp.match() to check where a pattern matches without modifying the image, and MorphOp.get_on_pixels() to get the coordinates of “on” pixels after pattern matching.

Example: pattern matching without modifying the image:

op = ImageMorph.MorphOp(op_name="edge")
result = op.match(im)

# result is a list of (x, y) coordinates
print("Edge pixels found:", len(result))

Saving and loading LUTs

LUTs created by LutBuilder can be serialized and reused later. This is helpful when repeatedly applying the same pattern in a batch-processing workflow.

Example:

lb = ImageMorph.LutBuilder(op_name="custom")
lb.add_patterns(patterns)
lb.build_lut()
lb.save_lut("custom.lut")

# Later...
op = ImageMorph.MorphOp()
op.load_lut("custom.lut")
count, out = op.apply(im)
class PIL.ImageMorph.LutBuilder(patterns: list[str] | None = None, op_name: str | None = None)[source]

Bases: object

A class for building a MorphLut from a descriptive language

The input patterns is a list of a strings sequences like these:

4:(...
   .1.
   111)->1

(whitespaces including linebreaks are ignored). The option 4 describes a series of symmetry operations (in this case a 4-rotation), the pattern is described by:

  • . or X - Ignore

  • 1 - Pixel is on

  • 0 - Pixel is off

The result of the operation is described after “->” string.

The default is to return the current pixel value, which is returned if no other match is found.

Operations:

  • 4 - 4 way rotation

  • N - Negate

  • 1 - Dummy op for no other operation (an op must always be given)

  • M - Mirroring

Example:

lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
lut = lb.build_lut()
add_patterns(patterns: list[str]) None[source]
build_default_lut() None[source]
build_lut() bytearray[source]

Compile all patterns into a morphology lut.

TBD :Build based on (file) morphlut:modify_lut

get_lut() bytearray | None[source]
class PIL.ImageMorph.MorphOp(lut: bytearray | None = None, op_name: str | None = None, patterns: list[str] | None = None)[source]

Bases: object

A class for binary morphological operators

apply(image: Image) tuple[int, Image][source]

Run a single morphological operation on an image

Returns a tuple of the number of changed pixels and the morphed image

get_on_pixels(image: Image) list[tuple[int, int]][source]

Get a list of all turned on pixels in a binary image

Returns a list of tuples of (x,y) coordinates of all matching pixels. See Coordinate system.

load_lut(filename: str) None[source]

Load an operator from an mrl file

match(image: Image) list[tuple[int, int]][source]

Get a list of coordinates matching the morphological operation on an image.

Returns a list of tuples of (x,y) coordinates of all matching pixels. See Coordinate system.

save_lut(filename: str) None[source]

Save an operator to an mrl file

set_lut(lut: bytearray | None) None[source]

Set the lut from an external source