diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 85043f2c..baaf064b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -28,7 +28,7 @@ - Tony Yu Reading of paletted images; build, bug and doc fixes. Code to generate skimage logo. - Otsu thresholding, histogram equalisation, and more. + Otsu thresholding, histogram equalisation, template matching, and more. - Zachary Pincus Tracing of low cost paths, FreeImage I/O plugin, iso-contours, @@ -46,6 +46,7 @@ - Pieter Holtzhausen Incorporating CellProfiler's Sobel edge detector, build and bug fixes. + Radon transform, template matching. - Emmanuelle Guillart Total variation noise filtering, integration of CellProfiler's @@ -105,3 +106,4 @@ - Johannes Schönberger Polygon, circle and ellipse drawing functions Adaptive thresholding + Implementation of Matlab's `regionprops` diff --git a/RELEASE.txt b/RELEASE.txt index e63c4e74..cea5adb7 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -2,7 +2,7 @@ How to make a new release of ``skimage`` ======================================== - Update release notes. -- Update the version number in setup.py and commit +- Update the version number in setup.py and bento.info and commit - Update the docs: - Edit ``doc/source/themes/agogo/static/docversions.js`` and commit - Build a clean version of the docs. Run "make" in the root dir, then diff --git a/bento.info b/bento.info new file mode 100644 index 00000000..f2d303db --- /dev/null +++ b/bento.info @@ -0,0 +1,100 @@ +Name: scikits-image +Version: 0.6 +Summary: Image processing routines for SciPy +Url: http://scikits-image.org +DownloadUrl: http://github.com/scikits-image/scikits-image +Description: Image Processing SciKit + + Image processing algorithms for SciPy, including IO, morphology, filtering, + warping, color manipulation, object detection, etc. + + Please refer to the online documentation at + http://scikits-image.org/ +Maintainer: Stefan van der Walt +MaintainerEmail: stefan@sun.ac.za +License: Modified BSD +Classifiers: + Development Status :: 4 - Beta, + Environment :: Console, + Intended Audience :: Developers, + Intended Audience :: Science/Research, + License :: OSI Approved :: BSD License, + Programming Language :: C, + Programming Language :: Python, + Programming Language :: Python :: 3, + Topic :: Scientific/Engineering, + Operating System :: Microsoft :: Windows, + Operating System :: POSIX, + Operating System :: Unix, + Operating System :: MacOS + +HookFile: bscript +UseBackends: Waf + +Library: + Packages: + skimage, skimage.color, skimage.data, skimage.draw, skimage.exposure, + skimage.feature, skimage.filter, skimage.graph, skimage.io, + skimage.io._plugins, skimage.measure, skimage.morphology, + skimage.scripts, skimage.segmentation, skimage.transform, skimage.util + Extension: skimage.morphology._pnpoly + Sources: + skimage/morphology/_pnpoly.pyx + Extension: skimage.feature._greycomatrix + Sources: + skimage/feature/_greycomatrix.pyx + Extension: skimage.feature._template + Sources: + skimage/feature/_template.pyx + Extension: skimage.io._plugins._colormixer + Sources: + skimage/io/_plugins/_colormixer.pyx + Extension: skimage.measure._find_contours + Sources: + skimage/measure/_find_contours.pyx + Extension: skimage.measure._moments + Sources: + skimage/measure/_moments.pyx + Extension: skimage.graph._mcp + Sources: + skimage/graph/_mcp.pyx + Extension: skimage.io._plugins._histograms + Sources: + skimage/io/_plugins/_histograms.pyx + Extension: skimage.transform._hough_transform + Sources: + skimage/transform/_hough_transform.pyx + Extension: skimage.filter._ctmf + Sources: + skimage/filter/_ctmf.pyx + Extension: skimage.morphology.ccomp + Sources: + skimage/morphology/ccomp.pyx + Extension: skimage.morphology._watershed + Sources: + skimage/morphology/_watershed.pyx + Extension: skimage.morphology._convex_hull + Sources: + skimage/morphology/_convex_hull.pyx + Extension: skimage.morphology._skeletonize + Sources: + skimage/morphology/_skeletonize.pyx + Extension: skimage.draw._draw + Sources: + skimage/draw/_draw.pyx + Extension: skimage.transform._project + Sources: + skimage/transform/_project.pyx + Extension: skimage.graph._spath + Sources: + skimage/graph/_spath.pyx + Extension: skimage.morphology.cmorph + Sources: + skimage/morphology/cmorph.pyx + Extension: skimage.graph.heap + Sources: + skimage/graph/heap.pyx + +Executable: skivi + Module: skimage.scripts.skivi + Function: main diff --git a/bscript b/bscript new file mode 100644 index 00000000..44b3f3d6 --- /dev/null +++ b/bscript @@ -0,0 +1,12 @@ +import os.path as op + +from numpy.distutils.misc_util \ + import \ + get_numpy_include_dirs + +from bento.commands import hooks + +@hooks.post_configure +def post_configure(context): + conf = context.waf_context + conf.env.INCLUDES = get_numpy_include_dirs() + [op.join("skimage", "morphology")] diff --git a/doc/examples/applications/plot_coins_segmentation.py b/doc/examples/applications/plot_coins_segmentation.py index f2d65658..f46bbd33 100644 --- a/doc/examples/applications/plot_coins_segmentation.py +++ b/doc/examples/applications/plot_coins_segmentation.py @@ -3,57 +3,40 @@ Comparing edge-based segmentation and region-based segmentation =============================================================== -In this example, we will see how to segment objects from a background. -We use the ``coins`` image from ``skimage.data``. This image shows -several coins outlined against a darker background. The segmentation of -the coins cannot be done directly from the histogram of grey values, -because the background shares enough grey levels with the coins that a -thresholding segmentation is not sufficient. Simply thresholding the image -leads either to missing significant parts of the coins, or to merging parts -of the background with the coins. - -We first try an edge-based segmentation. We use the Canny detector to -delineate the contours of the coins. These contours are filled using -mathematical morphology (``scipy.ndimage.binary_fill_holes``). Small spurious -objects are easily removed by applying a threshold on the size of -unconnected objects. However, this method is not very robust, since contours -that are not perfectly closed are not filled correctly. This happens for one -of the coins. - -We therefore try a second method, that is region-based. Here we use the -watershed transform. An elevation map is provided by the Sobel gradient -of the image. Markers of the background and the coins are determined from -the extreme parts of the histogram of grey values. - -This second method works even better, and the coins can be segmented and -labeled individually. - +In this example, we will see how to segment objects from a background. We use +the ``coins`` image from ``skimage.data``, which shows several coins outlined +against a darker background. """ - import numpy as np -from scipy import ndimage import matplotlib.pyplot as plt -import skimage -from skimage.filter import canny, sobel -from skimage.morphology import watershed -#------------------ Loading data -------------------------------- from skimage import data -coins = data.coins() -#------------ Histogram of grey values --------------------------- -histo = np.histogram(coins, bins=np.arange(0, 256)) +coins = data.coins() +hist = np.histogram(coins, bins=np.arange(0, 256)) plt.figure(figsize=(8, 3)) plt.subplot(121) plt.imshow(coins, cmap=plt.cm.gray, interpolation='nearest') plt.axis('off') plt.subplot(122) -plt.plot(histo[1][:-1], histo[0], lw=2) +plt.plot(hist[1][:-1], hist[0], lw=2) plt.title('histogram of grey values') -#------------------ Tentative thresholding -------------------------------- +""" +.. image:: PLOT2RST.current_figure + +Thresholding +============ + +A simple way to segment the coins is to choose a threshold based on the +histogram of grey values. Unfortunately, thresholding this image gives a binary +image that either misses significant parts of the coins or merges parts of the +background with the coins: + +""" + plt.figure(figsize=(6, 3)) plt.subplot(121) plt.imshow(coins > 100, cmap=plt.cm.gray, interpolation='nearest') @@ -63,73 +46,126 @@ plt.subplot(122) plt.imshow(coins > 150, cmap=plt.cm.gray, interpolation='nearest') plt.title('coins > 150') plt.axis('off') +margins = dict(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) +plt.subplots_adjust(**margins) -plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, - right=1) +""" +.. image:: PLOT2RST.current_figure -#------------------ Edge-based segmentation -------------------------------- +Edge-based segmentation +======================= + +Next, we try to delineate the contours of the coins using edge-based +segmentation. To do this, we first get the edges of features using the Canny +edge-detector. +""" + +from skimage.filter import canny edges = canny(coins/255.) +plt.figure(figsize=(4, 3)) +plt.imshow(edges, cmap=plt.cm.gray, interpolation='nearest') +plt.axis('off') +plt.title('Canny detector') + +""" +.. image:: PLOT2RST.current_figure + +These contours are then filled using mathematical morphology. +""" + +from scipy import ndimage + fill_coins = ndimage.binary_fill_holes(edges) +plt.figure(figsize=(4, 3)) +plt.imshow(fill_coins, cmap=plt.cm.gray, interpolation='nearest') +plt.axis('off') +plt.title('Filling the holes') + +""" +.. image:: PLOT2RST.current_figure + +Small spurious objects are easily removed by setting a minimum size for valid +objects. +""" + label_objects, nb_labels = ndimage.label(fill_coins) sizes = np.bincount(label_objects.ravel()) mask_sizes = sizes > 20 mask_sizes[0] = 0 coins_cleaned = mask_sizes[label_objects] -plt.figure(figsize=(7, 3.)) -plt.subplot(131) -plt.imshow(edges, cmap=plt.cm.gray, interpolation='nearest') -plt.axis('off') -plt.title('Canny detector') -plt.subplot(132) -plt.imshow(fill_coins, cmap=plt.cm.gray, interpolation='nearest') -plt.axis('off') -plt.title('Filling the holes') -plt.subplot(133) +plt.figure(figsize=(4, 3)) plt.imshow(coins_cleaned, cmap=plt.cm.gray, interpolation='nearest') plt.axis('off') plt.title('Removing small objects') -plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, - right=1) + +""" +.. image:: PLOT2RST.current_figure + +However, this method is not very robust, since contours that are not perfectly +closed are not filled correctly, as is the case for one unfilled coin above. -#------------------ Region-based segmentation -------------------------------- +Region-based segmentation +========================= + +We therefore try a region-based method using the +watershed transform. First, we find an elevation map using the Sobel gradient of the +image. + +""" + +from skimage.filter import sobel + +elevation_map = sobel(coins) + +plt.figure(figsize=(4, 3)) +plt.imshow(elevation_map, cmap=plt.cm.jet, interpolation='nearest') +plt.axis('off') +plt.title('elevation_map') + +""" +.. image:: PLOT2RST.current_figure + +Next we find markers of the background and the coins based on the extreme parts +of the histogram of grey values. +""" markers = np.zeros_like(coins) markers[coins < 30] = 1 markers[coins > 150] = 2 -elevation_map = sobel(coins) - - -segmentation = watershed(elevation_map, markers) - - -plt.figure(figsize=(7, 3)) -plt.subplot(131) +plt.figure(figsize=(4, 3)) plt.imshow(markers, cmap=plt.cm.spectral, interpolation='nearest') plt.axis('off') plt.title('markers') -plt.subplot(132) -plt.imshow(elevation_map, cmap=plt.cm.jet, interpolation='nearest') -plt.axis('off') -plt.title('elevation_map') -plt.subplot(133) + +""" +.. image:: PLOT2RST.current_figure + +Finally, we use the watershed transform to fill regions of the elevation map starting from the markers determined above: + +""" +from skimage.morphology import watershed +segmentation = watershed(elevation_map, markers) + +plt.figure(figsize=(4, 3)) plt.imshow(segmentation, cmap=plt.cm.gray, interpolation='nearest') plt.axis('off') plt.title('segmentation') -plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, - right=1) +""" +.. image:: PLOT2RST.current_figure -# ------------------- Removing a few small holes --------------------- +This last method works even better, and the coins can be segmented and +labeled individually. + +""" segmentation = ndimage.binary_fill_holes(segmentation - 1) - -#------------------ Labeling the coins -------------------------------- labeled_coins, _ = ndimage.label(segmentation) plt.figure(figsize=(6, 3)) @@ -141,8 +177,11 @@ plt.subplot(122) plt.imshow(labeled_coins, cmap=plt.cm.spectral, interpolation='nearest') plt.axis('off') -plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, - right=1) +plt.subplots_adjust(**margins) +""" +.. image:: PLOT2RST.current_figure + +""" plt.show() diff --git a/doc/examples/plot_regionprops.py b/doc/examples/plot_regionprops.py new file mode 100644 index 00000000..90b40a89 --- /dev/null +++ b/doc/examples/plot_regionprops.py @@ -0,0 +1,64 @@ +""" +========================= +Measure region properties +========================= + +This example shows how to measure properties of labelled image regions. +""" + +import math +import matplotlib.pyplot as plt +import numpy as np + +from skimage.draw import ellipse +from skimage.morphology import label +from skimage.measure import regionprops +from scipy.ndimage import geometric_transform + + +ANGLE = 0.2 + +def rotate(xy): + x, y = xy + out_x = math.cos(ANGLE) * x - math.sin(ANGLE) * y + out_y = math.sin(ANGLE) * x + math.cos(ANGLE) * y + return (out_x, out_y) + +image = np.zeros((600, 600), 'int') + +rr, cc = ellipse(300, 350, 100, 220) +image[rr,cc] = 1 + +image = geometric_transform(image, rotate) + +label_img = label(image) +props = regionprops(label_img, [ + 'BoundingBox', + 'Centroid', + 'Orientation', + 'MajorAxisLength', + 'MinorAxisLength' +]) + +plt.imshow(image) + +for prop in props: + x0 = prop['Centroid'][1] + y0 = prop['Centroid'][0] + x1 = x0 + math.cos(prop['Orientation']) * 0.5 * prop['MajorAxisLength'] + y1 = y0 - math.sin(prop['Orientation']) * 0.5 * prop['MajorAxisLength'] + x2 = x0 - math.sin(prop['Orientation']) * 0.5 * prop['MinorAxisLength'] + y2 = y0 - math.cos(prop['Orientation']) * 0.5 * prop['MinorAxisLength'] + + plt.plot((x0, x1), (y0, y1), '-r', linewidth=2.5) + plt.plot((x0, x2), (y0, y2), '-r', linewidth=2.5) + plt.plot(x0, y0, '.g', markersize=15) + + minr, minc, maxr, maxc = prop['BoundingBox'] + bx = (minc, maxc, maxc, minc, minc) + by = (minr, minr, maxr, maxr, minr) + plt.plot(bx, by, '-b', linewidth=2.5) + +plt.gray() +plt.axis((0, 600, 600, 0)) +plt.show() diff --git a/doc/examples/plot_ssim.py b/doc/examples/plot_ssim.py new file mode 100644 index 00000000..60419692 --- /dev/null +++ b/doc/examples/plot_ssim.py @@ -0,0 +1,68 @@ +''' +=========================== +Structural similarity index +=========================== + +When comparing images, the mean squared error (MSE)--while simple to +implement--is not highly indicative of perceived similarity. Structural +similarity aims to address this shortcoming by taking texture into account +[1]_, [2]_. + +The example shows two modifications of the input image, each with the same MSE, +but with very different mean structural similarity indices. + +.. [1] Zhou Wang; Bovik, A.C.; ,"Mean squared error: Love it or leave it? A new + look at Signal Fidelity Measures," Signal Processing Magazine, IEEE, + vol. 26, no. 1, pp. 98-117, Jan. 2009. + +.. [2] Z. Wang, A. C. Bovik, H. R. Sheikh and E. P. Simoncelli, "Image quality + assessment: From error visibility to structural similarity," IEEE + Transactions on Image Processing, vol. 13, no. 4, pp. 600-612, + Apr. 2004. + +''' + +from skimage import data, color, io, exposure, img_as_float +from skimage.measure import structural_similarity as ssim + +import numpy as np + +img = img_as_float(data.camera()) +rows, cols = img.shape + +noise = np.ones_like(img) * 0.2 * (img.max() - img.min()) +noise[np.random.random(size=noise.shape) > 0.5] *= -1 + +def mse(x, y): + return np.linalg.norm(x - y) + +img_noise = img + noise +img_const = img + abs(noise) + +import matplotlib.pyplot as plt +f, (ax0, ax1, ax2) = plt.subplots(1, 3) + +mse_none = mse(img, img) +ssim_none = ssim(img, img, dynamic_range=img.max() - img.min()) + +mse_noise = mse(img, img_noise) +ssim_noise = ssim(img, img_noise, dynamic_range=img_const.max() - img_const.min()) + +mse_const = mse(img, img_const) +ssim_const = ssim(img, img_const, dynamic_range=img_noise.max() - img_noise.min()) + +label = 'MSE: %2.f, SSIM: %.2f' + +ax0.imshow(img, cmap=plt.cm.gray, vmin=0, vmax=1) +ax0.set_xlabel(label % (mse_none, ssim_none)) +ax0.set_title('Original image') + +ax1.imshow(img_noise, cmap=plt.cm.gray, vmin=0, vmax=1) +ax1.set_xlabel(label % (mse_noise, ssim_noise)) +ax1.set_title('Image with noise') + +ax2.imshow(img_const, cmap=plt.cm.gray, vmin=0, vmax=1) +ax2.set_xlabel(label % (mse_const, ssim_const)) +ax2.set_title('Image plus constant') + +plt.show() diff --git a/doc/examples/plot_swirl.py b/doc/examples/plot_swirl.py new file mode 100644 index 00000000..18947dcb --- /dev/null +++ b/doc/examples/plot_swirl.py @@ -0,0 +1,83 @@ +r""" +===== +Swirl +===== + +Image swirling is a non-linear image deformation that creates a whirlpool +effect. This example describes the implementation of this transform in +``skimage``, as well as the underlying warp mechanism. + +Image warping +````````````` +When applying a geometric transformation on an image, we typically make use of +a reverse mapping, i.e., for each pixel in the output image, we compute its +corresponding position in the input. The reason is that, if we were to do it +the other way around (map each input pixel to its new output position), some +pixels in the output may be left empty. On the other hand, each output +coordinate has exactly one corresponding location in (or outside) the input +image, and even if that position is non-integer, we may use interpolation to +compute the corresponding image value. + +Performing a reverse mapping +```````````````````````````` +To perform a geometric warp in ``skimage``, you simply need to provide the +reverse mapping to the ``skimage.transform.warp`` function. E.g., consider the +case where we would like to shift an image 50 pixels to the left. The reverse +mapping for such a shift would be:: + + def shift_left(xy): + xy[:, 0] += 50 + return xy + +The corresponding call to warp is:: + + from skimage.transform import warp + warp(image, shift_left) + +The swirl transformation +```````````````````````` +Consider the coordinate :math:`(x, y)` in the output image. The reverse +mapping for the swirl transformation first computes, relative to a center +:math:`(x_0, y_0)`, its polar coordinates, + +.. math:: + + \theta = \arctan(y/x) + + \rho = \sqrt{(x - x_0)^2 + (y - y_0)^2}, + +and then transforms them according to + +.. math:: + + r = \ln(2) \, \mathtt{radius} / 5 + + \phi = \mathtt{rotation} + + s = \mathtt{strength} + + \theta' = \phi + s \, e^{-\rho / r + \theta} + +where ``strength`` is a parameter for the amount of swirl, ``radius`` indicates +the swirl extent in pixels, and ``rotation`` adds a rotation angle. The +transformation of ``radius`` into :math:`r` is to ensure that the +transformation decays to :math:`\approx 1/1000^{\mathsf{th}}` within the +specified radius. +""" + +from skimage import data +from skimage.transform import swirl + +import matplotlib.pyplot as plt + +image = data.checkerboard() +swirled = swirl(image, rotation=0, strength=10, radius=120, order=2) + +f, (ax0, ax1) = plt.subplots(1, 2, figsize=(8, 3)) + +ax0.imshow(image, cmap=plt.cm.gray, interpolation='none') +ax0.axis('off') +ax1.imshow(swirled, cmap=plt.cm.gray, interpolation='none') +ax1.axis('off') + +plt.show() diff --git a/doc/examples/plot_template.py b/doc/examples/plot_template.py new file mode 100644 index 00000000..65b4571b --- /dev/null +++ b/doc/examples/plot_template.py @@ -0,0 +1,56 @@ +""" +================= +Template Matching +================= + +In this example, we use template matching to identify the occurrence of an +image patch (in this case, a sub-image centered on a single coin). Here, we +return a single match (the exact same coin), so the maximum value in the +``match_template`` result corresponds to the coin location. The other coins +look similar, and thus have local maxima; if you expect multiple matches, you +should use a proper peak-finding function. + +The ``match_template`` function uses fast, normalized cross-correlation [1]_ +to find instances of the template in the image. Note that the peaks in the +output of ``match_template`` correspond to the origin (i.e. top-left corner) of +the template. + +.. [1] J. P. Lewis, "Fast Normalized Cross-Correlation", Industrial Light and + Magic. +""" + +import numpy as np +import matplotlib.pyplot as plt +from skimage import data +from skimage.feature import match_template + +image = data.coins() +coin = image[170:220, 75:130] + +result = match_template(image, coin) +ij = np.unravel_index(np.argmax(result), result.shape) +x, y = ij[::-1] + +fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(8, 3)) + +ax1.imshow(coin) +ax1.set_axis_off() +ax1.set_title('template') + +ax2.imshow(image) +ax2.set_axis_off() +ax2.set_title('image') +# highlight matched region +hcoin, wcoin = coin.shape +rect = plt.Rectangle((x, y), wcoin, hcoin, edgecolor='r', facecolor='none') +ax2.add_patch(rect) + +ax3.imshow(result) +ax3.set_axis_off() +ax3.set_title('`match_template`\nresult') +# highlight matched region +ax3.autoscale(False) +ax3.plot(x, y, 'o', markeredgecolor='r', markerfacecolor='none', markersize=10) + +plt.show() + diff --git a/doc/examples/plot_watershed.py b/doc/examples/plot_watershed.py index e9699642..a1cd18cf 100644 --- a/doc/examples/plot_watershed.py +++ b/doc/examples/plot_watershed.py @@ -21,7 +21,7 @@ line. See Wikipedia_ for more details on the algorithm. -.. _Wikipedia: +.. _Wikipedia: http://en.wikipedia.org/wiki/Watershed_(image_processing) """ diff --git a/doc/ext/gen_rst.py b/doc/ext/gen_rst.py deleted file mode 100644 index de34d4b8..00000000 --- a/doc/ext/gen_rst.py +++ /dev/null @@ -1,299 +0,0 @@ -""" -Example generation for the scikit image. - -Generate the rst files for the examples by iterating over the python -example files. - -Files that generate images should start with 'plot'. - -This code was taken from the scikit-learn. - -""" -import os -import shutil -import traceback -import glob - -import matplotlib -matplotlib.use('Agg') - -import token, tokenize - -rst_template = """ - -.. _example_%(short_fname)s: - -%(docstring)s - -**Python source code:** :download:`%(fname)s <%(fname)s>` -(generated using ``skimage`` |version|) - -.. literalinclude:: %(fname)s - :lines: %(end_row)s- - """ - -plot_rst_template = """ - -.. _example_%(short_fname)s: - -%(docstring)s - -%(image_list)s - -**Python source code:** :download:`%(fname)s <%(fname)s>` -(generated using ``skimage`` |version|) - -.. literalinclude:: %(fname)s - :lines: %(end_row)s- - """ - - -toctree_template = """ - -.. toctree:: - :hidden: - - %s - -""" - -# The following strings are used when we have several pictures: we use -# an html div tag that our CSS uses to turn the lists into horizontal -# lists. -HLIST_HEADER = """ -.. rst-class:: horizontal - -""" - -HLIST_IMAGE_TEMPLATE = """ - * - - .. image:: images/%s - :scale: 50 -""" - -SINGLE_IMAGE = """ -.. image:: images/%s - :align: center -""" - -def extract_docstring(filename): - """ Extract a module-level docstring, if any - """ - lines = file(filename).readlines() - start_row = 0 - if lines[0].startswith('#!'): - lines.pop(0) - start_row = 1 - - docstring = '' - first_par = '' - tokens = tokenize.generate_tokens(lines.__iter__().next) - for tok_type, tok_content, _, (erow, _), _ in tokens: - tok_type = token.tok_name[tok_type] - if tok_type in ('NEWLINE', 'COMMENT', 'NL', 'INDENT', 'DEDENT'): - continue - elif tok_type == 'STRING': - docstring = eval(tok_content) - # If the docstring is formatted with several paragraphs, extract - # the first one: - paragraphs = '\n'.join(line.rstrip() - for line in docstring.split('\n')).split('\n\n') - if len(paragraphs) > 0: - first_par = paragraphs[0] - break - return docstring, first_par, erow+1+start_row - - -def generate_example_rst(app): - """ Generate the list of examples, as well as the contents of - examples. - """ - root_dir = os.path.join(app.builder.srcdir, 'auto_examples') - example_dir = os.path.abspath(app.builder.srcdir + '/../' + 'examples') - try: - plot_gallery = eval(app.builder.config.plot_gallery) - except TypeError: - plot_gallery = bool(app.builder.config.plot_gallery) - if not os.path.exists(example_dir): - os.makedirs(example_dir) - if not os.path.exists(root_dir): - os.makedirs(root_dir) - - # we create an index.rst with all examples - fhindex = file(os.path.join(root_dir, 'index.txt'), 'w') - fhindex.write("""\ - -Examples -======== - -.. _examples-index: -""") - # Here we don't use an os.walk, but we recurse only twice: flat is - # better than nested. - generate_dir_rst('.', fhindex, example_dir, root_dir, plot_gallery) - for dir in sorted(os.listdir(example_dir)): - if os.path.isdir(os.path.join(example_dir, dir)): - generate_dir_rst(dir, fhindex, example_dir, root_dir, plot_gallery) - fhindex.flush() - - -def generate_dir_rst(dir, fhindex, example_dir, root_dir, plot_gallery): - """ Generate the rst file for an example directory. - """ - if not dir == '.': - target_dir = os.path.join(root_dir, dir) - src_dir = os.path.join(example_dir, dir) - else: - target_dir = root_dir - src_dir = example_dir - if not os.path.exists(os.path.join(src_dir, 'README.txt')): - print 80*'_' - print ('Example directory %s does not have a README.txt file' - % src_dir) - print 'Skipping this directory' - print 80*'_' - return - - example_description = file(os.path.join(src_dir, 'README.txt')).read() - fhindex.write("""\n\n%s\n\n""" % example_description) - - if not os.path.exists(target_dir): - os.makedirs(target_dir) - - def sort_key(a): - # put last elements without a plot - if not a.startswith('plot') and a.endswith('.py'): - return 'zz' + a - return a - - examples = [fname for fname in sorted(os.listdir(src_dir), key=sort_key) - if fname.endswith('py')] - ex_names = [ex[:-3] for ex in examples] # strip '.py' extension - fhindex.write(toctree_template % '\n '.join(ex_names)) - - for fname in examples: - generate_file_rst(fname, target_dir, src_dir, plot_gallery) - thumb = os.path.join(dir, 'images', 'thumb', fname[:-3] + '.png') - link_name = os.path.join(dir, fname).replace(os.path.sep, '_') - fhindex.write('.. figure:: %s\n' % thumb) - if link_name.startswith('._'): - link_name = link_name[2:] - fhindex.write(' :figclass: gallery\n') - if dir != '.': - fhindex.write(' :target: ./%s/%s.html\n\n' % (dir, fname[:-3])) - else: - fhindex.write(' :target: ./%s.html\n\n' % link_name[:-3]) - fhindex.write(' :ref:`example_%s`\n\n' % link_name) - fhindex.write(""" -.. raw:: html - -
- """) # clear at the end of the section - - -def generate_file_rst(fname, target_dir, src_dir, plot_gallery): - """ Generate the rst file for a given example. - """ - base_image_name = os.path.splitext(fname)[0] - image_fname = '%s_%%s.png' % base_image_name - - this_template = rst_template - last_dir = os.path.split(src_dir)[-1] - # to avoid leading . in file names, and wrong names in links - if last_dir == '.' or last_dir == 'examples': - last_dir = '' - else: - last_dir += '_' - short_fname = last_dir + fname - src_file = os.path.join(src_dir, fname) - example_file = os.path.join(target_dir, fname) - shutil.copyfile(src_file, example_file) - - # The following is a list containing all the figure names - figure_list = [] - - image_dir = os.path.join(target_dir, 'images') - thumb_dir = os.path.join(image_dir, 'thumb') - if not os.path.exists(image_dir): - os.makedirs(image_dir) - if not os.path.exists(thumb_dir): - os.makedirs(thumb_dir) - image_path = os.path.join(image_dir, image_fname) - thumb_file = os.path.join(thumb_dir, fname[:-3] + '.png') - if plot_gallery and fname.startswith('plot'): - # generate the plot as png image if file name - # starts with plot and if it is more recent than an - # existing image. - first_image_file = image_path % 1 - - if (not os.path.exists(first_image_file) or - os.stat(first_image_file).st_mtime <= - os.stat(src_file).st_mtime): - # We need to execute the code - print 'plotting %s' % fname - import matplotlib.pyplot as plt - plt.close('all') - cwd = os.getcwd() - try: - # First CD in the original example dir, so that any file created - # by the example get created in this directory - os.chdir(os.path.dirname(src_file)) - execfile(os.path.basename(src_file), {'pl' : plt}) - os.chdir(cwd) - - # In order to save every figure we have two solutions : - # * iterate from 1 to infinity and call plt.fignum_exists(n) - # (this requires the figures to be numbered - # incrementally: 1, 2, 3 and not 1, 2, 5) - # * iterate over [fig_mngr.num for fig_mngr in - # matplotlib._pylab_helpers.Gcf.get_all_fig_managers()] - for fig_num in (fig_mngr.num for fig_mngr in - matplotlib._pylab_helpers.Gcf.get_all_fig_managers()): - # Set the fig_num figure as the current figure as we can't - # save a figure that's not the current figure. - plt.figure(fig_num) - plt.savefig(image_path % fig_num) - figure_list.append(image_fname % fig_num) - except: - print 80*'_' - print '%s is not compiling:' % fname - traceback.print_exc() - print 80*'_' - finally: - os.chdir(cwd) - else: - figure_list = [f[len(image_dir):] - for f in glob.glob(image_path % '[1-9]')] - #for f in glob.glob(image_path % '*')] - - # generate thumb file - this_template = plot_rst_template - from matplotlib import image - if os.path.exists(first_image_file): - image.thumbnail(first_image_file, thumb_file, 0.25) - - if not os.path.exists(thumb_file): - # create something not to replace the thumbnail - shutil.copy('source/auto_examples/images/blank_image.png', thumb_file) - - docstring, short_desc, end_row = extract_docstring(example_file) - - # Depending on whether we have one or more figures, we're using a - # horizontal list or a single rst call to 'image'. - if len(figure_list) == 1: - figure_name = figure_list[0] - image_list = SINGLE_IMAGE % figure_name.lstrip('/') - else: - image_list = HLIST_HEADER - for figure_name in figure_list: - image_list += HLIST_IMAGE_TEMPLATE % figure_name.lstrip('/') - - f = open(os.path.join(target_dir, fname[:-2] + 'txt'),'w') - f.write(this_template % locals()) - f.flush() - - -def setup(app): - app.connect('builder-inited', generate_example_rst) - app.add_config_value('plot_gallery', True, 'html') diff --git a/doc/ext/plot2rst.py b/doc/ext/plot2rst.py new file mode 100644 index 00000000..67355289 --- /dev/null +++ b/doc/ext/plot2rst.py @@ -0,0 +1,502 @@ +""" +Example generation from python files. + +Generate the rst files for the examples by iterating over the python +example files. Files that generate images should start with 'plot'. + +To generate your own examples, add this extension to the list of +``extensions``in your Sphinx configuration file. In addition, make sure the +example directory(ies) in `plot2rst_paths` (see below) points to a directory +with examples named `plot_*.py` and include an `index.rst` file. + +This code was adapted from scikits-image, which took it from scikits-learn. + +Options +------- +The ``plot2rst`` extension accepts the following options: + +plot2rst_paths : length-2 tuple, or list of tuples + Tuple or list of tuples of paths to (python plot, generated rst) files, + i.e. (source, destination). Note that both paths are relative to Sphinx + 'source' directory. Defaults to ('../examples', 'auto_examples') + +plot2rst_rcparams : dict + Matplotlib configuration parameters. See + http://matplotlib.sourceforge.net/users/customizing.html for details. + +plot2rst_default_thumb : str + Path (relative to doc root) of default thumbnail image. + +plot2rst_thumb_scale : float + Scale factor for thumbnail (e.g., 0.2 to scale plot to 1/5th the + original size). + +plot2rst_plot_tag : str + When this tag is found in the example file, the current plot is saved and + tag is replaced with plot path. Defaults to 'PLOT2RST.current_figure'. + + +Suggested CSS definitions +------------------------- + + div.body h2 { + border-bottom: 1px solid #BBB; + clear: left; + } + + /*---- example gallery ----*/ + + .gallery.figure { + float: left; + margin: 1em; + } + + .gallery.figure img{ + display: block; + margin-left: auto; + margin-right: auto; + width: 200px; + } + + .gallery.figure .caption { + width: 200px; + text-align: center !important; + } + +""" +import os +import shutil +import token +import tokenize + +import numpy as np +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +from matplotlib import image + + +LITERALINCLUDE = """ +.. literalinclude:: {src_name} + :lines: {code_start}- + +""" + +CODE_LINK = """ + +**Python source code:** :download:`download <{0}>` +(generated using ``skimage`` |version|) + +""" + +TOCTREE_TEMPLATE = """ +.. toctree:: + :hidden: + + %s + +""" + +IMAGE_TEMPLATE = """ +.. image:: images/%s + :align: center + +""" + +GALLERY_IMAGE_TEMPLATE = """ +.. figure:: %(thumb)s + :figclass: gallery + :target: ./%(source)s.html + + :ref:`example_%(link_name)s` + +""" + + +class Path(str): + """Path object for manipulating directory and file paths.""" + + def __init__(self, path): + super(Path, self).__init__(path) + + @property + def isdir(self): + return os.path.isdir(self) + + @property + def exists(self): + """Return True if path exists""" + return os.path.exists(self) + + def pjoin(self, *args): + """Join paths. `p` prefix prevents confusion with string method.""" + return self.__class__(os.path.join(self, *args)) + + def psplit(self): + """Split paths. `p` prefix prevents confusion with string method.""" + return [self.__class__(p) for p in os.path.split(self)] + + def makedirs(self): + if not self.exists: + os.makedirs(self) + + def listdir(self): + return os.listdir(self) + + def format(self, *args, **kwargs): + return self.__class__(super(Path, self).format(*args, **kwargs)) + + def __add__(self, other): + return self.__class__(super(Path, self).__add__(other)) + + def __iadd__(self, other): + return self.__add__(other) + + +def setup(app): + app.connect('builder-inited', generate_example_galleries) + + app.add_config_value('plot2rst_paths', + ('../examples', 'auto_examples'), True) + app.add_config_value('plot2rst_rcparams', {}, True) + app.add_config_value('plot2rst_default_thumb', None, True) + app.add_config_value('plot2rst_thumb_scale', 0.25, True) + app.add_config_value('plot2rst_plot_tag', 'PLOT2RST.current_figure', True) + app.add_config_value('plot2rst_index_name', 'index', True) + + +def generate_example_galleries(app): + cfg = app.builder.config + + doc_src = Path(os.path.abspath(app.builder.srcdir)) # path/to/doc/source + + if isinstance(cfg.plot2rst_paths, tuple): + cfg.plot2rst_paths = [cfg.plot2rst_paths] + for src_dest in cfg.plot2rst_paths: + plot_path, rst_path = [Path(p) for p in src_dest] + example_dir = doc_src.pjoin(plot_path) + rst_dir = doc_src.pjoin(rst_path) + generate_examples_and_gallery(example_dir, rst_dir, cfg) + + +def generate_examples_and_gallery(example_dir, rst_dir, cfg): + """Generate rst from examples and create gallery to showcase examples.""" + if not example_dir.exists: + print "No example directory found at", example_dir + return + rst_dir.makedirs() + + # we create an index.rst with all examples + gallery_index = file(rst_dir.pjoin('index'+cfg.source_suffix), 'w') + + # Here we don't use an os.walk, but we recurse only twice: flat is + # better than nested. + write_gallery(gallery_index, example_dir, rst_dir, cfg) + for d in sorted(example_dir.listdir()): + example_sub = example_dir.pjoin(d) + if example_sub.isdir: + rst_sub = rst_dir.pjoin(d) + rst_sub.makedirs() + write_gallery(gallery_index, example_sub, rst_sub, cfg, depth=1) + gallery_index.flush() + + +def write_gallery(gallery_index, src_dir, rst_dir, cfg, depth=0): + """Generate the rst files for an example directory, i.e. gallery. + + Write rst files from python examples and add example links to gallery. + + Parameters + ---------- + gallery_index : file + Index file for plot gallery. + src_dir : 'str' + Source directory for python examples. + rst_dir : 'str' + Destination directory for rst files generated from python examples. + cfg : config object + Sphinx config object created by Sphinx. + """ + index_name = cfg.plot2rst_index_name + cfg.source_suffix + gallery_template = src_dir.pjoin(index_name) + if not os.path.exists(gallery_template): + print src_dir + print 80*'_' + print ('Example directory %s does not have a %s file' + % (src_dir, index_name)) + print 'Skipping this directory' + print 80*'_' + return + + gallery_description = file(gallery_template).read() + gallery_index.write('\n\n%s\n\n' % gallery_description) + + rst_dir.makedirs() + examples = [fname for fname in sorted(src_dir.listdir(), key=_plots_first) + if fname.endswith('py')] + ex_names = [ex[:-3] for ex in examples] # strip '.py' extension + if depth == 0: + sub_dir = Path('') + else: + sub_dir_list = src_dir.psplit()[-depth:] + sub_dir = Path('/'.join(sub_dir_list) + '/') + gallery_index.write(TOCTREE_TEMPLATE % (sub_dir + '\n '.join(ex_names))) + + for src_name in examples: + write_example(src_name, src_dir, rst_dir, cfg) + + link_name = sub_dir.pjoin(src_name) + link_name = link_name.replace(os.path.sep, '_') + if link_name.startswith('._'): + link_name = link_name[2:] + + info = {} + info['thumb'] = sub_dir.pjoin('images/thumb', src_name[:-3] + '.png') + info['source'] = sub_dir + src_name[:-3] + info['link_name'] = link_name + gallery_index.write(GALLERY_IMAGE_TEMPLATE % info) + + +def _plots_first(fname): + """Decorate filename so that examples with plots are displayed first.""" + if not (fname.startswith('plot') and fname.endswith('.py')): + return 'zz' + fname + return fname + + +def write_example(src_name, src_dir, rst_dir, cfg): + """Write rst file from a given python example. + + Parameters + ---------- + src_name : str + Name of example file. + src_dir : 'str' + Source directory for python examples. + rst_dir : 'str' + Destination directory for rst files generated from python examples. + cfg : config object + Sphinx config object created by Sphinx. + """ + last_dir = src_dir.psplit()[-1] + # to avoid leading . in file names, and wrong names in links + if last_dir == '.' or last_dir == 'examples': + last_dir = Path('') + else: + last_dir += '_' + + src_path = src_dir.pjoin(src_name) + example_file = rst_dir.pjoin(src_name) + shutil.copyfile(src_path, example_file) + + image_dir = rst_dir.pjoin('images') + thumb_dir = image_dir.pjoin('thumb') + image_dir.makedirs() + thumb_dir.makedirs() + + base_image_name = os.path.splitext(src_name)[0] + image_path = image_dir.pjoin(base_image_name + '_{0}.png') + + basename, py_ext = os.path.splitext(src_name) + rst_path = rst_dir.pjoin(basename + cfg.source_suffix) + + if _plots_are_current(src_path, image_path) and rst_path.exists: + return + + blocks = split_code_and_text_blocks(example_file) + if blocks[0][2].startswith('#!'): + blocks.pop(0) # don't add shebang line to rst file. + + rst_link = '.. _example_%s:\n\n' % (last_dir + src_name) + figure_list, rst = process_blocks(blocks, src_path, image_path, cfg) + + has_inline_plots = any(cfg.plot2rst_plot_tag in b[2] for b in blocks) + if has_inline_plots: + example_rst = ''.join([rst_link, rst]) + else: + # print first block of text, display all plots, then display code. + first_text_block = [b for b in blocks if b[0] == 'text'][0] + label, (start, end), content = first_text_block + figure_list = save_all_figures(image_path) + rst_blocks = [IMAGE_TEMPLATE % f.lstrip('/') for f in figure_list] + + example_rst = rst_link + example_rst += eval(content) + example_rst += ''.join(rst_blocks) + code_info = dict(src_name=src_name, code_start=end) + example_rst += LITERALINCLUDE.format(**code_info) + + example_rst += CODE_LINK.format(src_name) + + f = open(rst_path,'w') + f.write(example_rst) + f.flush() + + thumb_path = thumb_dir.pjoin(src_name[:-3] + '.png') + first_image_file = image_dir.pjoin(figure_list[0].lstrip('/')) + if first_image_file.exists: + image.thumbnail(first_image_file, thumb_path, cfg.plot2rst_thumb_scale) + + if not thumb_path.exists: + if cfg.plot2rst_default_thumb is None: + print "WARNING: No plots found and default thumbnail not defined." + print "Specify 'plot2rst_default_thumb' in Sphinx config file." + else: + shutil.copy(cfg.plot2rst_default_thumb, thumb_path) + + +def _plots_are_current(src_path, image_path): + first_image_file = Path(image_path.format(1)) + needs_replot = (not first_image_file.exists or + _mod_time(first_image_file) <= _mod_time(src_path)) + return not needs_replot + + +def _mod_time(file_path): + return os.stat(file_path).st_mtime + + +def split_code_and_text_blocks(source_file): + """Return list with source file separated into code and text blocks. + + Returns + ------- + blocks : list of (label, (start, end+1), content) + List where each element is a tuple with the label ('text' or 'code'), + the (start, end+1) line numbers, and content string of block. + """ + block_edges, idx_first_text_block = get_block_edges(source_file) + + with open(source_file) as f: + source_lines = f.readlines() + + # Every other block should be a text block + idx_text_block = np.arange(idx_first_text_block, len(block_edges), 2) + blocks = [] + slice_ranges = zip(block_edges[:-1], block_edges[1:]) + for i, (start, end) in enumerate(slice_ranges): + block_label = 'text' if i in idx_text_block else 'code' + # subtract 1 from indices b/c line numbers start at 1, not 0 + content = ''.join(source_lines[start-1:end-1]) + blocks.append((block_label, (start, end), content)) + return blocks + + +def get_block_edges(source_file): + """Return starting line numbers of code and text blocks + + Returns + ------- + block_edges : list of int + Line number for the start of each block. Note the + idx_first_text_block : {0 | 1} + 0 if first block is text then, else 1 (second block better be text). + """ + block_edges = [] + with open(source_file) as f: + token_iter = tokenize.generate_tokens(f.readline) + for token_tuple in token_iter: + t_id, t_str, (srow, scol), (erow, ecol), src_line = token_tuple + if (token.tok_name[t_id] == 'STRING' and scol == 0): + # Add one point to line after text (for later slicing) + block_edges.extend((srow, erow+1)) + idx_first_text_block = 0 + # when example doesn't start with text block. + if not block_edges[0] == 1: + block_edges.insert(0, 1) + idx_first_text_block = 1 + # when example doesn't end with text block. + if not block_edges[-1] == erow: # iffy: I'm using end state of loop + block_edges.append(erow) + return block_edges, idx_first_text_block + + +def process_blocks(blocks, src_path, image_path, cfg): + """Run source, save plots as images, and convert blocks to rst. + + Parameters + ---------- + blocks : list of block tuples + Code and text blocks from example. See `split_code_and_text_blocks`. + src_path : str + Path to example file. + image_path : str + Path where plots are saved (format string which accepts figure number). + cfg : config object + Sphinx config object created by Sphinx. + + Returns + ------- + figure_list : list + List of figure names saved by the example. + rst_text : str + Text with code wrapped code-block directives. + """ + src_dir, src_name = src_path.psplit() + if not src_name.startswith('plot'): + return [], '' + + # index of blocks which have inline plots + inline_tag = cfg.plot2rst_plot_tag + idx_inline_plot = [i for i, b in enumerate(blocks) + if inline_tag in b[2]] + + image_dir, image_fmt_str = image_path.psplit() + + figure_list = [] + plt.rcdefaults() + plt.rcParams.update(cfg.plot2rst_rcparams) + plt.close('all') + + example_globals = {} + rst_blocks = [] + fig_num = 1 + for i, (blabel, brange, bcontent) in enumerate(blocks): + if blabel == 'code': + exec(bcontent, example_globals) + rst_blocks.append(codestr2rst(bcontent)) + else: + if i in idx_inline_plot: + plt.savefig(image_path.format(fig_num)) + figure_name = image_fmt_str.format(fig_num) + fig_num += 1 + figure_list.append(figure_name) + figure_link = os.path.join('images', figure_name) + bcontent = bcontent.replace(inline_tag, figure_link) + rst_blocks.append(docstr2rst(bcontent)) + return figure_list, '\n'.join(rst_blocks) + + +def codestr2rst(codestr): + """Return reStructuredText code block from code string""" + code_directive = ".. code-block:: python\n\n" + indented_block = '\t' + codestr.replace('\n', '\n\t') + return code_directive + indented_block + + +def docstr2rst(docstr): + """Return reStructuredText from docstring""" + idx_whitespace = len(docstr.rstrip()) - len(docstr) + whitespace = docstr[idx_whitespace:] + return eval(docstr) + whitespace + + +def save_all_figures(image_path): + """Save all matplotlib figures. + + Parameters + ---------- + image_path : str + Path where plots are saved (format string which accepts figure number). + """ + figure_list = [] + image_dir, image_fmt_str = image_path.psplit() + fig_mngr = matplotlib._pylab_helpers.Gcf.get_all_fig_managers() + for fig_num in (m.num for m in fig_mngr): + # Set the fig_num figure as the current figure as we can't + # save a figure that's not the current figure. + plt.figure(fig_num) + plt.savefig(image_path.format(fig_num)) + figure_list.append(image_fmt_str.format(fig_num)) + return figure_list + diff --git a/doc/release/release_0.6.txt b/doc/release/release_0.6.txt new file mode 100644 index 00000000..8c525cb7 --- /dev/null +++ b/doc/release/release_0.6.txt @@ -0,0 +1,40 @@ +Announcement: scikits-image 0.6 +=============================== + +We're happy to announce the 6th version of scikits-image! + +Scikits-image is an image processing toolbox for SciPy that includes algorithms +for segmentation, geometric transformations, color space manipulation, +analysis, filtering, morphology, feature detection, and more. + +For more information, examples, and documentation, please visit our website + + http://skimage.org + +New Features +------------ +- Packaged in Debian as ``python-skimage`` +- Template matching +- Fast user-defined image warping +- Adaptive thresholding +- Structural similarity index +- Polygon, circle and ellipse drawing +- Peak detection +- Region properties +- TiffFile I/O plugin + +... along with some bug fixes and performance tweaks. + +Contributors to this release +---------------------------- +- Vincent Albufera +- David Cournapeau +- Christoph Gohlke +- Emmanuelle Gouillart +- Pieter Holtzhausen +- Zachary Pincus +- Johannes Schönberger +- Tom (tangofoxtrotmike) +- James Turner +- Stefan van der Walt +- Tony S Yu diff --git a/doc/source/conf.py b/doc/source/conf.py index 7b3ed3c9..c46ab806 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -23,17 +23,10 @@ sys.path.append(os.path.join(curpath, '..', 'ext')) # -- General configuration ----------------------------------------------------- -try: - import gen_rst -except: - pass - - # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.pngmath', 'numpydoc', - 'sphinx.ext.autosummary', 'sphinx.ext.inheritance_diagram', - 'plot_directive', 'gen_rst'] + 'sphinx.ext.autosummary', 'plot_directive', 'plot2rst'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -258,3 +251,8 @@ matplotlib.rcParams.update({ """ plot_include_source = True plot_formats = [('png', 100)] + +plot2rst_index_name = 'README' +plot2rst_rcparams = {'image.cmap' : 'gray', + 'image.interpolation' : 'none'} + diff --git a/doc/source/themes/agogo/static/agogo.css_t b/doc/source/themes/agogo/static/agogo.css_t index e35202c2..2ff4abad 100644 --- a/doc/source/themes/agogo/static/agogo.css_t +++ b/doc/source/themes/agogo/static/agogo.css_t @@ -79,6 +79,7 @@ h1, h2, h3, h4 { font-weight: normal; color: {{ theme_headercolor2 }}; margin-bottom: .8em; + clear: left; } h1 { diff --git a/doc/source/themes/agogo/static/docversions.js b/doc/source/themes/agogo/static/docversions.js index 4b36cf9c..d0144ee4 100644 --- a/doc/source/themes/agogo/static/docversions.js +++ b/doc/source/themes/agogo/static/docversions.js @@ -1,5 +1,5 @@ function insert_version_links() { - var labels = ['dev', '0.5', '0.4', '0.3']; + var labels = ['dev', '0.6', '0.5', '0.4', '0.3']; document.write('