Search code examples
pythonimage-processingpython-imaging-librarytransformationskew

Is it possible to achieve negative X-Skewing without cropping in Python PIL?


Im working on to transform images using X-skew using the following code

from PIL import Image

def x_skew_image(input_path, output_path, skew_factor):
    # Open the input image
    input_image = Image.open(input_path)

    # Get the image dimensions
    width, height = input_image.size

    # Calculate the new width after skewing
    new_width = int(width + abs(skew_factor) * height)

    # Create a new image with the calculated width and the same height
    output_image = Image.new("RGB", (new_width, height))

    # Apply the skew transformation
    for y in range(height):
        x_offset = int(skew_factor * y)
        for x in range(width):
            if 0 <= x + x_offset < new_width:
                output_image.putpixel((x + x_offset, y), input_image.getpixel((x, y)))

    # Save the skewed image
    output_image.save(output_path)

# Replace these paths and skew_factor as needed
input_path = r'input_path'  # Replace with the path to your input image
output_path = r'output_path'  # Replace with the desired output path
skew_factor = -0.4  # Adjust the skew factor as needed

x_skew_image(input_path, output_path, skew_factor)

However, Im facing issue when trying to with negative X-skewing (change skew_factor to negative value) and it seems that the image get cropped. How can i modify the code to solve the issue?

Original Image

Positive X-skew

Negative X-skew


Solution

  • Your code works if you change it to this:

    #!/usr/bin/env python3
    
    from PIL import Image
    
    def x_skew_image(input_path, output_path, skew_factor):
        # Open the input image
        input_image = Image.open(input_path)
    
        # Get the image dimensions
        width, height = input_image.size
    
        # Calculate the new width after skewing
        new_width = int(width + abs(skew_factor) * height)
    
        # Create a new image with the calculated width and the same height
        output_image = Image.new("RGB", (new_width, height))
    
        # Apply the skew transformation
        for y in range(height):
            x_offset = int(skew_factor * y)
            if skew_factor < 0:
                x_offset = int(-skew_factor * (height - y))
                
            for x in range(width):
                new_x = x + x_offset
                output_image.putpixel((new_x, y), input_image.getpixel((x, y)))
    
        # Save the skewed image
        output_image.save(output_path)
    
    # Replace these paths and skew_factor as needed
    input_path = 'CrazyCat.jpg'
    output_path = 'result.jpg'
    skew_factor = -0.4  # Adjust the skew factor as needed
    
    x_skew_image(input_path, output_path, skew_factor)
    

    enter image description here

    With skew_factor=0.4:

    enter image description here

    With skew_factor=-0.4:

    enter image description here


    However, I would really advise AGAINST using for loops for image processing in Python - they are slow and error-prone. There is a noticeable delay when running your code versus using the built-in affine transform method which is instantaneous and simpler:

    #!/usr/bin/env python3
    
    from PIL import Image
    
    input_path  = 'CrazyCat.jpg'
    output_path = 'result.jpg'
    
    im = Image.open(input_path)
    w , h = im.size
    
    shear_factor = 0.4
    new_width = int(w + abs(shear_factor)*h)
    if shear_factor > 0:
       res = im.transform((new_width,h), Image.AFFINE, (1, shear_factor, -shear_factor*h, 0, 1, 0))
    else:
       res = im.transform((new_width,h), Image.AFFINE, (1, shear_factor, 0, 0, 1, 0))
    
    res.save('result.jpg')
    

    Referring to your secondary question about filling the surrounding black area, you can try this for a solid greenish colour:

    res = im.transform((new_width,h), Image.AFFINE, (1, shear_factor, -shear_factor*h, 0, 1, 0), fillcolor='#506050')
    

    enter image description here


    If you want to fill the undefined parts of your sheared image with a blurred version of your original image, you can do that like this - your tastes on blur radius and so on may vary but you should get the idea:

    #!/usr/bin/env python3
    
    from PIL import Image, ImageFilter
    
    input_path = 'CrazyCat.jpg'
    output_path = 'result.jpg'
    
    # Open image and get dimensions
    im = Image.open(input_path)
    w , h = im.size
    
    # Generate same-size mask for choosing where to paste sheared image onto blurred background
    mask = Image.new('L', (w,h), 'white')
    mask.save('DEBUG-mask.jpg') # debug only
    
    # Shear using affine transform
    shear_factor = 0.4
    new_width = int(w + abs(shear_factor)*h)
    if shear_factor > 0:
       res  = im.transform((new_width,h), Image.AFFINE, (1, shear_factor, -shear_factor*h, 0, 1, 0))
       mask = mask.transform((new_width,h), Image.AFFINE, (1, shear_factor, -shear_factor*h, 0, 1, 0))
    else:
       res  = im.transform((new_width,h), Image.AFFINE, (1, shear_factor, 0, 0, 1, 0))
       mask = mask.transform((new_width,h), Image.AFFINE, (1, shear_factor, 0, 0, 1, 0))
    
    # Generate blurred, extended backgound
    bg = im.copy().resize((new_width,h)).filter(ImageFilter.GaussianBlur(50))
    bg.save('DEBUG-bg.jpg') # debug only
    
    # Paste sheared original over blurred background using mask to select which image to source from
    bg.paste(res, mask)
    bg.save('result.jpg')
    

    enter image description here