Trim whitespace using PIL
Asked Answered
B

5

68

Is there a simple solution to trim whitespace on the image in PIL?

ImageMagick has easy support for it in the following way:

convert test.jpeg -fuzz 7% -trim test_trimmed.jpeg

I found a solution for PIL:

from PIL import Image, ImageChops

def trim(im, border):
    bg = Image.new(im.mode, im.size, border)
    diff = ImageChops.difference(im, bg)
    bbox = diff.getbbox()
    if bbox:
        return im.crop(bbox)

But this solution has disadvantages:

  1. I need to define border color, it is not a big deal for me, my images has a white background
  2. And the most disadvantage, This PIL solution doesn't support ImageMagick's -fuzz key. To add some fuzzy cropping. as I can have some jpeg compression artifacts and unneeded huge shadows.

Maybe PIL has some built-in functions for it? Or there is some fast solution?

Beckmann answered 16/5, 2012 at 9:40 Comment(1)
I know the code is exactly the same there, but it can also be found here - gist.github.com/mattjmorrison/932345Botha
D
157

I don't think there is anything built in to PIL that can do this. But I've modified your code so it will do it.

  • It gets the border colour from the top left pixel, using getpixel, so you don't need to pass the colour.
  • Subtracts a scalar from the differenced image, this is a quick way of saturating all values under 100, 100, 100 (in my example) to zero. So is a neat way to remove any 'wobble' resulting from compression.

Code:

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()

Heavily compressed jpeg:

enter image description here Cropped: enter image description here

Noisy jpeg:

enter image description here Cropped: enter image description here

Depository answered 16/5, 2012 at 10:32 Comment(8)
Note that the operation you are doing is very dangerous: it does compensate for noise in the border, but you can no longer handle images where the background and image itself are very similar - for example photos if white articles positioned on a white background.Cementation
What is this line doing? " diff = ImageChops.add(diff, diff, 2.0, -100)" Are you just trying to avoid the edges being zero for getbbox?Borlase
@WichertAkkerman Please, tell me how resolve this problem, if you know? In my purpose i need to trim only strictly the whitespace (255, 255, 255, 255)Memento
@WichertAkkerman Probably, I found the solution: to replace diff = ImageChops.add(diff, diff, 2.0, -100) with diff = ImageChops.add(diff, diff)Memento
Instead of getting the border color from just one pixel, what about getting the average border color of all perimetral pixels, or at least of the 4 corners pixels? How could it be implemented?Apostolic
I've found that this doesn't work for images with mode "RGBA" (the ImageChops.difference returns an entirely transparent image). Instead changing bg = Image.new(im.mode, im.size, im.getpixel((0,0))) to bg = Image.new("RGB", im.size, im.getpixel((0,0))) and diff = ImageChops.difference(im, bg) to diff = ImageChops.difference(im.convert("RGB"), bg) works.Paxton
This works great to crop empty space around text. Thank you!!Licko
An alternative, probably more general, solution with RBGA images is to use diff.getbbox(alpha_only=False)Entrap
D
6

Use wand http://docs.wand-py.org/en/0.3-maintenance/wand/image.html

trim(color=None, fuzz=0) Remove solid border from image. Uses top left pixel as a guide by default, or you can also specify the color to remove.

Dissimulation answered 13/12, 2013 at 1:26 Comment(2)
Could you elaborate the answer please ? (with an example)Indebted
I just tried using the trim() feature in Wand in addition to trying the accepted answer and can conclude that Wand produced far inferior trim results, which was very surprising considering my images were sharp and the border was completely solid.Faradize
I
4

The answer by fraxel works, but as pointed by Matt Pitkin sometimes the image should be converted to "RGB", otherwise the borders are not detected:

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)
    else: 
        # Failed to find the borders, convert to "RGB"        
        return trim(im.convert('RGB'))
Iceblink answered 11/10, 2022 at 16:22 Comment(2)
i faced max recursion depth for a caseAnklebone
i removed recursion and paste into white bg first as shown in this answer: https://mcmap.net/q/95258/-how-replace-transparent-with-a-color-in-pillowAnklebone
A
2

using trim function at ufp.image module.

import ufp.image
import PIL
im = PIL.Image.open('test.jpg', 'r')
trimed = ufp.image.trim(im, fuzz=13.3)
trimed.save('trimed.jpg')
Augie answered 22/3, 2015 at 7:38 Comment(2)
I can't pip install ufp. I get the following error message ERROR: Command "python setup.py egg_info" failed with error code 1 in /private/var/folders/vd/5ccxv4957f1_prjqt1l_ppsw0000gq/T/pip-install-ya7p01_3/ufp/. Further, there is not github repository for ufp so that I can contact the developer.Turgescent
ufp is not Python 3 compatible.Tedium
D
0

If your image is off-white then this will help to set a threshold and use that to cut out the borders.

Input and output are both pillow images.

import cv2

# Trim Whitespace Section
def trim_whitespace_image(image):
    # Convert the image to grayscale
    gray_image = image.convert('L')

    # Convert the grayscale image to a NumPy array
    img_array = np.array(gray_image)

    # Apply binary thresholding to create a binary image 
    # (change the value here default is 250)    ↓
    _, binary_array = cv2.threshold(img_array, 250, 255, cv2.THRESH_BINARY_INV)

    # Find connected components in the binary image
    num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(binary_array)

    # Find the largest connected component (excluding the background)
    largest_component_label = np.argmax(stats[1:, cv2.CC_STAT_AREA]) + 1
    largest_component_mask = (labels == largest_component_label).astype(np.uint8) * 255

    # Find the bounding box of the largest connected component
    x, y, w, h = cv2.boundingRect(largest_component_mask)

    # Crop the image to the bounding box
    cropped_image = image.crop((x, y, x + w, y + h))

    return cropped_image
Deerdre answered 20/7, 2023 at 5:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.