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