From 30c2a09ad75e4c2aeaf96bd941e156c90abcf8af Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Thu, 5 Nov 2015 21:55:50 -0600 Subject: [PATCH 1/8] Add 'flatten' kwarg to backend_agg savefig for print_png and print_raw to properly handle transparency in animations --- lib/matplotlib/animation.py | 4 +-- lib/matplotlib/backends/backend_agg.py | 46 ++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 6a62384d1c54..ba61ccdc5ce2 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -222,7 +222,7 @@ def grab_frame(self, **savefig_kwargs): # Tell the figure to save its data to the sink, using the # frame format and dpi. self.fig.savefig(self._frame_sink(), format=self.frame_format, - dpi=self.dpi, **savefig_kwargs) + dpi=self.dpi, flatten=True, **savefig_kwargs) except RuntimeError: out, err = self._proc.communicate() verbose.report('MovieWriter -- Error ' @@ -358,7 +358,7 @@ def grab_frame(self, **savefig_kwargs): # frame format and dpi. myframesink = self._frame_sink() self.fig.savefig(myframesink, format=self.frame_format, - dpi=self.dpi, **savefig_kwargs) + dpi=self.dpi, flatten=True, **savefig_kwargs) myframesink.close() except RuntimeError: diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 5dea52363e86..1c0487c4eff4 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -508,14 +508,49 @@ def print_raw(self, filename_or_obj, *args, **kwargs): else: fileobj = filename_or_obj close = False + img = renderer._renderer.buffer_rgba() + + # Flatten RGBA if used with fileformat that doesn't handle trnasparency + if kwargs.get('flatten', False): + w, h = int(renderer.width), int(renderer.height) + img = np.array(memoryview(img)).reshape((w, h, 4)) + img = self.flatten_rgba(img) + try: - fileobj.write(renderer._renderer.buffer_rgba()) + fileobj.write(img) finally: if close: filename_or_obj.close() renderer.dpi = original_dpi print_rgba = print_raw + def flatten_rgba(self, src, bg=None): + """ + Flatten an RGBA image `src` with a background color `bg`. + The resulting image will have an alpha channel, but no + transparency. This is useful when interfacing with + file formats that don't support transparency or only support + boolean transparency. + """ + if bg is None: + bg = mcolors.colorConverter.to_rgb( + rcParams.get('savefig.facecolor', 'white')) + bg = tuple([int(x * 255.0) for x in bg]) + + # Numpy images have dtype=uint8 which will overflow + src = src.astype(dtype = np.uint16) + + alpha = src[:,:,3] + src_rgb = src[:,:,:3] + h, w, _ = src.shape + + result = np.empty((h,w,4)) + result[:,:,0] = (255 - alpha)*bg[0] + alpha*src_rgb[:,:,0] + result[:,:,1] = (255 - alpha)*bg[1] + alpha*src_rgb[:,:,1] + result[:,:,2] = (255 - alpha)*bg[2] + alpha*src_rgb[:,:,2] + result = (result/255).astype(np.uint8) + return result + def print_png(self, filename_or_obj, *args, **kwargs): FigureCanvasAgg.draw(self) renderer = self.get_renderer() @@ -528,7 +563,14 @@ def print_png(self, filename_or_obj, *args, **kwargs): close = False try: - _png.write_png(renderer._renderer, filename_or_obj, self.figure.dpi) + if kwargs.get('flatten', False): + w, h = int(renderer.width), int(renderer.height) + img = renderer._renderer.buffer_rgba() + img = np.array(memoryview(img)) + img = self.flatten_rgba(img).reshape((h, w, 4)) + _png.write_png(img, filename_or_obj, self.figure.dpi) + else: + _png.write_png(renderer._renderer, filename_or_obj, self.figure.dpi) finally: if close: filename_or_obj.close() From b8175a144dbe4bcde6aa1ac05f9e56ba59b61bef Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Thu, 5 Nov 2015 22:43:55 -0600 Subject: [PATCH 2/8] Fix width/height locations, and forgot to add alpha layer to flatten_rgba --- lib/matplotlib/backends/backend_agg.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 1c0487c4eff4..129164b7c423 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -542,13 +542,14 @@ def flatten_rgba(self, src, bg=None): alpha = src[:,:,3] src_rgb = src[:,:,:3] - h, w, _ = src.shape + w, h, _ = src.shape - result = np.empty((h,w,4)) + result = np.empty((w, h, 4)) result[:,:,0] = (255 - alpha)*bg[0] + alpha*src_rgb[:,:,0] result[:,:,1] = (255 - alpha)*bg[1] + alpha*src_rgb[:,:,1] result[:,:,2] = (255 - alpha)*bg[2] + alpha*src_rgb[:,:,2] result = (result/255).astype(np.uint8) + result[:,:,3] = 255 return result def print_png(self, filename_or_obj, *args, **kwargs): @@ -566,8 +567,8 @@ def print_png(self, filename_or_obj, *args, **kwargs): if kwargs.get('flatten', False): w, h = int(renderer.width), int(renderer.height) img = renderer._renderer.buffer_rgba() - img = np.array(memoryview(img)) - img = self.flatten_rgba(img).reshape((h, w, 4)) + img = np.array(memoryview(img)).reshape((h,w,4)) + img = self.flatten_rgba(img) _png.write_png(img, filename_or_obj, self.figure.dpi) else: _png.write_png(renderer._renderer, filename_or_obj, self.figure.dpi) From 2fa18897ec49fb3fcba1aa5cc892c100b91891a9 Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Fri, 6 Nov 2015 10:41:26 -0600 Subject: [PATCH 3/8] Move flatten_rgba to image.py and implment flatten_rgba for jpeg files. --- lib/matplotlib/backends/backend_agg.py | 65 ++++++++------------------ lib/matplotlib/image.py | 42 +++++++++++++++++ 2 files changed, 61 insertions(+), 46 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 129164b7c423..abd53596f8bb 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -39,6 +39,7 @@ from matplotlib.path import Path from matplotlib.transforms import Bbox, BboxBase from matplotlib import colors as mcolors +from matplotlib import image as mimage from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg from matplotlib import _png @@ -514,7 +515,7 @@ def print_raw(self, filename_or_obj, *args, **kwargs): if kwargs.get('flatten', False): w, h = int(renderer.width), int(renderer.height) img = np.array(memoryview(img)).reshape((w, h, 4)) - img = self.flatten_rgba(img) + img = mimage.flatten_rgba(img) try: fileobj.write(img) @@ -524,34 +525,6 @@ def print_raw(self, filename_or_obj, *args, **kwargs): renderer.dpi = original_dpi print_rgba = print_raw - def flatten_rgba(self, src, bg=None): - """ - Flatten an RGBA image `src` with a background color `bg`. - The resulting image will have an alpha channel, but no - transparency. This is useful when interfacing with - file formats that don't support transparency or only support - boolean transparency. - """ - if bg is None: - bg = mcolors.colorConverter.to_rgb( - rcParams.get('savefig.facecolor', 'white')) - bg = tuple([int(x * 255.0) for x in bg]) - - # Numpy images have dtype=uint8 which will overflow - src = src.astype(dtype = np.uint16) - - alpha = src[:,:,3] - src_rgb = src[:,:,:3] - w, h, _ = src.shape - - result = np.empty((w, h, 4)) - result[:,:,0] = (255 - alpha)*bg[0] + alpha*src_rgb[:,:,0] - result[:,:,1] = (255 - alpha)*bg[1] + alpha*src_rgb[:,:,1] - result[:,:,2] = (255 - alpha)*bg[2] + alpha*src_rgb[:,:,2] - result = (result/255).astype(np.uint8) - result[:,:,3] = 255 - return result - def print_png(self, filename_or_obj, *args, **kwargs): FigureCanvasAgg.draw(self) renderer = self.get_renderer() @@ -563,15 +536,17 @@ def print_png(self, filename_or_obj, *args, **kwargs): else: close = False + # Flatten RGBA if used as intermediate fileformat for something + # that doesn't support transparency (ie: Animations) + img = renderer._renderer + if kwargs.get('flatten', False): + img = img.buffer_rgba() + w, h = int(renderer.width), int(renderer.height) + img = renderer._renderer.buffer_rgba() + img = np.array(memoryview(img)).reshape((h, w, 4)) + img = mimage.flatten_rgba(img) try: - if kwargs.get('flatten', False): - w, h = int(renderer.width), int(renderer.height) - img = renderer._renderer.buffer_rgba() - img = np.array(memoryview(img)).reshape((h,w,4)) - img = self.flatten_rgba(img) - _png.write_png(img, filename_or_obj, self.figure.dpi) - else: - _png.write_png(renderer._renderer, filename_or_obj, self.figure.dpi) + _png.write_png(img, filename_or_obj, self.figure.dpi) finally: if close: filename_or_obj.close() @@ -606,24 +581,22 @@ def print_jpg(self, filename_or_obj, *args, **kwargs): *progressive*: If present, indicates that this image should be stored as a progressive JPEG file. """ - buf, size = self.print_to_buffer() + buf, (w, h) = self.print_to_buffer() + buf = np.array(memoryview(buf)).reshape((w, h, 4)) + if kwargs.pop("dryrun", False): return # The image is "pasted" onto a white background image to safely # handle any transparency - image = Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1) - color = mcolors.colorConverter.to_rgb( - rcParams.get('savefig.facecolor', 'white')) - color = tuple([int(x * 255.0) for x in color]) - background = Image.new('RGB', size, color) - background.paste(image, image) + buf = mimage.flatten_rgba(buf) + img = Image.frombuffer('RGBA', (w, h), buf, 'raw', 'RGBA', 0, 1) + options = restrict_dict(kwargs, ['quality', 'optimize', 'progressive']) - if 'quality' not in options: options['quality'] = rcParams['savefig.jpeg_quality'] - return background.save(filename_or_obj, format='jpeg', **options) + return img.save(filename_or_obj, format='jpeg', **options) print_jpeg = print_jpg # add TIFF support diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 7a3cd1dc9c39..28d012927637 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1502,3 +1502,45 @@ def thumbnail(infile, thumbfile, scale=0.1, interpolation='bilinear', ax.imshow(im, aspect='auto', resample=True, interpolation=interpolation) fig.savefig(thumbfile, dpi=dpi) return fig + +def flatten_rgba(src, bg=None): + """ + Flatten an RGBA image *src* with a background color *bg*. + The resulting image will have an alpha channel, but no transparency. + This can be useful when interfacing with file formats that don't support + transparency or only support boolean transparency. + + Parameters + ---------- + src : MxNx4 Numpy array, dtype=uint8 + Image source in RGBA to be flattened. + + bg : Tuple(int,int,int), optional + Background color to merge *src* with. If no bg color is provided + the color from the rcParam 'savefig.facecolor' will be use. + + Returns + ------- + dest : MxNx4 Numpy array, dtype=uint8 + """ + + if bg is None: + bg = mcolors.colorConverter.to_rgb( + rcParams.get('savefig.facecolor', 'white')) + bg = tuple([int(x * 255.0) for x in bg]) + + # Numpy images have dtype=uint8 which will overflow + src = src.astype(np.uint16) + + alpha = src[:,:,3] + src_rgb = src[:,:,:3] + w, h, _ = src.shape + + dest = np.empty((w, h, 4)) + dest[:,:,0] = (255 - alpha)*bg[0] + alpha*src_rgb[:,:,0] + dest[:,:,1] = (255 - alpha)*bg[1] + alpha*src_rgb[:,:,1] + dest[:,:,2] = (255 - alpha)*bg[2] + alpha*src_rgb[:,:,2] + dest = (dest/255).astype(np.uint8) + dest[:,:,3] = 255 + + return dest From e0f1fd8efc6138ba26038a2782f9edb75cc56842 Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Fri, 6 Nov 2015 10:58:34 -0600 Subject: [PATCH 4/8] Removed typo --- lib/matplotlib/backends/backend_agg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index abd53596f8bb..81f2cca89a56 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -542,7 +542,6 @@ def print_png(self, filename_or_obj, *args, **kwargs): if kwargs.get('flatten', False): img = img.buffer_rgba() w, h = int(renderer.width), int(renderer.height) - img = renderer._renderer.buffer_rgba() img = np.array(memoryview(img)).reshape((h, w, 4)) img = mimage.flatten_rgba(img) try: From d5fbd89fba493b80b0355add90f127c406c6bb22 Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Fri, 6 Nov 2015 11:04:03 -0600 Subject: [PATCH 5/8] Clarify comment --- lib/matplotlib/backends/backend_agg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 81f2cca89a56..902f4d07f064 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -585,8 +585,8 @@ def print_jpg(self, filename_or_obj, *args, **kwargs): if kwargs.pop("dryrun", False): return - # The image is "pasted" onto a white background image to safely - # handle any transparency + + # Flatten RGBA image to safely handle transparent regions buf = mimage.flatten_rgba(buf) img = Image.frombuffer('RGBA', (w, h), buf, 'raw', 'RGBA', 0, 1) From a57be1ff208c7828c5351a49c1b2ca26f2bc0967 Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Fri, 6 Nov 2015 11:33:23 -0600 Subject: [PATCH 6/8] Appease the PEP8 gods. Somewhat of a sacraficial lamb if you ask me. --- lib/matplotlib/image.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 28d012927637..4ae8cd1409f0 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1503,6 +1503,7 @@ def thumbnail(infile, thumbfile, scale=0.1, interpolation='bilinear', fig.savefig(thumbfile, dpi=dpi) return fig + def flatten_rgba(src, bg=None): """ Flatten an RGBA image *src* with a background color *bg*. @@ -1517,7 +1518,7 @@ def flatten_rgba(src, bg=None): bg : Tuple(int,int,int), optional Background color to merge *src* with. If no bg color is provided - the color from the rcParam 'savefig.facecolor' will be use. + the color from the rcParam 'savefig.facecolor' will be used. Returns ------- @@ -1529,18 +1530,18 @@ def flatten_rgba(src, bg=None): rcParams.get('savefig.facecolor', 'white')) bg = tuple([int(x * 255.0) for x in bg]) - # Numpy images have dtype=uint8 which will overflow + # Numpy images have dtype=uint8 which will overflow for these calculations src = src.astype(np.uint16) - alpha = src[:,:,3] - src_rgb = src[:,:,:3] + alpha = src[:, :, 3] + src_rgb = src[:, :, :3] w, h, _ = src.shape dest = np.empty((w, h, 4)) - dest[:,:,0] = (255 - alpha)*bg[0] + alpha*src_rgb[:,:,0] - dest[:,:,1] = (255 - alpha)*bg[1] + alpha*src_rgb[:,:,1] - dest[:,:,2] = (255 - alpha)*bg[2] + alpha*src_rgb[:,:,2] + dest[:, :, 0] = (255 - alpha)*bg[0] + alpha*src_rgb[:, :, 0] + dest[:, :, 1] = (255 - alpha)*bg[1] + alpha*src_rgb[:, :, 1] + dest[:, :, 2] = (255 - alpha)*bg[2] + alpha*src_rgb[:, :, 2] dest = (dest/255).astype(np.uint8) - dest[:,:,3] = 255 + dest[:, :, 3] = 255 return dest From 0193d5530799dca52d9a454764ae181c7295eec9 Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Sat, 7 Nov 2015 08:11:41 -0600 Subject: [PATCH 7/8] Add tests for flatten kwarg Fix (h, w) consistency --- lib/matplotlib/backends/backend_agg.py | 4 +-- lib/matplotlib/tests/test_image.py | 42 ++++++++++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 902f4d07f064..56299e4447ba 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -514,7 +514,7 @@ def print_raw(self, filename_or_obj, *args, **kwargs): # Flatten RGBA if used with fileformat that doesn't handle trnasparency if kwargs.get('flatten', False): w, h = int(renderer.width), int(renderer.height) - img = np.array(memoryview(img)).reshape((w, h, 4)) + img = np.array(memoryview(img)).reshape((h, w, 4)) img = mimage.flatten_rgba(img) try: @@ -581,7 +581,7 @@ def print_jpg(self, filename_or_obj, *args, **kwargs): should be stored as a progressive JPEG file. """ buf, (w, h) = self.print_to_buffer() - buf = np.array(memoryview(buf)).reshape((w, h, 4)) + buf = np.array(memoryview(buf)).reshape((h, w, 4)) if kwargs.pop("dryrun", False): return diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index fde7c7cc18c8..1fcfeac0b404 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -455,7 +455,7 @@ def test_nonuniformimage_setnorm(): @knownfailureif(not HAS_PIL) @cleanup -def test_jpeg_alpha(): +def test_flatten(): plt.figure(figsize=(1, 1), dpi=300) # Create an image that is all black, with a gradient from 0-1 in # the alpha channel from left to right. @@ -464,21 +464,43 @@ def test_jpeg_alpha(): plt.figimage(im) - buff = io.BytesIO() + jpg_buf = io.BytesIO() + pngF_buf = io.BytesIO() + png_buf = io.BytesIO() + with rc_context({'savefig.facecolor': 'red'}): - plt.savefig(buff, transparent=True, format='jpg', dpi=300) + plt.savefig(jpg_buf, transparent=True, format='jpg', dpi=300) + plt.savefig(pngF_buf, transparent=True, format='png', + flatten=True, dpi=300) + plt.savefig(png_buf, transparent=True, format='png', dpi=300) - buff.seek(0) - image = Image.open(buff) + jpg_buf.seek(0) + pngF_buf.seek(0) + png_buf.seek(0) + + jpg_im = Image.open(jpg_buf) + pngF_im = Image.open(pngF_buf) + png_im = Image.open(png_buf) # If this fails, there will be only one color (all black). If this # is working, we should have all 256 shades of grey represented. - print("num colors: ", len(image.getcolors(256))) - assert len(image.getcolors(256)) >= 175 and len(image.getcolors(256)) <= 185 + print("num colors [jpg]: ", len(jpg_im.getcolors(256))) + print("num colors [png, flattened]: ", len(pngF_im.getcolors(256))) + print("num colors [png, not flattened]: ", len(png_im.getcolors(256))) + + assert len(jpg_im.getcolors(256)) >= 175 and len(jpg_im.getcolors(256)) <= 185 + assert len(pngF_im.getcolor(256)) == 256 + assert len(png_im.getcolor(256)) == 256 + # The fully transparent part should be red, not white or black - # or anything else - print("corner pixel: ", image.getpixel((0, 0))) - assert image.getpixel((0, 0)) == (254, 0, 0) + # or anything else when flattened. + print("corner pixel [jpg]: ", jpg_im.getpixel((0, 0))) + print("corner pixel [png, flattened]: ", pngF_im.getpixel((0,0))) + print("corner pixel [png, not flattened]: ", png_im.getpixel((0,0))) + + assert jpg_im.getpixel((0, 0)) == (254, 0, 0) + assert pngF_im.getpixel((0,0)) == (255, 0, 0, 255) + assert png_im.getpixel((0,0)) == (255, 255, 255, 0) if __name__=='__main__': From b2c4560cee26074891ea2bef4d825c47c1506524 Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Sat, 7 Nov 2015 10:12:43 -0600 Subject: [PATCH 8/8] Fix typo in tests --- lib/matplotlib/tests/test_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 1fcfeac0b404..ce54aff1780e 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -489,8 +489,8 @@ def test_flatten(): print("num colors [png, not flattened]: ", len(png_im.getcolors(256))) assert len(jpg_im.getcolors(256)) >= 175 and len(jpg_im.getcolors(256)) <= 185 - assert len(pngF_im.getcolor(256)) == 256 - assert len(png_im.getcolor(256)) == 256 + assert len(pngF_im.getcolors(256)) == 256 + assert len(png_im.getcolors(256)) == 256 # The fully transparent part should be red, not white or black # or anything else when flattened.