如何使用 PIL 使所有的白色像素透明?

我正在尝试使用 Python 图像库使所有白色像素透明。(我是一个 C 黑客,正在尝试学习 python,所以要温柔一点) 我已经完成了转换(至少像素值看起来是正确的) ,但是我不知道如何将列表转换成一个缓冲区来重新创建图像。这是密码

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")
128502 次浏览

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

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.

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]);

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)

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.

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

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


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

@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

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