Tuesday, 25 December 2012

Customize matplotlib plots

Link to the notebook.

IPython is my favorite tool ever, so matplotlib has to be my favorite plotting lib. I heard that matplotlib doesn't look nice or at least not Web-2.1-stylish [1]: that is not true, but you have to write some code to make the plots stylish. My result actually isn't that pretty, I'm sure if my friend the desginer would help, we could make an awesome pie-chart.
I just want to show that the tools are there.

BTW the matplotlib xkcd-style inspired me to try this.

In [22]:
%pylab inline
%config InlineBackend.figure_format = 'png'
import matplotlib.font_manager as fm
import matplotlib.colors as mc
from matplotlib.artist import Artist
import sys
sys.version
Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].
For more information, type 'help(pylab)'.
Out[22]:
'3.2.3 (default, Oct 27 2012, 21:51:31) \n[GCC 4.2.1 Compatible Apple Clang 4.1 ((tags/Apple/clang-421.11.66))]'

Helpers

These nice helpers are from demo_agg_filter.py, I only added those we need. We will treat these as black box, I havent studied them, but I show how to write a simple filter. Show helpers.
In [2]:
class FilteredArtistList(Artist):
    def __init__(self, artist_list, filter):
        self._artist_list = artist_list
        self._filter = filter
        Artist.__init__(self)

    def draw(self, renderer):
        renderer.start_rasterizing()
        renderer.start_filter()
        for a in self._artist_list:
            a.draw(renderer)
        renderer.stop_filter(self._filter)
        renderer.stop_rasterizing()
        
def smooth1d(x, window_len):
    # copied from http://www.scipy.org/Cookbook/SignalSmooth

    s=np.r_[2*x[0]-x[window_len:1:-1],x,2*x[-1]-x[-1:-window_len:-1]]
    w = np.hanning(window_len)
    y=np.convolve(w/w.sum(),s,mode='same')
    return y[window_len-1:-window_len+1]

def smooth2d(A, sigma=3):

    window_len = max(int(sigma), 3)*2+1
    A1 = np.array([smooth1d(x, window_len) for x in np.asarray(A)])
    A2 = np.transpose(A1)
    A3 = np.array([smooth1d(x, window_len) for x in A2])
    A4 = np.transpose(A3)

    return A4

class BaseFilter(object):
    def prepare_image(self, src_image, dpi, pad):
        ny, nx, depth = src_image.shape
        #tgt_image = np.zeros([pad*2+ny, pad*2+nx, depth], dtype="d")
        padded_src = np.zeros([pad*2+ny, pad*2+nx, depth], dtype="d")
        padded_src[pad:-pad, pad:-pad,:] = src_image[:,:,:]

        return padded_src#, tgt_image

    def get_pad(self, dpi):
        return 0

    def __call__(self, im, dpi):
        pad = self.get_pad(dpi)
        padded_src = self.prepare_image(im, dpi, pad)
        tgt_image = self.process_image(padded_src, dpi)
        return tgt_image, -pad, -pad
    
class OffsetFilter(BaseFilter):
    def __init__(self, offsets=None):
        if offsets is None:
            self.offsets = (0, 0)
        else:
            self.offsets = offsets

    def get_pad(self, dpi):
        return int(max(*self.offsets)/72.*dpi)

    def process_image(self, padded_src, dpi):
        ox, oy = self.offsets
        a1 = np.roll(padded_src, int(ox/72.*dpi), axis=1)
        a2 = np.roll(a1, -int(oy/72.*dpi), axis=0)
        return a2

class GaussianFilter(BaseFilter):
    def __init__(self, sigma, alpha=0.5, color=None):
        self.sigma = sigma
        self.alpha = alpha
        if color is None:
            self.color=(0, 0, 0)
        else:
            self.color=color

    def get_pad(self, dpi):
        return int(self.sigma*3/72.*dpi)


    def process_image(self, padded_src, dpi):
        #offsetx, offsety = int(self.offsets[0]), int(self.offsets[1])
        tgt_image = np.zeros_like(padded_src)
        aa = smooth2d(padded_src[:,:,-1]*self.alpha,
                      self.sigma/72.*dpi)
        tgt_image[:,:,-1] = aa
        tgt_image[:,:,:-1] = self.color
        return tgt_image

class DropShadowFilter(BaseFilter):
    def __init__(self, sigma, alpha=0.3, color=None, offsets=None):
        self.gauss_filter = GaussianFilter(sigma, alpha, color)
        self.offset_filter = OffsetFilter(offsets)

    def get_pad(self, dpi):
        return max(self.gauss_filter.get_pad(dpi),
                   self.offset_filter.get_pad(dpi))

    def process_image(self, padded_src, dpi):
        t1 = self.gauss_filter.process_image(padded_src, dpi)
        t2 = self.offset_filter.process_image(t1, dpi)
        return t2

Axes customizers

In [3]:
def dark_edges(ax):
    # Iterate over the patches in the axes
    for patch in ax.patches:
        # Get the facecolor of the patch
        ec = patch.get_facecolor()
        # Make that color a bit darker and set it as edge color
        patch.set_edgecolor(tuple(x * 0.7 for x in ec[:3]) + (ec[3],))
        patch.set_linewidth(1.4)
        
def change_fonts(ax):
    # Load a font family
    prop = fm.FontProperties(family=['Arial'], size=20)
    # Set it to all texts in the axes
    for text in ax.texts:
        text.set_fontproperties(prop)
        
def shade_patches(ax):
    # Set our custom shadow_filter for all patches in the axes
    for patch in ax.patches:
        patch.set_agg_filter(shadow_filter)

My shadow filter

The shadow filter is quite simple and demonstrates how to write agg filters for matplotlib. I'll explain it below.
In [4]:
obj = None
def shadow_filter(image, dpi):
    global obj
    # Get the shape of the image
    nx, ny, depth = image.shape
    # Create a mash grid
    xx, yy = numpy.mgrid[0:nx, 0:ny]
    # Draw a circular "shadow"
    circle = (xx + nx * 4) ** 2 + (yy + ny) ** 2
    # Normalize
    circle -= circle.min()
    circle = circle / circle.max()
    # Steepness
    value = circle.clip(0.3, 0.6) + 0.4
    saturation = 1 - circle.clip(0.7, 0.8)
    # Normalize
    saturation -= saturation.min() - 0.1
    saturation = saturation / saturation.max()
    # Convert the rgb part (without alpha) to hsv
    hsv = mc.rgb_to_hsv(image[:,:,:3])
    # Multiply the value of hsv image with the shadow
    hsv[:,:,2] = hsv[:,:,2] * value
    # Highlights with saturation
    hsv[:,:,1] = hsv[:,:,1] * saturation
    # Copy the hsv back into the image (we haven't touched alpha)
    image[:,:,:3] = mc.hsv_to_rgb(hsv)
    # the return values are: new_image, offset_x, offset_y
    return image, 0, 0

Plot the pie with all customizations

In [5]:
# We create a nice large figure
figure(1, figsize=(12,12))
ax = axes([0.1, 0.1, 0.8, 0.8])
labels = 'Frogs', 'Hogs', 'Dogs', 'Logs'
fracs = [15, 30, 45, 10]
# Explode in a regular fashion (the circle is still "round")
expl = [x / 1000 for x in fracs]
# We draw the whole pie
ax.pie(
    fracs,
    explode=expl,
    labels=labels,
    autopct='%1.1f%%',
    startangle=90,
    colors=[
        '#FF8A8A',
        '#86BCFF',
        '#33FDC0',
        '#FFFFAA'
    ]
)

# We draw all the the patches again,
# this time with the drop-shadow filter
shadow = FilteredArtistList(
    ax.patches,
    DropShadowFilter(
        36,
        offsets=(-5,-7), 
        alpha=0.4
    )
)
# This tells the axes to draw the drop-shadow
ax.add_artist(shadow)
# Replace the black edges by darkened edges
dark_edges(ax)
# Add a shade to the patches
shade_patches(ax)
# Change the font
change_fonts(ax)

How the shadow filter works

NumPy is fast, but only if it hasn't to return to python and native code is running. So how do you draw a shadow (gradient in photoshop)? You need things you can multipy with your image, they're called mesh-grids.
In [6]:
xx, yy = numpy.mgrid[0:10, 0:10]
In [7]:
xx
Out[7]:
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
       [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
       [3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
       [4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
       [5, 5, 5, 5, 5, 5, 5, 5, 5, 5],
       [6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
       [7, 7, 7, 7, 7, 7, 7, 7, 7, 7],
       [8, 8, 8, 8, 8, 8, 8, 8, 8, 8],
       [9, 9, 9, 9, 9, 9, 9, 9, 9, 9]])
In [8]:
yy
Out[8]:
array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
It is basicly the y and x coordinate but expanded to the whole image that you can easily multiply these.
Lets make a circular shadow (gradient):
In [9]:
circle = (xx - 5) ** 2 + (yy - 5) ** 2
circle
Out[9]:
array([[50, 41, 34, 29, 26, 25, 26, 29, 34, 41],
       [41, 32, 25, 20, 17, 16, 17, 20, 25, 32],
       [34, 25, 18, 13, 10,  9, 10, 13, 18, 25],
       [29, 20, 13,  8,  5,  4,  5,  8, 13, 20],
       [26, 17, 10,  5,  2,  1,  2,  5, 10, 17],
       [25, 16,  9,  4,  1,  0,  1,  4,  9, 16],
       [26, 17, 10,  5,  2,  1,  2,  5, 10, 17],
       [29, 20, 13,  8,  5,  4,  5,  8, 13, 20],
       [34, 25, 18, 13, 10,  9, 10, 13, 18, 25],
       [41, 32, 25, 20, 17, 16, 17, 20, 25, 32]])
In [10]:
imshow(circle, cmap = cm.Greys_r);
We can change the center of the circle:
In [11]:
circle = (xx) ** 2 + (yy) ** 2
imshow(circle, cmap = cm.Greys_r);
We can change the brightness of the circle:
In [12]:
circle = (xx - 5) ** 2 + (yy - 5) ** 2 - 20
circle = circle.clip(0)
imshow(circle, cmap = cm.Greys_r);
In [13]:
circle = (xx - 5) ** 2 + (yy - 5) ** 2
circle = circle.clip(0, 20)
imshow(circle, cmap = cm.Greys_r);
If the center of the circle is far enough away, the shadow looses the roundness, please forgive me that it is still called circle. :-)
In [14]:
circle = (xx+100) ** 2 + (yy+100) ** 2
imshow(circle, cmap = cm.Greys_r);
Clipping makes the shadow steeper:
In [15]:
circle = (xx+50) ** 2 + (yy+100) ** 2
# Normalize the gradient
circle -= circle.min()
circle = circle / circle.max()
# Steepness
circle = circle.clip(0.3, 0.7)
imshow(circle, cmap = cm.Greys_r);
Now we create a red image. We initialze all zeros and set the red channel to 1. In image[:, :, 0] the first : means all rows, the second means all columns and 0 means the first rgb channel -> red.
In [16]:
image = zeros((10, 10, 3))
image[:, :, 0] = 1
In [17]:
imshow(image);
In numpy x[] = means replace. So we replace the red channel of all pixels image[:, :, 0] with the red channel of all pixels times circle.
In [18]:
image[:, :, 0] = image[:, :, 0] * circle
imshow(image);
[1] I never use .0 versions of anything

No comments:

Post a Comment