haseman
haseman

Reputation: 11313

How to use PIL to make all white pixels transparent?

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

Upvotes: 105

Views: 150798

Answers (11)

Dardo
Dardo

Reputation: 91

from PIL import Image

def make_color_transparent_pil(image: Image, color: tuple[int, int, int], thresh2: int = 0) -> Image:
    """
    Add alpha to image pixels within threshold distance of a color using PIL.ImageMath.

    Args:
        image: the RGB or RGBA PIL Image
        color: the RGB color
        thresh2: the distance threshold squared (default 0)
        
    Returns:
        An RGBA PIL Image
    """
    from PIL import ImageMath

    image.putalpha(
        ImageMath.lambda_eval(
            lambda a: a["convert"](((((a["t"] - a["d"](a["c"], (a["r"], a["g"], a["b"]))) >> 31) + 1) ^ 1) * a["a"], "L"),  # pylint: disable=line-too-long
            d=lambda a, b: sum((x - y) ** 2 for (x, y) in zip(a, b)),
            t=thresh2,
            c=color,
            **dict(zip(["r", "g", "b", "a"], image.convert("RGBA").split()))
        )
    )
    return image

def make_color_transparent_np(image: Image, color: tuple[int, int, int], thresh2: int = 0) -> Image:
    """
    Add alpha to image pixels within threshold distance of a color using numpy.

    Args:
        image: the RGB or RGBA PIL Image
        color: the RGB color
        thresh2: the distance threshold squared (default 0)
        
    Returns:
        An RGBA PIL Image
    """
    import numpy as np

    r, g, b, a = np.rollaxis(np.asarray(image.convert("RGBA")), axis=-1).astype(np.int32)
    image.putalpha(Image.fromarray((((((thresh2 - sum((x - y) ** 2 for (x, y) in zip(color, (r, g, b)))) >> 31) + 1) ^ 1) * a).astype(np.uint8)))  # pylint: disable=line-too-long

    return image


if __name__ == "__main__":
    import sys

    make_color_transparent_pil(Image.open(sys.argv[1]), (255, 255, 255)).save(sys.argv[2])

Upvotes: 9

FabienRohrer
FabienRohrer

Reputation: 1834

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

Upvotes: 1

L.Lauenburg
L.Lauenburg

Reputation: 472

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

Upvotes: 2

leifdenby
leifdenby

Reputation: 1478

@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

Upvotes: 2

Jonathan Dauwe
Jonathan Dauwe

Reputation: 313

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

Upvotes: 5

egeres
egeres

Reputation: 117

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://stackoverflow.com/a/62334218/5189462 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")

Upvotes: 2

kdebugging
kdebugging

Reputation: 561

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

Upvotes: 5

keithb
keithb

Reputation: 1990

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.

Upvotes: 57

Marco Spinaci
Marco Spinaci

Reputation: 1879

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.

Upvotes: 16

PythonProgrammi
PythonProgrammi

Reputation: 23463

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)

Upvotes: 4

cr333
cr333

Reputation: 1625

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

Upvotes: 130

Related Questions