66

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?

Eugene Nagorny
  • 1,626
  • 3
  • 18
  • 32
  • 1
    I know the code is exactly the same there, but it can also be found here - https://gist.github.com/mattjmorrison/932345 – Sergey M Apr 26 '14 at 06:46

5 Answers5

150

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

fraxel
  • 34,470
  • 11
  • 98
  • 102
  • 7
    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. – Wichert Akkerman Aug 03 '12 at 08:26
  • 1
    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? – Ezekiel Kruglick Jun 11 '15 at 20:01
  • @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) – Vladimir Chub Aug 14 '15 at 07:35
  • 3
    @WichertAkkerman Probably, I found the solution: to replace `diff = ImageChops.add(diff, diff, 2.0, -100)` with `diff = ImageChops.add(diff, diff)` – Vladimir Chub Aug 14 '15 at 08:00
  • 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? – mmj Jul 22 '21 at 14:10
  • 5
    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. – Matt Pitkin Jan 07 '22 at 11:24
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.

Jie Bao
  • 838
  • 9
  • 5
  • 2
    Could you elaborate the answer please ? (with an example) – secavfr Jul 04 '18 at 13:38
  • 3
    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. – ptk Jul 25 '18 at 07:20
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'))
Victor
  • 398
  • 1
  • 2
  • 11
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')
Matt
  • 3,483
  • 4
  • 36
  • 46
c2o93y50
  • 211
  • 2
  • 4
  • 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. – logic1976 Jul 22 '19 at 22:25
  • 6
    ufp is not Python 3 compatible. – Dustin Oprea Jun 14 '21 at 09:29
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
Gaurav Hazra
  • 333
  • 1
  • 11