How to use PIL to make all white pixels transparent?
Asked Answered
A

11

101

I'm trying to make all white pixels transparent using the Python Image Library. (I'm a C hacker trying to learn python so be gentle) I've got the conversion working (at least the pixel values look correct) but I can't figure out how to convert the list into a buffer to re-create the image. Here's the code

img = Image.open('img.png')
imga = img.convert("RGBA")
datas = imga.getdata()

newData = list()
for item in datas:
    if item[0] == 255 and item[1] == 255 and item[2] == 255:
        newData.append([255, 255, 255, 0])
    else:
        newData.append(item)

imgb = Image.frombuffer("RGBA", imga.size, newData, "raw", "RGBA", 0, 1)
imgb.save("img2.png", "PNG")
Amu answered 19/4, 2009 at 17:13 Comment(0)
B
125

You need to make the following changes:

  • append a tuple (255, 255, 255, 0) and not a list [255, 255, 255, 0]
  • use img.putdata(newData)

This is the working code:

from PIL import Image

img = Image.open('img.png')
img = img.convert("RGBA")
datas = img.getdata()

newData = []
for item in datas:
    if item[0] == 255 and item[1] == 255 and item[2] == 255:
        newData.append((255, 255, 255, 0))
    else:
        newData.append(item)

img.putdata(newData)
img.save("img2.png", "PNG")
Balfour answered 19/4, 2009 at 17:38 Comment(9)
Just to potentially safe you some time: If you are working with Python3 you have to go for Pillow(python-pillow.org) instead of PIL.Remus
For GIF, it seems transparency is needed as argument for save (Pillow 5.1.0). Also see How to CREATE a transparent gif (or png) with PIL (python-imaging).Darnley
The A in "RGBA" stands for "alpha," and means "opacity." So here the 0 in newData.append((255,255,255,0)) means "0 opacity;" in other words, "completely transparent." Further explanation might help curious newbies. I'm guessing putdata() mutates the PIL object, but I don't know what's going on under the hoodNoon
this flips some images interestingly enough - any idea why?Monogram
What sort of flipping? Can you be more specific?Balfour
Hey, is there a way to make all colors transparent except for one? Like the png must only have white, and the remaining colors must be transparent. I thought of running it through a for loop with range from 0 to 254, kinda makes it do too much workBudding
This didn't work for me because iterating through pixdata was just giving be nothing but 0s. The age of the answer might have something to do with it. Giovanni G. PY's answer further down worked better.Lobbyism
Can you make changes to your answer to show how to make it all partially transparent?Televise
rgba[rgba[...,-1]==0] = [255,255,255,0]Robey
F
56

You can also use pixel access mode to modify the image in-place:

from PIL import Image

img = Image.open('img.png')
img = img.convert("RGBA")

pixdata = img.load()

width, height = img.size
for y in range(height):
    for x in range(width):
        if pixdata[x, y] == (255, 255, 255, 255):
            pixdata[x, y] = (255, 255, 255, 0)

img.save("img2.png", "PNG")

You can probably also wrap the above into a script if you use it often.

Fidget answered 19/4, 2009 at 18:17 Comment(6)
As a point of reference on efficiency, the above loop takes about 0.05 seconds on a 256x256 image on my average machine. That's faster than I was expecting.Consul
Upside: this actually works on giant images (32000x32000 px). Testing on a high-end server, all the other methods I tried died with memory errors on that size, but had been able to handle (22000x22000 px). Downside: this is slower than other methods I've tried like using numpy to replace the values, then Image.fromarray to get it back to a PIL object. To add to @MKatz 's point of reference, this ran in 7 minutes, 15 seconds for a 32000x32000 px image.Keciakeck
Hey, is there a way to make all colors transparent except one color? I tried using a for loop, but it takes too much time! HelpBudding
@NithinSai how about creating a copy which only copies one color from the original picture?Sounding
@Sounding I did try that, but I haven't the slightest clue on how to use python, I'm a beginner and don't know how to use PIL to make a new image by copying one color from another png. Please help meBudding
@NithinSai lmk if this helps: #52316395Sounding
M
14

Since this is currently the first Google result while looking for "Pillow white to transparent", I'd like to add that the same can be achieved with numpy, and in my benchmark (a single 8MP image with lots of white background) is about 10 times faster (about 300ms vs 3.28s for the proposed solution). The code is also a bit shorter:

import numpy as np

def white_to_transparency(img):
    x = np.asarray(img.convert('RGBA')).copy()

    x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(np.uint8)

    return Image.fromarray(x)

It is also easily exchanble to a version where the "almost white" (e.g. one channel is 254 instead of 255) is "almost transparent". Of course this will make the entire picture partly transparent, except for the pure black:

def white_to_transparency_gradient(img):
    x = np.asarray(img.convert('RGBA')).copy()

    x[:, :, 3] = (255 - x[:, :, :3].mean(axis=2)).astype(np.uint8)

    return Image.fromarray(x)

Remark: the .copy() is needed because by default Pillow images are converted to read-only arrays.

Malamute answered 11/1, 2019 at 14:24 Comment(5)
This function will cost lots of memorys.Byplay
Why a lot? It is still linear in space, sure you need to create a few additional arrays but even if you take everything into account it's maybe 5x space (probably less), for a 10x speedup it's a good tradeoff (also, if you are working in such tight conditions that you can't create 5 images in memory, then probably python is not the right language for your task...)Malamute
I use this in a 1G VPS always get memory error exception, while increaing the VPS memory everything is OK.Byplay
can you explain why axis=2 is used ? i was assuming it should be axis =3 since we are making the Alpha 'A' channel transparent.Tatty
An image has 3 axes in total - height, width, and channels - so axis=3 would raise an error. The fact that we're saving to alpha is encompassed by the lhs of the assignment, i.e. we're writing in the index 3 of the third ax (R=0, G=1, B=2, alpha=3). The .any(axis=2) on the rhs means that you want to get the pixels where at least one of the first three indices (R, G, or B) of the third dimension (because it's [:, :, :3]) is different from 255.Malamute
P
8
import Image
import ImageMath

def distance2(a, b):
    return (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]) + (a[2] - b[2]) * (a[2] - b[2])

def makeColorTransparent(image, color, thresh2=0):
    image = image.convert("RGBA")
    red, green, blue, alpha = image.split()
    image.putalpha(ImageMath.eval("""convert(((((t - d(c, (r, g, b))) >> 31) + 1) ^ 1) * a, 'L')""",
        t=thresh2, d=distance2, c=color, r=red, g=green, b=blue, a=alpha))
    return image

if __name__ == '__main__':
    import sys
    makeColorTransparent(Image.open(sys.argv[1]), (255, 255, 255)).save(sys.argv[2]);
Passenger answered 25/12, 2010 at 19:34 Comment(0)
N
5

A more pythonic way since looping take a very long time for a big image

from PIL import Image

img = Image.open('img.png')
img = img.convert("RGBA")

imgnp = np.array(img)

white = np.sum(imgnp[:,:,:3], axis=2)
white_mask = np.where(white == 255*3, 1, 0)

alpha = np.where(white_mask, 0, imgnp[:,:,-1])

imgnp[:,:,-1] = alpha 

img = Image.fromarray(np.uint8(imgnp))
img.save("img2.png", "PNG")
Naker answered 15/9, 2020 at 10:32 Comment(0)
S
5

This function combines all the advantages of the previous solutions: it allows any background and uses numpy (that is faster than the classical lists).

import numpy as np
from PIL import Image

def convert_png_transparent(src_file, dst_file, bg_color=(255,255,255)):
    image = Image.open(src_file).convert("RGBA")
    array = np.array(image, dtype=np.ubyte)
    mask = (array[:,:,:3] == bg_color).all(axis=2)
    alpha = np.where(mask, 0, 255)
    array[:,:,-1] = alpha
    Image.fromarray(np.ubyte(array)).save(dst_file, "PNG")
Schumer answered 2/11, 2021 at 17:30 Comment(0)
M
4

Python 3 version with all the files in a dir

import glob
from PIL import Image

def transparent(myimage):
    img = Image.open(myimage)
    img = img.convert("RGBA")

    pixdata = img.load()

    width, height = img.size
    for y in range(height):
        for x in range(width):
            if pixdata[x, y] == (255, 255, 255, 255):
                pixdata[x, y] = (255, 255, 255, 0)

    img.save(myimage, "PNG")

for image in glob.glob("*.png"):
    transparent(image)
Marivelmariya answered 29/11, 2018 at 6:36 Comment(0)
H
2

I'm surprised no one has seen the need to not just change a specific color, but rather the blends of that color with others as well. This would be what Gimp does with the functionality "color to alpha". Extending cr333's code with https://mcmap.net/q/212384/-what-algorithm-is-behind-the-gimp-39-s-quot-color-to-alpha-quot-feature we get something that resembles this functionality:

from PIL import Image

target_color = (255, 255, 255)

img   = Image.open('img.png')
imga  = img.convert("RGBA")
datas = imga.getdata()

newData = list()
for item in datas:
    newData.append((
        item[0], item[1], item[2],
        max( 
            abs(item[0] - target_color[0]), 
            abs(item[1] - target_color[1]), 
            abs(item[2] - target_color[2]), 
        )  
    ))

imgb = Image.frombuffer("RGBA", imga.size, newData, "raw", "RGBA", 0, 1)
imgb.save("img2.png", "PNG")

Hutner answered 23/1, 2021 at 10:24 Comment(0)
K
2

@egeres method of using using the distance to a target color to create an alpha value is really neat and creates a much nicer result. Here it is using numpy:

import numpy as np
import matplotlib.pyplot as plt

def color_to_alpha(im, target_color):
    alpha = np.max(
        [
            np.abs(im[..., 0] - target_color[0]),
            np.abs(im[..., 1] - target_color[1]),
            np.abs(im[..., 2] - target_color[2]),
        ],
        axis=0,
    )
    ny, nx, _ = im.shape
    im_rgba = np.zeros((ny, nx, 4), dtype=im.dtype)
    for i in range(3):
        im_rgba[..., i] = im[..., i]
    im_rgba[..., 3] = alpha
    return im_rgba

target_color = (0.0, 0.0, 0.0)
im = plt.imread("img.png")
im_rgba = color_to_alpha(im, target_color)

For completeness I've included a comparison with the mask-based version applied to the matplotlib logo below:

from pathlib import Path
import matplotlib.pyplot as pl
import numpy as np


def color_to_alpha(im, alpha_color):
    alpha = np.max(
        [
            np.abs(im[..., 0] - alpha_color[0]),
            np.abs(im[..., 1] - alpha_color[1]),
            np.abs(im[..., 2] - alpha_color[2]),
        ],
        axis=0,
    )
    ny, nx, _ = im.shape
    im_rgba = np.zeros((ny, nx, 4), dtype=im.dtype)
    for i in range(3):
        im_rgba[..., i] = im[..., i]
    im_rgba[..., 3] = alpha
    return im_rgba


def color_to_alpha_mask(im, alpha_color):
    mask = (im[..., :3] == alpha_color).all(axis=2)
    alpha = np.where(mask, 0, 255)
    ny, nx, _ = im.shape
    im_rgba = np.zeros((ny, nx, 4), dtype=im.dtype)
    im_rgba[..., :3] = im
    im_rgba[..., -1] = alpha
    return im_rgba


# load example from images included with matplotlib
fn_img = Path(plt.__file__).parent / "mpl-data" / "images" / "matplotlib_large.png"
im = plt.imread(fn_img)[..., :3]  # get rid of alpha channel already in image

target_color = [1.0, 1.0, 1.0]
im_rgba = color_to_alpha(im, target_color)
im_rgba_masked = color_to_alpha_mask(im, target_color)

fig, axes = plt.subplots(ncols=3, figsize=(12, 4))
[ax.set_facecolor("lightblue") for ax in axes]
axes[0].imshow(im)
axes[0].set_title("original")
axes[1].imshow(im_rgba)
axes[1].set_title("using distance to color")
axes[2].imshow(im_rgba_masked)
axes[2].set_title("mask on color")

comparison of different color-to-alpha techniques

Kenway answered 8/11, 2021 at 10:17 Comment(0)
J
2

I like Jonathan's answer a lot. An alternative way of how this could be achieved using NumPy and without the use of np.where:

import numpy as np
from PIL import Image

img = Image.open('img.png') # n x m x 3
imga = img.convert("RGBA")  # n x m x 4

imga = np.asarray(imga) 
r, g, b, a = np.rollaxis(imga, axis=-1) # split into 4 n x m arrays 
r_m = r != 255 # binary mask for red channel, True for all non white values
g_m = g != 255 # binary mask for green channel, True for all non white values
b_m = b != 255 # binary mask for blue channel, True for all non white values

# combine the three masks using the binary "or" operation 
# multiply the combined binary mask with the alpha channel
a = a * ((r_m == 1) | (g_m == 1) | (b_m == 1))

# stack the img back together 
imga =  Image.fromarray(np.dstack([r, g, b, a]), 'RGBA')

I benchmarked my method against keithb's (highest rated answer), and mine is 18 faster (averaged over 102 images of size 124*124).

Jessalyn answered 13/4, 2022 at 15:13 Comment(1)
This solution is indeed 2.5x faster than Jonathan's.Lambrequin
B
1

I also have background colors to remove from complexe images (heterogenous thresholds and color, halo, etc.) and spent few hours with this issue. None of the solutions here were satisfying, because too simplistic.

So I decided to use directly the master code in this topic: Color-To-Alpha Gimp plugin. And it was surprisingly simple!

I simply copied col_to_alpha.py to my project, and called it like this:

import numpy as np
from col_to_alpha import color_to_alpha

def makeColorTransparent(image):
    image = image.convert("RGBA")
    pixels = np.array(image, dtype=np.ubyte)
    new_pixels = color_to_alpha(pixels, (255, 0, 255), 0.5 * 18, 0.75 * 193, 'cube', 'smooth')  # 0.5 and 0.75 match with the plugin sliders.
    return Image.fromarray(np.ubyte(new_pixels))

def main():
    image = Image.open('input.png')
    result = makeColorTransparent(image)
    result.save('output.png')

if __name__ == '__main__':
    main()
Biotechnology answered 12/10, 2023 at 7:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.