2025, Oct 05 13:00
Understanding Pillow's affine transform: inverse mapping, translation signs, and the correct way to shift images right
Learn why Pillow affine transform translations seem reversed: it samples via the inverse mapping. See a fix to shift images right by using a negative offset.
Shifting an image with Pillow’s affine transform looks straightforward until the translation goes the “wrong” way. A positive horizontal offset seems like it should move pixels to the right, but the output slides left. The root cause is how Pillow defines the affine mapping for transform: it samples the output from the input using the inverse transform.
Problem demonstration
The following snippet aims to translate an image 85 pixels to the right. Instead, the content appears to move left.
from PIL import Image
img_src = Image.open('Test image.png')
params_wrong = [1, 0, 85, 0, 1, 0]
img_shifted = img_src.transform(
    img_src.size,
    Image.AFFINE,
    params_wrong,
    Image.BILINEAR,
    fillcolor=(150, 150, 150)
)
img_shifted.save('Transformed image.png')
Why this happens
In Pillow’s affine transform, the parameter tuple (a, b, c, d, e, f) represents the first two rows of the inverse transform. The mapping direction is from output to input. For each output pixel at coordinates (x, y), the value is sourced from the input at (a x + b y + c, d x + e y + f).
This function takes a 6-tuple (a, b, c, d, e, f) which contain the first two rows from the inverse of an affine transform matrix. For each pixel (x, y) in the output image, the new value is taken from a position (a x + b y + c, d x + e y + f) in the input image, rounded to nearest pixel.
That means a positive c asks the output to sample from input positions shifted to the right, which visually pulls content left. To make the content move to the right on the canvas, the sampling must come from input positions shifted to the left by the same amount.
Fix and correct example
To shift the image 85 pixels to the right, use a negative c. Each output pixel (x, y) will take its value from input coordinate (x − 85, y), producing a rightward translation.
from PIL import Image
source_img = Image.open('Test image.png')
params_fix = [1, 0, -85, 0, 1, 0]
result_img = source_img.transform(
    source_img.size,
    Image.AFFINE,
    params_fix,
    Image.BILINEAR,
    fillcolor=(150, 150, 150)
)
result_img.save('Transformed image.png')
Why this matters
Confusing output from geometric transforms wastes time and introduces subtle bugs in preprocessing pipelines, synthetic data generation, and image augmentation. Understanding that Pillow’s transform samples via the inverse mapping clarifies why translation signs appear inverted, and prevents accidentally moving content in the opposite direction.
Conclusion
If an affine translation in Pillow heads the wrong way, check the sign. The tuple (a, b, c, d, e, f) applies to the inverse mapping, so a horizontal shift to the right requires c to be negative. Think in terms of where the output samples from in the input image, and the directionality falls into place.
The article is based on a question from StackOverflow by Christopher Pratt and an answer by J Earls.