## Tuesday, 25 December 2012

### Customize matplotlib plots

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].

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):
ny, nx, depth = src_image.shape

return 0

def __call__(self, im, dpi):

class OffsetFilter(BaseFilter):
def __init__(self, offsets=None):
if offsets is None:
self.offsets = (0, 0)
else:
self.offsets = offsets

return int(max(*self.offsets)/72.*dpi)

ox, oy = self.offsets
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

return int(self.sigma*3/72.*dpi)

#offsetx, offsety = int(self.offsets[0]), int(self.offsets[1])
self.sigma/72.*dpi)
tgt_image[:,:,-1] = aa
tgt_image[:,:,:-1] = self.color
return tgt_image

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

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):
prop = fm.FontProperties(family=['Arial'], size=20)
# Set it to all texts in the axes
for text in ax.texts:
text.set_fontproperties(prop)

# Set our custom shadow_filter for all patches in the axes
for patch in ax.patches:


The shadow filter is quite simple and demonstrates how to write agg filters for matplotlib. I'll explain it below.
In [4]:
obj = None
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]
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
ax.patches,
36,
offsets=(-5,-7),
alpha=0.4
)
)
# This tells the axes to draw the drop-shadow
# Replace the black edges by darkened edges
dark_edges(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.
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);

In [15]:
circle = (xx+50) ** 2 + (yy+100) ** 2
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