Convert RGBA PNG to RGB with PIL

I use PIL to convert a transparent PNG image downloaded from Django to a JPG file. The result looks broken.

Original file

transparent source file

The code

Image.open(object.logo.path).save('/tmp/output.jpg', 'JPEG') 

or

 Image.open(object.logo.path).convert('RGB').save('/tmp/output.png') 

Result

In both directions, the resulting image is as follows:

resulting file

Is there any way to fix this? I would like to have a white background where there used to be a transparent background.




Decision

Thanks to the excellent answers, I came up with the following collection of functions:

 import Image import numpy as np def alpha_to_color(image, color=(255, 255, 255)): """Set all fully transparent pixels of an RGBA image to the specified color. This is a very simple solution that might leave over some ugly edges, due to semi-transparent areas. You should use alpha_composite_with color instead. Source: http://stackoverflow.com/a/9166671/284318 Keyword Arguments: image -- PIL RGBA Image object color -- Tuple r, g, b (default 255, 255, 255) """ x = np.array(image) r, g, b, a = np.rollaxis(x, axis=-1) r[a == 0] = color[0] g[a == 0] = color[1] b[a == 0] = color[2] x = np.dstack([r, g, b, a]) return Image.fromarray(x, 'RGBA') def alpha_composite(front, back): """Alpha composite two RGBA images. Source: http://stackoverflow.com/a/9166671/284318 Keyword Arguments: front -- PIL RGBA Image object back -- PIL RGBA Image object """ front = np.asarray(front) back = np.asarray(back) result = np.empty(front.shape, dtype='float') alpha = np.index_exp[:, :, 3:] rgb = np.index_exp[:, :, :3] falpha = front[alpha] / 255.0 balpha = back[alpha] / 255.0 result[alpha] = falpha + balpha * (1 - falpha) old_setting = np.seterr(invalid='ignore') result[rgb] = (front[rgb] * falpha + back[rgb] * balpha * (1 - falpha)) / result[alpha] np.seterr(**old_setting) result[alpha] *= 255 np.clip(result, 0, 255) # astype('uint8') maps np.nan and np.inf to 0 result = result.astype('uint8') result = Image.fromarray(result, 'RGBA') return result def alpha_composite_with_color(image, color=(255, 255, 255)): """Alpha composite an RGBA image with a single color image of the specified color and the same size as the original image. Keyword Arguments: image -- PIL RGBA Image object color -- Tuple r, g, b (default 255, 255, 255) """ back = Image.new('RGBA', size=image.size, color=color + (255,)) return alpha_composite(image, back) def pure_pil_alpha_to_color_v1(image, color=(255, 255, 255)): """Alpha composite an RGBA Image with a specified color. NOTE: This version is much slower than the alpha_composite_with_color solution. Use it only if numpy is not available. Source: http://stackoverflow.com/a/9168169/284318 Keyword Arguments: image -- PIL RGBA Image object color -- Tuple r, g, b (default 255, 255, 255) """ def blend_value(back, front, a): return (front * a + back * (255 - a)) / 255 def blend_rgba(back, front): result = [blend_value(back[i], front[i], front[3]) for i in (0, 1, 2)] return tuple(result + [255]) im = image.copy() # don't edit the reference directly p = im.load() # load pixel array for y in range(im.size[1]): for x in range(im.size[0]): p[x, y] = blend_rgba(color + (255,), p[x, y]) return im def pure_pil_alpha_to_color_v2(image, color=(255, 255, 255)): """Alpha composite an RGBA Image with a specified color. Simpler, faster version than the solutions above. Source: http://stackoverflow.com/a/9459208/284318 Keyword Arguments: image -- PIL RGBA Image object color -- Tuple r, g, b (default 255, 255, 255) """ image.load() # needed for split() background = Image.new('RGB', image.size, color) background.paste(image, mask=image.split()[3]) # 3 is the alpha channel return background 

Performance

The simple non-compositional function alpha_to_color is the fastest solution, but leaves ugly borders because it does not handle translucent areas.

Both pure PIL and numpy solutions give great results, but alpha_composite_with_color much faster (8.93 ms) than pure_pil_alpha_to_color (79.6 ms). If numpy is available on your system, this will be the way to go. (Update: The new clean version of PIL is the fastest of all the solutions mentioned.)

 $ python -m timeit "import Image; from apps.front import utils; i = Image.open(u'logo.png'); i2 = utils.alpha_to_color(i)" 10 loops, best of 3: 4.67 msec per loop $ python -m timeit "import Image; from apps.front import utils; i = Image.open(u'logo.png'); i2 = utils.alpha_composite_with_color(i)" 10 loops, best of 3: 8.93 msec per loop $ python -m timeit "import Image; from apps.front import utils; i = Image.open(u'logo.png'); i2 = utils.pure_pil_alpha_to_color(i)" 10 loops, best of 3: 79.6 msec per loop $ python -m timeit "import Image; from apps.front import utils; i = Image.open(u'logo.png'); i2 = utils.pure_pil_alpha_to_color_v2(i)" 10 loops, best of 3: 1.1 msec per loop 
+69
python png rgba jpeg python-imaging-library
Feb 06 2018-12-12T00:
source share
7 answers

Here's a version that is much simpler - not sure how effective it is. Strongly based on some django snippet I found when creating RGBA -> JPG + BG support for sorting thumbnails.

 from PIL import Image png = Image.open(object.logo.path) png.load() # required for png.split() background = Image.new("RGB", png.size, (255, 255, 255)) background.paste(png, mask=png.split()[3]) # 3 is the alpha channel background.save('foo.jpg', 'JPEG', quality=80) 

Result @ 80%

enter image description here

Result @ 50%
enter image description here

+91
Feb 27 '12 at 2:03
source share

Using Image.alpha_composite , Yuji 'Tomita' Tomita's solution is simplified. This code can avoid the tuple index out of range error if png does not have an alpha channel.

 from PIL import Image png = Image.open(img_path).convert('RGBA') background = Image.new('RGBA', png.size, (255,255,255)) alpha_composite = Image.alpha_composite(background, png) alpha_composite.save('foo.jpg', 'JPEG', quality=80) 
+23
Nov 03 '15 at 19:08
source share

The transparent portions are mostly RGBA (0,0,0,0). Since the JPG has no transparency, the jpeg value is set to (0,0,0), which is black.

Around the circular icon there are pixels with non-zero RGB values, where A = 0. Thus, they look transparent in PNG, but funny in JPG.

You can set all the pixels where A == 0 to have R = G = B = 255 using numpy as follows:

 import Image import numpy as np FNAME = 'logo.png' img = Image.open(FNAME).convert('RGBA') x = np.array(img) r, g, b, a = np.rollaxis(x, axis = -1) r[a == 0] = 255 g[a == 0] = 255 b[a == 0] = 255 x = np.dstack([r, g, b, a]) img = Image.fromarray(x, 'RGBA') img.save('/tmp/out.jpg') 

enter image description here




Note that the logo also has some translucent pixels used to smooth the edges around words and icons. Saving in jpeg ignores translucency, which makes the resulting jpeg look uneven.

A better result can be done using the imagemagick convert command:

 convert logo.png -background white -flatten /tmp/out.jpg 

enter image description here




To make a better mix using numpy, you can use the alpha layout :

 import Image import numpy as np def alpha_composite(src, dst): ''' Return the alpha composite of src and dst. Parameters: src -- PIL RGBA Image object dst -- PIL RGBA Image object The algorithm comes from http://en.wikipedia.org/wiki/Alpha_compositing ''' # http://stackoverflow.com/a/3375291/190597 # http://stackoverflow.com/a/9166671/190597 src = np.asarray(src) dst = np.asarray(dst) out = np.empty(src.shape, dtype = 'float') alpha = np.index_exp[:, :, 3:] rgb = np.index_exp[:, :, :3] src_a = src[alpha]/255.0 dst_a = dst[alpha]/255.0 out[alpha] = src_a+dst_a*(1-src_a) old_setting = np.seterr(invalid = 'ignore') out[rgb] = (src[rgb]*src_a + dst[rgb]*dst_a*(1-src_a))/out[alpha] np.seterr(**old_setting) out[alpha] *= 255 np.clip(out,0,255) # astype('uint8') maps np.nan (and np.inf) to 0 out = out.astype('uint8') out = Image.fromarray(out, 'RGBA') return out FNAME = 'logo.png' img = Image.open(FNAME).convert('RGBA') white = Image.new('RGBA', size = img.size, color = (255, 255, 255, 255)) img = alpha_composite(img, white) img.save('/tmp/out.jpg') 

enter image description here

+12
Feb 06 '12 at 20:18
source share

Here's the solution in pure PIL.

 def blend_value(under, over, a): return (over*a + under*(255-a)) / 255 def blend_rgba(under, over): return tuple([blend_value(under[i], over[i], over[3]) for i in (0,1,2)] + [255]) white = (255, 255, 255, 255) im = Image.open(object.logo.path) p = im.load() for y in range(im.size[1]): for x in range(im.size[0]): p[x,y] = blend_rgba(white, p[x,y]) im.save('/tmp/output.png') 
+4
Feb 06 '12 at 22:17
source share

It is not broken. He does exactly what you told him; these pixels are black with full transparency. You will need to iterate over all the pixels and convert them with full transparency to white.

+1
Feb 06 2018-12-12T00:
source share
 import numpy as np import PIL def convert_image(image_file): image = Image.open(image_file) # this could be a 4D array PNG (RGBA) original_width, original_height = image.size np_image = np.array(image) new_image = np.zeros((np_image.shape[0], np_image.shape[1], 3)) # create 3D array for each_channel in range(3): new_image[:,:,each_channel] = np_image[:,:,each_channel] # only copy first 3 channels. # flushing np_image = [] return new_image 
0
Dec 12 '18 at 0:19
source share

import image

def fig2img (fig): "" "@brief Convert Matplotlib picture to PIL image in RGBA format and return it @param fig matplotlib @return picture Python image library image (PIL)" "" # put picture bitmap into empty array buf = fig2data (fig) w, h, d = buf.shape return Image.frombytes ("RGBA", (w, h), buf.tostring ())

def fig2data (fig): "" "@brief Convert the Matplotlib shape to a 4-dimensional array with RGBA channels and return it. @param fig The matplotlib shape @ return a zero-three-dimensional array of RGBA values" "" # draw pic. canvas.draw ()

 # Get the RGBA buffer from the figure w,h = fig.canvas.get_width_height() buf = np.fromstring ( fig.canvas.tostring_argb(), dtype=np.uint8 ) buf.shape = ( w, h, 4 ) # canvas.tostring_argb give pixmap in ARGB mode. Roll the ALPHA channel to have it in RGBA mode buf = np.roll ( buf, 3, axis = 2 ) return buf 

def rgba2rgb (img, c = (0, 0, 0), path = 'foo.jpg', is_already_saved = False, if_load = True): if not is_already_saved: background = Image.new ("RGB", img.size, c) background.paste (img, mask = img.split () [3]) # 3 - alpha channel

  background.save(path, 'JPEG', quality=100) is_already_saved = True if if_load: if is_already_saved: im = Image.open(path) return np.array(im) else: raise ValueError('No image to load.') 
-one
Jan 10 '18 at 11:37
source share



All Articles