jack
jack

Reputation: 17951

Crop a PNG image to its minimum size

How to cut off the blank border area of a PNG image and shrink it to its minimum size using Python?

NB: The border size is not a fixed value, but may vary per image.

Upvotes: 23

Views: 20927

Answers (7)

Ilya Bossov
Ilya Bossov

Reputation: 1

The other answers did not work for me while writing a Blender script (cannot use PIL), so maybe someone else will find this useful.

import numpy as np

def crop(crop_file):
    """crop the image, removing invisible borders"""
    image = bpy.data.images.load(crop_file, check_existing=False)
    w, h = image.size

    print("Original size: " + str(w) + " x " + str(h))

    linear_pixels = image.pixels[:]
    pixels4d = np.reshape(linear_pixels, (h, w, 4))
    
    mask = pixels4d [:,:,3] != 0.
    coords = np.argwhere(mask)
    y0, x0 = coords.min(axis=0)
    y1, x1 = coords.max(axis=0) + 1
    cropped_box = pixels4d[y0:y1, x0:x1, :]
    
    w1, h1 = x1 - x0, y1 - y0
    print("Crop size: " + str(w1) + " x " + str(h1))
    
    temp_image = bpy.data.images.new(crop_file, alpha=True, width=w1, height=h1)
    temp_image.pixels[:] = cropped_box.ravel()
    temp_image.filepath_raw = crop_file
    temp_image.file_format = 'PNG'
    temp_image.alpha_mode = 'STRAIGHT'
    temp_image.save()

Upvotes: 0

YOU
YOU

Reputation: 123917

PIL's getbbox is working for me

im.getbbox() => 4-tuple or None

Calculates the bounding box of the non-zero regions in the image. The bounding box is returned as a 4-tuple defining the left, upper, right, and lower pixel coordinate. If the image is completely empty, this method returns None.

Code Sample that I tried, I have tested with bmp, but it should work for png too.

import Image
im = Image.open("test.bmp")
im.size  # (364, 471)
im.getbbox()  # (64, 89, 278, 267)
im2 = im.crop(im.getbbox())
im2.size  # (214, 178)
im2.save("test2.bmp")

Upvotes: 47

Basj
Basj

Reputation: 46363

Here is ready-to-use solution:

import numpy as np
from PIL import Image

def bbox(im):
    a = np.array(im)[:,:,:3]  # keep RGB only
    m = np.any(a != [255, 255, 255], axis=2)
    coords = np.argwhere(m)
    y0, x0, y1, x1 = *np.min(coords, axis=0), *np.max(coords, axis=0)
    return (x0, y0, x1+1, y1+1)

im = Image.open('test.png')
print(bbox(im))  # (33, 12, 223, 80)
im2 = im.crop(bbox(im))
im2.save('test_cropped.png')

Example input (download link if you want to try):

enter image description here

Output:

enter image description here

Upvotes: 7

I had the same problem today. Here is my solution to crop the transparent borders. Just throw this script in your folder with your batch .png files:

from PIL import Image
import numpy as np
from os import listdir

def crop(png_image_name):
    pil_image = Image.open(png_image_name)
    np_array = np.array(pil_image)
    blank_px = [255, 255, 255, 0]
    mask = np_array != blank_px
    coords = np.argwhere(mask)
    x0, y0, z0 = coords.min(axis=0)
    x1, y1, z1 = coords.max(axis=0) + 1
    cropped_box = np_array[x0:x1, y0:y1, z0:z1]
    pil_image = Image.fromarray(cropped_box, 'RGBA')
    print(pil_image.width, pil_image.height)
    pil_image.save(png_image_name)
    print(png_image_name)

for f in listdir('.'):
    if f.endswith('.png'):
        crop(f)

Upvotes: 5

AaronJPung
AaronJPung

Reputation: 1144

I think it's necessary to supplement @Frank Krueger's answer. He makes a good point, but it doesn't include how to properly crop extra border color out of an image. I found that here. Specifically, I found this useful:

from PIL import Image, ImageChops

def trim(im):
    bg = Image.new(im.mode, im.size, im.getpixel((0,0)))
    diff = ImageChops.difference(im, bg)
    diff = ImageChops.add(diff, diff, 2.0, -100)
    bbox = diff.getbbox()
    if bbox:
        return im.crop(bbox)

im = Image.open("bord3.jpg")
im = trim(im)
im.show()

Upvotes: 2

noj
noj

Reputation: 139

https://gist.github.com/3141140

import Image
import sys
import glob

# Trim all png images with alpha in a folder
# Usage "python PNGAlphaTrim.py ../someFolder"

try:
    folderName = sys.argv[1]
except :
    print "Usage: python PNGPNGAlphaTrim.py ../someFolder"
    sys.exit(1)

filePaths = glob.glob(folderName + "/*.png") #search for all png images in the folder

for filePath in filePaths:
    image=Image.open(filePath)
    image.load()

    imageSize = image.size
    imageBox = image.getbbox()

    imageComponents = image.split()

    if len(imageComponents) < 4: continue #don't process images without alpha

    rgbImage = Image.new("RGB", imageSize, (0,0,0))
    rgbImage.paste(image, mask=imageComponents[3])
    croppedBox = rgbImage.getbbox()

    if imageBox != croppedBox:
        cropped=image.crop(croppedBox)
        print filePath, "Size:", imageSize, "New Size:",croppedBox
        cropped.save(filePath)

Upvotes: 3

Frank Krueger
Frank Krueger

Reputation: 71053

You can use PIL to find rows and cols of your image that are made up purely of your border color.

Using this information, you can easily determine the extents of the inlaid image.

PIL again will then allow you to crop the image to remove the border.

Upvotes: 0

Related Questions