Configure notebook style (see NBCONFIG.ipynb), add imports and paths. The %run magic used below requires IPython 2.0 or higher.
%run NBCONFIG.ipynb
Canvas is the primary object that users will deal with use in pyparty
. The canvas itself is a composite class, storing both an image (numpy.ndarray : .image), optional background, and a custom container for storing and manipulating particles (pyparty.ParticleManager : .particles). The canvas maintains this separation, whereas simple masking tends to merge these entities.
To create a canvas, which is an image (ndarray) and a special container (ParticleManager) that stores, tracks and draws particles into the image (via array indexing).
from pyparty import Canvas
c=Canvas()
The canvas tries to acts as an intuitive hybrid object for an image (c.image) with particles (c.particles), and selectively promotes the APIs of both objects.
print type(c.image), type(c.particles)
<type 'numpy.ndarray'> <class 'pyparty.tools.manager.ParticleManager'>
The reason that _particles is a private method is because Canvas promotes its own ParticleManager api through particles, which be default, returns a list of particle names (see next section). Since canvas is a composite class between an ndarray and Particle manager, I decided that attribute lookup should defer to the image (eg):
print c.image.shape
print c.shape
(512, 512, 3) (512, 512, 3)
While slicing/indexing (see below) should defer to the particles, as the user could easily manipulate the image before and after applying particles.
Currently we have no particles. When there are 0 particles, or more than 50 particles, the printout will be explicit. Otherwise it is succinct:
print c.particles
<< 0 particles / at 0xbc01f2c >>
A variety of particles can be added directly to the canvas through the add() attribute. At any point, the image with drawn particles can be visualized with show(), a wrapper to matplotlib.plt.imshow() First let's see what available particle types there are and then add a circle:
c.available()
(('simple:', ['bezier', 'circle', 'ellipse', 'line']), ('n-circle:', ['dimer', 'tetramer', 'trimer']), ('polygon:', ['polygon', 'rectangle', 'square', 'triangle']))
We can also specify a subtype:
c.available('simple')
('bezier', 'circle', 'ellipse', 'line')
c.add('circle')
c.show()
print c.particles
NAME PTYPE CENTER PHI 0 circle_0 circle (256, 256) 0.0
Notice that by default .add() returned a blue circle of radius 20 pixels at position 256, 256. This default position is arbitrary, and does not care what background or canvas size we are using (eg, for a 10x10 pixel image, this would not be visible). The particle was assigned a unique name, "circle_0"
I'll add some simple shapes with some custom names, which we can access through the names attribute. It's sometimes use to have the names handy.
# Randomize the centers (but avoid edges)
from random import randint
def cran(): return (randint(100, 400), randint(100,400))
c.add('circle', name='top_right', radius=75, center=(400,100), color='y')
c.add('ellipse', name='bottom_left', center=(50,400), color='green', phi=0)
c.add('circle', name='topleft_corner', radius=100, center=(0,0), color=(20,30,50) )
c.add('circle', name='off_image', radius=50, center=(900,200), color='teal')
# Randomized centers
c.add('line', color='purple', length=200, width=10, center=cran())
c.add('square', length=50, color='magenta', phi=10, center=cran())
c.add('square', color='honeydew', length=50, center=cran())
c.add('triangle', color='orange', length=50, center=cran())
print c.particles
NAME PTYPE CENTER PHI 0 circle_0 circle (256, 256) 0.0 1 top_right circle (400, 100) 0.0 2 bottom_left ellipse (50, 400) 0.0 3 topleft_corner circle (0, 0) 0.0 4 off_image circle (900, 200) 0.0 5 line_0 line (156, 257) 0.0 6 square_0 square (147, 346) 10.0 7 square_1 square (120, 327) 0.0 8 triangle_0 triangle (272, 306) 0.0
If we don't provide a name, they are automatically generated of the form (shape_index).
Let's store these particles for later use.
STORED_PARTICLES = c.particles
c.area
array([ 1245., 17647., 617., 31397., 7825., 987., 1192., 1225., 1087.])
Anytime new particles are set (as above, or returned by slicing) a new copy is returned to avoid referring to the same particles in Memory. This behavior can be changed in the CONFIG file.
At any point, the image with drawn particles can be visualized with canvas.show(), a wrapper to matplotlib.plt.imshow(). I've put a special title keyword for convience.
c.show(title='Example Plot');
The default background is a (512 x 512 X 3) white RGB array. This was used implicitly above. We can actually pass any array (ie image) and the particles will be redrawn over it. In the next cell, we'll load an image from skimage's pre-packaged data, which is 512 x 512 as well.
Let's also show off the annotate argument to show, which adds a generic title and axis labels.
from skimage.data import moon, lena
MOON = moon()
LENA = lena()
c.background = MOON
c.show(annotate=True);
print c.particles
NAME PTYPE CENTER PHI 0 circle_0 circle (256, 256) 0.0 1 top_right circle (400, 100) 0.0 2 bottom_left ellipse (50, 400) 0.0 3 topleft_corner circle (0, 0) 0.0 4 off_image circle (900, 200) 0.0 5 line_0 line (156, 257) 0.0 6 square_0 square (147, 346) 10.0 7 square_1 square (120, 327) 0.0 8 triangle_0 triangle (272, 306) 0.0
No handlers could be found for logger "pyparty.utils"
The moon() background is an example of passing an ndarray directly as a background. Backgrounds can be generated a number of ways, including:
By default, the resolution of the image will be set to the new resolution of the background. For example:
BLACK_BACKGROUND = np.zeros( (200,200) )
c.background = BLACK_BACKGROUND
c.show();
We can used Canvas set_bg(bg, keepres=False) method to set the background and resolution seprately. The keepres paramater implements the following behavior:
set_bg() returns a new Canvas, unless the inplace flag is used.
These are best illustrated by example
from pyparty import splot
c.reset_background()
ax1, ax2, ax3, ax4 = splot(2,2)
c.show(ax1, title='(%s, %s)'%c.rez)
# same as setting c.background = BLACK_BACKGROUND
c2 = c.set_bg(BLACK_BACKGROUND, keepres=False)
c2.show(ax2, title='(%s, %s)'%c2.rez)
# black background; keep original res
c3 = c.set_bg(BLACK_BACKGROUND, keepres=True)
c3.show(ax3, title='(%s, %s)'%c3.rez)
# set background to entirely different res
c4 = c.set_bg(BLACK_BACKGROUND, keepres=(300,600))
c4.show(ax4, title='(%s, %s)'%c4.rez);
The resolution can also be changed directly through the rez attribute. The grid will implicitly scale itself to the rez attribute! Remember, a new array-type background sets the resolution automatically . Therefore, changing the resolution before / after setting a background will scale the canvas differently.
ax1, ax2 = splot(1,2)
c.reset_background()
c.rez = (700,700)
c.background='orange'
c.show(ax1, title="Resolution set first")
c.reset_background()
c.background='orange'
c.rez = (700,700)
c.show(ax2, title="BG reset first");
The behavior is intentional by design. The rez refers to the image resolution, which is kept separate from the inherent background resolution, and I didn't want pypary
altering the user-supplied background in any manner to compensate for resolution changes (for example, image stretching).
It is also possible to set the background to a zoomed region within the current background. This will automatically set the rez. This is done through canvas.zoom_bg(xi,yi,xf,yf), inplace=False. Merely provide the (xi,yi) and (xf,yf), the corners of the cropping rectangle.
c.background = LENA
ax1, ax2 = splot(1,2)
c.show(ax1, title='Full lena')
c2 = c.zoom_bg(200,200,400,400)
c2.show(ax2, title='Sup girl');
If you want to crop the image (bg + particles) instead of just the background, use the crop utility. Note that coords have to be passed as a container in this case. As with any image-altering function, only an image is returned, not a Canvas.
from pyparty import crop
coords = (200,200,400,400) #Must be in list/tuple
imshow(crop( c.image, coords ) );
Relative or absolute paths are valid background parameters. If the string does not lead to a valid filepath, the string is interpreted as a color string. Instead of loading from a file, I'll load some pyparty example data which has different resolution; but this should work from paths as well.
from pyparty.data import lena_who
c.background = lena_who()
c.show()
print 'newshape = ', c.shape
newshape = (737, 596, 3)
The particles appear smaller because the resolution of this second image is larger. It is important to keep in mind that particle coordinates are absolute in pixels. To illustrate this better, let's extend the background to (1024, 1024).
c.rez = (1024, 1024)
print 'newshape = ', c.shape
c.show();
newshape = (1024, 1024, 3)
This example illustrates several important features:
reset_background reverts to original background AND resolution in canvas
c.reset_background()
c.show();
Canvas's .show() method wraps skimage/matplotlib.imshow(), but also optionally accepts matplotlib AxesSubPlot objects via the axes keyword. Additionally, the cmap keyword (or passing a color map directly) will gray-convert the image and apply the color map. In addition, I will import subplot() which is a light wrapper to plt.subplots() that returns AxesSubPlot instances, either grouped or flat through the flatten=True keyword argument.
c.background=MOON
ax1, ax2, ax3, ax4 = splot(2, 2, figsize = (10,10))
c.show(ax1, title='ax1')
c.show(ax2, 'Blues', title='ax2 (colomapped)')
c.show(ax3, 'Blues', bgonly=True, title='ax3 (only bg colormapped)');
#Change background color to pink
c.background = 'pink'
c.show(ax4, title='ax4 (pure background color)');
Canvas has a cartesian grid attribute (c.grid), which is described further in its own tutorial. It can be displayed through the show(gcolor=color) keyword:
ax1, ax2 = splot(1,2)
c.show(ax1, gcolor='r')
c.grid.xdiv=10 #Ten divisions total
c.show(ax2, gcolor='black')
print c.grid;
CartesianGrid (512 X 512) at 0xbc0147c: X --> 10 divisions (51.2 pixels / div) Y --> 15 divisions (34.1 pixels / div)
Faded look to grid seems to be a rendering issue of imshow()...
pyparty
primitives (particles, grid) have backend support for matplotlib patches:
http://matplotlib.org/examples/api/patch_collection.html
The c.patchshow() method accesses these patches in a plotting API that tries to by synchronous with c.show()
ax1, ax2, ax3, ax4 = splot(2,2)
c.background = LENA
c.show( ax1, alpha=.5, gcolor='white', title="Alpha applied to image")
c.patchshow(ax2, alpha=.5, gcolor='white', edgecolor='r', title="Alpha on particles")
c.show( ax3, 'Blues_r', bgonly=True, gunder=True, gcolor='white', title="cmap applied only to bg")
c.patchshow(ax4, 'Blues_r', alpha=.7, bgonly=False, gunder=True, gcolor='white',
gstyle='--', edgecolor='r', hatch='*', title='CMAP also applied to particles');
Patches provide more options and nice plots.
The canvas object publicizes a few image attributes for convienence for example:
However, when using ndarray functions, pass the .image array directly (see below).
from skimage.filter import gaussian_filter, sobel
from skimage.color import rgb2gray
ax1, ax2 = splot(1,2)
# These are ndimages, not canvases, so use imshow
c_filtered = gaussian_filter(c.image, 10, multichannel=True)
# Sobel requires gray image; we convert from rgb
c_sobel = sobel(rgb2gray(c_filtered))
ax1.imshow(c_filtered)
ax2.imshow(c_sobel, 'gray')
ax1.set_title("Gaussian blur")
ax2.set_title('Sobel gradient of blur');
ONE IMPORTANT CAVEAT: Since guassian_filter returns an array, c_filtered is no longer a Canvas. I may add some support later to return a Canvas object with it's image attribute overwritten; however, one should keep in mind that the underlying particles are unchanged by the guassian filter; only the final "drawn" image has been altered. It may therefore be misleading to add this feature because one would need to remain aware that these blurry particles are not representative of what is actually stored by the Canvas. In any case, this is at least a little more convienent than referring to c.image so often!
type(c), type(c_filtered)
(pyparty.tools.canvas.Canvas, numpy.ndarray)
You hopefully noticed that when plotting directly from a canvas, we use c.show(axes, title), which conviently accepts axes and title argumetns. When dealing directly with an ndimage array (previous cell), I used pyplot.imshow() API, albeit with pyparty.subplots(). To provide consistency between these API's, the showim(image, axes, title) pyparty utility has the same call signature as c.show(), but takes an ndimage as input.
from pyparty import showim
ax1, ax2 = splot(1,2)
showim(c_filtered, ax1, title="Blur again")
showim(c_sobel, ax2, 'gray', title='Sobel again');
One nice aspect about pyparty is that it wraps skimage descriptors into the Particle classes, allowing for manipulations of particles based on descriptors such as circularity, eccentricity and area. Let's check out the descriptors:
from pyparty.descriptors.api import ALL_DESCRIPTORS, CUSTOM_DESCRIPTORS
', '.join(sorted(ALL_DESCRIPTORS))
'area, centroid, convex_area, convex_image, eccentricity, equivalent_diameter, evenorodd, extent, filled_area, inertia_tensor, inertia_tensor_eigenvalues, label, major_axis_length, minor_axis_length, moments, moments_central, moments_hu, moments_normalized, orientation, perimeter, solidity'
The evenorodd is actually a user-defined descriptor which determines if the number of labels in a particle sum to an even or an odd number. The rest of the descriptors are wrapping skimage.descriptor.
CUSTOM_DESCRIPTORS
{'evenorodd': <function pyparty.descriptors.user_descriptors.even_or_odd>}
You may notice that not all of the skimage descriptors are available yet. We are in the process of refining some of the descriptors to decouple them from any knowledge of the image. While a descriptor like perimeter is just a single value, intrinsice to the particle and irrespective of the image in which it appears, a descriptor like weighted_centroid depends on the background of the image from which it appears.
Any descriptor can be accessed directly from the canvas, as attribute lookup delegates down to the particle manager. For example, we can look at the area of all of the particles:
c.area
array([ 1245., 17647., 617., 31397., 7825., 987., 1192., 1225., 1087.])
The sortby method returns a Canvas with the particles sorted by a descriptor or other shared attribute (eg radius)
areasorted = c.sortby('area')
print 'No sorting:\n', c[0:4], '\n'
print 'Area sorting:\n', areasorted[0:4]
No sorting: NAME PTYPE CENTER PHI 0 circle_0 circle (256, 256) 0.0 1 top_right circle (400, 100) 0.0 2 bottom_left ellipse (50, 400) 0.0 3 topleft_corner circle (0, 0) 0.0 Area sorting: NAME PTYPE CENTER PHI 0 bottom_left ellipse (50, 400) 0.0 1 line_0 line (156, 257) 0.0 2 triangle_0 triangle (272, 306) 0.0 3 square_0 square (147, 346) 10.0
Boundary Descriptors
The canvas implements three attributes pin, pedge, pout to track the particles which are fully inside, outside or on the boundary of the image. These are simple wrappers to the function pyparty.utils.coords_in_image()
print '(in)\n', c.pin
print '\n(edge)\n',c.pedge
print '\n(out)\n', c.pout
(in) NAME PTYPE CENTER PHI 0 circle_0 circle (256, 256) 0.0 1 top_right circle (400, 100) 0.0 2 bottom_left ellipse (50, 400) 0.0 3 line_0 line (156, 257) 0.0 4 square_0 square (147, 346) 10.0 5 square_1 square (120, 327) 0.0 6 triangle_0 triangle (272, 306) 0.0 (edge) NAME PTYPE CENTER PHI 0 topleft_corner circle (0, 0) 0.0 (out) NAME PTYPE CENTER PHI 0 off_image circle (900, 200) 0.0
When possible, it's preferrable to use mapping over directly particle attribute setting. To set particle attributes directly, use the plist attribute as such:
c.plist[0].color = 'r'
c.show();
MAPPING
The pmap() functions will apply a function to every particle. The pixelmap() function will apply to an image. All mapping can be done optionally inplace.
Let's map a function that sets particle color to blue or red based on an area size threshold.
MAPPING TO PARTICLES: pmap()
c.reset_background()
def red_or_blue(p):
if p.area > 2000:
p.color = 'red'
else:
p.color = (0,0,1) #blue
return p
c_map = c.pmap(red_or_blue)
c_map.show();
Another example:
inflection of center coordinates (cx --> cy / cy --> cx) inplace
. This is simple since cx, cy and center are properties of any centered particle type. Since at the moment, line is not a supported type, I'll drop the line from the image
c.particles = STORED_PARTICLES
def inflection(p):
try:
cx, cy = p.cx, p.cy
p.cx, p.cy = cy, cx
except Exception:
return p
return p
c_map.pmap(inflection, inplace=True)
c_map.show();
Point operations/ image mapping: pixelmap()
def half_pixels(pixel):
""" Reduce the pixel value by half and square"""
return (pixel/2.0)**2
phalf = c.pixelmap(half_pixels)
showim(phalf);
It's important to realize that pixelmap() maps a function to EACH PIXEL in the image. This is not the same as passing an image into a function. To apply a function on the image per-se, just do
fcn(c.image) EG: median_filter(c.image)
pixelmap() is merely wrappering np.appy_along_axis()
The important methods are:
First, let's clear out the cyan particle that was just added. Since we named it "cyanparticle", this it is easy to access
print c.particles
NAME PTYPE CENTER PHI 0 circle_0 circle (256, 256) 0.0 1 top_right circle (400, 100) 0.0 2 bottom_left ellipse (50, 400) 0.0 3 topleft_corner circle (0, 0) 0.0 4 off_image circle (900, 200) 0.0 5 line_0 line (156, 257) 0.0 6 square_0 square (147, 346) 10.0 7 square_1 square (120, 327) 0.0 8 triangle_0 triangle (272, 306) 0.0
c.clear_canvas()
print c.particles
c.show();
<< 0 particles / at 0xbc5071c >>
# Get our original particles back
c.particles=STORED_PARTICLES
c.show();
Slicing and indexing are implemented as a list with a bit of flare. Basic list slicing by index works...
print c[0], '\n'
print c[1:3]
NAME PTYPE CENTER PHI 0 circle_0 circle (256, 256) 0.0 NAME PTYPE CENTER PHI 0 top_right circle (400, 100) 0.0 1 bottom_left ellipse (50, 400) 0.0
We've also added the ability to slice by comma-separated indicies for selective choosing
print c[0,1,2]
NAME PTYPE CENTER PHI 0 circle_0 circle (256, 256) 0.0 1 top_right circle (400, 100) 0.0 2 bottom_left ellipse (50, 400) 0.0
You can also lookup by name (although you can't slice by name). Names and integers lookup be mixed.
print c['circle_0'], '\n'
print c[0,'top_right', 1]
NAME PTYPE CENTER PHI 0 circle_0 circle (256, 256) 0.0 NAME PTYPE CENTER PHI 0 circle_0 circle (256, 256) 0.0 1 top_right circle (400, 100) 0.0 2 top_right circle (400, 100) 0.0
idx() allow for easy index access (useful when slicing)
print c.idx('circle_0'), c.idx('line_0', 'circle_0', 'top_right')
0 (5, 0, 1)
Logical Indexing on attributes/descriptors (numpy style) is also supported:
print [c.area > 5000]
[array([False, True, False, True, True, False, False, False, False], dtype=bool)]
print c[c.area > 5000]
NAME PTYPE CENTER PHI 0 top_right circle (400, 100) 0.0 1 topleft_corner circle (0, 0) 0.0 2 off_image circle (900, 200) 0.0
Let's return all non-circular particles, that are not ellipses. (This should just return the line)
print c[(c.eccentricity > .1) & (c.ptype != 'ellipse')]
NAME PTYPE CENTER PHI 0 line_0 line (156, 257) 0.0
Beware!
pyparty
particles are intended to be referenced by name. Anytime the particle list is modified (eg sorting, slicing) the indicies update, while the name remains fixed. Therefore, referencing by name is the best practice!
Let's make a two canvases, c1 and c2, for use in addition/subtraction
c1 = Canvas()
c2 = Canvas()
for idx, i in enumerate(range(40,50)):
j = (i * idx)
if idx != 0:
c1.add('circle', radius=i, center=(j,j), color=(.1*idx, 0, 0))
c2.add('dimer', radius_1=i/2, center=(512-j, j), \
overlap=.3, phi=15*idx, color=(0, 0, 1.0 -.1*idx))
ax1, ax2 = splot(1, 2)
c1.show(ax1, title='c1')
c2.show(ax2, title='c2');
Canvas Addition:
Adding canvases (c1, c2) will result in the following behavior:
To change this behavior, use the tools.concact_canvas() function
c2.background = MOON
c12 = c1 + c2
c21 = c2 + c1
ax1, ax2 = splot(1,2)
c12.show(ax1, title="c1 + c2 (c2's background)")
c21.show(ax2, title="c2 + c1 (c1's background)");
tools.concat_canvas() allows us to add particles in alternating order
from pyparty import concat_canvas
c12_alter = concat_canvas(c1, c2, alternate=True)
c12_alter.show();
Particle Manager Addition:
If you don't want the background to be included in the addition., just add the particles directly. Congruent with concact_canvas(), there is also a concat_particles method to change the default additive behavior.
c1.particles = c1.particles + c2.particles
c1.show(title='Addition of only particles (c2 appears on top)');
Canvas Subtraction: Not implemented
Particle Subtraction: Particles subtraction subtracts by name only. So two particles that are overlapping but have different names are nto subtracted!!!
#Subtract first 3 particles from first 5
print c1.particles[0:5] - c1.particles[0:3]
NAME PTYPE CENTER PHI 0 circle_3 circle (176, 176) 0.0 1 circle_4 circle (225, 225) 0.0
You can always do subtraction on the image itself which will affect the color of the particles (in this case, subtracting the "moon background" affects the particles obviously)
from skimage.color import gray2rgb
from pyparty import any2rgb
ax1, ax2, ax3 = splot(1,3, figsize=(10,7))
ax1.imshow( c1.image - gray2rgb(MOON), plt.cm.gray )
ax2.imshow( c1.image - any2rgb(MOON), plt.cm.gray )
ax3.imshow( moon(), plt.cm.gray );
Note that any2rgb() gave different results than gray2rgb(). This actually due to pyplot.imshow(), and is explained further in the enxt section.
pyparty's
any2rgb() utility will convert any image to a valid pyparty normalized 3-channel RGB image. As the above example illustrates, this conversion can also be done with skimage.colors.gray2rgb(); however, this will not enforce a strict normalized-float datatype. Because of this variability, showim() may attempt to normalize to max pixel intensity, rather than the 8-bit image max of value 255. While Canvas will internally type-cast its ndarrays (particles/backgrounds), dealing directly with an image can result in inconsistent plot outputs depending on dtype and values. In other words, any2rgb should produce more consistent plot colors. Let's illustrate this through Lena.
The goal is to divide Lena by 2 (i.e. cut the brightness in half), resulting in a darkened image. Lena has a few pixels at 255, so showim()'s "relative" normalization won't affect her. But how would imshow() handle it if her max intensity was 128? Furthermore, if her dtype was changed from an int to a float? What about int to uint?
# Cut LENA in half ; keep int version
FLOAT = LENA.astype('float')
HALFFLOAT = LENA/2.0
HALFINT = HALFFLOAT.astype('int32')
HALFUINT = HALFFLOAT.astype('uint8')
ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8, ax9, ax10 = \
splot(2,5, figsize=(10,5) )
showim(LENA, ax1, title='Int (max=255)')
showim(FLOAT, ax2, title='Float (max=255.0)')
showim(HALFINT, ax3, title='Int /2 (max=128)')
showim(HALFFLOAT, ax4, title='Flat / 2 (max=128.0)')
showim(HALFUINT, ax5, title='UInt / 2 (max=128)')
showim(any2rgb(LENA), ax6)
showim(any2rgb(FLOAT), ax7)
showim(any2rgb(HALFINT), ax8)
showim(any2rgb(HALFFLOAT), ax9)
showim(any2rgb(HALFUINT), ax10);
We see that showim() handles floats differently from ints, and that it normalizes to the max value in a pixel. It also acts differently towards ints and unsigned ints. By strictly enforcing RGB colors, normalized between (0.0-1.0), we avoid showim's quirks. The grayimage is always immediately accessible from the canvas (c.grayimage) when you need it, and passing a colormap into c.show() will yield the gray image!
c.clear_particles()
c.background=HALFINT
ax1, ax2 = splot(1,2)
c.show(ax1, title='half lena rgb')
c.show(ax2, plt.cm.Blues, title='half lena gray / 1 channel');
The canvas has a special pbinary attribute to return a boolean image of JUST the particles. This returns masked particles REGARDLESS of background, and does not alter the underlying particle instance. Masking in this manner allows us to define useful attributes like pixelarea. This is a nice approach to masking because:
Direct masking of the image could result in issues with background subtraction, and direct summing of particle area may overcount due to the reasons above.
For convience, pbinary may be passed as a colormap to canvas.show(), which will draw in black and white. Use pbinary_r for white on black.
from skimage import img_as_bool
ax1, ax2 = splot(1, 2)
c1.show('pbinary', ax1, gcolor='r',
title="Masked area is %.1f percent" % ( c1.pixarea*100 ));
c1.show('pbinary_r', ax2, gcolor='b', title='inverted');
We can compare this directly to the pixel area that would be occupied by all of the particles (ON AND OFF IMAGE) without accounting for overlap. To do so, sum the area of all of the particles and divide by the image area. The image area is stored in .pixelcount but is just image.shape[0] * image.shape[1] (length * width)
p_area = sum(p.area for p in c1.particles)
print round( 100. * (p_area / c1.pixcount), 1), 'percent'
31.4 percent
Thus, about 3-4% area is hidden due to overlap, or off-screen particles. In any case, this value is a bit hairy because techincally our dimers are inherently overlapping particles, but this overlap does not count! I suggest stick with the binarize method.
NumPy arays support many data types. Scikit image supports a subset of these types (http://scikit-image.org/docs/dev/user_guide/data_types.html). pyparty
applies even more stringent type requirements. All image/particle data is stored internally in pyparty
as a 3-channel RGB normalized (0.0-1.0) floats. For example (.2, .4, .8).
This ensures color compatibility between particle-image operations, as well as the results of imshow(), which implements its own color-typing scheme.
I've tried to hide most of this under the public api, which attempts to provide a flexible interface to this strictly-typed backend. As a first excercise, let's demonstrate all of the available color inputs (which are converted to normalized rgbs internally). The supported color inputs are:
By strictly enforcing this datatype, pyparty
can easily output images to other representations such as gray, binary (HSV coming soon).
EXAMPLE: string/hex colors (if blank, run this cell twice)
ax1, ax2, ax3, ax4, ax5, ax6 = splot(2, 3)
def colormap(p, color):
p.color = color
return p
c.reset_background()
c.pmap(colormap, 'r').show(ax1, title='colorstring')
c.pmap(colormap, '#00FF00').show(ax2, title='hex color') #green
c.pmap(colormap, (0, 0, 1)).show(ax3, title='rgb tuple') #blue
# These become the same color (.5, .5, .5) ...
c.pmap(colormap, .5).show(ax4, title='0.5 --> (.5, .5, .5)')
c.pmap(colormap, 128).show(ax5, title='128 --> (.5, .5, .5)')
c.pmap(colormap, (255, 255, 1)).show(ax6, title='tuple (normalized)');
All color validation goes through utils.to_normrgb(). This can be used to directly convert color names directly to rgb. For example, if I want to shade colors by yellow:
from pyparty import to_normrgb as rgb
AXES = splot(1, 3)
# Return (r,g,b) corresponding to yellow
ry, gy, by = rgb('yellow')
for idx, alpha in enumerate( (.3, .7, 1.0) ):
shade = ry*alpha, gy*alpha, by*alpha
c.pmap(colormap, shade).show(AXES[idx], title='Color %s'% str(shade))
While pbinary should be used for accurately masking ONLY particles, Canvas can also return the image or just background as either gray or binary representation, with all the conversion idiosynchrasies taken care of. These attributes are:
Anytime we've passed a colormap to show(), the grayimage was implicitly used.
ax1, ax2, ax3, ax4, ax5, ax6 = splot(2, 3, figsize=(10,6))
c.particles = STORED_PARTICLES
c.background = LENA
showim(c.image, ax1)
showim(c.grayimage, ax2, plt.cm.gray)
showim(c.binaryimage, ax3, plt.cm.gray)
showim(c.background, ax4)
showim(c.graybackground, ax5, plt.cm.gray)
showim(c.binarybackground, ax6, plt.cm.gray);
By default, scikit's img_to_bool is used to binarize an image; which splits the pixels at the mean of the histogram. Canvas' set_threshfcn() method allows a user to specify a custom thresholding function. In addition, a few common thresholding styles can be accessed via string. The available ones are:
These merely provide a convienent wrapping system for scikit-image's adaptive, otsu, and image_as_bool utilities (http://scikit-image.org/docs/dev/auto_examples/plot_threshold_adaptive.html); plus some trivial convienence thresholds. The wrappers accept all of the same arguments with the caveat that they must be keyword arguments; in addition, EVERY THRESHOLD ACCEPTS AN INVERT KEYWORD FOR CONVIENENCE:
ax1, ax2, ax3, ax4 = splot(2,2, figsize=(8,8))
c.set_threshfcn('img2bool' )
showim(c.binaryimage, ax1, 'gray', title='img2bool (with particles)')
c.set_threshfcn('adaptive', block_size=199, method='gaussian')
showim(c.binarybackground, ax2, 'gray', title='local adaptive')
c.set_threshfcn('otsu', invert=True)
showim(c.binarybackground, ax3, 'gray', title='otsu inverted')
c.set_threshfcn('double', nlow=100, nhigh=150)
showim(c.binarybackground, ax4, 'gray', title='low-high range (100-150)');
Once the threshfcn is set, it will be copied to new canvases. To reset to the default img2bool, merely do:
pyparty
uses img2bool instead of img_to_bool to avoid namespace conflicts with scikit-image.
For completeness, let's define our own threshold function that merely sets the threshold to 100. Of course this is already accessible via set_threshfcn('single', n=100). We will add an invertible decorator to allow easy inverting. The only requirement is that our function accepts a grayimage as its first positional argument, and at least one keyword argument (necessary for reasons having to do with functools.partial). If no keywords are needed, just use kwargs as such:
from pyparty.tools.thresholding import invertible
@invertible
def foo_thresh(grayimage, **kwargs):
return (grayimage >= 100)
c.set_threshfcn(foo_thresh, invert=True)
showim(c.binarybackground, 'gray',
title='"%s"function (inverted)'%c.threshfcn);
There are several default settings that can be changed in the canvas.config file. The ones of interest are:
In regard to PRINTDISPLAY, can be meta attributes (eg color), particle attributes (center, radius etc...) or descriptors (eg eccentricity). If an attribute is not found on a particle, "????" will be printed in the output. For example:
NAME PTYPE CENTER FOO
0 circle_0 circle (18, 29) ????