1""" 2========== 3AGG filter 4========== 5 6Most pixel-based backends in Matplotlib use `Anti-Grain Geometry (AGG)`_ for 7rendering. You can modify the rendering of Artists by applying a filter via 8`.Artist.set_agg_filter`. 9 10.. _Anti-Grain Geometry (AGG): http://antigrain.com 11""" 12 13import matplotlib.cm as cm 14import matplotlib.pyplot as plt 15import matplotlib.transforms as mtransforms 16from matplotlib.colors import LightSource 17from matplotlib.artist import Artist 18import numpy as np 19 20 21def smooth1d(x, window_len): 22 # copied from http://www.scipy.org/Cookbook/SignalSmooth 23 s = np.r_[2*x[0] - x[window_len:1:-1], x, 2*x[-1] - x[-1:-window_len:-1]] 24 w = np.hanning(window_len) 25 y = np.convolve(w/w.sum(), s, mode='same') 26 return y[window_len-1:-window_len+1] 27 28 29def smooth2d(A, sigma=3): 30 window_len = max(int(sigma), 3) * 2 + 1 31 A = np.apply_along_axis(smooth1d, 0, A, window_len) 32 A = np.apply_along_axis(smooth1d, 1, A, window_len) 33 return A 34 35 36class BaseFilter: 37 38 def get_pad(self, dpi): 39 return 0 40 41 def process_image(padded_src, dpi): 42 raise NotImplementedError("Should be overridden by subclasses") 43 44 def __call__(self, im, dpi): 45 pad = self.get_pad(dpi) 46 padded_src = np.pad(im, [(pad, pad), (pad, pad), (0, 0)], "constant") 47 tgt_image = self.process_image(padded_src, dpi) 48 return tgt_image, -pad, -pad 49 50 51class OffsetFilter(BaseFilter): 52 53 def __init__(self, offsets=(0, 0)): 54 self.offsets = offsets 55 56 def get_pad(self, dpi): 57 return int(max(self.offsets) / 72 * dpi) 58 59 def process_image(self, padded_src, dpi): 60 ox, oy = self.offsets 61 a1 = np.roll(padded_src, int(ox / 72 * dpi), axis=1) 62 a2 = np.roll(a1, -int(oy / 72 * dpi), axis=0) 63 return a2 64 65 66class GaussianFilter(BaseFilter): 67 """Simple Gaussian filter.""" 68 69 def __init__(self, sigma, alpha=0.5, color=(0, 0, 0)): 70 self.sigma = sigma 71 self.alpha = alpha 72 self.color = color 73 74 def get_pad(self, dpi): 75 return int(self.sigma*3 / 72 * dpi) 76 77 def process_image(self, padded_src, dpi): 78 tgt_image = np.empty_like(padded_src) 79 tgt_image[:, :, :3] = self.color 80 tgt_image[:, :, 3] = smooth2d(padded_src[:, :, 3] * self.alpha, 81 self.sigma / 72 * dpi) 82 return tgt_image 83 84 85class DropShadowFilter(BaseFilter): 86 87 def __init__(self, sigma, alpha=0.3, color=(0, 0, 0), offsets=(0, 0)): 88 self.gauss_filter = GaussianFilter(sigma, alpha, color) 89 self.offset_filter = OffsetFilter(offsets) 90 91 def get_pad(self, dpi): 92 return max(self.gauss_filter.get_pad(dpi), 93 self.offset_filter.get_pad(dpi)) 94 95 def process_image(self, padded_src, dpi): 96 t1 = self.gauss_filter.process_image(padded_src, dpi) 97 t2 = self.offset_filter.process_image(t1, dpi) 98 return t2 99 100 101class LightFilter(BaseFilter): 102 103 def __init__(self, sigma, fraction=0.5): 104 self.gauss_filter = GaussianFilter(sigma, alpha=1) 105 self.light_source = LightSource() 106 self.fraction = fraction 107 108 def get_pad(self, dpi): 109 return self.gauss_filter.get_pad(dpi) 110 111 def process_image(self, padded_src, dpi): 112 t1 = self.gauss_filter.process_image(padded_src, dpi) 113 elevation = t1[:, :, 3] 114 rgb = padded_src[:, :, :3] 115 alpha = padded_src[:, :, 3:] 116 rgb2 = self.light_source.shade_rgb(rgb, elevation, 117 fraction=self.fraction) 118 return np.concatenate([rgb2, alpha], -1) 119 120 121class GrowFilter(BaseFilter): 122 """Enlarge the area.""" 123 124 def __init__(self, pixels, color=(1, 1, 1)): 125 self.pixels = pixels 126 self.color = color 127 128 def __call__(self, im, dpi): 129 alpha = np.pad(im[..., 3], self.pixels, "constant") 130 alpha2 = np.clip(smooth2d(alpha, self.pixels / 72 * dpi) * 5, 0, 1) 131 new_im = np.empty((*alpha2.shape, 4)) 132 new_im[:, :, :3] = self.color 133 new_im[:, :, 3] = alpha2 134 offsetx, offsety = -self.pixels, -self.pixels 135 return new_im, offsetx, offsety 136 137 138class FilteredArtistList(Artist): 139 """A simple container to filter multiple artists at once.""" 140 141 def __init__(self, artist_list, filter): 142 super().__init__() 143 self._artist_list = artist_list 144 self._filter = filter 145 146 def draw(self, renderer): 147 renderer.start_rasterizing() 148 renderer.start_filter() 149 for a in self._artist_list: 150 a.draw(renderer) 151 renderer.stop_filter(self._filter) 152 renderer.stop_rasterizing() 153 154 155def filtered_text(ax): 156 # mostly copied from contour_demo.py 157 158 # prepare image 159 delta = 0.025 160 x = np.arange(-3.0, 3.0, delta) 161 y = np.arange(-2.0, 2.0, delta) 162 X, Y = np.meshgrid(x, y) 163 Z1 = np.exp(-X**2 - Y**2) 164 Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) 165 Z = (Z1 - Z2) * 2 166 167 # draw 168 ax.imshow(Z, interpolation='bilinear', origin='lower', 169 cmap=cm.gray, extent=(-3, 3, -2, 2), aspect='auto') 170 levels = np.arange(-1.2, 1.6, 0.2) 171 CS = ax.contour(Z, levels, 172 origin='lower', 173 linewidths=2, 174 extent=(-3, 3, -2, 2)) 175 176 # contour label 177 cl = ax.clabel(CS, levels[1::2], # label every second level 178 inline=True, 179 fmt='%1.1f', 180 fontsize=11) 181 182 # change clabel color to black 183 from matplotlib.patheffects import Normal 184 for t in cl: 185 t.set_color("k") 186 # to force TextPath (i.e., same font in all backends) 187 t.set_path_effects([Normal()]) 188 189 # Add white glows to improve visibility of labels. 190 white_glows = FilteredArtistList(cl, GrowFilter(3)) 191 ax.add_artist(white_glows) 192 white_glows.set_zorder(cl[0].get_zorder() - 0.1) 193 194 ax.xaxis.set_visible(False) 195 ax.yaxis.set_visible(False) 196 197 198def drop_shadow_line(ax): 199 # copied from examples/misc/svg_filter_line.py 200 201 # draw lines 202 l1, = ax.plot([0.1, 0.5, 0.9], [0.1, 0.9, 0.5], "bo-") 203 l2, = ax.plot([0.1, 0.5, 0.9], [0.5, 0.2, 0.7], "ro-") 204 205 gauss = DropShadowFilter(4) 206 207 for l in [l1, l2]: 208 209 # draw shadows with same lines with slight offset. 210 xx = l.get_xdata() 211 yy = l.get_ydata() 212 shadow, = ax.plot(xx, yy) 213 shadow.update_from(l) 214 215 # offset transform 216 ot = mtransforms.offset_copy(l.get_transform(), ax.figure, 217 x=4.0, y=-6.0, units='points') 218 219 shadow.set_transform(ot) 220 221 # adjust zorder of the shadow lines so that it is drawn below the 222 # original lines 223 shadow.set_zorder(l.get_zorder() - 0.5) 224 shadow.set_agg_filter(gauss) 225 shadow.set_rasterized(True) # to support mixed-mode renderers 226 227 ax.set_xlim(0., 1.) 228 ax.set_ylim(0., 1.) 229 230 ax.xaxis.set_visible(False) 231 ax.yaxis.set_visible(False) 232 233 234def drop_shadow_patches(ax): 235 # Copied from barchart_demo.py 236 N = 5 237 men_means = [20, 35, 30, 35, 27] 238 239 ind = np.arange(N) # the x locations for the groups 240 width = 0.35 # the width of the bars 241 242 rects1 = ax.bar(ind, men_means, width, color='r', ec="w", lw=2) 243 244 women_means = [25, 32, 34, 20, 25] 245 rects2 = ax.bar(ind + width + 0.1, women_means, width, 246 color='y', ec="w", lw=2) 247 248 # gauss = GaussianFilter(1.5, offsets=(1, 1)) 249 gauss = DropShadowFilter(5, offsets=(1, 1)) 250 shadow = FilteredArtistList(rects1 + rects2, gauss) 251 ax.add_artist(shadow) 252 shadow.set_zorder(rects1[0].get_zorder() - 0.1) 253 254 ax.set_ylim(0, 40) 255 256 ax.xaxis.set_visible(False) 257 ax.yaxis.set_visible(False) 258 259 260def light_filter_pie(ax): 261 fracs = [15, 30, 45, 10] 262 explode = (0, 0.05, 0, 0) 263 pies = ax.pie(fracs, explode=explode) 264 265 light_filter = LightFilter(9) 266 for p in pies[0]: 267 p.set_agg_filter(light_filter) 268 p.set_rasterized(True) # to support mixed-mode renderers 269 p.set(ec="none", 270 lw=2) 271 272 gauss = DropShadowFilter(9, offsets=(3, 4), alpha=0.7) 273 shadow = FilteredArtistList(pies[0], gauss) 274 ax.add_artist(shadow) 275 shadow.set_zorder(pies[0][0].get_zorder() - 0.1) 276 277 278if __name__ == "__main__": 279 280 fix, axs = plt.subplots(2, 2) 281 282 filtered_text(axs[0, 0]) 283 drop_shadow_line(axs[0, 1]) 284 drop_shadow_patches(axs[1, 0]) 285 light_filter_pie(axs[1, 1]) 286 axs[1, 1].set_frame_on(True) 287 288 plt.show() 289