diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..28104a17 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +# vim ft=yaml +# travis-ci.org definition for skimage build +# +# We pretend to be erlang because we need can't use the python support in +# travis-ci; it uses virtualenvs, they do not have numpy, scipy, matplotlib, +# and it is impractical to build them + +language: erlang +env: + - PYTHON=python PYSUF='' + # - PYTHON=python3 PYSUF=3 : python3-numpy not currently available +install: + # - sudo apt-get build-dep $PYTHON-numpy + - sudo apt-get install $PYTHON-dev + - sudo apt-get install $PYTHON-numpy + - sudo apt-get install $PYTHON-scipy + - sudo apt-get install $PYTHON-setuptools + - sudo apt-get install $PYTHON-nose + - sudo apt-get install cython + - sudo apt-get install libfreeimage3 + - $PYTHON setup.py build + - sudo $PYTHON setup.py install +script: + # Change into an innocuous directory and find tests from installation + - mkdir for_test + - cd for_test + - nosetests --exe -v --cover-package=skimage skimage + diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index baaf064b..c73b14e2 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -2,9 +2,9 @@ Project coordination - Nicolas Pinto - Colour spaces and filters. + Colour spaces and filters, and image resizing. Shape views: ``util.shape.view_as_windows`` and ``util.shape.view_as_blocks`` - Montage helpers: ``util.montage`` + Montage helpers: ``util.montage``. - Damian Eads Morphological operators @@ -73,7 +73,8 @@ From whom we borrowed the example generation tools. - Andreas Mueller - Example data set loader. + Example data set loader. Nosetest compatibility functions. + Quickshift image segmentation, Felzenszwalbs fast graph based segmentation. - Yaroslav Halchenko For sharing his expert advice on Debian packaging. @@ -102,8 +103,17 @@ - Nicolas Poilvert Shape views: ``util.shape.view_as_windows`` and ``util.shape.view_as_blocks`` + Image resizing. - Johannes Schönberger - Polygon, circle and ellipse drawing functions - Adaptive thresholding - Implementation of Matlab's `regionprops` + Drawing functions, adaptive thresholding, regionprops, geometric + transformations, LBPs, polygon approximations, web layout, and more. + +- Pavel Campr + Fixes and tests for Histograms of Oriented Gradients. + +- Joshua Warner + Multichannel random walker segmentation. + +- Petter Strandmark + Perimeter calculation in regionprops. diff --git a/DEPENDS.txt b/DEPENDS.txt index 2792870b..b2858256 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -4,10 +4,13 @@ Build Requirements * `Numpy >= 1.6 `__ * `Cython >= 0.15 `__ - `Matplotlib >= 1.0 `__ is needed to generate the examples in the documentation. +Runtime requirements +-------------------- +* `SciPy >= 0.10 `__ + Known build errors ------------------ On Windows, the error ``Error:unable to find vcvarsall.bat`` means that @@ -34,3 +37,4 @@ functionality is only available with the following installed: `FreeImage `__ The ``freeimage`` plugin provides support for reading various types of image file formats, including multi-page TIFFs. + diff --git a/DEVELOPMENT.txt b/DEVELOPMENT.txt index 4b42cf37..509cf322 100644 --- a/DEVELOPMENT.txt +++ b/DEVELOPMENT.txt @@ -4,44 +4,79 @@ Development process :doc:`Read this overview ` of how to use Git with ``skimage``. Here's the long and short of it: - * Go to `https://github.com/scikits-image/scikits-image - `_ and follow the + * Go to `https://github.com/scikit-image/scikit-image + `_ and follow the instructions on making your own fork. * Create a new branch for the feature you want to work on. Since the branch name will appear in the merge message, use a sensible name - such as 'your_name-transform-speedups'. + such as 'transform-speedups'. * Commit locally as you progress. * Push your changes back to github and create a pull request by clicking "request pull" in GitHub. * Optionally, mail the mailing list, explaining your changes. +.. note:: + + Do *not* merge the main branch into yours. If GitHub indicates that the + Pull Request can no longer be merged automatically, rebase onto master. + + (If you are curious, here's a further discussion on + the `dangers of rebasing `__. Also + see this `LWN article `__.) + +* To reviewers: add a short explanation of what a branch did to the merge + message or, if closing a bug, add "Closes gh-XXXX". + You may also read this summary by Fernando Perez of the IPython project on how they manage to keep review overhead to a minimum: http://mail.scipy.org/pipermail/ipython-dev/2010-October/006746.html -.. note:: - - Do *not* merge the main branch into yours. You may rebase, - as long as you are `aware of its dangers `_ - (also see `LWN article `_). - -* To reviewers: add a short explanation of what a branch did to the merge - message or, if closing a bug, add "Closes gh-XXXX". - Guidelines `````````` * All code should have tests (see "Test coverage" below for more details). * All code should be documented, to the same `standard `_ - as NumPy and SciPy. If possible, also add a section to the user guide. + as NumPy and SciPy. For new functionality, always add an example to the + gallery. * Follow the `Python PEPs `_ where possible. * No major changes should be committed without review. Ask on the - `mailing list `_ if + `mailing list `_ if you get no response to your pull request. * Examples in the gallery should have a maximum figure width of 8 inches. +Stylistic Guidelines +```````````````````` + * Use numpy data types instead of strings (``np.uint8`` instead of + ``"uint8"``). + + * Use the following import conventions:: + + import numpy as np + import matplotlib.pyplot as plt + + cimport numpy as cnp # in Cython code + + * When documenting array parameters, use ``image : (M, N) ndarray``, + ``image : (M, N, 3) ndarray`` and then refer to ``M`` and ``N`` in the + docstring. + + * Set up your editor to remove trailing whitespace. Follow `PEP08 + `__. Check code with pyflakes / flake8. + + * If a function name, say ``segment(...)``, has the same name as the file in + which it is implemented, name that file ``_segment.py`` so that it can still + be imported. All Cython files start with an underscore, e.g. + ``_some_module.pyx``. + + * Functions should support all input image dtypes. Use utility functions such + as ``img_as_float`` to help convert to an appropriate type. The output + format can be whatever is most efficient. This allows us to string together + several functions into a pipeline, e.g.:: + + hough(canny(my_image)) + Test coverage ````````````` Tests for a module should ideally cover all code in that module, @@ -64,4 +99,4 @@ detailing the test coverage:: Bugs ```` -Please `report bugs on Github `_. +Please `report bugs on Github `_. diff --git a/LICENSE.txt b/LICENSE.txt index b75d150d..6586c853 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,7 @@ Unless otherwise specified by LICENSE.txt files in individual directories, all code is -Copyright (C) 2011, the scikits-image team +Copyright (C) 2011, the scikit-image team All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.rst b/README.rst index a419b83a..508db396 100644 --- a/README.rst +++ b/README.rst @@ -3,11 +3,11 @@ Image Processing SciKit Source ------ -https://github.com/scikits-image/scikits-image +https://github.com/scikit-image/scikit-image Mailing List ------------ -http://groups.google.com/group/scikits-image +http://groups.google.com/group/scikit-image Installation from source ------------------------ diff --git a/RELEASE.txt b/RELEASE.txt index cea5adb7..ce05e24e 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -2,30 +2,89 @@ How to make a new release of ``skimage`` ======================================== - Update release notes. -- Update the version number in setup.py and bento.info and commit + + - To show a list contributors, run ``doc/release/contributors.sh ``, + where ```` is the first commit since the previous release. + +- 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 + - Build a clean version of the docs. Run ``make`` in the root dir, then ``rm build -rf; make html`` in the docs. - - Push upstream using "make gh-pages" + - Run ``make html`` again to copy the newly generated ``random.js`` into + place. Double check ``random.js``, otherwise the skimage.org front + page gets broken! + - Build using ``make gh-pages``. + - Push upstream: ``git push`` in ``doc/gh-pages``. + - Add the version number as a tag in git:: - git tag v0.6 + git tag v0.X.0 - Push the new meta-data to github:: - git push --tags origin master + git push --tags origin master -- Publish on PyPi: +- Publish on PyPi:: - python setup.py register - python setup.py sdist upload + python setup.py register + python setup.py sdist upload -- Increase the version number in the setup.py file to ``0.Xdev``. +- Increase the version number + + - In ``setup.py``, set to ``0.Xdev``. + - In ``bento.info``, set to ``0.X.dev0``. - Update the web frontpage: - The webpage is kept in a separate repo: scikits-image-web - - ``_templates/sidebar_versions.html`` - - ``index.rst`` + The webpage is kept in a separate repo: scikit-image-web + + - Sync your branch with the remote repo: ``git pull``. + If you try to ``make gh-pages`` when your branch is out of sync, it + creates headaches. + - Update stable and development version numbers in + ``_templates/sidebar_versions.html``. + - Add release date to ``index.rst`` under "Announcements". + - Build using ``make gh-pages``. + - Push upstream: ``git push`` in ``gh-pages``. - Post release notes on mailing lists, blog, G+, etc. + +Debian +------ + +- Tag the release as per instructions above. +- git checkout debian +- git merge v0.x.x +- uscan <- not sure if this step is necessary +- Update changelog (emacs has a good mode, requires package dpkg-dev-el) + - C-C C-v add new version, C-c C-c timestamp / save +- git commit -m 'Changelog entry for 0.x.x' +- git-buildpackage -uc -us -rfakeroot +- Sign the changes: debsign skimage_0.x.x-x_amd64.changes +- cd ../build-area && dput mentors skimage_0.x.x-x_amd64.changes +- The package should now be available at: + + http://mentors.debian.net/package/skimage + +For the last lines above to work, you need ``~/.gbp.conf``:: + + [DEFAULT] + upstream-tag = %(version)s + + [git-buildpackage] + sign-tags = True + export-dir = ../build-area/ + tarball-dir = ../tarballs/ + +As well as ``~/dput.cf``:: + + [mentors] + fqdn = mentors.debian.net + incoming = /upload + method = http + allow_unsigned_uploads = 0 + progress_indicator = 2 + # Allow uploads for UNRELEASED packages + allowed_distributions = .* diff --git a/TASKS.txt b/TASKS.txt index 24cb31eb..a2fcffe2 100644 --- a/TASKS.txt +++ b/TASKS.txt @@ -1,5 +1,6 @@ .. role:: strike + .. _howto_contribute: How to contribute to ``skimage`` @@ -15,111 +16,14 @@ How to contribute to ``skimage`` Developing Open Source is great fun! Join us on the `skimage mailing -list `_ and tell us which of the +list `_ and tell us which of the following challenges you'd like to solve. * Mentoring is available for those new to scientific programming in Python. * The technical detail of the `development process`_ is given below. * :doc:`How to use GitHub ` when developing skimage +* If you're looking something to implement, you can find a list of `requested features on github `__. In addition, you can browse the `open issues on github `__. .. contents:: :local: -Tasks ------ - -.. :doc:`gsoc2011` -.. :doc:`coverage_table` - -Implement Algorithms -```````````````````` -- Graph cut segmentation -- `Image colorization `__ -- Fast 2D convex hull (consider using CellProfiler version) - `Algorithm overview `__. - `One free implementation - `_. - [Compare against current implementation] -- Convex hulls of objects in a labels matrix (simply adapt current convex hull - image code--this one's low hanging fruit). Generalise this solution to also - skeletonize objects in a labels matrix. - -Drawing (directly on an ndarray) -```````````````````````````````` -- Wu's algorithm for circles -- Text rendering - -Infrastructure --------------- -- :strike:`Implement a new backend system so that we may start including - PyOpenCL-based algorithms` - -Adapt existing code for use -``````````````````````````` -These snippets and packages have already been written. Some need to be -modified to work as part of the scikit, others may be lacking in documentation -or tests. - - * :strike:`Connected components` - * Nadav's bilateral filtering (first compare against CellProfiler's - code, based on http://groups.csail.mit.edu/graphics/bilagrid/bilagrid_web.pdf) - Also see https://github.com/stefanv/scikits-image/tree/bilateral - * 2D image warping via thin-plate splines [ask Zach Pincus] - -Merge code provided by `CellProfiler `_ team -````````````````````````````````````````````````````````````````````````` -* Roberts filter - convolution with diagonal and anti-diagonal - kernels to detect edges -* Minimum enclosing circles of objects in a labels matrix -* spur removal, thinning, thickening, and other morphological operations on - binary images, framework for creating arbitrary morphological operations - using a 3x3 grid. - -Their SVN repository is read-accessible at - -- https://svn.broadinstitute.org/CellProfiler/trunk/CellProfiler/cellprofiler - -The files for the above algorithms are - -- https://svn.broadinstitute.org/CellProfiler/trunk/CellProfiler/cellprofiler/cpmath/cpmorphology.py -- https://svn.broadinstitute.org/CellProfiler/trunk/CellProfiler/cellprofiler/cpmath/filter.py - -There are test suites for the files at - -- https://svn.broadinstitute.org/CellProfiler/trunk/CellProfiler/cellprofiler/cpmath/tests/test_cpmorphology.py -- https://svn.broadinstitute.org/CellProfiler/trunk/CellProfiler/cellprofiler/cpmath/tests/test_filter.py - -Quoting a message from Lee Kamentsky to Stefan van der Walt sent on -5 August 2009:: - - We're part of the Broad Institute which is non-profit. We would be happy - to include our algorithm code in SciPy under the BSD license since that is - more appropriate for a library that might be integrated into a - commercial product whereas CellProfiler needs the more stringent - protection of GPL as an application. - -In 2010, Vebjorn Ljosa officially released parts of the code under a -BSD license (:doc:`cell_profiler` | `original message -`_). - -Thanks to Lee Kamentsky, Thouis Jones and Anne Carpenter and their colleagues -who contributed. - -Rework linear filters -````````````````````` -* Fast, SSE2 convolution (high priority) (see prototype in pull requests) -* Should take kernel or function for parameter (currently only takes function) -* Kernel shape should be specifiable (currently defaults to image shape) - -io -`` -* Update ``qt_plugin.py`` and other plugins to view collections. -* Rewrite GTK backend using GObject Introspection for Py3K compatibility. -* Add DICOM plugin for `GDCM `__. - -docs -```` -* Add examples to the gallery -* Write topics for the `user guide - `_ -* Integrate BiBTeX plugin into Sphinx build diff --git a/bento.info b/bento.info index f2d303db..a6424eb1 100644 --- a/bento.info +++ b/bento.info @@ -1,15 +1,15 @@ -Name: scikits-image -Version: 0.6 +Name: scikit-image +Version: 0.7.2 Summary: Image processing routines for SciPy -Url: http://scikits-image.org -DownloadUrl: http://github.com/scikits-image/scikits-image +Url: http://scikit-image.org +DownloadUrl: http://github.com/scikit-image/scikit-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/ + http://scikit-image.org/ Maintainer: Stefan van der Walt MaintainerEmail: stefan@sun.ac.za License: Modified BSD @@ -40,9 +40,6 @@ Library: 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 @@ -76,15 +73,9 @@ Library: 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 @@ -94,6 +85,36 @@ Library: Extension: skimage.graph.heap Sources: skimage/graph/heap.pyx + Extension: skimage.morphology._greyreconstruct + Sources: + skimage/morphology/_greyreconstruct.pyx + Extension: skimage.feature._texture + Sources: + skimage/feature/_texture.pyx + Extension: skimage._shared.transform + Sources: + skimage/_shared/transform.pyx + Extension: skimage.segmentation._slic + Sources: + skimage/segmentation/_slic.pyx + Extension: skimage.segmentation._quickshift + Sources: + skimage/segmentation/_quickshift.pyx + Extension: skimage.morphology._skeletonize_cy + Sources: + skimage/morphology/_skeletonize_cy.pyx + Extension: skimage.transform._warps_cy + Sources: + skimage/transform/_warps_cy.pyx + Extension: skimage._shared.interpolation + Sources: + skimage/_shared/interpolation.pyx + Extension: skimage.segmentation._felzenszwalb_cy + Sources: + skimage/segmentation/_felzenszwalb_cy.pyx + Extension: skimage._shared.geometry + Sources: + skimage/_shared/geometry.pyx Executable: skivi Module: skimage.scripts.skivi diff --git a/check_bento_build.py b/check_bento_build.py new file mode 100644 index 00000000..108b0aad --- /dev/null +++ b/check_bento_build.py @@ -0,0 +1,96 @@ +""" +Check that Cython extensions in setup.py files match those in bento.info. +""" +import os +import re + + +RE_CYTHON = re.compile("config.add_extension\(['\"]([\S]+)['\"]") + +BENTO_TEMPLATE = """ + Extension: {module_path} + Sources: + {dir_path}.pyx""" + + +def each_setup_in_pkg(top_dir): + """Yield path and file object for each setup.py file""" + for dir_path, dir_names, filenames in os.walk(top_dir): + for fname in filenames: + if fname == 'setup.py': + with open(os.path.join(dir_path, 'setup.py')) as f: + yield dir_path, f + + +def each_cy_in_setup(top_dir): + """Yield path and name for each cython extension package's setup file.""" + for dir_path, f in each_setup_in_pkg(top_dir): + text = f.read() + match = RE_CYTHON.findall(text) + if match: + for cy_file in match: + # if cython files in different directory than setup.py + if '.' in cy_file: + parts = cy_file.split('.') + cy_file = parts[-1] + # Don't overwrite dir_path for subsequent iterations. + path = os.path.join(dir_path, *parts[:-1]) + else: + path = dir_path + full_path = os.path.join(path, cy_file) + yield full_path, cy_file + + +def each_cy_in_bento(bento_file='bento.info'): + """Yield path and name for each cython extension in bento info file.""" + with open(bento_file) as f: + for line in f: + line = line.strip() + if line.startswith('Extension:'): + parts = line.split('.') + ext_name = parts[-1] + path = line.lstrip('Extension:').strip() + yield path, ext_name + + +def remove_common_extensions(cy_bento, cy_setup): + for ext_name in cy_bento.keys(): + if ext_name in cy_setup: + spath = cy_setup.pop(ext_name) + bpath = cy_bento.pop(ext_name) + if not spath.replace(os.path.sep, '.') == bpath: + print "Mismatched paths:" + print " setup.py: ", spath + print " bento.info:", bpath + +def print_results(cy_bento, cy_setup): + def info(text): + print + print(text) + print('-' * len(text)) + + print "Bento errors:" + print "-------------" + + if cy_bento: + info("The following extensions in 'bento.info' were not found:") + print('\n'.join(cy_bento.keys())) + + + if cy_setup: + info("The following cython files exist but were not in 'bento.info':") + print('\n'.join(cy_setup)) + info("Consider adding the following to the 'bento.info' Library:") + for ext_name, dir_path in cy_setup.iteritems(): + print BENTO_TEMPLATE.format(module_path=dir_path.replace('/', '.'), + dir_path=dir_path) + +if __name__ == '__main__': + # All cython extensions defined in 'setup.py' files. + cy_setup = dict((ext, path) for path, ext in each_cy_in_setup('skimage')) + + # All cython extensions defined 'bento.info' file. + cy_bento = dict((ext, path) for path, ext in each_cy_in_bento()) + + remove_common_extensions(cy_bento, cy_setup) + print_results(cy_bento, cy_setup) diff --git a/doc/Makefile b/doc/Makefile index f7866969..7552be6b 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -77,17 +77,17 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in build/qthelp, like this:" - @echo "# qcollectiongenerator build/qthelp/scikitsimage.qhcp" + @echo "# qcollectiongenerator build/qthelp/scikitimage.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile build/qthelp/scikitsimage.qhc" + @echo "# assistant -collectionFile build/qthelp/scikitimage.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(DEST)/devhelp @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/scikitsimage" - @echo "# ln -s build/devhelp $$HOME/.local/share/devhelp/scikitsimage" + @echo "# mkdir -p $$HOME/.local/share/devhelp/scikitimage" + @echo "# ln -s build/devhelp $$HOME/.local/share/devhelp/scikitimage" @echo "# devhelp" latex: @@ -123,9 +123,9 @@ gh-pages: python gh-pages.py gitwash: - python tools/gitwash/gitwash_dumper.py source scikits-image \ - --project-url=http://scikits-image.org \ - --project-ml-url=http://groups.google.com/group/scikits-image \ - --repo-name=scikits-image \ - --github-user=scikits-image \ + python tools/gitwash/gitwash_dumper.py source scikit-image \ + --project-url=http://scikit-image.org \ + --project-ml-url=http://groups.google.com/group/scikit-image \ + --repo-name=scikit-image \ + --github-user=scikit-image \ --source-suffix=.txt diff --git a/doc/examples/applications/plot_geometric.py b/doc/examples/applications/plot_geometric.py new file mode 100644 index 00000000..337ecad7 --- /dev/null +++ b/doc/examples/applications/plot_geometric.py @@ -0,0 +1,132 @@ +""" +=============================== +Using geometric transformations +=============================== + +In this example, we will see how to use geometric transformations in the context +of image processing. +""" + +import math +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data +from skimage import transform as tf + +margins = dict(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) + +""" +Basics +====== + +Several different geometric transformation types are supported: similarity, +affine, projective and polynomial. + +Geometric transformations can either be created using the explicit parameters +(e.g. scale, shear, rotation and translation) or the transformation matrix: + +First we create a transformation using explicit parameters: +""" + +tform = tf.SimilarityTransform(scale=1, rotation=math.pi / 2, + translation=(0, 1)) +print tform._matrix + +""" +Alternatively you can define a transformation by the transformation matrix +itself: +""" + +matrix = tform._matrix.copy() +matrix[1, 2] = 2 +tform2 = tf.SimilarityTransform(matrix) + +""" +These transformation objects can then be used to apply forward and inverse +coordinate transformations between the source and destination coordinate +systems: +""" + +coord = [1, 0] +print tform2(coord) +print tform2.inverse(tform(coord)) + +""" +Image warping +============= + +Geometric transformations can also be used to warp images: +""" + +text = data.text() + +tform = tf.SimilarityTransform(scale=1, rotation=math.pi / 4, + translation=(text.shape[0] / 2, -100)) + +rotated = tf.warp(text, tform) +back_rotated = tf.warp(rotated, tform.inverse) + +fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(8, 3)) +fig.subplots_adjust(**margins) +plt.gray() +ax1.imshow(text) +ax1.axis('off') +ax2.imshow(rotated) +ax2.axis('off') +ax3.imshow(back_rotated) +ax3.axis('off') + +""" +.. image:: PLOT2RST.current_figure + +Parameter estimation +==================== + +In addition to the basic functionality mentioned above you can also estimate the +parameters of a geometric transformation using the least-squares method. + +This can amongst other things be used for image registration or rectification, +where you have a set of control points or homologous/corresponding points in two +images. + +Let's assume we want to recognize letters on a photograph which was not taken +from the front but at a certain angle. In the simplest case of a plane paper +surface the letters are projectively distorted. Simple matching algorithms would +not be able to match such symbols. One solution to this problem would be to warp +the image so that the distortion is removed and then apply a matching algorithm: +""" + +text = data.text() + +src = np.array(( + (0, 0), + (0, 50), + (300, 50), + (300, 0) +)) +dst = np.array(( + (155, 15), + (65, 40), + (260, 130), + (360, 95) +)) + +tform3 = tf.ProjectiveTransform() +tform3.estimate(src, dst) +warped = tf.warp(text, tform3, output_shape=(50, 300)) + +fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(8, 3)) +fig.subplots_adjust(**margins) +plt.gray() +ax1.imshow(text) +ax1.plot(dst[:, 0], dst[:, 1], '.r') +ax1.axis('off') +ax2.imshow(warped) +ax2.axis('off') + +""" +.. image:: PLOT2RST.current_figure +""" + +plt.show() diff --git a/doc/examples/plot_holes_and_peaks.py b/doc/examples/plot_holes_and_peaks.py new file mode 100644 index 00000000..a9c3a7fe --- /dev/null +++ b/doc/examples/plot_holes_and_peaks.py @@ -0,0 +1,86 @@ +""" +=============================== +Filling holes and finding peaks +=============================== + +In this example, we fill holes (i.e. isolated, dark spots) in an image using +morphological reconstruction by erosion. Erosion expands the minimal values of +the seed image until it encounters a mask image. Thus, the seed image and mask +image represent the maximum and minimum possible values of the reconstructed +image. + +We start with an image containing both peaks and holes: +""" +import matplotlib.pyplot as plt + +from skimage import data +from skimage.exposure import rescale_intensity + +image = data.moon() +# Rescale image intensity so that we can see dim features. +image = rescale_intensity(image, in_range=(50, 200)) + +# convenience function for plotting images +def imshow(image, **kwargs): + plt.figure(figsize=(5, 4)) + plt.imshow(image, **kwargs) + plt.axis('off') + +imshow(image) +plt.title('original image') + +""" +.. image:: PLOT2RST.current_figure + +Now we need to create the seed image, where the minima represent the starting +points for erosion. To fill holes, we initialize the seed image to the maximum +value of the original image. Along the borders, however, we use the original +values of the image. These border pixels will be the starting points for the +erosion process. We then limit the erosion by setting the mask to the values +of the original image. +""" + +import numpy as np +from skimage.morphology import reconstruction + +seed = np.copy(image) +seed[1:-1, 1:-1] = image.max() +mask = image + +filled = reconstruction(seed, mask, method='erosion') + +imshow(filled, vmin=image.min(), vmax=image.max()) +plt.title('after filling holes') + +""" +.. image:: PLOT2RST.current_figure + +As shown above, eroding inward from the edges removes holes, since (by +definition) holes are surrounded by pixels of brighter value. Finally, we can +isolate the dark regions by subtracting the reconstructed image from the +original image. +""" + +imshow(image - filled) +plt.title('holes') + +""" +.. image:: PLOT2RST.current_figure + +Alternatively, we can find bright spots in an image using morphological +reconstruction by dilation. Dilation is the inverse of erosion and expands the +*maximal* values of the seed image until it encounters a mask image. Since this +is an inverse operation, we initialize the seed image to the minimum image +intensity instead of the maximum. The remainder of the process is the same. +""" + +seed = np.copy(image) +seed[1:-1, 1:-1] = image.min() +rec = reconstruction(seed, mask, method='dilation') +imshow(image - rec) +plt.title('peaks') +plt.show() + +""" +.. image:: PLOT2RST.current_figure +""" diff --git a/doc/examples/plot_hough_transform.py b/doc/examples/plot_hough_transform.py index 51a90c64..5416d662 100644 --- a/doc/examples/plot_hough_transform.py +++ b/doc/examples/plot_hough_transform.py @@ -109,7 +109,7 @@ plt.title('Input image') plt.subplot(132) plt.imshow(edges, cmap=plt.cm.gray) -plt.title('Sobel edges') +plt.title('Canny edges') plt.subplot(133) plt.imshow(edges * 0) diff --git a/doc/examples/plot_label.py b/doc/examples/plot_label.py new file mode 100644 index 00000000..8c46cb8e --- /dev/null +++ b/doc/examples/plot_label.py @@ -0,0 +1,57 @@ +""" +=================== +Label image regions +=================== + +This example shows how to segment an image with image labelling. The following +steps are applied: + +1. Thresholding with automatic Otsu method +2. Close small holes with binary closing +3. Remove artifacts touching image border +4. Measure image regions to filter small objects + +""" + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches + +from skimage import data +from skimage.filter import threshold_otsu +from skimage.segmentation import clear_border +from skimage.morphology import label, closing, square +from skimage.measure import regionprops + + +image = data.coins()[50:-50, 50:-50] + +# apply threshold +thresh = threshold_otsu(image) +bw = closing(image > thresh, square(3)) + +# remove artifacts connected to image border +cleared = bw.copy() +clear_border(cleared) + +# label image regions +label_image = label(cleared) +borders = np.logical_xor(bw, cleared) +label_image[borders] = -1 + +fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) +ax.imshow(label_image, cmap='jet') + +for region in regionprops(label_image, ['Area', 'BoundingBox']): + + # skip small images + if region['Area'] < 100: + continue + + # draw rectangle around segmented coins + minr, minc, maxr, maxc = region['BoundingBox'] + rect = mpatches.Rectangle((minc, minr), maxc - minc, maxr - minr, + fill=False, edgecolor='red', linewidth=2) + ax.add_patch(rect) + +plt.show() diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py new file mode 100644 index 00000000..85fd5b95 --- /dev/null +++ b/doc/examples/plot_local_binary_pattern.py @@ -0,0 +1,87 @@ +""" +=============================================== +Local Binary Pattern for texture classification +=============================================== + +In this example, we will see how to classify textures based on LBP (Local +Binary Pattern). The histogram of the LBP result is a good measure to classify +textures. For simplicity the histogram distributions are then tested against +each other using the Kullback-Leibler-Divergence. +""" + +import numpy as np +import matplotlib +import matplotlib.pyplot as plt +import scipy.ndimage as nd +import skimage.feature as ft +from skimage import data + + +# settings for LBP +METHOD = 'uniform' +P = 16 +R = 2 +matplotlib.rcParams['font.size'] = 9 + + +def kullback_leibler_divergence(p, q): + p = np.asarray(p) + q = np.asarray(q) + filt = np.logical_and(p != 0, q != 0) + return np.sum(p[filt] * np.log2(p[filt] / q[filt])) + + +def match(refs, img): + best_score = 10 + best_name = None + lbp = ft.local_binary_pattern(img, P, R, METHOD) + hist, _ = np.histogram(lbp, normed=True, bins=P + 2, range=(0, P + 2)) + for name, ref in refs.items(): + ref_hist, _ = np.histogram(ref, normed=True, bins=P + 2, + range=(0, P + 2)) + score = kullback_leibler_divergence(hist, ref_hist) + if score < best_score: + best_score = score + best_name = name + return best_name + + +brick = data.load('brick.png') +grass = data.load('grass.png') +wall = data.load('rough-wall.png') + +refs = { + 'brick': ft.local_binary_pattern(brick, P, R, METHOD), + 'grass': ft.local_binary_pattern(grass, P, R, METHOD), + 'wall': ft.local_binary_pattern(wall, P, R, METHOD) +} + +# classify rotated textures +print 'Rotated images matched against references using LBP:' +print 'original: brick, rotated: 30deg, match result:', +print match(refs, nd.rotate(brick, angle=30, reshape=False)) +print 'original: brick, rotated: 70deg, match result:', +print match(refs, nd.rotate(brick, angle=70, reshape=False)) +print 'original: grass, rotated: 145deg, match result:', +print match(refs, nd.rotate(grass, angle=145, reshape=False)) + +# plot histograms of LBP of textures +fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(nrows=2, ncols=3, + figsize=(9, 6)) +plt.gray() + +ax1.imshow(brick) +ax1.axis('off') +ax4.hist(refs['brick'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +ax4.set_ylabel('Percentage') + +ax2.imshow(grass) +ax2.axis('off') +ax5.hist(refs['grass'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +ax5.set_xlabel('Uniform LBP values') + +ax3.imshow(wall) +ax3.axis('off') +ax6.hist(refs['wall'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) + +plt.show() diff --git a/doc/examples/plot_piecewise_affine.py b/doc/examples/plot_piecewise_affine.py new file mode 100644 index 00000000..2dcbd9f1 --- /dev/null +++ b/doc/examples/plot_piecewise_affine.py @@ -0,0 +1,41 @@ +""" +=============================== +Piecewise Affine Transformation +=============================== + +This example shows how to use the Piecewise Affine Transformation. +""" + +import numpy as np +import matplotlib.pyplot as plt +from skimage.transform import PiecewiseAffineTransform, warp +from skimage import data + + +image = data.lena() +rows, cols = image.shape[0], image.shape[1] + +src_cols = np.linspace(0, cols, 20) +src_rows = np.linspace(0, rows, 10) +src_rows, src_cols = np.meshgrid(src_rows, src_cols) +src = np.dstack([src_cols.flat, src_rows.flat])[0] + +# add sinusoidal oscillation to row coordinates +dst_rows = src[:, 1] - np.sin(np.linspace(0, 3 * np.pi, src.shape[0])) * 50 +dst_cols = src[:, 0] +dst_rows *= 1.5 +dst_rows -= 1.5 * 50 +dst = np.vstack([dst_cols, dst_rows]).T + + +tform = PiecewiseAffineTransform() +tform.estimate(src, dst) + +out_rows = image.shape[0] - 1.5 * 50 +out_cols = cols +out = warp(image, tform, output_shape=(out_rows, out_cols)) + +plt.imshow(out) +plt.plot(tform.inverse(src)[:, 0], tform.inverse(src)[:, 1], '.b') +plt.axis((0, out_cols, out_rows, 0)) +plt.show() diff --git a/doc/examples/plot_polygon.py b/doc/examples/plot_polygon.py new file mode 100644 index 00000000..05ca5359 --- /dev/null +++ b/doc/examples/plot_polygon.py @@ -0,0 +1,77 @@ +""" +================================== +Approximate and subdivide polygons +================================== + +This example shows how to approximate (Douglas-Peucker algorithm) and subdivide +(B-Splines) polygonal chains. +""" + +import numpy as np +import matplotlib.pyplot as plt +from skimage.draw import ellipse +from skimage.measure import find_contours, approximate_polygon, \ + subdivide_polygon + + +hand = np.array([[1.64516129, 1.16145833], + [1.64516129, 1.59375 ], + [1.35080645, 1.921875 ], + [1.375 , 2.18229167], + [1.68548387, 1.9375 ], + [1.60887097, 2.55208333], + [1.68548387, 2.69791667], + [1.76209677, 2.56770833], + [1.83064516, 1.97395833], + [1.89516129, 2.75 ], + [1.9516129 , 2.84895833], + [2.01209677, 2.76041667], + [1.99193548, 1.99479167], + [2.11290323, 2.63020833], + [2.2016129 , 2.734375 ], + [2.25403226, 2.60416667], + [2.14919355, 1.953125 ], + [2.30645161, 2.36979167], + [2.39112903, 2.36979167], + [2.41532258, 2.1875 ], + [2.1733871 , 1.703125 ], + [2.07782258, 1.16666667]]) + +# subdivide polygon using 2nd degree B-Splines +new_hand = hand.copy() +for _ in range(5): + new_hand = subdivide_polygon(new_hand, degree=2, preserve_ends=True) + +# approximate subdivided polygon with Douglas-Peucker algorithm +appr_hand = approximate_polygon(new_hand, tolerance=0.02) + +print "Number of coordinates:", len(hand), len(new_hand), len(appr_hand) + +fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(9, 4)) + +ax1.plot(hand[:, 0], hand[:, 1]) +ax1.plot(new_hand[:, 0], new_hand[:, 1]) +ax1.plot(appr_hand[:, 0], appr_hand[:, 1]) + + +# create two ellipses in image +img = np.zeros((800, 800), 'int32') +rr, cc = ellipse(250, 250, 180, 230, img.shape) +img[rr, cc] = 1 +rr, cc = ellipse(600, 600, 150, 90, img.shape) +img[rr, cc] = 1 + +plt.gray() +ax2.imshow(img) + +# approximate / simplify coordinates of the two ellipses +for contour in find_contours(img, 0): + coords = approximate_polygon(contour, tolerance=2.5) + ax2.plot(coords[:, 1], coords[:, 0], '-r', linewidth=2) + coords2 = approximate_polygon(contour, tolerance=39.5) + ax2.plot(coords2[:, 1], coords2[:, 0], '-g', linewidth=2) + print "Number of coordinates:", len(contour), len(coords), len(coords2) + +ax2.axis((0, 800, 0, 800)) + +plt.show() diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py new file mode 100644 index 00000000..eb3896f4 --- /dev/null +++ b/doc/examples/plot_pyramid.py @@ -0,0 +1,35 @@ +""" +==================== +Build image pyramids +==================== + +The `pyramid_gaussian` function takes an image and yields successive images +shrunk by a constant scale factor. Image pyramids are often used, e.g., to +implement algorithms for denoising, texture discrimination, and scale- invariant +detection. + +""" + +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data +from skimage.transform import pyramid_gaussian + + +image = data.lena() +rows, cols, dim = image.shape +pyramid = tuple(pyramid_gaussian(image, downscale=2)) + +composite_image = np.zeros((rows, cols + cols / 2, 3), dtype=np.double) + +composite_image[:rows, :cols, :] = pyramid[0] + +i_row = 0 +for p in pyramid[1:]: + n_rows, n_cols = p.shape[:2] + composite_image[i_row:i_row + n_rows, cols:cols + n_cols] = p + i_row += n_rows + +plt.imshow(composite_image) +plt.show() diff --git a/doc/examples/plot_regional_maxima.py b/doc/examples/plot_regional_maxima.py new file mode 100644 index 00000000..9d4de9b1 --- /dev/null +++ b/doc/examples/plot_regional_maxima.py @@ -0,0 +1,113 @@ +""" +========================= +Filtering regional maxima +========================= + +Here, we use morphological reconstruction to create a background image, which +we can subtract from the original image to isolate bright features (regional +maxima). + +First we try reconstruction by dilation starting at the edges of the image. We +initialize a seed image to the minimum intensity of the image, and set its +border to be the pixel values in the original image. These maximal pixels will +get dilated in order to reconstruct the background image. +""" +import numpy as np + +from skimage import data +from skimage import img_as_float +from skimage.morphology import reconstruction +from scipy.ndimage import gaussian_filter +import matplotlib.pyplot as plt + +# Convert to float: Important for subtraction later which won't work with uint8 +image = img_as_float(data.coins()) +image = gaussian_filter(image, 1) + +seed = np.copy(image) +seed[1:-1, 1:-1] = image.min() +mask = image + +dilated = reconstruction(seed, mask, method='dilation') + +""" +Subtracting the dilated image leaves an image with just the coins and a flat, +black background, as shown below. +""" + +fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(8, 2.5)) + +ax1.imshow(image) +ax1.set_title('original image') +ax1.axis('off') + +ax2.imshow(dilated, vmin=image.min(), vmax=image.max()) +ax2.set_title('dilated') +ax2.axis('off') + +ax3.imshow(image - dilated) +ax3.set_title('image - dilated') +ax3.axis('off') + +plt.tight_layout() + +""" + +.. image:: PLOT2RST.current_figure + +Although the features (i.e. the coins) are clearly isolated, the coins +surrounded by a bright background in the original image are dimmer in the +subtracted image. We can attempt to correct this using a different seed image. + +Instead of creating a seed image with maxima along the image border, we can use +the features of the image itself to seed the reconstruction process. Here, the +seed image is the original image minus a fixed value, ``h``. +""" + +h = 0.4 +seed = image - h +dilated = reconstruction(seed, mask, method='dilation') +hdome = image - dilated + +""" +To get a feel for the reconstruction process, we plot the intensity of the +mask, seed, and dilated images along a slice of the image (indicated by red +line). +""" + +fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(8, 2.5)) + +yslice = 197 + +ax1.plot(mask[yslice], '0.5', label='mask') +ax1.plot(seed[yslice], 'k', label='seed') +ax1.plot(dilated[yslice], 'r', label='dilated') +ax1.set_ylim(-0.2, 2) +ax1.set_title('image slice') +ax1.set_xticks([]) +ax1.legend() + +ax2.imshow(dilated, vmin=image.min(), vmax=image.max()) +ax2.axhline(yslice, color='r', alpha=0.4) +ax2.set_title('dilated') +ax2.axis('off') + +ax3.imshow(hdome) +ax3.axhline(yslice, color='r', alpha=0.4) +ax3.set_title('image - dilated') +ax3.axis('off') + +plt.tight_layout() +plt.show() + +""" +.. image:: PLOT2RST.current_figure + +As you can see in the image slice, each coin is given a different baseline +intensity in the reconstructed image; this is because we used the local +intensity (shifted by ``h``) as a seed value. As a result, the coins in the +subtracted image have similar pixel intensities. The final result is known as +the h-dome of an image since this tends to isolate regional maxima of height +``h``. This operation is particularly useful when your images are unevenly +illuminated. +""" diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py new file mode 100644 index 00000000..99bfefcc --- /dev/null +++ b/doc/examples/plot_segmentations.py @@ -0,0 +1,92 @@ +""" +==================================================== +Comparison of segmentation and superpixel algorithms +==================================================== + +This example compares three popular low-level image segmentation methods. As +it is difficult to obtain good segmentations, and the definition of "good" +often depends on the application, these methods are usually used for obtaining +an oversegmentation, also known as superpixels. These superpixels then serve as +a basis for more sophisticated algorithms such as conditional random fields +(CRF). + + +Felzenszwalb's efficient graph based segmentation +------------------------------------------------- +This fast 2D image segmentation algorithm, proposed in [1]_ is popular in the +computer vision community. +The algorithm has a single ``scale`` parameter that influences the segment +size. The actual size and number of segments can vary greatly, depending on +local contrast. + +.. [1] Efficient graph-based image segmentation, Felzenszwalb, P.F. and + Huttenlocher, D.P. International Journal of Computer Vision, 2004 + + +Quickshift image segmentation +----------------------------- + +Quickshift is a relatively recent 2D image segmentation algorithm, based on an +approximation of kernelized mean-shift. Therefore it belongs to the family of +local mode-seeking algorithms and is applied to the 5D space consisting of +color information and image location [2]_. + +One of the benefits of quickshift is that it actually computes a +hierarchical segmentation on multiple scales simultaneously. + +Quickshift has two main parameters: ``sigma`` controls the scale of the local +density approximation, ``max_dist`` selects a level in the hierarchical +segmentation that is produced. There is also a trade-off between distance in +color-space and distance in image-space, given by ``ratio``. + +.. [2] Quick shift and kernel methods for mode seeking, + Vedaldi, A. and Soatto, S. + European Conference on Computer Vision, 2008 + + +SLIC - K-Means based image segmentation +--------------------------------------- +This algorithm simply performs K-means in the 5d space of color information +and image location and is therefore closely related to quickshift. As the +clustering method is simpler, it is very efficient. It is essential for this +algorithm to work in Lab color space to obtain good results. The algorithm +quickly gained momentum and is now widely used. See [3] for details. The +``ratio`` parameter trades off color-similarity and proximity, as in the case +of Quickshift, while ``n_segments`` chooses the number of centers for kmeans. + +.. [3] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, + Pascal Fua, and Sabine Suesstrunk, SLIC Superpixels Compared to + State-of-the-art Superpixel Methods, TPAMI, May 2012. +""" + +import matplotlib.pyplot as plt +import numpy as np + +from skimage.data import lena +from skimage.segmentation import felzenszwalb, \ + visualize_boundaries, slic, quickshift +from skimage.util import img_as_float + +img = img_as_float(lena()[::2, ::2]) +segments_fz = felzenszwalb(img, scale=100, sigma=0.5, min_size=50) +segments_slic = slic(img, ratio=10, n_segments=250, sigma=1) +segments_quick = quickshift(img, kernel_size=3, max_dist=6, ratio=0.5) + +print("Felzenszwalb's number of segments: %d" % len(np.unique(segments_fz))) +print("Slic number of segments: %d" % len(np.unique(segments_slic))) +print("Quickshift number of segments: %d" % len(np.unique(segments_quick))) + +fig, ax = plt.subplots(1, 3) +fig.set_size_inches(8, 3, forward=True) +plt.subplots_adjust(0.05, 0.05, 0.95, 0.95, 0.05, 0.05) + +ax[0].imshow(visualize_boundaries(img, segments_fz)) +ax[0].set_title("Felzenszwalbs's method") +ax[1].imshow(visualize_boundaries(img, segments_slic)) +ax[1].set_title("SLIC") +ax[2].imshow(visualize_boundaries(img, segments_quick)) +ax[2].set_title("Quickshift") +for a in ax: + a.set_xticks(()) + a.set_yticks(()) +plt.show() diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 8107fc80..9e586209 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -19,11 +19,11 @@ import numpy as np img = np.zeros((500, 500, 3), 'uint8') -#: draw line +# draw line rr, cc = line(120, 123, 20, 400) img[rr,cc,0] = 255 -#: fill polygon +# fill polygon poly = np.array(( (300, 300), (480, 320), @@ -34,11 +34,11 @@ poly = np.array(( rr, cc = polygon(poly[:,0], poly[:,1], img.shape) img[rr,cc,1] = 255 -#: fill circle +# fill circle rr, cc = circle(200, 200, 100, img.shape) img[rr,cc,:] = (255, 255, 0) -#: fill ellipse +# fill ellipse rr, cc = ellipse(300, 300, 100, 200, img.shape) img[rr,cc,2] = 255 diff --git a/doc/examples/plot_skeleton.py b/doc/examples/plot_skeleton.py index a0ad692f..65a0ee48 100644 --- a/doc/examples/plot_skeleton.py +++ b/doc/examples/plot_skeleton.py @@ -16,7 +16,7 @@ In the case of boolean, 'True' indicates foreground, and for integer arrays, the foreground is 1's. """ from skimage.morphology import skeletonize -from skimage.draw import draw +from skimage import draw import numpy as np import matplotlib.pyplot as plt diff --git a/doc/ext/plot2rst.py b/doc/ext/plot2rst.py index 67355289..65c77521 100644 --- a/doc/ext/plot2rst.py +++ b/doc/ext/plot2rst.py @@ -9,7 +9,7 @@ To generate your own examples, add this extension to the list of 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. +This code was adapted from scikit-image, which took it from scikit-learn. Options ------- @@ -27,9 +27,10 @@ plot2rst_rcparams : dict 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_thumb_shape : float + Shape of thumbnail in pixels. The image is resized to fit within this shape + and the excess is filled with white pixels. This fixed size ensures that + that gallery images are displayed in a grid. plot2rst_plot_tag : str When this tag is found in the example file, the current plot is saved and @@ -73,7 +74,10 @@ import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt -from matplotlib import image + +from skimage import io +from skimage import transform +from skimage.util.dtype import dtype_range LITERALINCLUDE = """ @@ -160,7 +164,7 @@ def setup(app): ('../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_thumb_shape', (250, 300), True) app.add_config_value('plot2rst_plot_tag', 'PLOT2RST.current_figure', True) app.add_config_value('plot2rst_index_name', 'index', True) @@ -335,7 +339,8 @@ def write_example(src_name, src_dir, rst_dir, cfg): 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) + first_image = io.imread(first_image_file) + save_thumbnail(first_image, thumb_path, cfg.plot2rst_thumb_shape) if not thumb_path.exists: if cfg.plot2rst_default_thumb is None: @@ -345,6 +350,28 @@ def write_example(src_name, src_dir, rst_dir, cfg): shutil.copy(cfg.plot2rst_default_thumb, thumb_path) +def save_thumbnail(image, thumb_path, shape): + """Save image as a thumbnail with the specified shape. + + The image is first resized to fit within the specified shape and then + centered in an array of the specified shape before saving. + """ + rescale = min(float(w_1) / w_2 for w_1, w_2 in zip(shape, image.shape)) + small_shape = (rescale * np.asarray(image.shape[:2])).astype(int) + small_image = transform.resize(image, small_shape) + + if len(image.shape) == 3: + shape = shape + (image.shape[2],) + background_value = dtype_range[small_image.dtype.type][1] + thumb = background_value * np.ones(shape, dtype=small_image.dtype) + + i = (shape[0] - small_shape[0]) // 2 + j = (shape[1] - small_shape[1]) // 2 + thumb[i:i+small_shape[0], j:j+small_shape[1]] = small_image + + io.imsave(thumb_path, thumb) + + def _plots_are_current(src_path, image_path): first_image_file = Path(image_path.format(1)) needs_replot = (not first_image_file.exists or diff --git a/doc/gh-pages.py b/doc/gh-pages.py index 849b7053..ab29959e 100644 --- a/doc/gh-pages.py +++ b/doc/gh-pages.py @@ -30,7 +30,7 @@ from subprocess import Popen, PIPE, CalledProcessError, check_call pages_dir = 'gh-pages' html_dir = 'build/html' pdf_dir = 'build/latex' -pages_repo = 'git@github.com:scikits-image/docs.git' +pages_repo = 'git@github.com:scikit-image/docs.git' #----------------------------------------------------------------------------- # Functions @@ -108,7 +108,7 @@ if __name__ == '__main__': shutil.copytree(html_dir, dest) # copy pdf file into tree #shutil.copy(pjoin(pdf_dir, 'scikits.image.pdf'), pjoin(dest, 'scikits.image.pdf')) - + try: cd(pages_dir) status = sh2('git status | head -1') @@ -117,7 +117,7 @@ if __name__ == '__main__': e = 'On %r, git branch is %r, MUST be "gh-pages"' % (pages_dir, branch) raise RuntimeError(e) - sh("touch .nojekyll") + sh("touch .nojekyll") sh('git add .nojekyll') sh('git add index.html') sh('git add %s' % tag) @@ -131,4 +131,4 @@ if __name__ == '__main__': print print 'Now verify the build in: %r' % dest - print "If everything looks good, 'git push'" + print "If everything looks good, run 'git push' inside doc/gh-pages." diff --git a/doc/logo/Makefile b/doc/logo/Makefile index 46c36fda..1cf861bd 100644 --- a/doc/logo/Makefile +++ b/doc/logo/Makefile @@ -1,11 +1,11 @@ .PHONY: logo logo: green_orange_snake.png snake_logo.svg - inkscape --export-png=scikits_image_logo.png --export-dpi=100 \ + inkscape --export-png=scikit_image_logo.png --export-dpi=100 \ --export-area-drawing --export-background-opacity=1 \ snake_logo.svg python shrink_logo.py green_orange_snake.png: - python scikits_image_logo.py --no-plot + python scikit_image_logo.py --no-plot diff --git a/doc/logo/scikits_image_logo.py b/doc/logo/scikit_image_logo.py similarity index 100% rename from doc/logo/scikits_image_logo.py rename to doc/logo/scikit_image_logo.py diff --git a/doc/logo/shrink_logo.py b/doc/logo/shrink_logo.py index 3b7d91ba..205e1bf4 100644 --- a/doc/logo/shrink_logo.py +++ b/doc/logo/shrink_logo.py @@ -2,7 +2,7 @@ from skimage import io, transform s = 0.7 -img = io.imread('scikits_image_logo.png') +img = io.imread('scikit_image_logo.png') h, w, c = img.shape print "\nScaling down logo by %.1fx..." % s @@ -13,4 +13,4 @@ img = transform.homography(img, [[s, 0, 0], output_shape=(int(h*s), int(w*s), 4), order=3) -io.imsave('scikits_image_logo_small.png', img) +io.imsave('scikit_image_logo_small.png', img) diff --git a/doc/logo/snake_logo.svg b/doc/logo/snake_logo.svg index caa47694..5f9f634f 100644 --- a/doc/logo/snake_logo.svg +++ b/doc/logo/snake_logo.svg @@ -74,7 +74,7 @@ y="278.58533" x="261.22247" id="tspan3152" - sodipodi:role="line">scikits-image + sodipodi:role="line">scikit-image qcollectiongenerator build\qthelp\scikitsimage.qhcp + echo.^> qcollectiongenerator build\qthelp\scikitimage.qhcp echo.To view the help file: - echo.^> assistant -collectionFile build\qthelp\scikitsimage.ghc + echo.^> assistant -collectionFile build\qthelp\scikitimage.ghc goto end ) diff --git a/doc/release/contributors.sh b/doc/release/contributors.sh index caf553ee..023928d3 100755 --- a/doc/release/contributors.sh +++ b/doc/release/contributors.sh @@ -1,2 +1,2 @@ -git log $1..HEAD --format='* %aN' | sed 's/@/\-at\-/' | sed 's/<>//' | sort -u +git log $1..HEAD --format='- %aN' | sed 's/@/\-at\-/' | sed 's/<>//' | sort -u diff --git a/doc/release/release_0.7.txt b/doc/release/release_0.7.txt new file mode 100644 index 00000000..a6b591cb --- /dev/null +++ b/doc/release/release_0.7.txt @@ -0,0 +1,69 @@ +Announcement: scikits-image 0.7.0 +================================= + +We're happy to announce the 7th 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 +------------ + +It's been only 3 months since scikits-image 0.6 was released, but in that short +time, we've managed to add plenty of new features and enhancements, including + +- Geometric image transforms +- 3 new image segmentation routines (Felsenzwalb, Quickshift, SLIC) +- Local binary patterns for texture characterization +- Morphological reconstruction +- Polygon approximation +- CIE Lab color space conversion +- Image pyramids +- Multispectral support in random walker segmentation +- Slicing, concatenation, and natural sorting of image collections +- Perimeter and coordinates measurements in regionprops +- An extensible image viewer based on Qt and Matplotlib, with plugins for edge + detection, line-profiling, and viewing image collections + +Plus, this release adds a number of bug fixes, new examples, and performance +enhancements. + + +Contributors to this release +---------------------------- + +This release was only possible due to the efforts of many contributors, both +new and old. + +- Andreas Mueller +- Andreas Wuerl +- Andy Wilson +- Brian Holt +- Christoph Gohlke +- Dharhas Pothina +- Emmanuelle Gouillart +- Guillaume Gay +- Josh Warner +- James Bergstra +- Johannes Schonberger +- Jonathan J. Helmus +- Juan Nunez-Iglesias +- Leon Tietz +- Marianne Corvellec +- Matt McCormick +- Neil Yager +- Nicolas Pinto +- Nicolas Poilvert +- Pavel Campr +- Petter Strandmark +- Stefan van der Walt +- Tim Sheerman-Chase +- Tomas Kazmar +- Tony S Yu +- Wei Li diff --git a/doc/source/_static/default.css_t b/doc/source/_static/default.css_t deleted file mode 100644 index 4497c5e6..00000000 --- a/doc/source/_static/default.css_t +++ /dev/null @@ -1,144 +0,0 @@ -/* This CSS stylesheet is no longer used. Edit agogo.css instead. */ - -/** - * Sphinx stylesheet -- default theme - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: {{ theme_bodyfont }}; - font-size: 100%; - background-color: {{ theme_footerbgcolor }}; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - background-color: {{ theme_sidebarbgcolor }}; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 230px; -} - -div.body { - background-color: {{ theme_bgcolor }}; - color: {{ theme_textcolor }}; - padding: 0 20px 30px 20px; - overflow: auto; -} - -{%- if theme_rightsidebar|tobool %} -div.bodywrapper { - margin: 0 230px 0 0; -} -{%- endif %} - -div.footer { - color: {{ theme_footertextcolor }}; - width: 100%; - padding: 9px 0 9px 0; - text-align: center; - font-size: 75%; -} - -div.footer a { - color: {{ theme_footertextcolor }}; - text-decoration: underline; -} - -div.related { - background-color: {{ theme_relbarbgcolor }}; - line-height: 30px; - color: {{ theme_relbartextcolor }}; -} - -div.related a { - color: {{ theme_relbarlinkcolor }}; -} - -div.sphinxsidebar { - {%- if theme_stickysidebar|tobool %} - top: 30px; - margin: 0; - position: fixed; - overflow: auto; - height: 100%; - {%- endif %} - {%- if theme_rightsidebar|tobool %} - float: right; - {%- if theme_stickysidebar|tobool %} - right: 0; - {%- endif %} - {%- endif %} -} - -{%- if theme_stickysidebar|tobool %} -/* this is nice, but it it leads to hidden headings when jumping - to an anchor */ -/* -div.related { - position: fixed; -} - -div.documentwrapper { - margin-top: 30px; -} -*/ -{%- endif %} - -div.sphinxsidebar h3 { - font-family: {{ theme_headfont }}; - color: {{ theme_sidebartextcolor }}; - font-size: 1.4em; - font-weight: normal; - margin: 0; - padding: 0; -} - -div.sphinxsidebar h3 a { - color: {{ theme_sidebartextcolor }}; -} - -div.sphinxsidebar h4 { - font-family: {{ theme_headfont }}; - color: {{ theme_sidebartextcolor }}; - font-size: 1.3em; - font-weight: normal; - margin: 5px 0 0 0; - padding: 0; -} - -div.sphinxsidebar p { - color: {{ theme_sidebartextcolor }}; -} - -div.sphinxsidebar p.topless { - margin: 5px 10px 10px 10px; -} - -div.sphinxsidebar ul { - margin: 10px; - padding: 0; - color: {{ theme_sidebartextcolor }}; -} - -div.sphinxsidebar a { - color: {{ theme_sidebarlinkcolor }}; -} - -div.sphinxsidebar input { - border: 1px solid {{ theme_sidebarlinkcolor }}; - font-family: sans-serif; - font-size: 1em; -} diff --git a/doc/source/themes/agogo/static/docversions.js b/doc/source/_static/docversions.js similarity index 71% rename from doc/source/themes/agogo/static/docversions.js rename to doc/source/_static/docversions.js index d0144ee4..0b414325 100644 --- a/doc/source/themes/agogo/static/docversions.js +++ b/doc/source/_static/docversions.js @@ -1,7 +1,5 @@ function insert_version_links() { - var labels = ['dev', '0.6', '0.5', '0.4', '0.3']; - - document.write('
    \n'); + var labels = ['dev', '0.7.0', '0.6', '0.5', '0.4', '0.3']; for (i = 0; i < labels.length; i++){ open_list = '
  • ' @@ -14,7 +12,6 @@ function insert_version_links() { document.write(open_list); document.write('skimage VERSION
  • \n' .replace('VERSION', labels[i]) - .replace('URL', 'http://scikits-image.org/docs/' + labels[i])); + .replace('URL', 'http://scikit-image.org/docs/' + labels[i])); } - document.write('
\n'); } diff --git a/doc/source/_static/scikits_image_logo_small.png b/doc/source/_static/scikits_image_logo_small.png deleted file mode 100644 index efd71678..00000000 Binary files a/doc/source/_static/scikits_image_logo_small.png and /dev/null differ diff --git a/doc/source/_templates/layout.html b/doc/source/_templates/layout.html deleted file mode 100644 index 1ddc127c..00000000 --- a/doc/source/_templates/layout.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "!layout.html" %} -{% block rootrellink %} -
  • scikits-image home »
  • - {{ super() }} -{% endblock %} - diff --git a/doc/source/_templates/localtoc.html b/doc/source/_templates/localtoc.html index 3a26d6fa..6866649c 100644 --- a/doc/source/_templates/localtoc.html +++ b/doc/source/_templates/localtoc.html @@ -1,8 +1,10 @@ {% if pagename != 'index' %} - {%- if display_toc %} -

    Contents

    - {{ toc }} - {%- endif %} + {%- if display_toc %} + + + {%- endif %} {% endif %} diff --git a/doc/source/_templates/navbar.html b/doc/source/_templates/navbar.html new file mode 100644 index 00000000..267af8e7 --- /dev/null +++ b/doc/source/_templates/navbar.html @@ -0,0 +1,5 @@ +
  • Home
  • +
  • Download
  • +
  • Gallery
  • +
  • Documentation
  • +
  • Source
  • diff --git a/doc/source/_templates/navigation.html b/doc/source/_templates/navigation.html index 587d95a1..66244695 100644 --- a/doc/source/_templates/navigation.html +++ b/doc/source/_templates/navigation.html @@ -1,12 +1,22 @@ -{%- block navigation %} - - {% if pagename != 'index' %} -

    Navigation

    -

    - Documentation Home -

    -

     

    - {% endif %} - - -{%- endblock %} + + +{%- if prev %} + + +{%- endif %} +{%- if next %} + + +{%- endif %} diff --git a/doc/source/_templates/versions.html b/doc/source/_templates/versions.html index a0da9d70..6dbb1962 100644 --- a/doc/source/_templates/versions.html +++ b/doc/source/_templates/versions.html @@ -1,9 +1,9 @@ -{%- block versions %} - -

    Version

    - - - -{%- endblock %} + + diff --git a/doc/source/cell_profiler.txt b/doc/source/cell_profiler.txt index cf24f53c..13918596 100644 --- a/doc/source/cell_profiler.txt +++ b/doc/source/cell_profiler.txt @@ -4,7 +4,7 @@ CellProfiler BSD license announcement :: From: Vebjorn Ljosa - To: scikits-image@googlegroups.com + To: scikit-image@googlegroups.com Date: June 3, 2010 We have changed the license of some parts of CellProfiler from GNU GPL diff --git a/doc/source/conf.py b/doc/source/conf.py index c46ab806..5071fab1 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -26,7 +26,8 @@ sys.path.append(os.path.join(curpath, '..', 'ext')) # 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', 'plot_directive', 'plot2rst'] + 'sphinx.ext.autosummary', 'plot_directive', 'plot2rst', + 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -42,7 +43,7 @@ master_doc = 'index' # General information about the project. project = u'skimage' -copyright = u'2011, the scikits-image team' +copyright = u'2011, the scikit-image team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -102,8 +103,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'agogo' -html_style = 'agogo.css' +html_theme = 'scikit-image' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -120,10 +120,6 @@ html_title = 'skimage v%s docs' % version # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -html_logo = "scikits_image_logo_small.png" - # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. @@ -146,9 +142,7 @@ html_static_path = ['_static'] html_sidebars = { '**': ['navigation.html', 'localtoc.html', - 'relations.html', - 'versions.html', - 'searchbox.html'], + 'versions.html'], } # Additional templates that should be rendered to pages, maps page names to @@ -182,7 +176,7 @@ html_sidebars = { #html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'scikitsimagedoc' +htmlhelp_basename = 'scikitimagedoc' # -- Options for LaTeX output -------------------------------------------------- @@ -196,7 +190,7 @@ htmlhelp_basename = 'scikitsimagedoc' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('contents', 'scikitsimage.tex', u'The Image Scikit Documentation', + ('contents', 'scikitimage.tex', u'The Image Scikit Documentation', u'SciPy Developers', 'manual'), ] @@ -220,8 +214,7 @@ latex_documents = [ # ----------------------------------------------------------------------------- # Numpy extensions # ----------------------------------------------------------------------------- -# Make numpydoc to generate plots for example sections -#numpydoc_use_plots = True +numpydoc_show_class_members = False # ----------------------------------------------------------------------------- # Plots @@ -256,3 +249,13 @@ plot2rst_index_name = 'README' plot2rst_rcparams = {'image.cmap' : 'gray', 'image.interpolation' : 'none'} +# ----------------------------------------------------------------------------- +# intersphinx +# ----------------------------------------------------------------------------- +_python_doc_base = 'http://docs.python.org/2.7' +intersphinx_mapping = { + _python_doc_base: None, + 'http://docs.scipy.org/doc/numpy': None, + 'http://docs.scipy.org/doc/scipy/reference': None, + 'http://scikit-learn.org/stable': None +} diff --git a/doc/source/gitwash/development_workflow.txt b/doc/source/gitwash/development_workflow.txt index b5344da8..47bdead0 100644 --- a/doc/source/gitwash/development_workflow.txt +++ b/doc/source/gitwash/development_workflow.txt @@ -4,14 +4,14 @@ Development workflow #################### -You already have your own forked copy of the scikits-image_ repository, by +You already have your own forked copy of the scikit-image_ repository, by following :ref:`forking`. You have :ref:`set-up-fork`. You have configured git by following :ref:`configure-git`. Now you are ready for some real work. Workflow summary ================ -In what follows we'll refer to the upstream scikits-image ``master`` branch, as +In what follows we'll refer to the upstream scikit-image ``master`` branch, as "trunk". * Don't use your ``master`` branch for anything. Consider deleting it. @@ -22,9 +22,9 @@ In what follows we'll refer to the upstream scikits-image ``master`` branch, as * Name your branch for the purpose of the changes - e.g. ``bugfix-for-issue-14`` or ``refactor-database-code``. * If you can possibly avoid it, avoid merging trunk or any other branches into - your feature branch while you are working. + your feature branch while you are working. * If you do find yourself merging from trunk, consider :ref:`rebase-on-trunk` -* Ask on the `scikits-image mailing list`_ if you get stuck. +* Ask on the `scikit-image mailing list`_ if you get stuck. * Ask for code review! This way of working helps to keep work well organized, with readable history. @@ -81,7 +81,7 @@ what the changes in the branch are for. For example ``add-ability-to-fly``, or git checkout my-new-feature Generally, you will want to keep your feature branches on your public github_ -fork of scikits-image_. To do this, you `git push`_ this new branch up to your +fork of scikit-image_. To do this, you `git push`_ this new branch up to your github repo. Generally (if you followed the instructions in these pages, and by default), git will have a link to your github repo, called ``origin``. You push up to your own repo on github with:: @@ -150,7 +150,7 @@ Ask for your changes to be reviewed or merged When you are ready to ask for someone to review your code and consider a merge: #. Go to the URL of your forked repo, say - ``http://github.com/your-user-name/scikits-image``. + ``http://github.com/your-user-name/scikit-image``. #. Use the 'Switch Branches' dropdown menu near the top left of the page to select the branch with your changes: @@ -192,10 +192,10 @@ If you want to work on some stuff with other people, where you are all committing into the same repository, or even the same branch, then just share it via github. -First fork scikits-image into your account, as from :ref:`forking`. +First fork scikit-image into your account, as from :ref:`forking`. Then, go to your forked repository github page, say -``http://github.com/your-user-name/scikits-image`` +``http://github.com/your-user-name/scikit-image`` Click on the 'Admin' button, and add anyone else to the repo as a collaborator: @@ -204,7 +204,7 @@ collaborator: Now all those people can do:: - git clone git@githhub.com:your-user-name/scikits-image.git + git clone git@githhub.com:your-user-name/scikit-image.git Remember that links starting with ``git@`` use the ssh protocol and are read-write; links starting with ``git://`` are read-only. diff --git a/doc/source/gitwash/following_latest.txt b/doc/source/gitwash/following_latest.txt index 69e5ee13..df0842a0 100644 --- a/doc/source/gitwash/following_latest.txt +++ b/doc/source/gitwash/following_latest.txt @@ -5,12 +5,12 @@ ============================= These are the instructions if you just want to follow the latest -*scikits-image* source, but you don't need to do any development for now. +*scikit-image* source, but you don't need to do any development for now. The steps are: * :ref:`install-git` -* get local copy of the `scikits-image github`_ git repository +* get local copy of the `scikit-image github`_ git repository * update local copy from time to time Get the local copy of the code @@ -18,19 +18,19 @@ Get the local copy of the code From the command line:: - git clone git://github.com/scikits-image/scikits-image.git + git clone git://github.com/scikit-image/scikit-image.git -You now have a copy of the code tree in the new ``scikits-image`` directory. +You now have a copy of the code tree in the new ``scikit-image`` directory. Updating the code ================= From time to time you may want to pull down the latest code. Do this with:: - cd scikits-image + cd scikit-image git pull -The tree in ``scikits-image`` will now have the latest changes from the initial +The tree in ``scikit-image`` will now have the latest changes from the initial repository. .. include:: links.inc diff --git a/doc/source/gitwash/forking_hell.txt b/doc/source/gitwash/forking_hell.txt index 58e12fee..02c14b25 100644 --- a/doc/source/gitwash/forking_hell.txt +++ b/doc/source/gitwash/forking_hell.txt @@ -1,13 +1,13 @@ .. _forking: ============================================ -Making your own copy (fork) of scikits-image +Making your own copy (fork) of scikit-image ============================================ You need to do this only once. The instructions here are very similar to the instructions at http://help.github.com/forking/ |emdash| please see that page for more detail. We're repeating some of it here just to give the -specifics for the scikits-image_ project, and to suggest some default names. +specifics for the scikit-image_ project, and to suggest some default names. Set up and configure a github account ====================================== @@ -17,17 +17,17 @@ If you don't have a github account, go to the github page, and make one. You then need to configure your account to allow write access |emdash| see the ``Generating SSH keys`` help on `github help`_. -Create your own forked copy of scikits-image_ +Create your own forked copy of scikit-image_ ============================================= #. Log into your github account. -#. Go to the scikits-image_ github home at `scikits-image github`_. +#. Go to the scikit-image_ github home at `scikit-image github`_. #. Click on the *fork* button: .. image:: forking_button.png Now, after a short pause and some 'Hardcore forking action', you - should find yourself at the home page for your own forked copy of scikits-image_. + should find yourself at the home page for your own forked copy of scikit-image_. .. include:: links.inc diff --git a/doc/source/gitwash/git_intro.txt b/doc/source/gitwash/git_intro.txt index cb8e16cc..8b4d42bc 100644 --- a/doc/source/gitwash/git_intro.txt +++ b/doc/source/gitwash/git_intro.txt @@ -2,11 +2,11 @@ Introduction ============== -These pages describe a git_ and github_ workflow for the scikits-image_ +These pages describe a git_ and github_ workflow for the scikit-image_ project. There are several different workflows here, for different ways of -working with *scikits-image*. +working with *scikit-image*. This is not a comprehensive git reference, it's just a workflow for our own project. It's tailored to the github hosting service. You may well diff --git a/doc/source/gitwash/index.txt b/doc/source/gitwash/index.txt index d0eac669..7d26110e 100644 --- a/doc/source/gitwash/index.txt +++ b/doc/source/gitwash/index.txt @@ -1,6 +1,6 @@ .. _using-git: -Working with *scikits-image* source code +Working with *scikit-image* source code ======================================== Contents: diff --git a/doc/source/gitwash/maintainer_workflow.txt b/doc/source/gitwash/maintainer_workflow.txt index 7e2b21ab..e661784c 100644 --- a/doc/source/gitwash/maintainer_workflow.txt +++ b/doc/source/gitwash/maintainer_workflow.txt @@ -16,7 +16,7 @@ access to the upstream repo. Being a maintainer, you've got read-write access. It's good to have your upstream remote have a scary name, to remind you that it's a read-write remote:: - git remote add upstream-rw git@github.com:scikits-image/scikits-image.git + git remote add upstream-rw git@github.com:scikit-image/scikit-image.git git fetch upstream-rw ******************* @@ -29,7 +29,7 @@ Let's say you have some changes that need to go into trunk The changes are in some branch that you are currently on. For example, you are looking at someone's changes like this:: - git remote add someone git://github.com/someone/scikits-image.git + git remote add someone git://github.com/someone/scikit-image.git git fetch someone git branch cool-feature --track someone/cool-feature git checkout cool-feature diff --git a/doc/source/gitwash/patching.txt b/doc/source/gitwash/patching.txt index acec9fa6..b40118af 100644 --- a/doc/source/gitwash/patching.txt +++ b/doc/source/gitwash/patching.txt @@ -3,7 +3,7 @@ ================ You've discovered a bug or something else you want to change -in scikits-image_ .. |emdash| excellent! +in scikit-image_ .. |emdash| excellent! You've worked out a way to fix it |emdash| even better! @@ -29,9 +29,9 @@ Overview git config --global user.email you@yourdomain.example.com git config --global user.name "Your Name Comes Here" # get the repository if you don't have it - git clone git://github.com/scikits-image/scikits-image.git + git clone git://github.com/scikit-image/scikit-image.git # make a branch for your patching - cd scikits-image + cd scikit-image git branch the-fix-im-thinking-of git checkout the-fix-im-thinking-of # hack, hack, hack @@ -44,7 +44,7 @@ Overview # make the patch files git format-patch -M -C master -Then, send the generated patch files to the `scikits-image +Then, send the generated patch files to the `scikit-image mailing list`_ |emdash| where we will thank you warmly. In detail @@ -57,10 +57,10 @@ In detail git config --global user.name "Your Name Comes Here" #. If you don't already have one, clone a copy of the - scikits-image_ repository:: + scikit-image_ repository:: - git clone git://github.com/scikits-image/scikits-image.git - cd scikits-image + git clone git://github.com/scikit-image/scikit-image.git + cd scikit-image #. Make a 'feature branch'. This will be where you work on your bug fix. It's nice and safe and leaves you with @@ -100,7 +100,7 @@ In detail 0001-BF-added-tests-for-Funny-bug.patch 0002-BF-added-fix-for-Funny-bug.patch - Send these files to the `scikits-image mailing list`_. + Send these files to the `scikit-image mailing list`_. When you are done, to switch back to the main copy of the code, just return to the ``master`` branch:: @@ -115,7 +115,7 @@ more feature branches, you will probably want to switch to development mode. You can do this with the repository you have. -Fork the scikits-image_ repository on github |emdash| :ref:`forking`. +Fork the scikit-image_ repository on github |emdash| :ref:`forking`. Then:: # checkout and refresh master branch from main repo @@ -124,7 +124,7 @@ Then:: # rename pointer to main repository to 'upstream' git remote rename origin upstream # point your repo to default read / write to your fork on github - git remote add origin git@github.com:your-user-name/scikits-image.git + git remote add origin git@github.com:your-user-name/scikit-image.git # push up any branches you've made and want to keep git push origin the-fix-im-thinking-of diff --git a/doc/source/gitwash/set_up_fork.txt b/doc/source/gitwash/set_up_fork.txt index f59aa57a..36153e7d 100644 --- a/doc/source/gitwash/set_up_fork.txt +++ b/doc/source/gitwash/set_up_fork.txt @@ -11,9 +11,9 @@ Overview :: - git clone git@github.com:your-user-name/scikits-image.git - cd scikits-image - git remote add upstream git://github.com/scikits-image/scikits-image.git + git clone git@github.com:your-user-name/scikit-image.git + cd scikit-image + git remote add upstream git://github.com/scikit-image/scikit-image.git In detail ========= @@ -22,8 +22,8 @@ Clone your fork --------------- #. Clone your fork to the local computer with ``git clone - git@github.com:your-user-name/scikits-image.git`` -#. Investigate. Change directory to your new repo: ``cd scikits-image``. Then + git@github.com:your-user-name/scikit-image.git`` +#. Investigate. Change directory to your new repo: ``cd scikit-image``. Then ``git branch -a`` to show you all branches. You'll get something like:: @@ -35,7 +35,7 @@ Clone your fork What remote repository is ``remote/origin``? Try ``git remote -v`` to see the URLs for the remote. They will point to your github fork. - Now you want to connect to the upstream `scikits-image github`_ repository, so + Now you want to connect to the upstream `scikit-image github`_ repository, so you can merge in changes from trunk. .. _linking-to-upstream: @@ -45,11 +45,11 @@ Linking your repository to the upstream repo :: - cd scikits-image - git remote add upstream git://github.com/scikits-image/scikits-image.git + cd scikit-image + git remote add upstream git://github.com/scikit-image/scikit-image.git ``upstream`` here is just the arbitrary name we're using to refer to the -main scikits-image_ repository at `scikits-image github`_. +main scikit-image_ repository at `scikit-image github`_. Note that we've used ``git://`` for the URL rather than ``git@``. The ``git://`` URL is read only. This means we that we can't accidentally @@ -59,10 +59,10 @@ use it to merge into our own code. Just for your own satisfaction, show yourself that you now have a new 'remote', with ``git remote -v show``, giving you something like:: - upstream git://github.com/scikits-image/scikits-image.git (fetch) - upstream git://github.com/scikits-image/scikits-image.git (push) - origin git@github.com:your-user-name/scikits-image.git (fetch) - origin git@github.com:your-user-name/scikits-image.git (push) + upstream git://github.com/scikit-image/scikit-image.git (fetch) + upstream git://github.com/scikit-image/scikit-image.git (push) + origin git@github.com:your-user-name/scikit-image.git (fetch) + origin git@github.com:your-user-name/scikit-image.git (push) .. include:: links.inc diff --git a/doc/source/gitwash/this_project.inc b/doc/source/gitwash/this_project.inc index 0bfd7306..349b92ba 100644 --- a/doc/source/gitwash/this_project.inc +++ b/doc/source/gitwash/this_project.inc @@ -1,5 +1,5 @@ -.. scikits-image -.. _scikits-image: http://scikits-image.org -.. _`scikits-image github`: http://github.com/scikits-image/scikits-image +.. scikit-image +.. _scikit-image: http://scikit-image.org +.. _`scikit-image github`: http://github.com/scikit-image/scikit-image -.. _`scikits-image mailing list`: http://groups.google.com/group/scikits-image +.. _`scikit-image mailing list`: http://groups.google.com/group/scikit-image diff --git a/doc/source/install.txt b/doc/source/install.txt index 5aa3f43f..7c886549 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -1,8 +1,6 @@ Pre-built installation ---------------------- -.. !! Also update scikits-image-web !! - `Windows binaries `__ are kindly provided by Christoph Gohlke. @@ -12,37 +10,37 @@ Distribution (EPD) `__ and `Python(x,y) `__. On systems that support setuptools, the package can be installed from the -`Python packaging index `__ using +`Python packaging index `__ using :: - easy_install -U scikits-image + easy_install -U scikit-image or :: - pip -U scikits-image + pip install -U scikit-image Installation from source ------------------------ Obtain the source from the git-repository at -`http://github.com/scikits-image/scikits-image -`_. +`http://github.com/scikit-image/scikit-image +`_. by running :: - git clone http://github.com/scikits-image/scikits-image.git + git clone http://github.com/scikit-image/scikit-image.git in a terminal (You will need to have git installed on your machine). If you do not have git installed, you can also download a zipball from -`https://github.com/scikits-image/scikits-image/zipball/master -`_. +`https://github.com/scikit-image/scikit-image/zipball/master +`_. The SciKit can be installed globally using diff --git a/doc/source/overview.txt b/doc/source/overview.txt index 0bd9d658..5a4e6123 100644 --- a/doc/source/overview.txt +++ b/doc/source/overview.txt @@ -1,7 +1,7 @@ Image Processing SciKit ======================= -The `scikits-image `__ SciKit (toolkit for +The `scikit-image `__ SciKit (toolkit for `SciPy `__) extends ``scipy.ndimage`` to provide a versatile set of image processing routines. It is written in the `Python `_ language. @@ -12,15 +12,15 @@ mailing list (address provided below). Homepage -------- -http://scikits-image.org +http://scikit-image.org Source, bugs and patches ------------------------ -http://github.com/scikits-image/scikits-image +http://github.com/scikit-image/scikit-image Mailing List ------------ -http://groups.google.com/group/scikits-image +http://groups.google.com/group/scikit-image Contact ------- diff --git a/doc/source/random_gallery.py b/doc/source/random_gallery.py index 6c5a40a2..47ce89a2 100644 --- a/doc/source/random_gallery.py +++ b/doc/source/random_gallery.py @@ -32,8 +32,8 @@ gallery_div = '''\ examples = glob.glob(os.path.join(example_dir, 'plot_*.py')) images, links = [], [] -image_url = 'http://scikits-image.org/docs/dev/_images/%s.png' -link_url = 'http://scikits-image.org/docs/dev/auto_examples/%s.html' +image_url = 'http://scikit-image.org/docs/dev/_images/%s.png' +link_url = 'http://scikit-image.org/docs/dev/auto_examples/%s.html' for e in examples: e = os.path.basename(e) diff --git a/doc/source/themes/agogo/layout.html b/doc/source/themes/agogo/layout.html deleted file mode 100644 index 260f06bf..00000000 --- a/doc/source/themes/agogo/layout.html +++ /dev/null @@ -1,58 +0,0 @@ -{# - agogo/layout.html - ~~~~~~~~~~~~~~~~~ - - Sphinx layout template for the agogo theme, originally written - by Andi Albrecht. - - :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} -{% extends "basic/layout.html" %} -{% set script_files = script_files + ['_static/docversions.js'] %} - -{% block header %} -
    -
    - {%- block headertitle %} - - {%- if logo %} - - {%- endif %} - {#

    {{ shorttitle|e }}

    #} - {%- endblock %} -
    -
    -{% endblock %} - - - -{% block content %} -
    -
    - {%- block sidebar2 %} - {# We don't want the logo here #} - {%- block sidebarlogo %} {% endblock %} - {{ sidebar() }} - {% endblock %} -
    - {%- block document %} - {{ super() }} - {%- endblock %} -
    -
    -
    -
    -{% endblock %} - -{% block footer %} - -{% endblock %} - -{% block relbar1 %}{% endblock %} - -{% block relbar2 %}{% endblock %} diff --git a/doc/source/themes/agogo/static/agogo.css_t b/doc/source/themes/agogo/static/agogo.css_t deleted file mode 100644 index 2ff4abad..00000000 --- a/doc/source/themes/agogo/static/agogo.css_t +++ /dev/null @@ -1,753 +0,0 @@ -/* - * agogo.css_t - * ~~~~~~~~~~~ - * - * Sphinx stylesheet -- agogo theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -* { - margin: 0px; - padding: 0px; -} - - -div.header-wrapper { - border-top: 0px solid #babdb6; - padding: 1em 1em 0; - min-height: 0px; -} - -body { - font-family: {{ theme_bodyfont }}; - line-height: 1.4em; - color: black; - background-color: {{ theme_bgcolor }}; -} - - -/* Page layout */ - -div.header, div.content, div.footer { - max-width: {{ theme_pagewidth }}; - margin-left: auto; - margin-right: auto; -} - -div.header-wrapper { - border-bottom: 0px solid #2e3436; -} - - -/* Default body styles */ -a { - color: {{ theme_linkcolor }}; -} - -div.bodywrapper a, div.footer a { - text-decoration: underline; -} - -.clearer { - clear: both; -} - -.left { - float: left; -} - -.right { - float: right; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -h1, h2, h3, h4 { - font-weight: normal; - color: {{ theme_headercolor2 }}; - margin-bottom: .8em; - clear: left; -} - -h1 { - color: {{ theme_headercolor1 }}; - line-height: 1.1em; - text-align: left; -} - -h2 { - padding-bottom: .5em; - color: {{ theme_headercolor1 }}; - border-bottom: 1px solid {{ theme_headercolor1 }}; -} - -a.headerlink { - visibility: hidden; - color: #dddddd; - padding-left: .3em; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -img { - border: 0; -} - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 2px 7px 1px 7px; - border-left: 0.2em solid black; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -dt:target, .highlighted { - background-color: #fbe54e; -} - -/* Header */ - -div.header {} - -div.header h1 { - font-family: "Trebuchet MS", Helvetica, sans-serif; - font-weight: normal; - font-size: 250%; - letter-spacing: .08em; - line-height: 70px; - margin-bottom: 0; -} - -div.header h1 a { - color: white; -} - -div.header h1 a:hover { - text-decoration: none; -} - -div.header div.rel { - margin-top: 1em; - border-top: 1px solid #AAA; - border-bottom: 1px solid #AAA; - border-radius: 5px; - padding: 3px 1em; -} - -div.header div.rel a { - color: {{ theme_linkcolor }}; - letter-spacing: .05em; - font-weight: bold; -} - -div.logo {} - -img.logo { - border: 0; -} - - -/* Content */ -div.content-wrapper { - background-color: white; - padding: 1em; -} - -div.document { - max-width: {{ theme_documentwidth }}; -} - -div.body { - padding-right: 2em; - min-width: 20em; - overflow: hidden; - font-size: 90%; - text-align: {{ theme_textalign }}; -} - -div.document ul { - margin: 1.5em; - list-style-type: square; -} - -div.document dd { - margin-left: 1.2em; - margin-top: .4em; - margin-bottom: 1em; -} - -div.document .section { - margin-top: 1.7em; -} -div.document .section:first-child { - margin-top: 0px; -} - -div.document div.highlight { - padding: 3px; - background-color: #eeeeec; - border-top: 2px solid #dddddd; - border-bottom: 2px solid #dddddd; - margin-top: .8em; - margin-bottom: .8em; -} - -div.document h2 { - margin-top: .7em; -} - -div.document p { - margin-bottom: 1.5em; -} - -div.document li.toctree-l1 { - margin-bottom: 1em; -} - -div.document .descname { - font-weight: bold; -} - -div.document .docutils.literal { - background-color: #eeeeec; - padding: 1px; -} - -div.document .docutils.xref.literal { - background-color: transparent; - padding: 0px; -} - -div.document blockquote { - margin: 1em; -} - -div.document ol { - margin: 1.5em; -} - - -/* Sidebar */ - -div.sphinxsidebar { - width: {{ theme_sidebarwidth }}; - padding: 0 1em; - float: right; - font-size: .93em; - background-color: white; -} - -div.sphinxsidebar a, div.header a { - text-decoration: none; -} - -div.sphinxsidebar a:hover, div.header a:hover { - text-decoration: underline; -} - -div.sphinxsidebar h3 { - color: #2e3436; - text-transform: uppercase; - font-size: 130%; - letter-spacing: .1em; - margin-bottom: .4em; -} - -div.sphinxsidebar h4 { - margin-bottom: 0; - font-weight: bold; -} - -div.sphinxsidebar .tile { - border: 1px solid #D1DDE2; - border-radius: 10px; - background-color: #E1E8EC; - padding-left: 0.5em; - margin: 1em 0; -} - -div.sphinxsidebar ul { - margin-bottom: 1.5em; - list-style-type: none; -} - -div.sphinxsidebar li.toctree-l1 a { - display: block; - padding: 1px; - border: 1px solid #dddddd; - background-color: #eeeeec; - margin-bottom: .4em; - padding-left: 3px; - color: #2e3436; -} - -div.sphinxsidebar li.toctree-l2 a { - background-color: transparent; - border: none; - margin-left: 1em; - border-bottom: 1px solid #dddddd; -} - -div.sphinxsidebar li.toctree-l3 a { - background-color: transparent; - border: none; - margin-left: 2em; - border-bottom: 1px solid #dddddd; -} - -div.sphinxsidebar li.toctree-l2:last-child a { - border-bottom: none; -} - -div.sphinxsidebar li.toctree-l1.current a { - border-right: 5px solid {{ theme_headerlinkcolor }}; -} - -div.sphinxsidebar li.toctree-l1.current li.toctree-l2 a { - border-right: none; -} - -div.sidebarblock { - padding-bottom: .4em; - border-bottom: 1px solid #AAA; - margin-bottom: .8em; -} - -ul.versions li { - color: gray; - list-style: circle inside; -} - -ul.versions li#current { - list-style: disc inside; -} - -/* Footer */ - -div.footer-wrapper { - padding-top: 10px; - padding-bottom: 10px; -} - -div.footer { - border-top: 2px solid #aaa; - text-align: right; -} - -div.footer, div.footer a { - color: #888a85; -} - -.figure { - float: left; - margin: 1em; -} - -.figure img { - display: block; - margin-left: auto; - margin-right: auto; - max-height: 150px; -} - -.figure .caption { - width: 200px; - text-align: center !important; -} - - -/* Styles copied from basic theme */ - -img.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - clear: both; - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li div.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable p.biglink { - line-height: 150%; -} - -table.contentstable, table.contentstable td, table.contentstable th { - border-style: none; -} - -div.body table.contentstable p { - margin: 0.5em; - text-align: left; -} - -table.contentstable a { - text-decoration: none; - font-size: 120%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable dl, table.indextable dd { - margin-top: 0; - margin-bottom: 0; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -/* -- viewcode extension ---------------------------------------------------- */ - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family:: {{ theme_bodyfont }}; -} - -div.viewcode-block:target { - margin: -1px -3px; - padding: 0 3px; - background-color: #f4debf; - border-top: 1px solid #ac9; - border-bottom: 1px solid #ac9; -} - -span.strike { text-decoration: line-through; } - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: {{ theme_linkcolor }}; - text-decoration: none; -} - -a:visited { - color: {{ theme_visitedlinkcolor }}; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: {{ theme_headfont }}; - background-color: {{ theme_headbgcolor }}; - font-weight: normal; - color: {{ theme_headtextcolor }}; - border-bottom: 1px solid #ccc; - margin: 20px 20px 10px 0px; - padding: 3px 0 3px 0px; -} - -div.body h1 { margin-top: 0; font-size: 180%; } -div.body h2 { font-size: 150%; } -div.body h3 { font-size: 130%; } -div.body h4 { font-size: 120%; } -div.body h5 { font-size: 110%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: {{ theme_headlinkcolor }}; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: {{ theme_headlinkcolor }}; - color: white; -} - -div.body p, div.body dd, div.body li { - text-align: justify; - line-height: 130%; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre { - padding: 1em; - margin-bottom: 0.5em; - background-color: {{ theme_codebgcolor }}; - color: {{ theme_codetextcolor }}; - line-height: 120%; - border: 0px solid #ac9; - border-left: none; - border-right: none; -} - -sup { - font-size: x-small; - line-height: 0; -} - -tt { - background-color: #ecf0f3; - padding: 0 1px 0 1px; - font-size: 1em; -} - -table { - border-collapse: collapse; - margin-bottom: 1em; - margin-top: 1em; -} - -table, th, td { - border: 1px solid #ccc; -} - -th, td { - padding: 5px; -} - -th { - color: #333; - background-color: #eee; -} - -#api-reference ul:first-child { - float: left; - width: 35em; - margin-top: 0; - padding-top: 0; - list-style: none; - overflow: auto; -} - -#api-reference li { - float: left; - position: relative; - margin-right: 1em; - width: 17em; - padding: 0; -} - -.field-list { - font-size: 80%; -} - -/* ----------------- Example Gallery ----------------- */ - -.gallery { - height: 200px; -} - -.gallery p.caption a{ - text-decoration: none; -} - -/* ----------------- Coverage States ----------------- */ -span.missing{ - color: #000; - background-color: #ff5840; - border-color: #A77272; - font-weight: bold; -} -span.partial{ - color: #806600; - background-color: #ffc343; - font-weight: bold; -} -span.done{ - color: #106600; - background-color: #60f030; - border-color: #4F8530; - font-weight: bold; -} -span.na{ - color: #A8A8A8; - border-color: #4F8530; -} - -table.coverage { - border: solid 1px; -} - -td.missing-bar{ - color: #ff5840; - background-color: #ff5840; - border-color: #A77272; - font-weight: normal; - font-style: normal; -} -td.partial-bar{ - color: #ffc343; - background-color: #ffc343; - font-weight: normal; - font-style: normal; -} -td.done-bar{ - color: #60f030; - background-color: #60f030; - border-color: #4F8530; - font-weight: normal; - font-style: normal; -} -td.na-bar{ - color: #FFF; - background-color: #FFF; - border-color: #4F8530; - font-weight: normal; - font-style: normal; -} - -/* Adjust doc headers such as Notes, References, etc. */ -p.rubric { - font-weight: bold; - font-size: 120%; -} - -/* Math */ -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} diff --git a/doc/source/themes/agogo/theme.conf b/doc/source/themes/agogo/theme.conf deleted file mode 100644 index a991e5c0..00000000 --- a/doc/source/themes/agogo/theme.conf +++ /dev/null @@ -1,19 +0,0 @@ -[theme] -inherit = basic -stylesheet = agogo.css -pygments_style = tango - -[options] -bodyfont = "Verdana", Arial, sans-serif -pagewidth = 70em -documentwidth = 55em -sidebarwidth = 14em -bgcolor = white -headerbg = url(bgtop.png) top left repeat-x -footerbg = url(bgfooter.png) top left repeat-x -linkcolor = #FC852B -headercolor1 = #555 -headercolor2 = #555 -headerlinkcolor = #fcaf3e -codebgcolor = #EEE -textalign = justify diff --git a/doc/source/themes/scikit-image/layout.html b/doc/source/themes/scikit-image/layout.html new file mode 100644 index 00000000..58424f2f --- /dev/null +++ b/doc/source/themes/scikit-image/layout.html @@ -0,0 +1,113 @@ +{# + scikit-image/layout.html + ~~~~~~~~~~~~~~~~~ + + Sphinx layout template for the scikit-image theme, written by + Johannes Schönberger. + +#} + +{%- set url_root = pathto('', 1) %} +{# XXX necessary? #} +{%- if url_root == '#' %}{% set url_root = '' %}{% endif %} +{%- if not embedded and docstitle %} + {%- set titlesuffix = " — "|safe + docstitle|e %} +{%- else %} + {%- set titlesuffix = "" %} +{%- endif %} + +{%- macro script() %} + + + + {%- for scriptfile in script_files %} + + {%- endfor %} +{%- endmacro %} + +{%- macro css() %} + + + + + {%- for cssfile in css_files %} + + {%- endfor %} +{%- endmacro %} + + + + + {%- block htmltitle %} + {{ title|striptags|e }}{{ titlesuffix }} + {%- endblock %} + {{ metatags }} + {{ css() }} + {{ script() }} + {%- if hasdoc('about') %} + + {%- endif %} + {%- if hasdoc('genindex') %} + + {%- endif %} + {%- if hasdoc('search') %} + + {%- endif %} + {%- if hasdoc('copyright') %} + + {%- endif %} + + {%- if parents %} + + {%- endif %} + {%- if next %} + + {%- endif %} + {%- if prev %} + + {%- endif %} + + + {%- block extrahead %}{% endblock %} + + + +
    + +
    +
    + {% block body %}{% endblock %} +
    +
    + {%- for sidebartemplate in sidebars %} + {%- include sidebartemplate %} + {%- endfor %} +
    +
    + + + diff --git a/doc/source/themes/scikit-image/search.html b/doc/source/themes/scikit-image/search.html new file mode 100644 index 00000000..61520e6b --- /dev/null +++ b/doc/source/themes/scikit-image/search.html @@ -0,0 +1,46 @@ +{# + basic/search.html + ~~~~~~~~~~~~~~~~~ + + Template for the search page. + + :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +#} +{% extends "layout.html" %} +{% set title = _('Search') %} +{% set script_files = script_files + ['_static/searchtools.js'] %} +{% set script_files = script_files + ['searchindex.js'] %} +{% block body %} +

    {{ _('Search') }}

    +
    + +

    + {% trans %}Please activate JavaScript to enable the search + functionality.{% endtrans %} +

    +
    +

    + {% trans %}From here you can search these documents. Enter your search + words into the box in the navigation bar and press "Enter". Note that the + search function will automatically search for all of the words. Pages + containing fewer words won't appear in the result list.{% endtrans %} +

    + {% if search_performed %} + {% if not search_results %} +

    {{ _('Your search did not match any results.') }}

    + {% endif %} + {% endif %} +
    + {% if search_results %} +
      + {% for href, caption, context in search_results %} +
    • {{ caption }}
    • + {% endfor %} +
    + {% endif %} +
    +{% endblock %} diff --git a/doc/source/themes/scikit-image/static/css/bootstrap-responsive.min.css b/doc/source/themes/scikit-image/static/css/bootstrap-responsive.min.css new file mode 100644 index 00000000..7b0158da --- /dev/null +++ b/doc/source/themes/scikit-image/static/css/bootstrap-responsive.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap Responsive v2.1.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade.in{top:auto}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:block;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} diff --git a/doc/source/themes/scikit-image/static/css/bootstrap.min.css b/doc/source/themes/scikit-image/static/css/bootstrap.min.css new file mode 100644 index 00000000..31d8b960 --- /dev/null +++ b/doc/source/themes/scikit-image/static/css/bootstrap.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v2.1.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}.text-warning{color:#c09853}.text-error{color:#b94a48}.text-info{color:#3a87ad}.text-success{color:#468847}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:1;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1{font-size:36px;line-height:40px}h2{font-size:30px;line-height:40px}h3{font-size:24px;line-height:40px}h4{font-size:18px;line-height:20px}h5{font-size:14px;line-height:20px}h6{font-size:12px;line-height:20px}h1 small{font-size:24px}h2 small{font-size:18px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:25px}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:9px;font-size:14px;line-height:20px;color:#555;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal;cursor:pointer}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:18px;padding-left:18px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"]{float:left}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info>label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{margin-bottom:5px;font-size:0;white-space:nowrap}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;font-size:14px;vertical-align:top;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-append .add-on,.input-append .btn{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child,.table-bordered tfoot:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child,.table-bordered tfoot:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topleft:4px}.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9}.table-hover tbody tr:hover td,.table-hover tbody tr:hover th{background-color:#f5f5f5}table [class*=span],.row-fluid table [class*=span]{display:table-cell;float:none;margin-left:0}.table .span1{float:none;width:44px;margin-left:0}.table .span2{float:none;width:124px;margin-left:0}.table .span3{float:none;width:204px;margin-left:0}.table .span4{float:none;width:284px;margin-left:0}.table .span5{float:none;width:364px;margin-left:0}.table .span6{float:none;width:444px;margin-left:0}.table .span7{float:none;width:524px;margin-left:0}.table .span8{float:none;width:604px;margin-left:0}.table .span9{float:none;width:684px;margin-left:0}.table .span10{float:none;width:764px;margin-left:0}.table .span11{float:none;width:844px;margin-left:0}.table .span12{float:none;width:924px;margin-left:0}.table .span13{float:none;width:1004px;margin-left:0}.table .span14{float:none;width:1084px;margin-left:0}.table .span15{float:none;width:1164px;margin-left:0}.table .span16{float:none;width:1244px;margin-left:0}.table .span17{float:none;width:1324px;margin-left:0}.table .span18{float:none;width:1404px;margin-left:0}.table .span19{float:none;width:1484px;margin-left:0}.table .span20{float:none;width:1564px;margin-left:0}.table .span21{float:none;width:1644px;margin-left:0}.table .span22{float:none;width:1724px;margin-left:0}.table .span23{float:none;width:1804px;margin-left:0}.table .span24{float:none;width:1884px;margin-left:0}.table tbody tr.success td{background-color:#dff0d8}.table tbody tr.error td{background-color:#f2dede}.table tbody tr.warning td{background-color:#fcf8e3}.table tbody tr.info td{background-color:#d9edf7}.table-hover tbody tr.success:hover td{background-color:#d0e9c6}.table-hover tbody tr.error:hover td{background-color:#ebcccc}.table-hover tbody tr.warning:hover td{background-color:#faf2cc}.table-hover tbody tr.info:hover td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-tabs>.active>a>[class^="icon-"],.nav-tabs>.active>a>[class*=" icon-"],.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu li>a:hover,.dropdown-menu li>a:focus,.dropdown-submenu:hover>a{color:#fff;text-decoration:none;background-color:#08c;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#fff;text-decoration:none;background-color:#08c;background-color:#0081c2;background-image:linear-gradient(to bottom,#08c,#0077b3);background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu .disabled>a,.dropdown-menu .disabled>a:hover{color:#999}.dropdown-menu .disabled>a:hover{text-decoration:none;cursor:default;background-color:transparent}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 14px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;*line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #bbb;*border:0;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-bottom-color:#a2a2a2;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover{color:#333;text-decoration:none;background-color:#e6e6e6;*background-color:#d9d9d9;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-color:#e6e6e6;background-color:#d9d9d9 \9;background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-color:#e6e6e6;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:9px 14px;font-size:16px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.btn-large [class^="icon-"]{margin-top:2px}.btn-small{padding:3px 9px;font-size:12px;line-height:18px}.btn-small [class^="icon-"]{margin-top:0}.btn-mini{padding:2px 6px;font-size:11px;line-height:17px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn{border-color:#c5c5c5;border-color:rgba(0,0,0,0.15) rgba(0,0,0,0.15) rgba(0,0,0,0.25)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-image:-moz-linear-gradient(top,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-image:-moz-linear-gradient(top,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-image:-moz-linear-gradient(top,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover{color:#333;text-decoration:none}.btn-group{position:relative;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1}.btn-toolbar .btn+.btn,.btn-toolbar .btn-group+.btn,.btn-toolbar .btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu{font-size:14px}.btn-group>.btn-mini{font-size:11px}.btn-group>.btn-small{font-size:12px}.btn-group>.btn-large{font-size:16px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-mini .caret,.btn-small .caret,.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.dropup .btn-large .caret{border-top:0;border-bottom:5px solid #000}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical .btn{display:block;float:none;width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical .btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical .btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical .btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical .btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical .btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible;color:#777}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px}.navbar-link{color:#777}.navbar-link:hover{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;width:100%;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.1),0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.1),0 1px 10px rgba(0,0,0,0.1);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.1),0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:inset 0 1px 0 rgba(0,0,0,0.1),0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 0 rgba(0,0,0,0.1),0 -1px 10px rgba(0,0,0,0.1);box-shadow:inset 0 1px 0 rgba(0,0,0,0.1),0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse{color:#999}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover{color:#fff}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-image:-moz-linear-gradient(top,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb .divider{padding:0 5px;color:#ccc}.breadcrumb .active{color:#999}.pagination{height:40px;margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:0 14px;line-height:38px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager a,.pager span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager a:hover{text-decoration:none;background-color:#f5f5f5}.pager .next a,.pager .next span{float:right}.pager .previous a{float:left}.pager .disabled a,.pager .disabled a:hover,.pager .disabled span{color:#999;cursor:default;background-color:#fff}.modal-open .modal .dropdown-menu{z-index:2050}.modal-open .modal .dropdown.open{*z-index:2050}.modal-open .modal .popover{z-index:2060}.modal-open .modal .tooltip{z-index:2080}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:50%;left:50%;z-index:1050;width:560px;margin:-250px 0 0 -280px;overflow:auto;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:50%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.tooltip{position:absolute;z-index:1030;display:block;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{margin-top:-3px}.tooltip.right{margin-left:3px}.tooltip.bottom{margin-top:3px}.tooltip.left{margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;width:236px;padding:1px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-bottom:10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-right:10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0}.popover .arrow,.popover .arrow:after{position:absolute;display:inline-block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow:after{z-index:-1;content:""}.popover.top .arrow{bottom:-10px;left:50%;margin-left:-10px;border-top-color:#fff;border-width:10px 10px 0}.popover.top .arrow:after{bottom:-1px;left:-11px;border-top-color:rgba(0,0,0,0.25);border-width:11px 11px 0}.popover.right .arrow{top:50%;left:-10px;margin-top:-10px;border-right-color:#fff;border-width:10px 10px 10px 0}.popover.right .arrow:after{bottom:-11px;left:-1px;border-right-color:rgba(0,0,0,0.25);border-width:11px 11px 11px 0}.popover.bottom .arrow{top:-10px;left:50%;margin-left:-10px;border-bottom-color:#fff;border-width:0 10px 10px}.popover.bottom .arrow:after{top:-1px;left:-11px;border-bottom-color:rgba(0,0,0,0.25);border-width:0 11px 11px}.popover.left .arrow{top:50%;right:-10px;margin-top:-10px;border-left-color:#fff;border-width:10px 0 10px 10px}.popover.left .arrow:after{right:-1px;bottom:-11px;border-left-color:rgba(0,0,0,0.25);border-width:11px 0 11px 11px}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.label,.badge{font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{padding:1px 4px 2px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding:1px 9px 2px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}a.label:hover,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel .item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel .item>img{display:block;line-height:1}.carousel .active,.carousel .next,.carousel .prev{display:block}.carousel .active{left:0}.carousel .next,.carousel .prev{position:absolute;top:0;width:100%}.carousel .next{left:100%}.carousel .prev{left:-100%}.carousel .next.left,.carousel .prev.right{left:0}.carousel .active.left{left:-100%}.carousel .active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit p{font-size:18px;font-weight:200;line-height:30px;color:inherit}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/doc/source/themes/scikit-image/static/css/custom.css b/doc/source/themes/scikit-image/static/css/custom.css new file mode 100644 index 00000000..85a174df --- /dev/null +++ b/doc/source/themes/scikit-image/static/css/custom.css @@ -0,0 +1,224 @@ +body { + font-family: "Raleway"; +} +a { + color: #CE5C00; +} +input, +button, +select, +textarea { + font-family: "Raleway"; +} +pre { + font-size: 11px; +} + +h1, h2, h3, h4, h5, h6 { + clear: left; +} +h1 { + font-size: 30px; + line-height: 36px; +} +h2 { + font-size: 24px; + line-height: 30px; +} +h3 { + font-size: 19px; + line-height: 21px; +} +h4 { + font-size: 17px; + line-height: 19px; +} +h5 { + font-size: 15px; + line-height: 17px; +} +h6 { + font-size: 13px; + line-height: 15px; +} + +.logo { + float: left; + margin: 20px 0 20px 22px; +} +.logo img { + height: 70px; +} + +.hero { + padding: 10px 25px; +} + +.gallery-random { + float: right; + line-height: 180px; +} +.gallery-random img { + max-height: 180px; +} + +.coins-sample { + padding: 5px; +} + +.sidebar-box { + padding: 0; +} +.sidebar-box-heading { + padding-left: 15px; +} + +.headerlink { + margin-left: 10px; + color: #ddd; + display: none; +} +h1:hover .headerlink, +h2:hover .headerlink, +h3:hover .headerlink, +h4:hover .headerlink, +h5:hover .headerlink, +h6:hover .headerlink { + display: inline; +} +.headerlink:hover { + color: #CE5C00; + text-decoration: none; +} + +.footer { + margin-top: 30px; + padding: 5px 10px; + color: #999; +} +.footer a { + color: #999; + text-decoration: underline; +} + +.ohloh-use, .gplus-use { + float: left; + margin: 0 0 10px 15px; +} + +/* Documentation */ + +/* general table settings */ +table.docutils { + margin-bottom: 10px; + border-color: #ccc; +} +table.docutils td, table.docutils th { + padding: 5px; + border-color: #ccc; + text-align: left; +} + +.toc ul ul { + font-size: 13px; + margin-right: -15px; +} + +/* master content table */ +.contentstable.docutils, .contentstable.docutils td { + border-color: transparent; +} +.contentstable.docutils .first { + font-weight: bold; +} +.contentstable.docutils .last { + padding-left: 10px; +} + +.docutils .label, .docutils .badge { + background: transparent; + text-shadow: none; + font-size: 13px; + padding: 5px; + line-height: 20px; + color: #333; +} + +/* module summary table */ +.longtable.docutils { + font-size: 12px; + margin-bottom: 30px; +} +.longtable.docutils, .longtable.docutils td { + border-color: #ccc; +} + +/* function and class description */ +dl.class, dl.function, dl.method, dl.attribute { + border-top: 1px solid #ccc; + padding-top: 10px; +} +.descclassname { + color: #aaa; + font-weight: normal; + font-family: monospace; +} +.descname { + font-family: monospace; +} +dl.class em, dl.function em, dl.class big, dl.function big { + font-weight: normal; + font-family: monospace; +} +dl.class dd, dl.function dd { + padding: 10px; +} +.docutils.field-list th { + background-color: #eee; + padding: 10px; + text-align: left; + vertical-align: top; + width: 100px; +} +.docutils.field-list td { + padding: 10px 10px 10px 20px; + text-align: left; + vertical-align: top; +} +.docutils.field-list td blockquote p { + font-size: 13px; + line-height: 18px; +} +p.rubric { + font-weight: bold; + font-size: 19px; + margin: 15px 0 10px 0; +} +p.admonition-title { + font-weight: bold; + text-decoration: underline; +} + +/* example gallery */ + +.gallery { + height: 200px; +} + +.figure { + float: left; + margin: 1em; +} + +.figure img { + display: block; + margin-left: auto; + margin-right: auto; + max-height: 150px; + max-width: 200px; +} + +.figure .caption { + width: 200px; + text-align: center !important; +} diff --git a/doc/source/themes/scikit-image/static/img/favicon.ico b/doc/source/themes/scikit-image/static/img/favicon.ico new file mode 100644 index 00000000..d7ae6012 Binary files /dev/null and b/doc/source/themes/scikit-image/static/img/favicon.ico differ diff --git a/doc/source/themes/scikit-image/static/img/glyphicons-halflings-white.png b/doc/source/themes/scikit-image/static/img/glyphicons-halflings-white.png new file mode 100644 index 00000000..3bf6484a Binary files /dev/null and b/doc/source/themes/scikit-image/static/img/glyphicons-halflings-white.png differ diff --git a/doc/source/themes/scikit-image/static/img/glyphicons-halflings.png b/doc/source/themes/scikit-image/static/img/glyphicons-halflings.png new file mode 100644 index 00000000..a9969993 Binary files /dev/null and b/doc/source/themes/scikit-image/static/img/glyphicons-halflings.png differ diff --git a/doc/source/themes/scikit-image/static/img/logo.png b/doc/source/themes/scikit-image/static/img/logo.png new file mode 100644 index 00000000..2d630cf7 Binary files /dev/null and b/doc/source/themes/scikit-image/static/img/logo.png differ diff --git a/doc/source/themes/scikit-image/static/js/bootstrap.min.js b/doc/source/themes/scikit-image/static/js/bootstrap.min.js new file mode 100644 index 00000000..0e33fb16 --- /dev/null +++ b/doc/source/themes/scikit-image/static/js/bootstrap.min.js @@ -0,0 +1,6 @@ +/*! +* Bootstrap.js by @fat & @mdo +* Copyright 2012 Twitter, Inc. +* http://www.apache.org/licenses/LICENSE-2.0.txt +*/ +!function(e){e(function(){"use strict";e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()},e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e(function(){e("body").on("click.alert.data-api",t,n.prototype.close)})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")},e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e(function(){e("body").on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=n,this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},to:function(t){var n=this.$element.find(".item.active"),r=n.parent().children(),i=r.index(n),s=this;if(t>r.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){s.to(t)}):i==t?this.pause().cycle():this.slide(t>i?"next":"prev",e(r[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f=e.Event("slide",{relatedTarget:i[0]});this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u]();if(i.hasClass("active"))return;if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}},e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e(function(){e("body").on("click.carousel.data-api","[data-slide]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=!i.data("modal")&&e.extend({},i.data(),n.data());i.carousel(s),t.preventDefault()})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning)return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning)return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=typeof n=="object"&&n;i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e(function(){e("body").on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})})}(window.jQuery),!function(e){"use strict";function r(){i(e(t)).removeClass("open")}function i(t){var n=t.attr("data-target"),r;return n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=e(n),r.length||(r=t.parent()),r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||(s.toggleClass("open"),n.focus()),!1},keydown:function(t){var n,r,s,o,u,a;if(!/(38|40|27)/.test(t.keyCode))return;n=e(this),t.preventDefault(),t.stopPropagation();if(n.is(".disabled, :disabled"))return;o=i(n),u=o.hasClass("open");if(!u||u&&t.keyCode==27)return n.click();r=e("[role=menu] li:not(.divider) a",o);if(!r.length)return;a=r.index(r.filter(":focus")),t.keyCode==38&&a>0&&a--,t.keyCode==40&&a').appendTo(document.body),this.options.backdrop!="static"&&this.$backdrop.click(e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,e.proxy(this.removeBackdrop,this)):this.removeBackdrop()):t&&t()}},e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e(function(){e("body").on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,this.options.trigger=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):this.options.trigger!="manual"&&(i=this.options.trigger=="hover"?"mouseenter":"focus",s=this.options.trigger=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this))),this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,t,this.$element.data()),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);if(!n.options.delay||!n.options.delay.show)return n.show();clearTimeout(this.timeout),n.hoverState="in",this.timeout=setTimeout(function(){n.hoverState=="in"&&n.show()},n.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var e,t,n,r,i,s,o;if(this.hasContent()&&this.enabled){e=this.tip(),this.setContent(),this.options.animation&&e.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,e[0],this.$element[0]):this.options.placement,t=/in/.test(s),e.remove().css({top:0,left:0,display:"block"}).appendTo(t?this.$element:document.body),n=this.getPosition(t),r=e[0].offsetWidth,i=e[0].offsetHeight;switch(t?s.split(" ")[1]:s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}e.css(o).addClass(s).addClass("in")}},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function r(){var t=setTimeout(function(){n.off(e.support.transition.end).remove()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.remove()})}var t=this,n=this.tip();return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?r():n.remove(),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").removeAttr("title")},hasContent:function(){return this.getTitle()},getPosition:function(t){return e.extend({},t?{top:0,left:0}:this.$element.offset(),{width:this.$element[0].offsetWidth,height:this.$element[0].offsetHeight})},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(){this[this.tip().hasClass("in")?"hide":"show"]()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}},e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
    ',trigger:"hover",title:"",delay:0,html:!0}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content > *")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-content")||(typeof n.content=="function"?n.content.call(t[0]):n.content),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}}),e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

    '})}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var t=e(this),n=t.data("target")||t.attr("href"),r=/^#\w/.test(n)&&e(n);return r&&r.length&&[[r.position().top,n]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}},e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active a").last()[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}},e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e(function(){e("body").on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.$menu=e(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:t.top+t.height,left:t.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),(e.browser.chrome||e.browser.webkit||e.browser.msie)&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this))},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=!~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},blur:function(e){var t=this;setTimeout(function(){t.hide()},150)},click:function(e){e.stopPropagation(),e.preventDefault(),this.select()},mouseenter:function(t){this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")}},e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e(function(){e("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;t.preventDefault(),n.typeahead(n.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))},e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); \ No newline at end of file diff --git a/doc/source/themes/scikit-image/theme.conf b/doc/source/themes/scikit-image/theme.conf new file mode 100644 index 00000000..f83ec96d --- /dev/null +++ b/doc/source/themes/scikit-image/theme.conf @@ -0,0 +1,4 @@ +[theme] +inherit = basic +stylesheet = none +pygments_style = default diff --git a/doc/source/user_guide.txt b/doc/source/user_guide.txt index 01344806..178e130d 100644 --- a/doc/source/user_guide.txt +++ b/doc/source/user_guide.txt @@ -8,3 +8,4 @@ User Guide user_guide/plugins user_guide/tutorials user_guide/getting_help + user_guide/viewer diff --git a/doc/source/user_guide/data/denoise_plugin_window.png b/doc/source/user_guide/data/denoise_plugin_window.png new file mode 100644 index 00000000..3c1ff3e3 Binary files /dev/null and b/doc/source/user_guide/data/denoise_plugin_window.png differ diff --git a/doc/source/user_guide/data/denoise_viewer_window.png b/doc/source/user_guide/data/denoise_viewer_window.png new file mode 100644 index 00000000..a3867889 Binary files /dev/null and b/doc/source/user_guide/data/denoise_viewer_window.png differ diff --git a/doc/source/user_guide/data_types.txt b/doc/source/user_guide/data_types.txt index 1db55543..9801c244 100644 --- a/doc/source/user_guide/data_types.txt +++ b/doc/source/user_guide/data_types.txt @@ -14,13 +14,13 @@ Data type Range uint8 0 to 255 uint16 0 to 65535 uint32 0 to 2\ :sup:`32` -float 0 to 1 +float -1 to 1 int8 -128 to 127 int16 -32768 to 32767 int32 -2\ :sup:`31` to 2\ :sup:`31` - 1 ========= ================================= -Note that float images are restricted to the range 0 to 1 even though the data +Note that float images are restricted to the range -1 to 1 even though the data type itself can exceed this range; all integer dtypes, on the other hand, have pixel intensities that can span the entire data type range. Currently, *64-bit (u)int images are not supported*. @@ -142,6 +142,24 @@ By default, ``rescale_intensity`` stretches the values of ``in_range`` to match the range of the dtype. +Note about negative values +========================== + +People very often represent images in signed dtypes, even though they only +manipulate the positive values of the image (e.g., using only 0-127 in an int8 +image). For this reason, conversion functions *only spread the positive values* +of a signed dtype over the entire range of an unsigned dtype. In other words, +negative values are clipped to 0 when converting from signed to unsigned +dtypes. (Negative values are preserved when converting between signed dtypes.) +To prevent this clipping behavior, you should rescale your image beforehand:: + + >>> image = exposure.rescale_intensity(img_int32, out_range=(0, 2**31 - 1)) + >>> img_uint8 = img_as_ubyte(image) + +This behavior is symmetric: The values in an unsigned dtype are spread over +just the positive range of a signed dtype. + + References ========== diff --git a/doc/source/user_guide/getting_help.txt b/doc/source/user_guide/getting_help.txt index 6dedb6f2..ea444856 100644 --- a/doc/source/user_guide/getting_help.txt +++ b/doc/source/user_guide/getting_help.txt @@ -44,13 +44,13 @@ functions include one or more examples. Mailing-list ------------ -The scikits-image mailing-list is scikits-image@googlegroups.com (users +The scikit-image mailing-list is scikit-image@googlegroups.com (users should join the `Google Group -`_ before posting). This +`_ before posting). This mailing-list is shared by users and developers, and it is the right place to ask any question about ``skimage``, or in general, image processing using Python. Posting snippets of code with minimal examples -ensures to get more relevant and focused answers. +ensures to get more relevant and focused answers. We would love to hear from how you use ``skimage`` for your work on the -mailing-list! +mailing-list! diff --git a/doc/source/user_guide/tutorial_segmentation.txt b/doc/source/user_guide/tutorial_segmentation.txt index eda202d9..cff7c651 100644 --- a/doc/source/user_guide/tutorial_segmentation.txt +++ b/doc/source/user_guide/tutorial_segmentation.txt @@ -47,10 +47,6 @@ detection, we use the `Canny detector As the background is very smooth, almost all edges are found at the boundary of the coins, or inside the coins. -Now that we have contours that delineate the outer boundary of the coins, -we fill the inner part of the coins using the -``ndimage.binary_fill_holes`` function, which uses mathematical morphology -to fill the holes. :: @@ -61,6 +57,15 @@ to fill the holes. :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center +Now that we have contours that delineate the outer boundary of the coins, +we fill the inner part of the coins using the +``ndimage.binary_fill_holes`` function, which uses mathematical morphology +to fill the holes. + +.. image:: ../../_images/plot_coins_segmentation_4.png + :target: ../auto_examples/applications/plot_coins_segmentation.html + :align: center + Most coins are well segmented out of the background. Small objects from the background can be easily removed using the ``ndimage.label`` function to remove objects smaller than a small threshold. @@ -78,6 +83,10 @@ has not been segmented correctly at all. The reason is that the contour that we got from the Canny detector was not completely closed, therefore the filling function did not fill the inner part of the coin. +.. image:: ../../_images/plot_coins_segmentation_5.png + :target: ../auto_examples/applications/plot_coins_segmentation.html + :align: center + Therefore, this segmentation method is not very robust: if we miss a single pixel of the contour of the object, we will not be able to fill it. Of course, we could try to dilate the contours in order to @@ -117,12 +126,29 @@ separate the coins from the background. .. image:: data/elevation_map.jpg :align: center +and here is the corresponding 2-D plot: + +.. image:: ../../_images/plot_coins_segmentation_6.png + :target: ../auto_examples/applications/plot_coins_segmentation.html + :align: center + +The next step is to 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 + +.. image:: ../../_images/plot_coins_segmentation_7.png + :target: ../auto_examples/applications/plot_coins_segmentation.html + :align: center + Let us now compute the watershed transform:: >>> from skimage.morphology import watershed >>> segmentation = watershed(elevation_map, markers) -.. image:: ../../_images/plot_coins_segmentation_4.png +.. image:: ../../_images/plot_coins_segmentation_8.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center @@ -139,7 +165,7 @@ We can now label all the coins one by one using ``ndimage.label``:: >>> labeled_coins, _ = ndimage.label(segmentation) -.. image:: ../../_images/plot_coins_segmentation_5.png +.. image:: ../../_images/plot_coins_segmentation_9.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center diff --git a/doc/source/user_guide/viewer.txt b/doc/source/user_guide/viewer.txt new file mode 100644 index 00000000..82bb5949 --- /dev/null +++ b/doc/source/user_guide/viewer.txt @@ -0,0 +1,85 @@ +Image Viewer +============ + + +Quick Start +----------- + + +``skimage.viewer`` provides a matplotlib_-based canvas for displaying images and +a Qt-based GUI-toolkit, with the goal of making it easy to create interactive +image editors. You can simply use it to display an image: + +.. code-block:: python + + from skimage import data + from skimage.viewer import ImageViewer + + image = data.coins() + viewer = ImageViewer(image) + viewer.show() + +Of course, you could just as easily use ``imshow`` from matplotlib_ (or +alternatively, ``skimage.io.imshow`` which adds support for multiple +io-plugins) to display images. The advantage of ``ImageViewer`` is that you can +easily add plugins for manipulating images. Currently, only a few plugins are +implemented, but it is easy to write your own. Before going into the details, +let's see an example of how a plugin is added to the viewer: + +.. code-block:: python + + from skimage.viewer.plugins import Canny + + viewer = ImageViewer(image) + viewer += Canny(view) + viewer.show() + +At the moment, there aren't very many plugins pre-defined, but there's a really +simple interface for creating your own plugin. First, let's create a plugin to +call the total-variation denoising function, ``tv_denoise``: + +.. code-block:: python + + from skimage.filter import tv_denoise + from skimage.viewer.plugins.base import Plugin + + denoise_plugin = Plugin(image_filter=tv_denoise) + +.. note:: + + The ``Plugin`` assumes the first argument given to the image filter is the + image from the image viewer. In the future, this should be changed so you + can pass the image to a different argument of the filter function. + +To actually interact with the filter, you have to add widgets that adjust the +parameters of the function. Typically, that means adding a slider widget and +connecting it to the filter parameter and the minimum and maximum values of the +slider: + +.. code-block:: python + + from skimage.viewer.widgets import Slider + from skimage.viewer.widgets.history import SaveButtons + + denoise_plugin += Slider('weight', 0.01, 0.5, update_on='release') + denoise_plugin += SaveButtons() + +Here, we connect a slider widget to the filter's 'weight' argument. We also +added some buttons for saving the image to file or to the ``scikit-image`` +image stack (see ``skimage.io.push`` and ``skimage.io.pop``). + +All that's left is to create an image viewer and add the plugin to that viewer. + +.. code-block:: python + + viewer = ImageViewer(image) + viewer += denoise_plugin + viewer.show() + + +.. image:: data/denoise_viewer_window.png +.. image:: data/denoise_plugin_window.png + + +.. _matplotlib: http://matplotlib.sourceforge.net/ + diff --git a/doc/tools/apigen.py b/doc/tools/apigen.py index 86791383..49c7e6e3 100644 --- a/doc/tools/apigen.py +++ b/doc/tools/apigen.py @@ -269,10 +269,6 @@ class ApiDocWriter(object): ad = '.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n' - chap_title = uri_short - #ad += (chap_title+'\n'+ self.rst_section_levels[1] * len(chap_title) - # + '\n\n') - # Set the chapter title to read 'module' for all modules except for the # main packages if '.' in uri: @@ -281,21 +277,8 @@ class ApiDocWriter(object): title = ':mod:`' + uri_short + '`' ad += title + '\n' + self.rst_section_levels[1] * len(title) - if len(classes): - ad += '\nInheritance diagram for ``%s``:\n\n' % uri - ad += '.. inheritance-diagram:: %s \n' % uri - ad += ' :parts: 3\n' - ad += '\n.. automodule:: ' + uri + '\n' ad += '\n.. currentmodule:: ' + uri + '\n' -# multi_class = len(classes) > 1 -# multi_fx = len(functions) > 1 -# if multi_class: -# ad += '\n' + 'Classes' + '\n' + \ -# self.rst_section_levels[2] * 7 + '\n' -# elif len(classes) and multi_fx: -# ad += '\n' + 'Class' + '\n' + \ -# self.rst_section_levels[2] * 5 + '\n' for c in classes: ad += '\n:class:`' + c + '`\n' \ + self.rst_section_levels[2] * \ @@ -305,15 +288,8 @@ class ApiDocWriter(object): ad += ' :members:\n' \ ' :undoc-members:\n' \ ' :show-inheritance:\n' \ - ' :inherited-members:\n' \ '\n' \ ' .. automethod:: __init__\n' -# if multi_fx: -# ad += '\n' + 'Functions' + '\n' + \ -# self.rst_section_levels[2] * 9 + '\n\n' -# elif len(functions) and multi_class: -# ad += '\n' + 'Function' + '\n' + \ -# self.rst_section_levels[2] * 8 + '\n\n' ad += '.. autosummary::\n\n' for f in functions: ad += ' ' + uri + '.' + f + '\n' @@ -405,16 +381,9 @@ class ApiDocWriter(object): modules.append(package_uri) else: dirnames.remove(dirname) - # Check filenames for modules - for filename in filenames: - module_name = filename[:-3] - module_uri = '.'.join((root_uri, module_name)) - if (self._uri2path(module_uri) and - self._survives_exclude(module_uri, 'module')): - modules.append(module_uri) return sorted(modules) - def write_modules_api(self, modules,outdir): + def write_modules_api(self, modules, outdir): # write the list written_modules = [] for m in modules: @@ -451,14 +420,6 @@ class ApiDocWriter(object): os.mkdir(outdir) # compose list of modules modules = self.discover_modules() - # group modules so we have one less level - module_depth = max([len(item.split('.')) for item in modules]) - # modifying modules in-place, so make a copy - for item in modules[:]: - # Do not treat the .py files all as separate modules. - # Like this, only the objects exported in __all__ get picked up. - if not (len(item.split('.')) < module_depth): - modules.remove(item) self.write_modules_api(modules,outdir) def write_index(self, outdir, froot='gen', relative_to=None): diff --git a/doc/tools/plot_pr.py b/doc/tools/plot_pr.py index 29f3b094..09a9f91e 100644 --- a/doc/tools/plot_pr.py +++ b/doc/tools/plot_pr.py @@ -23,14 +23,17 @@ releases = OrderedDict([ #('0.1', u'2009-10-07 13:52:19 +0200'), #('0.2', u'2009-11-12 14:48:45 +0200'), ('0.3', u'2011-10-10 03:28:47 -0700'), - ('0.4', u'2011-12-03 14:31:32 -0800')]) + ('0.4', u'2011-12-03 14:31:32 -0800'), + ('0.5', u'2012-02-26 21:00:51 -0800'), + ('0.6', u'2012-06-24 21:37:05 -0700')]) -month_duration = 16 + +month_duration = 24 for r in releases: releases[r] = dateutil.parser.parse(releases[r]) -def fetch_PRs(user='scikits-image', repo='scikits-image', state='open'): +def fetch_PRs(user='scikit-image', repo='scikit-image', state='open'): params = {'state': state, 'per_page': 100, 'page': 1} diff --git a/setup.py b/setup.py index 63176520..e4a7fc07 100644 --- a/setup.py +++ b/setup.py @@ -1,25 +1,32 @@ #! /usr/bin/env python -descr = """Image Processing SciKit +descr = """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/ +http://scikit-image.org/ """ -DISTNAME = 'scikits-image' +DISTNAME = 'scikit-image' DESCRIPTION = 'Image processing routines for SciPy' LONG_DESCRIPTION = descr MAINTAINER = 'Stefan van der Walt' MAINTAINER_EMAIL = 'stefan@sun.ac.za' -URL = 'http://scikits-image.org' +URL = 'http://scikit-image.org' LICENSE = 'Modified BSD' -DOWNLOAD_URL = 'http://github.com/scikits-image/scikits-image' -VERSION = '0.6' +DOWNLOAD_URL = 'http://github.com/scikit-image/scikit-image' +VERSION = '0.7.2' +PYTHON_VERSION = (2, 5) +DEPENDENCIES = { + 'numpy': (1, 6), + 'Cython': (0, 15), + } + import os +import sys import setuptools from numpy.distutils.core import setup try: @@ -27,6 +34,7 @@ try: except ImportError: from distutils.command.build_py import build_py + def configuration(parent_package='', top_path=None): if os.path.exists('MANIFEST'): os.remove('MANIFEST') @@ -44,6 +52,7 @@ def configuration(parent_package='', top_path=None): return config + def write_version_py(filename='skimage/version.py'): template = """# THIS FILE IS GENERATED FROM THE SKIMAGE SETUP.PY version='%s' @@ -57,7 +66,46 @@ version='%s' finally: vfile.close() + +def get_package_version(package): + version = [] + for version_attr in ('version', 'VERSION', '__version__'): + if hasattr(package, version_attr) \ + and isinstance(getattr(package, version_attr), str): + version_info = getattr(package, version_attr) + for part in version_info.split('.'): + try: + version.append(int(part)) + except ValueError: + pass + return tuple(version) + + +def check_requirements(): + if sys.version_info < PYTHON_VERSION: + raise SystemExit('You need Python version %d.%d or later.' \ + % PYTHON_VERSION) + + for package_name, min_version in DEPENDENCIES.items(): + dep_error = False + try: + package = __import__(package_name) + except ImportError: + dep_error = True + else: + package_version = get_package_version(package) + if min_version > package_version: + dep_error = True + + if dep_error: + raise ImportError('You need `%s` version %d.%d or later.' \ + % ((package_name, ) + min_version)) + + if __name__ == "__main__": + + check_requirements() + write_version_py() setup( @@ -71,32 +119,31 @@ if __name__ == "__main__": download_url=DOWNLOAD_URL, version=VERSION, - 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', - ], + 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', + ], configuration=configuration, - install_requires=[], + packages=setuptools.find_packages(), include_package_data=True, zip_safe=False, # the package can run out of an .egg file entry_points={ - 'console_scripts': [ - 'skivi = skimage.scripts.skivi:main'] - }, + 'console_scripts': ['skivi = skimage.scripts.skivi:main'], + }, cmdclass={'build_py': build_py}, - ) + ) diff --git a/skimage/__init__.py b/skimage/__init__.py index bcdaf2b0..1ac121b3 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -1,6 +1,6 @@ """Image Processing SciKit (Toolbox for SciPy) -``scikits-image`` (a.k.a. ``skimage``) is a collection of algorithms for image +``scikit-image`` (a.k.a. ``skimage``) is a collection of algorithms for image processing and computer vision. The main package of ``skimage`` only provides a few utilities for converting @@ -61,37 +61,32 @@ try: except ImportError: __version__ = "unbuilt-dev" + def _setup_test(verbose=False): - import gzip import functools - args = ['', '--exe', '-w', pkg_dir] + args = ['', pkg_dir, '--exe'] if verbose: args.extend(['-v', '-s']) try: import nose as _nose except ImportError: - print("Could not load nose. Unit tests not available.") - return None + def broken_test_func(): + """This would invoke the skimage test suite, but nose couldn't be + imported so the test suite can not run. + """ + raise ImportError("Could not load nose. Unit tests not available.") + return broken_test_func else: f = functools.partial(_nose.run, 'skimage', argv=args) f.__doc__ = 'Invoke the skimage test suite.' return f -test = _setup_test() -if test is None: - try: - del test - except NameError: - pass +test = _setup_test() test_verbose = _setup_test(verbose=True) -if test_verbose is None: - try: - del test - except NameError: - pass + def get_log(name=None): """Return a console logger. @@ -120,26 +115,28 @@ def get_log(name=None): log = logging.getLogger(name) return log + def _setup_log(): """Configure root logger. """ - import logging, sys + import logging + import sys - log = logging.getLogger() + formatter = logging.Formatter( + '%(name)s: %(levelname)s: %(message)s' + ) try: handler = logging.StreamHandler(stream=sys.stdout) except TypeError: handler = logging.StreamHandler(strm=sys.stdout) - - formatter = logging.Formatter( - '%(name)s: %(levelname)s: %(message)s' - ) handler.setFormatter(formatter) + log = get_log() log.addHandler(handler) log.setLevel(logging.WARNING) + log.propagate = False _setup_log() diff --git a/skimage/_build.py b/skimage/_build.py index dbf5e678..8f255f29 100644 --- a/skimage/_build.py +++ b/skimage/_build.py @@ -1,9 +1,8 @@ import sys import os -import shutil import hashlib import subprocess -import platform + def cython(pyx_files, working_path=''): """Use Cython to convert the given files to C. @@ -39,17 +38,18 @@ def cython(pyx_files, working_path=''): print(cmd) try: - status = subprocess.call(['cython', '-o', c_file, pyxfile]) + subprocess.call(['cython', '-o', c_file, pyxfile]) except WindowsError: # On Windows cython.exe may be missing if Cython was installed # via distutils. Run the cython.py script instead. - status = subprocess.call( + subprocess.call( [sys.executable, os.path.join(os.path.dirname(sys.executable), 'Scripts', 'cython.py'), '-o', c_file, pyxfile], shell=True) + def _md5sum(f): m = hashlib.new('md5') while True: @@ -60,6 +60,7 @@ def _md5sum(f): m.update(d) return m.hexdigest() + def _changed(filename): """Compare the hash of a Cython file to the cached hash value on disk. diff --git a/skimage/_shared/__init__.py b/skimage/_shared/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/skimage/_shared/geometry.pxd b/skimage/_shared/geometry.pxd new file mode 100644 index 00000000..afdc6b5b --- /dev/null +++ b/skimage/_shared/geometry.pxd @@ -0,0 +1,6 @@ +cdef unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, + double x, double y) + +cdef void points_in_polygon(int nr_verts, double *xp, double *yp, + int nr_points, double *x, double *y, + unsigned char *result) diff --git a/skimage/_shared/geometry.pyx b/skimage/_shared/geometry.pyx new file mode 100644 index 00000000..3f4850b0 --- /dev/null +++ b/skimage/_shared/geometry.pyx @@ -0,0 +1,54 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + + +cdef inline unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, + double x, double y): + """Test whether point lies inside a polygon. + + Parameters + ---------- + nr_verts : int + Number of vertices of polygon. + xp, yp : double array + Coordinates of polygon with length nr_verts. + x, y : double + Coordinates of point. + """ + cdef int i + cdef unsigned char c = 0 + cdef int j = nr_verts - 1 + for i in range(nr_verts): + if ( + (((yp[i] <= y) and (y < yp[j])) or + ((yp[j] <= y) and (y < yp[i]))) + and (x < (xp[j] - xp[i]) * (y - yp[i]) / (yp[j] - yp[i]) + xp[i]) + ): + c = not c + j = i + return c + + +cdef void points_in_polygon(int nr_verts, double *xp, double *yp, + int nr_points, double *x, double *y, + unsigned char *result): + """Test whether points lie inside a polygon. + + Parameters + ---------- + nr_verts : int + Number of vertices of polygon. + xp, yp : double array + Coordinates of polygon with length nr_verts. + nr_points : int + Number of points to test. + x, y : double array + Coordinates of points. + result : unsigned char array + Test results for each point. + """ + cdef int n + for n in range(nr_points): + result[n] = point_in_polygon(nr_verts, xp, yp, x[n], y[n]) diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd new file mode 100644 index 00000000..f43ff25e --- /dev/null +++ b/skimage/_shared/interpolation.pxd @@ -0,0 +1,24 @@ + +cdef double nearest_neighbour_interpolation(double* image, int rows, + int cols, double r, + double c, char mode, + double cval) + +cdef double bilinear_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval) + +cdef double quadratic_interpolation(double x, double[3] f) +cdef double biquadratic_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval) + +cdef double cubic_interpolation(double x, double[4] f) +cdef double bicubic_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval) + +cdef double get_pixel(double* image, int rows, int cols, int r, int c, + char mode, double cval) + +cdef int coord_map(int dim, int coord, char mode) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx new file mode 100644 index 00000000..e0fd0067 --- /dev/null +++ b/skimage/_shared/interpolation.pyx @@ -0,0 +1,297 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +from libc.math cimport ceil, floor + + +cdef inline int round(double r): + return ((r + 0.5) if (r > 0.0) else (r - 0.5)) + + +cdef inline double nearest_neighbour_interpolation(double* image, int rows, + int cols, double r, + double c, char mode, + double cval): + """Nearest neighbour interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + + return get_pixel(image, rows, cols, round(r), round(c), + mode, cval) + + +cdef inline double bilinear_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval): + """Bilinear interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + cdef double dr, dc + cdef int minr, minc, maxr, maxc + + minr = floor(r) + minc = floor(c) + maxr = ceil(r) + maxc = ceil(c) + dr = r - minr + dc = c - minc + top = (1 - dc) * get_pixel(image, rows, cols, minr, minc, mode, cval) \ + + dc * get_pixel(image, rows, cols, minr, maxc, mode, cval) + bottom = (1 - dc) * get_pixel(image, rows, cols, maxr, minc, mode, cval) \ + + dc * get_pixel(image, rows, cols, maxr, maxc, mode, cval) + return (1 - dr) * top + dr * bottom + + +cdef inline double quadratic_interpolation(double x, double[3] f): + """Quadratic interpolation. + + Parameters + ---------- + x : double + Position in the interval [-1, 1]. + f : double[4] + Function values at positions [-1, 0, 1]. + + Returns + ------- + value : double + Interpolated value. + + """ + return f[1] - 0.25 * (f[0] - f[2]) * x + + +cdef inline double biquadratic_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval): + """Biquadratic interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + + cdef int r0 = round(r) + cdef int c0 = round(c) + if r < 0: + r0 -= 1 + if c < 0: + c0 -= 1 + # scale position to range [-1, 1] + cdef double xr = (r - r0) - 1 + cdef double xc = (c - c0) - 1 + if r == r0: + xr += 1 + if c == c0: + xc += 1 + + cdef double fc[3], fr[3] + + cdef int pr, pc + + # row-wise cubic interpolation + for pr in range(r0, r0 + 3): + for pc in range(c0, c0 + 3): + fc[pc - c0] = get_pixel(image, rows, cols, pr, pc, mode, cval) + fr[pr - r0] = quadratic_interpolation(xc, fc) + + # cubic interpolation for interpolated values of each row + return quadratic_interpolation(xr, fr) + + +cdef inline double cubic_interpolation(double x, double[4] f): + """Cubic interpolation. + + Parameters + ---------- + x : double + Position in the interval [0, 1]. + f : double[4] + Function values at positions [0, 1/3, 2/3, 1]. + + Returns + ------- + value : double + Interpolated value. + + """ + return \ + f[1] + 0.5 * x * \ + (f[2] - f[0] + x * \ + (2.0 * f[0] - 5.0 * f[1] + 4.0 * f[2] - f[3] + x * \ + (3.0 * (f[1] - f[2]) + f[3] - f[0]))) + + +cdef inline double bicubic_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval): + """Bicubic interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + + cdef int r0 = r - 1 + cdef int c0 = c - 1 + if r < 0: + r0 -= 1 + if c < 0: + c0 -= 1 + # scale position to range [0, 1] + cdef double xr = (r - r0) / 3 + cdef double xc = (c - c0) / 3 + + cdef double fc[4], fr[4] + + cdef int pr, pc + + # row-wise cubic interpolation + for pr in range(r0, r0 + 4): + for pc in range(c0, c0 + 4): + fc[pc - c0] = get_pixel(image, rows, cols, pr, pc, mode, cval) + fr[pr - r0] = cubic_interpolation(xc, fc) + + # cubic interpolation for interpolated values of each row + return cubic_interpolation(xr, fr) + + +cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, + char mode, double cval): + """Get a pixel from the image, taking wrapping mode into consideration. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : int + Position at which to get the pixel. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Pixel value at given position. + + """ + if mode == 'C': + if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): + return cval + else: + return image[r * cols + c] + else: + return image[coord_map(rows, r, mode) * cols + coord_map(cols, c, mode)] + + +cdef inline int coord_map(int dim, int coord, char mode): + """ + Wrap a coordinate, according to a given mode. + + Parameters + ---------- + dim : int + Maximum coordinate. + coord : int + Coord provided by user. May be < 0 or > dim. + mode : {'W', 'R', 'N'} + Whether to wrap or reflect the coordinate if it + falls outside [0, dim). + + """ + dim = dim - 1 + if mode == 'R': # reflect + if coord < 0: + # How many times times does the coordinate wrap? + if (-coord / dim) % 2 != 0: + return dim - (-coord % dim) + else: + return (-coord % dim) + elif coord > dim: + if (coord / dim) % 2 != 0: + return (dim - (coord % dim)) + else: + return (coord % dim) + elif mode == 'W': # wrap + if coord < 0: + return (dim - (-coord % dim)) + elif coord > dim: + return (coord % dim) + elif mode == 'N': # nearest + if coord < 0: + return 0 + elif coord > dim: + return dim + + return coord diff --git a/skimage/_shared/setup.py b/skimage/_shared/setup.py new file mode 100644 index 00000000..765c30de --- /dev/null +++ b/skimage/_shared/setup.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +import os + +from skimage._build import cython + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs + + config = Configuration('_shared', parent_package, top_path) + config.add_data_dir('tests') + + cython(['geometry.pyx'], working_path=base_path) + cython(['interpolation.pyx'], working_path=base_path) + cython(['transform.pyx'], working_path=base_path) + + config.add_extension('geometry', sources=['geometry.c']) + config.add_extension('interpolation', sources=['interpolation.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('transform', sources=['transform.c'], + include_dirs=[get_numpy_include_dirs()]) + + return config + + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='scikit-image Developers', + author='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Transforms', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py new file mode 100644 index 00000000..eab83a56 --- /dev/null +++ b/skimage/_shared/testing.py @@ -0,0 +1,26 @@ +"""Testing utilities.""" + + +def _assert_less(a, b, msg=None): + message = "%r is not lower than %r" % (a, b) + if msg is not None: + message += ": " + msg + assert a < b, message + + +def _assert_greater(a, b, msg=None): + message = "%r is not greater than %r" % (a, b) + if msg is not None: + message += ": " + msg + assert a > b, message + + +try: + from nose.tools import assert_less +except ImportError: + assert_less = _assert_less + +try: + from nose.tools import assert_greater +except ImportError: + assert_greater = _assert_greater diff --git a/skimage/_shared/transform.pxd b/skimage/_shared/transform.pxd new file mode 100644 index 00000000..0edc22a4 --- /dev/null +++ b/skimage/_shared/transform.pxd @@ -0,0 +1,5 @@ +cimport numpy as cnp + + +cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, + int r0, int c0, int r1, int c1) diff --git a/skimage/_shared/transform.pyx b/skimage/_shared/transform.pyx new file mode 100644 index 00000000..ba0efc71 --- /dev/null +++ b/skimage/_shared/transform.pyx @@ -0,0 +1,44 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +cimport numpy as cnp + + +cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, + int r0, int c0, int r1, int c1): + """ + Using a summed area table / integral image, calculate the sum + over a given window. + + This function is the same as the `integrate` function in + `skimage.transform.integrate`, but this Cython version significantly + speeds up the code. + + Parameters + ---------- + sat : ndarray of float + Summed area table / integral image. + r0, c0 : int + Top-left corner of block to be summed. + r1, c1 : int + Bottom-right corner of block to be summed. + + Returns + ------- + S : int + Sum over the given window. + """ + cdef float S = 0 + + S += sat[r1, c1] + + if (r0 - 1 >= 0) and (c0 - 1 >= 0): + S += sat[r0 - 1, c0 - 1] + + if (r0 - 1 >= 0): + S -= sat[r0 - 1, c1] + + if (c0 - 1 >= 0): + S -= sat[r1, c0 - 1] + return S diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 36165d68..ec5551d6 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -44,7 +44,9 @@ References from __future__ import division __all__ = ['convert_colorspace', 'rgb2hsv', 'hsv2rgb', 'rgb2xyz', 'xyz2rgb', - 'rgb2rgbcie', 'rgbcie2rgb', 'rgb2grey', 'rgb2gray', 'gray2rgb'] + 'rgb2rgbcie', 'rgbcie2rgb', 'rgb2grey', 'rgb2gray', 'gray2rgb', + 'xyz2lab', 'lab2xyz', 'lab2rgb', 'rgb2lab' + ] __docformat__ = "restructuredtext en" @@ -81,11 +83,8 @@ def convert_colorspace(arr, fromspace, tospace): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> from skimage import data + >>> lena = data.lena() >>> lena_hsv = convert_colorspace(lena, 'RGB', 'HSV') """ fromdict = {'RGB': lambda im: im, 'HSV': hsv2rgb, 'RGB CIE': rgbcie2rgb, @@ -149,11 +148,9 @@ def rgb2hsv(rgb): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> from skimage import color + >>> from skimage import data + >>> lena = data.lena() >>> lena_hsv = color.rgb2hsv(lena) """ arr = _prepare_colorarray(rgb) @@ -164,8 +161,10 @@ def rgb2hsv(rgb): # -- S channel delta = arr.ptp(-1) + # Ignore warning for zero divided by zero + old_settings = np.seterr(invalid='ignore') out_s = delta / out_v - out_s[delta == 0] = 0 + out_s[delta == 0.] = 0. # -- H channel # red is max @@ -180,6 +179,9 @@ def rgb2hsv(rgb): idx = (arr[:, :, 2] == out_v) out[idx, 0] = 4. + (arr[idx, 0] - arr[idx, 1]) / delta[idx] out_h = (out[:, :, 0] / 6.) % 1. + out_h[delta == 0.] = 0. + + np.seterr(**old_settings) # -- output out[:, :, 0] = out_h @@ -224,11 +226,8 @@ def hsv2rgb(hsv): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> from skimage import data + >>> lena = data.lena() >>> lena_hsv = rgb2hsv(lena) >>> lena_rgb = hsv2rgb(lena_hsv) """ @@ -286,6 +285,9 @@ grey_from_rgb = np.array([[0.2125, 0.7154, 0.0721], [0, 0, 0], [0, 0, 0]]) +# CIE LAB constants for Observer= 2A, Illuminant= D65 +lab_ref_white = np.array([0.95047, 1., 1.08883]) + #------------------------------------------------------------- # The conversion functions that make use of the matrices above #------------------------------------------------------------- @@ -346,14 +348,11 @@ def xyz2rgb(xyz): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread + >>> from skimage import data >>> from skimage.color import rgb2xyz, xyz2rgb - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena = data.lena() >>> lena_xyz = rgb2xyz(lena) - >>> lena_rgb = xyz2rgb(lena_hsv) + >>> lena_rgb = xyz2rgb(lena_xyz) """ return _convert(rgb_from_xyz, xyz) @@ -387,11 +386,8 @@ def rgb2xyz(rgb): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> from skimage import data + >>> lena = data.lena() >>> lena_xyz = rgb2xyz(lena) """ return _convert(xyz_from_rgb, rgb) @@ -421,12 +417,9 @@ def rgb2rgbcie(rgb): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread + >>> from skimage import data >>> from skimage.color import rgb2rgbcie - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena = data.lena() >>> lena_rgbcie = rgb2rgbcie(lena) """ return _convert(rgbcie_from_rgb, rgb) @@ -456,14 +449,11 @@ def rgbcie2rgb(rgbcie): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread + >>> from skimage import data >>> from skimage.color import rgb2rgbcie, rgbcie2rgb - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena = data.lena() >>> lena_rgbcie = rgb2rgbcie(lena) - >>> lena_rgb = rgbcie2rgb(lena_hsv) + >>> lena_rgb = rgbcie2rgb(lena_rgbcie) """ return _convert(rgb_from_rgbcie, rgbcie) @@ -485,7 +475,7 @@ def rgb2grey(rgb): Raises ------ ValueError - If `rgb2grey` is not a 3-D array of shape (.., .., 3) or + If `rgb2grey` is not a 3-D array of shape (.., .., 3) or (.., .., 4). References @@ -503,14 +493,14 @@ def rgb2grey(rgb): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread >>> from skimage.color import rgb2grey - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> from skimage import data + >>> lena = data.lena() >>> lena_grey = rgb2grey(lena) """ + if rgb.ndim == 2: + return rgb + return _convert(grey_from_rgb, rgb[:, :, :3])[..., 0] rgb2gray = rgb2grey @@ -540,3 +530,157 @@ def gray2rgb(image): M, N = image.shape return np.dstack((image, image, image)) + + +def xyz2lab(xyz): + """XYZ to CIE-LAB color space conversion. + + Parameters + ---------- + xyz : array_like + The image in XYZ format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in CIE-LAB format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `xyz` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + Observer= 2A, Illuminant= D65 + CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 + + References + ---------- + .. [1] http://www.easyrgb.com/index.php?X=MATH&H=07#text7 + .. [2] http://en.wikipedia.org/wiki/Lab_color_space + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2xyz, xyz2lab + >>> lena = data.lena() + >>> lena_xyz = rgb2xyz(lena) + >>> lena_lab = xyz2lab(lena_xyz) + """ + arr = _prepare_colorarray(xyz) + + # scale by CIE XYZ tristimulus values of the reference white point + arr = arr / lab_ref_white + + # Nonlinear distortion and linear transformation + mask = arr > 0.008856 + arr[mask] = np.power(arr[mask], 1. / 3.) + arr[~mask] = 7.787 * arr[~mask] + 16. / 116. + + x, y, z = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + + # Vector scaling + L = (116. * y) - 16. + a = 500.0 * (x - y) + b = 200.0 * (y - z) + + return np.dstack([L, a, b]) + + +def lab2xyz(lab): + """CIE-LAB to XYZcolor space conversion. + + Parameters + ---------- + lab : array_like + The image in lab format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in XYZ format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `lab` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + Observer= 2A, Illuminant= D65 + CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 + + References + ---------- + .. [1] http://www.easyrgb.com/index.php?X=MATH&H=07#text7 + .. [2] http://en.wikipedia.org/wiki/Lab_color_space + + """ + + arr = _prepare_colorarray(lab).copy() + + L, a, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + y = (L + 16.) / 116. + x = (a / 500.) + y + z = y - (b / 200.) + + out = np.dstack([x, y, z]) + + mask = out > 0.2068966 + out[mask] = np.power(out[mask], 3.) + out[~mask] = (out[~mask] - 16.0 / 116.) / 7.787 + + # rescale Observer= 2 deg, Illuminant= D65 + out *= lab_ref_white + return out + + +def rgb2lab(rgb): + """RGB to lab color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in Lab format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `rgb` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + This function uses rgb2xyz and xyz2lab. + """ + return xyz2lab(rgb2xyz(rgb)) + + +def lab2rgb(lab): + """Lab to RGB color space conversion. + + Parameters + ---------- + rgb : array_like + The image in Lab format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in RGB format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `lab` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + This function uses lab2xyz and xyz2rgb. + """ + return xyz2rgb(lab2xyz(lab)) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index be0d330c..316d801a 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -23,7 +23,9 @@ from skimage.color import ( rgb2xyz, xyz2rgb, rgb2rgbcie, rgbcie2rgb, convert_colorspace, - rgb2grey, gray2rgb + rgb2grey, gray2rgb, + xyz2lab, lab2xyz, + lab2rgb, rgb2lab ) from skimage import data_dir @@ -43,6 +45,19 @@ class TestColorconv(TestCase): colbars_point75 = colbars * 0.75 colbars_point75_array = np.swapaxes(colbars_point75.reshape(3, 4, 2), 0, 2) + xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], # red + [[0, 0, 0]], # black + [[.9505, 1., 1.089]], # white + [[.1805, .0722, .9505]], # blue + [[.07719, .15438, .02573]], # green + ]) + lab_array = np.array([[[53.233, 80.109, 67.220]], # red + [[0., 0., 0.]], # black + [[100.0, 0.005, -0.010]], # white + [[32.303, 79.197, -107.864]], # blue + [[46.229, -51.7, 49.898]], # green + ]) + # RGB to HSV def test_rgb2hsv_conversion(self): rgb = img_as_float(self.img_rgb)[::16, ::16] @@ -57,8 +72,7 @@ class TestColorconv(TestCase): self.assertRaises(ValueError, rgb2hsv, self.img_grayscale) def test_rgb2hsv_error_one_element(self): - self.assertRaises(ValueError, rgb2hsv, self.img_rgb[0,0]) - + self.assertRaises(ValueError, rgb2hsv, self.img_rgb[0, 0]) # HSV to RGB def test_hsv2rgb_conversion(self): @@ -74,19 +88,18 @@ class TestColorconv(TestCase): self.assertRaises(ValueError, hsv2rgb, self.img_grayscale) def test_hsv2rgb_error_one_element(self): - self.assertRaises(ValueError, hsv2rgb, self.img_rgb[0,0]) - + self.assertRaises(ValueError, hsv2rgb, self.img_rgb[0, 0]) # RGB to XYZ def test_rgb2xyz_conversion(self): - gt = np.array([[[ 0.950456, 1. , 1.088754], - [ 0.538003, 0.787329, 1.06942 ], - [ 0.592876, 0.28484 , 0.969561], - [ 0.180423, 0.072169, 0.950227]], - [[ 0.770033, 0.927831, 0.138527], - [ 0.35758 , 0.71516 , 0.119193], - [ 0.412453, 0.212671, 0.019334], - [ 0. , 0. , 0. ]]]) + gt = np.array([[[0.950456, 1. , 1.088754], + [0.538003, 0.787329, 1.06942 ], + [0.592876, 0.28484 , 0.969561], + [0.180423, 0.072169, 0.950227]], + [[0.770033, 0.927831, 0.138527], + [0.35758 , 0.71516 , 0.119193], + [0.412453, 0.212671, 0.019334], + [0. , 0. , 0. ]]]) assert_almost_equal(rgb2xyz(self.colbars_array), gt) # stop repeating the "raises" checks for all other functions that are @@ -95,8 +108,7 @@ class TestColorconv(TestCase): self.assertRaises(ValueError, rgb2xyz, self.img_grayscale) def test_rgb2xyz_error_one_element(self): - self.assertRaises(ValueError, rgb2xyz, self.img_rgb[0,0]) - + self.assertRaises(ValueError, rgb2xyz, self.img_rgb[0, 0]) # XYZ to RGB def test_xyz2rgb_conversion(self): @@ -104,7 +116,6 @@ class TestColorconv(TestCase): assert_almost_equal(xyz2rgb(rgb2xyz(self.colbars_array)), self.colbars_array) - # RGB to RGB CIE def test_rgb2rgbcie_conversion(self): gt = np.array([[[ 0.1488856 , 0.18288098, 0.19277574], @@ -117,7 +128,6 @@ class TestColorconv(TestCase): [ 0. , 0. , 0. ]]]) assert_almost_equal(rgb2rgbcie(self.colbars_array), gt) - # RGB CIE to RGB def test_rgbcie2rgb_conversion(self): # only roundtrip test, we checked rgb2rgbcie above already @@ -151,6 +161,24 @@ class TestColorconv(TestCase): assert_equal(g.shape, (1, 1)) + def test_rgb2grey_on_grey(self): + rgb2grey(np.random.random((5, 5))) + + # test matrices for xyz2lab and lab2xyz generated using http://www.easyrgb.com/index.php?X=CALC + # Note: easyrgb website displays xyz*100 + def test_xyz2lab(self): + assert_array_almost_equal(xyz2lab(self.xyz_array), + self.lab_array, decimal=3) + + def test_lab2xyz(self): + assert_array_almost_equal(lab2xyz(self.lab_array), + self.xyz_array, decimal=3) + + def test_lab_rgb_roundtrip(self): + img_rgb = img_as_float(self.img_rgb) + assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb) + + def test_gray2rgb(): x = np.array([0, 0.5, 1]) assert_raises(ValueError, gray2rgb, x) @@ -170,4 +198,3 @@ def test_gray2rgb(): if __name__ == "__main__": run_module_suite() - diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index c4467678..5e51d353 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -11,6 +11,7 @@ import os as _os from ..io import imread from skimage import data_dir + def load(f): """Load an image file located in the data directory. @@ -26,13 +27,16 @@ def load(f): """ return imread(_os.path.join(data_dir, f)) + def camera(): - """Gray-level "camera" image, often used for segmentation - and denoising examples. + """Gray-level "camera" image. + + Often used for segmentation and denoising examples. """ return load("camera.png") + def lena(): """Colour "Lena" image. @@ -44,8 +48,9 @@ def lena(): """ return load("lena.png") + def text(): - """ Gray-level "text" image used for corner detection. + """Gray-level "text" image used for corner detection. Notes ----- @@ -56,7 +61,8 @@ def text(): """ - return load("text.png") + return load("text.png") + def checkerboard(): """Checkerboard image. @@ -68,6 +74,7 @@ def checkerboard(): """ return load("chessboard_GRAY.png") + def coins(): """Greek coins from Pompeii. @@ -88,6 +95,7 @@ def coins(): """ return load("coins.png") + def moon(): """Surface of the moon. @@ -97,6 +105,7 @@ def moon(): """ return load("moon.png") + def page(): """Scanned page. diff --git a/skimage/data/brick.png b/skimage/data/brick.png new file mode 100644 index 00000000..c69e71b2 Binary files /dev/null and b/skimage/data/brick.png differ diff --git a/skimage/data/bw_text_skeleton.npy b/skimage/data/bw_text_skeleton.npy index 9492cb64..2933c484 100644 Binary files a/skimage/data/bw_text_skeleton.npy and b/skimage/data/bw_text_skeleton.npy differ diff --git a/skimage/data/grass.png b/skimage/data/grass.png new file mode 100644 index 00000000..16062a35 Binary files /dev/null and b/skimage/data/grass.png differ diff --git a/skimage/data/rough-wall.png b/skimage/data/rough-wall.png new file mode 100644 index 00000000..8e2ced60 Binary files /dev/null and b/skimage/data/rough-wall.png differ diff --git a/skimage/data/tests/test_data.py b/skimage/data/tests/test_data.py index c239d880..b5f6bcaa 100644 --- a/skimage/data/tests/test_data.py +++ b/skimage/data/tests/test_data.py @@ -1,22 +1,39 @@ import skimage.data as data -from numpy.testing import assert_equal, assert_array_equal -import numpy as np +from numpy.testing import assert_equal + def test_lena(): """ Test that "Lena" image can be loaded. """ lena = data.lena() assert_equal(lena.shape, (512, 512, 3)) + def test_camera(): """ Test that "camera" image can be loaded. """ cameraman = data.camera() assert_equal(cameraman.ndim, 2) + def test_checkerboard(): - """ Test that checkerboard image can be loaded. """ - checkerboard = data.checkerboard() + """ Test that "checkerboard" image can be loaded. """ + data.checkerboard() + + +def test_text(): + """ Test that "text" image can be loaded. """ + data.text() + + +def test_moon(): + """ Test that "moon" image can be loaded. """ + data.moon() + + +def test_page(): + """ Test that "page" image can be loaded. """ + data.page() + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() - diff --git a/skimage/draw/__init__.py b/skimage/draw/__init__.py index c6df1f73..4eac9b63 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -1 +1,2 @@ -from .draw import * +from ._draw import line, polygon, ellipse, circle +bresenham = line diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 9dc1d307..ca0c502a 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -3,11 +3,7 @@ import math from libc.math cimport sqrt cimport numpy as np cimport cython - - -cdef extern from "../morphology/_pnpoly.h": - int pnpoly(int nr_verts, double *xp, double *yp, - double x, double y) +from skimage._shared.geometry cimport point_in_polygon @cython.boundscheck(False) @@ -69,6 +65,7 @@ def line(int y, int x, int y2, int x2): return rr, cc + @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) @@ -78,9 +75,9 @@ def polygon(y, x, shape=None): Parameters ---------- y : (N,) ndarray - y coordinates of vertices of polygon + Y-coordinates of vertices of polygon. x : (N,) ndarray - x coordinates of vertices of polygon + X-coordinates of vertices of polygon. shape : tuple, optional image shape which is used to determine maximum extents of output pixel coordinates. This is useful for polygons which exceed the image size. @@ -119,12 +116,13 @@ def polygon(y, x, shape=None): for r in range(minr, maxr+1): for c in range(minc, maxc+1): - if pnpoly(nr_verts, cptr, rptr, c, r): + if point_in_polygon(nr_verts, cptr, rptr, c, r): rr.append(r) cc.append(c) return np.array(rr), np.array(cc) + @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) @@ -135,9 +133,9 @@ def ellipse(double cy, double cx, double b, double a, shape=None): Parameters ---------- cy, cx : double - centre coordinate of ellipse + Centre coordinate of ellipse. b, a: double - minor and major semi-axes. (x/a)**2 + (y/b)**2 = 1 + Minor and major semi-axes. ``(x/a)**2 + (y/b)**2 = 1``. Returns ------- @@ -170,20 +168,21 @@ def ellipse(double cy, double cx, double b, double a, shape=None): return np.array(rr), np.array(cc) + def circle(double cy, double cx, double radius, shape=None): """Generate coordinates of pixels within circle. Parameters ---------- cy, cx : double - centre coordinate of circle + Centre coordinate of circle. radius: double - radius of circle + Radius of circle. Returns ------- rr, cc : ndarray of int - Pixel coordinates of ellipse. + Pixel coordinates of circle. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. """ diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py deleted file mode 100644 index 4b55b0de..00000000 --- a/skimage/draw/draw.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Methods to draw on arrays. - -""" - -from ._draw import line, polygon, ellipse, circle -bresenham = line diff --git a/skimage/draw/setup.py b/skimage/draw/setup.py index d296fee8..5b8e237d 100644 --- a/skimage/draw/setup.py +++ b/skimage/draw/setup.py @@ -5,6 +5,7 @@ from skimage._build import cython base_path = os.path.abspath(os.path.dirname(__file__)) + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs @@ -14,17 +15,17 @@ def configuration(parent_package='', top_path=None): cython(['_draw.pyx'], working_path=base_path) config.add_extension('_draw', sources=['_draw.c'], - include_dirs=[get_numpy_include_dirs()]) + include_dirs=[get_numpy_include_dirs(), '../shared']) return config if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image developers', - author = 'scikits-image developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Drawing', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='scikit-image developers', + author='scikit-image developers', + maintainer_email='scikit-image@googlegroups.com', + description='Drawing', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index dd1c81e2..9e6ca8e8 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -15,6 +15,7 @@ def test_line_horizontal(): assert_array_equal(img, img_) + def test_line_vertical(): img = np.zeros((10, 10)) @@ -26,6 +27,7 @@ def test_line_vertical(): assert_array_equal(img, img_) + def test_line_reverse(): img = np.zeros((10, 10)) @@ -37,6 +39,7 @@ def test_line_reverse(): assert_array_equal(img, img_) + def test_line_diag(): img = np.zeros((5, 5)) @@ -52,20 +55,21 @@ def test_polygon_rectangle(): img = np.zeros((10, 10), 'uint8') poly = np.array(((1, 1), (4, 1), (4, 4), (1, 4), (1, 1))) - rr, cc = polygon(poly[:,0], poly[:,1]) - img[rr,cc] = 1 + rr, cc = polygon(poly[:, 0], poly[:, 1]) + img[rr, cc] = 1 img_ = np.zeros((10, 10)) - img_[1:4,1:4] = 1 + img_[1:4, 1:4] = 1 assert_array_equal(img, img_) + def test_polygon_rectangle_angular(): img = np.zeros((10, 10), 'uint8') poly = np.array(((0, 3), (4, 7), (7, 4), (3, 0), (0, 3))) - rr, cc = polygon(poly[:,0], poly[:,1]) - img[rr,cc] = 1 + rr, cc = polygon(poly[:, 0], poly[:, 1]) + img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -82,12 +86,13 @@ def test_polygon_rectangle_angular(): assert_array_equal(img, img_) + def test_polygon_parallelogram(): img = np.zeros((10, 10), 'uint8') poly = np.array(((1, 1), (5, 1), (7, 6), (3, 6), (1, 1))) - rr, cc = polygon(poly[:,0], poly[:,1]) - img[rr,cc] = 1 + rr, cc = polygon(poly[:, 0], poly[:, 1]) + img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -104,23 +109,25 @@ def test_polygon_parallelogram(): assert_array_equal(img, img_) + def test_polygon_exceed(): img = np.zeros((10, 10), 'uint8') poly = np.array(((1, -1), (100, -1), (100, 100), (1, 100), (1, 1))) - rr, cc = polygon(poly[:,0], poly[:,1], img.shape) - img[rr,cc] = 1 + rr, cc = polygon(poly[:, 0], poly[:, 1], img.shape) + img[rr, cc] = 1 img_ = np.zeros((10, 10)) - img_[1:,:] = 1 + img_[1:, :] = 1 assert_array_equal(img, img_) + def test_circle(): img = np.zeros((15, 15), 'uint8') rr, cc = circle(7, 7, 6) - img[rr,cc] = 1 + img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -142,11 +149,12 @@ def test_circle(): assert_array_equal(img, img_) + def test_ellipse(): img = np.zeros((15, 15), 'uint8') rr, cc = ellipse(7, 7, 3, 7) - img[rr,cc] = 1 + img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index a0a576d6..bffe660a 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -1,6 +1,6 @@ import numpy as np -import skimage +from skimage import img_as_float from skimage.util.dtype import dtype_range @@ -101,7 +101,7 @@ def equalize(image, nbins=256): .. [2] http://en.wikipedia.org/wiki/Histogram_equalization """ - image = skimage.img_as_float(image) + image = img_as_float(image) cdf, bin_centers = cumulative_distribution(image, nbins) out = np.interp(image.flat, bin_centers, cdf) return out.reshape(image.shape) @@ -135,30 +135,36 @@ def rescale_intensity(image, in_range=None, out_range=None): Examples -------- By default, intensities are stretched to the limits allowed by the dtype: + >>> image = np.array([51, 102, 153], dtype=np.uint8) >>> rescale_intensity(image) array([ 0, 127, 255], dtype=uint8) It's easy to accidentally convert an image dtype from uint8 to float: + >>> 1.0 * image array([ 51., 102., 153.]) Use `rescale_intensity` to rescale to the proper range for float dtypes: + >>> image_float = 1.0 * image >>> rescale_intensity(image_float) array([ 0. , 0.5, 1. ]) To maintain the low contrast of the original, use the `in_range` parameter: + >>> rescale_intensity(image_float, in_range=(0, 255)) array([ 0.2, 0.4, 0.6]) If the min/max value of `in_range` is more/less than the min/max image intensity, then the intensity levels are clipped: + >>> rescale_intensity(image_float, in_range=(0, 102)) array([ 0.5, 1. , 1. ]) If you have an image with signed integers but want to rescale the image to just the positive range, use the `out_range` parameter: + >>> image = np.array([-10, 0, 10], dtype=np.int8) >>> rescale_intensity(image, out_range=(0, 127)) array([ 0, 63, 127], dtype=int8) @@ -183,4 +189,3 @@ def rescale_intensity(image, in_range=None, out_range=None): image = (image - imin) / float(imax - imin) return dtype(image * (omax - omin) + omin) - diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 55fae5ae..b6ed0b12 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -74,4 +74,3 @@ def test_rescale_out_range(): if __name__ == '__main__': from numpy import testing testing.run_module_suite() - diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 70c154b8..1597e9a8 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -1,5 +1,5 @@ -from .hog import hog -from .greycomatrix import greycomatrix, greycoprops +from ._hog import hog +from .texture import greycomatrix, greycoprops, local_binary_pattern from .peak import peak_local_max -from .harris import harris +from ._harris import harris from .template import match_template diff --git a/skimage/feature/greycomatrix.py b/skimage/feature/_greycomatrix.py similarity index 89% rename from skimage/feature/greycomatrix.py rename to skimage/feature/_greycomatrix.py index 5b4bdb85..45476d33 100644 --- a/skimage/feature/greycomatrix.py +++ b/skimage/feature/_greycomatrix.py @@ -4,9 +4,8 @@ properties to characterize image textures. """ import numpy as np -import skimage.util -from ._greycomatrix import _glcm_loop +from ._texture import _glcm_loop def greycomatrix(image, distances, angles, levels=256, symmetric=False, @@ -28,17 +27,17 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, levels : int, optional The input image should contain integers in [0, levels-1], where levels indicate the number of grey-levels counted - (typically 256 for an 8-bit image). The maximum value is - 256. + (typically 256 for an 8-bit image). The maximum value is + 256. symmetric : bool, optional - If True, the output matrix `P[:, :, d, theta]` is symmetric. This - is accomplished by ignoring the order of value pairs, so both - (i, j) and (j, i) are accumulated when (i, j) is encountered - for a given offset. The default is False. + If True, the output matrix `P[:, :, d, theta]` is symmetric. This + is accomplished by ignoring the order of value pairs, so both + (i, j) and (j, i) are accumulated when (i, j) is encountered + for a given offset. The default is False. normed : bool, optional - If True, normalize each matrix `P[:, :, d, theta]` by dividing + If True, normalize each matrix `P[:, :, d, theta]` by dividing by the total number of accumulated co-occurrences for the given - offset. The elements of the resulting matrix sum to 1. The + offset. The elements of the resulting matrix sum to 1. The default is False. Returns @@ -54,10 +53,10 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, ---------- .. [1] The GLCM Tutorial Home Page, http://www.fp.ucalgary.ca/mhallbey/tutorial.htm - .. [2] Pattern Recognition Engineering, Morton Nadler & Eric P. + .. [2] Pattern Recognition Engineering, Morton Nadler & Eric P. Smith .. [3] Wikipedia, http://en.wikipedia.org/wiki/Co-occurrence_matrix - + Examples -------- @@ -74,7 +73,7 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, [0, 2, 0, 0], [0, 0, 3, 1], [0, 0, 0, 1]], dtype=uint32) - >>> result[:, :, 0, 1] + >>> result[:, :, 0, 1] array([[3, 0, 2, 0], [0, 2, 2, 0], [0, 0, 1, 2], @@ -82,7 +81,7 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, """ - assert levels <= 256 + assert levels <= 256 image = np.ascontiguousarray(image) assert image.ndim == 2 assert image.min() >= 0 @@ -95,7 +94,7 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, P = np.zeros((levels, levels, len(distances), len(angles)), dtype=np.uint32, order='C') - + # count co-occurences _glcm_loop(image, distances, angles, levels, P) @@ -103,8 +102,7 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, if symmetric: Pt = np.transpose(P, (1, 0, 2, 3)) P = P + Pt - - + # normalize each GLMC if normed: P = P.astype(np.float64) @@ -117,25 +115,25 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, def greycoprops(P, prop='contrast'): """Calculate texture properties of a GLCM. - - Compute a feature of a grey level co-occurrence matrix to serve as + + Compute a feature of a grey level co-occurrence matrix to serve as a compact summary of the matrix. The properties are computed as follows: - 'contrast': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}(i-j)^2` - 'dissimilarity': :math:`\\sum_{i,j=0}^{levels-1}P_{i,j}|i-j|` - 'homogeneity': :math:`\\sum_{i,j=0}^{levels-1}\\frac{P_{i,j}}{1+(i-j)^2}` - - 'ASM': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}^2` + - 'ASM': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}^2` - 'energy': :math:`\\sqrt{ASM}` - 'correlation': - .. math:: \\sum_{i,j=0}^{levels-1} P_{i,j}\\left[\\frac{(i-\\mu_i) \\ + .. math:: \\sum_{i,j=0}^{levels-1} P_{i,j}\\left[\\frac{(i-\\mu_i) \\ (j-\\mu_j)}{\\sqrt{(\\sigma_i^2)(\\sigma_j^2)}}\\right] - + Parameters - ---------- + ---------- P : ndarray - Input array. `P` is the grey-level co-occurrence histogram + Input array. `P` is the grey-level co-occurrence histogram for which to compute the specified property. The value `P[i,j,d,theta]` is the number of times that grey-level j occurs at a distance d and at an angle theta from @@ -144,80 +142,80 @@ def greycoprops(P, prop='contrast'): prop : {'contrast', 'dissimilarity', 'homogeneity', 'energy', \ 'correlation', 'ASM'}, optional The property of the GLCM to compute. The default is 'contrast'. - + Returns ------- results : 2-D ndarray - 2-dimensional array. `results[d, a]` is the property 'prop' for + 2-dimensional array. `results[d, a]` is the property 'prop' for the d'th distance and the a'th angle. - + References ---------- .. [1] The GLCM Tutorial Home Page, - http://www.fp.ucalgary.ca/mhallbey/tutorial.htm - + http://www.fp.ucalgary.ca/mhallbey/tutorial.htm + Examples -------- Compute the contrast for GLCMs with distances [1, 2] and angles - [0 degrees, 90 degrees] - + [0 degrees, 90 degrees] + >>> image = np.array([[0, 0, 1, 1], ... [0, 0, 1, 1], ... [0, 2, 2, 2], ... [2, 2, 3, 3]], dtype=np.uint8) - >>> g = greycomatrix(image, [1, 2], [0, np.pi/2], levels=4, + >>> g = greycomatrix(image, [1, 2], [0, np.pi/2], levels=4, ... normed=True, symmetric=True) >>> contrast = greycoprops(g, 'contrast') >>> contrast array([[ 0.58333333, 1. ], [ 1.25 , 2.75 ]]) - + """ - + assert P.ndim == 4 (num_level, num_level2, num_dist, num_angle) = P.shape assert num_level == num_level2 assert num_dist > 0 assert num_angle > 0 - + # create weights for specified property I, J = np.ogrid[0:num_level, 0:num_level] if prop == 'contrast': - weights = (I - J) ** 2 + weights = (I - J)**2 elif prop == 'dissimilarity': weights = np.abs(I - J) elif prop == 'homogeneity': - weights = 1. / (1. + (I - J) ** 2) + weights = 1. / (1. + (I - J)**2) elif prop in ['ASM', 'energy', 'correlation']: pass else: raise ValueError('%s is an invalid property' % (prop)) - # compute property for each GLCM + # compute property for each GLCM if prop == 'energy': - asm = np.apply_over_axes(np.sum, (P ** 2), axes=(0, 1))[0, 0] + asm = np.apply_over_axes(np.sum, (P**2), axes=(0, 1))[0, 0] results = np.sqrt(asm) elif prop == 'ASM': - results = np.apply_over_axes(np.sum, (P ** 2), axes=(0, 1))[0, 0] + results = np.apply_over_axes(np.sum, (P**2), axes=(0, 1))[0, 0] elif prop == 'correlation': results = np.zeros((num_dist, num_angle), dtype=np.float64) I = np.array(range(num_level)).reshape((num_level, 1, 1, 1)) J = np.array(range(num_level)).reshape((1, num_level, 1, 1)) diff_i = I - np.apply_over_axes(np.sum, (I * P), axes=(0, 1))[0, 0] diff_j = J - np.apply_over_axes(np.sum, (J * P), axes=(0, 1))[0, 0] - - std_i = np.sqrt(np.apply_over_axes(np.sum, (P * (diff_i) ** 2), + + std_i = np.sqrt(np.apply_over_axes(np.sum, (P * (diff_i)**2), axes=(0, 1))[0, 0]) - std_j = np.sqrt(np.apply_over_axes(np.sum, (P * (diff_j) ** 2), + std_j = np.sqrt(np.apply_over_axes(np.sum, (P * (diff_j)**2), axes=(0, 1))[0, 0]) - cov = np.apply_over_axes(np.sum, (P * (diff_i * diff_j)), + cov = np.apply_over_axes(np.sum, (P * (diff_i * diff_j)), axes=(0, 1))[0, 0] - + # handle the special case of standard deviations near zero mask_0 = std_i < 1e-15 mask_0[std_j < 1e-15] = True results[mask_0] = 1 - + # handle the standard case mask_1 = mask_0 == False results[mask_1] = cov[mask_1] / (std_i[mask_1] * std_j[mask_1]) diff --git a/skimage/feature/_greycomatrix.pyx b/skimage/feature/_greycomatrix.pyx deleted file mode 100644 index b9ff7f23..00000000 --- a/skimage/feature/_greycomatrix.pyx +++ /dev/null @@ -1,67 +0,0 @@ -"""Cython implementation for computing a grey level co-occurance matrix -""" - -import numpy as np -cimport numpy as np -cimport cython - -cdef extern from "math.h": - double sin(double) - double cos(double) - -@cython.boundscheck(False) -def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, - negative_indices=False, mode='c'] image, - np.ndarray[dtype=np.float64_t, ndim=1, - negative_indices=False, mode='c'] distances, - np.ndarray[dtype=np.float64_t, ndim=1, - negative_indices=False, mode='c'] angles, - int levels, - np.ndarray[dtype=np.uint32_t, ndim=4, - negative_indices=False, mode='c'] out - ): - """Perform co-occurnace matrix accumulation - - Parameters - ---------- - image : ndarray - Input image, which is converted to the uint8 data type. - distances : ndarray - List of pixel pair distance offsets. - angles : ndarray - List of pixel pair angles in radians. - levels : int - The input image should contain integers in [0, levels-1], - where levels indicate the number of grey-levels counted - (typically 256 for an 8-bit image) - out : ndarray - On input a 4D array of zeros, and on output it contains - the results of the GLCM computation. - - """ - cdef: - np.int32_t a_inx, d_idx - np.int32_t r, c, rows, cols, row, col - np.int32_t i, j - - rows = image.shape[0] - cols = image.shape[1] - - for a_idx, angle in enumerate(angles): - for d_idx, distance in enumerate(distances): - for r in range(rows): - for c in range(cols): - i = image[r, c] - - # compute the location of the offset pixel - row = r + (sin(angle) * distance + 0.5) - col = c + (cos(angle) * distance + 0.5); - - # make sure the offset is within bounds - if row >= 0 and row < rows and \ - col >= 0 and col < cols: - j = image[row, col] - - if i >= 0 and i < levels and \ - j >= 0 and j < levels: - out[i, j, d_idx, a_idx] += 1 diff --git a/skimage/feature/harris.py b/skimage/feature/_harris.py similarity index 94% rename from skimage/feature/harris.py rename to skimage/feature/_harris.py index 6d0da9c0..ae30a29e 100644 --- a/skimage/feature/harris.py +++ b/skimage/feature/_harris.py @@ -42,7 +42,7 @@ def _compute_harris_response(image, eps=1e-6, gaussian_deviation=1): Wyy = ndimage.gaussian_filter(imy * imy, 1.5, mode='constant') # determinant and trace - Wdet = Wxx * Wyy - Wxy ** 2 + Wdet = Wxx * Wyy - Wxy**2 Wtr = Wxx + Wyy # Alternate formula for Harris response. # Alison Noble, "Descriptions of Image Surfaces", PhD thesis (1989) @@ -76,7 +76,7 @@ def harris(image, min_distance=10, threshold=0.1, eps=1e-6, ------- coordinates : (N, 2) array (row, column) coordinates of interest points. - + Examples ------- >>> square = np.zeros([10,10]) @@ -93,18 +93,17 @@ def harris(image, min_distance=10, threshold=0.1, eps=1e-6, [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]) >>> harris(square, min_distance=1) - + Corners of the square - + array([[3, 3], [3, 6], [6, 3], - [6, 6]]) + [6, 6]]) """ - - harrisim = _compute_harris_response(image, eps=eps, - gaussian_deviation=gaussian_deviation) - coordinates = peak.peak_local_max(harrisim, min_distance=min_distance, - threshold=threshold) - return coordinates + harrisim = _compute_harris_response(image, eps=eps, + gaussian_deviation=gaussian_deviation) + coordinates = peak.peak_local_max(harrisim, min_distance=min_distance, + threshold_rel=threshold) + return coordinates diff --git a/skimage/feature/hog.py b/skimage/feature/_hog.py similarity index 88% rename from skimage/feature/hog.py rename to skimage/feature/_hog.py index fc5c19da..ce7db462 100644 --- a/skimage/feature/hog.py +++ b/skimage/feature/_hog.py @@ -2,6 +2,7 @@ import numpy as np from scipy import sqrt, pi, arctan2, cos, sin from scipy.ndimage import uniform_filter + def hog(image, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(3, 3), visualise=False, normalise=False): """Extract Histogram of Oriented Gradients (HOG) for a given image. @@ -58,7 +59,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), shadowing and illumination variations. """ - if image.ndim > 3: + if image.ndim > 2: raise ValueError("Currently only supports grey-level images") if normalise: @@ -74,6 +75,11 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), e.g. bar like structures in bicycles and limbs in humans. """ + if image.dtype.kind == 'u': + # convert uint image to float + # to avoid problems with subtracting unsigned numbers in np.diff() + image = image.astype('float') + gx = np.zeros(image.shape) gy = np.zeros(image.shape) gx[:, :-1] = np.diff(image, n=1, axis=1) @@ -94,8 +100,8 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), cell are used to vote into the orientation histogram. """ - magnitude = sqrt(gx ** 2 + gy ** 2) - orientation = arctan2(gy, (gx + 1e-15)) * (180 / pi) + 90 + magnitude = sqrt(gx**2 + gy**2) + orientation = arctan2(gy, gx) * (180 / pi) % 180 sy, sx = image.shape cx, cy = pixels_per_cell @@ -106,40 +112,38 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # compute orientations integral images orientation_histogram = np.zeros((n_cellsy, n_cellsx, orientations)) + subsample = np.index_exp[cy / 2:cy * n_cellsy:cy, cx / 2:cx * n_cellsx:cx] for i in range(orientations): #create new integral image for this orientation # isolate orientations in this range temp_ori = np.where(orientation < 180 / orientations * (i + 1), - orientation, 0) + orientation, -1) temp_ori = np.where(orientation >= 180 / orientations * i, - temp_ori, 0) + temp_ori, -1) # select magnitudes for those orientations - cond2 = temp_ori > 0 + cond2 = temp_ori > -1 temp_mag = np.where(cond2, magnitude, 0) - orientation_histogram[:,:,i] = uniform_filter(temp_mag, size=(cy, cx))[cy/2::cy, cx/2::cx] - + temp_filt = uniform_filter(temp_mag, size=(cy, cx)) + orientation_histogram[:, :, i] = temp_filt[subsample] # now for each cell, compute the histogram - #orientation_histogram = np.zeros((n_cellsx, n_cellsy, orientations)) - - radius = min(cx, cy) // 2 - 1 hog_image = None - if visualise: - hog_image = np.zeros((sy, sx), dtype=float) if visualise: from skimage import draw - + + radius = min(cx, cy) // 2 - 1 + hog_image = np.zeros((sy, sx), dtype=float) for x in range(n_cellsx): for y in range(n_cellsy): for o in range(orientations): centre = tuple([y * cy + cy // 2, x * cx + cx // 2]) dx = radius * cos(float(o) / orientations * np.pi) dy = radius * sin(float(o) / orientations * np.pi) - rr, cc = draw.bresenham(centre[0] - dx, centre[1] - dy, - centre[0] + dx, centre[1] + dy) + rr, cc = draw.bresenham(centre[0] - dy, centre[1] - dx, + centre[0] + dy, centre[1] + dx) hog_image[rr, cc] += orientation_histogram[y, x, o] """ @@ -166,7 +170,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), for y in range(n_blocksy): block = orientation_histogram[y:y + by, x:x + bx, :] eps = 1e-5 - normalised_blocks[y, x, :] = block / sqrt(block.sum() ** 2 + eps) + normalised_blocks[y, x, :] = block / sqrt(block.sum()**2 + eps) """ The final step collects the HOG descriptors from all blocks of a dense diff --git a/skimage/feature/_template.pyx b/skimage/feature/_template.pyx index b83761a8..58d48524 100644 --- a/skimage/feature/_template.pyx +++ b/skimage/feature/_template.pyx @@ -35,51 +35,8 @@ cimport numpy as np import numpy as np from scipy.signal import fftconvolve from skimage.transform import integral - - -cdef extern from "math.h": - float sqrt(float x) - float fabs(float x) - - -@cython.boundscheck(False) -cdef float integrate(np.ndarray[float, ndim=2, mode="c"] sat, - int r0, int c0, int r1, int c1): - """ - Using a summed area table / integral image, calculate the sum - over a given window. - - This function is the same as the `integrate` function in - `skimage.transform.integrate`, but this Cython version significantly - speeds up the code. - - Parameters - ---------- - sat : ndarray of float - Summed area table / integral image. - r0, c0 : int - Top-left corner of block to be summed. - r1, c1 : int - Bottom-right corner of block to be summed. - - Returns - ------- - S : int - Sum over the given window. - """ - cdef float S = 0 - - S += sat[r1, c1] - - if (r0 - 1 >= 0) and (c0 - 1 >= 0): - S += sat[r0 - 1, c0 - 1] - - if (r0 - 1 >= 0): - S -= sat[r0 - 1, c1] - - if (c0 - 1 >= 0): - S -= sat[r1, c0 - 1] - return S +from libc.math cimport sqrt, fabs +from skimage._shared.transform cimport integrate @cython.boundscheck(False) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx new file mode 100644 index 00000000..70a446bb --- /dev/null +++ b/skimage/feature/_texture.pyx @@ -0,0 +1,182 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +cimport numpy as np +from libc.math cimport sin, cos, abs +from skimage._shared.interpolation cimport bilinear_interpolation + + +def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, + negative_indices=False, mode='c'] image, + np.ndarray[dtype=np.float64_t, ndim=1, + negative_indices=False, mode='c'] distances, + np.ndarray[dtype=np.float64_t, ndim=1, + negative_indices=False, mode='c'] angles, + int levels, + np.ndarray[dtype=np.uint32_t, ndim=4, + negative_indices=False, mode='c'] out + ): + """Perform co-occurrence matrix accumulation. + + Parameters + ---------- + image : ndarray + Input image, which is converted to the uint8 data type. + distances : ndarray + List of pixel pair distance offsets. + angles : ndarray + List of pixel pair angles in radians. + levels : int + The input image should contain integers in [0, levels-1], + where levels indicate the number of grey-levels counted + (typically 256 for an 8-bit image) + out : ndarray + On input a 4D array of zeros, and on output it contains + the results of the GLCM computation. + + """ + cdef: + np.int32_t a_inx, d_idx + np.int32_t r, c, rows, cols, row, col + np.int32_t i, j + + rows = image.shape[0] + cols = image.shape[1] + + for a_idx, angle in enumerate(angles): + for d_idx, distance in enumerate(distances): + for r in range(rows): + for c in range(cols): + i = image[r, c] + + # compute the location of the offset pixel + row = r + (sin(angle) * distance + 0.5) + col = c + (cos(angle) * distance + 0.5); + + # make sure the offset is within bounds + if row >= 0 and row < rows and \ + col >= 0 and col < cols: + j = image[row, col] + + if i >= 0 and i < levels and \ + j >= 0 and j < levels: + out[i, j, d_idx, a_idx] += 1 + + +cdef inline int _bit_rotate_right(int value, int length): + """Cyclic bit shift to the right. + + Parameters + ---------- + value : int + integer value to shift + length : int + number of bits of integer + + """ + return (value >> 1) | ((value & 1) << (length - 1)) + + +def _local_binary_pattern(np.ndarray[double, ndim=2] image, + int P, float R, char method='D'): + """Gray scale and rotation invariant LBP (Local Binary Patterns). + + LBP is an invariant descriptor that can be used for texture classification. + + Parameters + ---------- + image : (N, M) double array + Graylevel image. + P : int + Number of circularly symmetric neighbour set points (quantization of the + angular space). + R : float + Radius of circle (spatial resolution of the operator). + method : {'D', 'R', 'U', 'V'} + Method to determine the pattern. + + * 'D': 'default' + * 'R': 'ror' + * 'U': 'uniform' + * 'V': 'var' + + Returns + ------- + output : (N, M) array + LBP image. + """ + + # texture weights + cdef np.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype=np.int32) + # local position of texture elements + rp = - R * np.sin(2 * np.pi * np.arange(P, dtype=np.double) / P) + cp = R * np.cos(2 * np.pi * np.arange(P, dtype=np.double) / P) + cdef np.ndarray[double, ndim=2] coords = np.round(np.vstack([rp, cp]).T, 5) + + # pre allocate arrays for computation + cdef np.ndarray[double, ndim=1] texture = np.zeros(P, np.double) + cdef np.ndarray[char, ndim=1] signed_texture = np.zeros(P, np.int8) + cdef np.ndarray[int, ndim=1] rotation_chain = np.zeros(P, np.int32) + + output_shape = (image.shape[0], image.shape[1]) + cdef np.ndarray[double, ndim=2] output = np.zeros(output_shape, np.double) + + cdef int rows = image.shape[0] + cdef int cols = image.shape[1] + + cdef double lbp + cdef int r, c, changes, i + for r in range(image.shape[0]): + for c in range(image.shape[1]): + for i in range(P): + texture[i] = bilinear_interpolation(image.data, + rows, cols, r + coords[i, 0], c + coords[i, 1], 'C', 0) + # signed / thresholded texture + for i in range(P): + if texture[i] - image[r, c] >= 0: + signed_texture[i] = 1 + else: + signed_texture[i] = 0 + + lbp = 0 + + # if method == 'uniform' or method == 'var': + if method == 'U' or method == 'V': + # determine number of 0 - 1 changes + changes = 0 + for i in range(P - 1): + changes += abs(signed_texture[i] - signed_texture[i + 1]) + + if changes <= 2: + for i in range(P): + lbp += signed_texture[i] + else: + lbp = P + 1 + + if method == 'V': + var = np.var(texture) + if var != 0: + lbp /= var + else: + lbp = np.nan + else: + # method == 'default' + for i in range(P): + lbp += signed_texture[i] * weights[i] + + # method == 'ror' + if method == 'R': + # shift LBP P times to the right and get minimum value + rotation_chain[0] = lbp + for i in range(1, P): + rotation_chain[i] = \ + _bit_rotate_right(rotation_chain[i - 1], P) + lbp = rotation_chain[0] + for i in range(1, P): + lbp = min(lbp, rotation_chain[i]) + + output[r, c] = lbp + + return output diff --git a/skimage/feature/peak.py b/skimage/feature/peak.py index 5d3f375b..4765974e 100644 --- a/skimage/feature/peak.py +++ b/skimage/feature/peak.py @@ -15,21 +15,16 @@ def peak_local_max(image, min_distance=10, threshold='deprecated', Parameters ---------- - image: ndarray of floats + image : ndarray of floats Input image. - - min_distance: int + min_distance : int Minimum number of pixels separating peaks and image boundary. - threshold : float Deprecated. See `threshold_rel`. - - threshold_abs: float + threshold_abs : float Minimum intensity of peaks. - - threshold_rel: float + threshold_rel : float Minimum intensity of peaks calculated as `max(image) * threshold_rel`. - num_peaks : int Maximum number of peaks. When the number of peaks exceeds `num_peaks`, return `num_peaks` coordinates based on peak intensity. @@ -38,15 +33,15 @@ def peak_local_max(image, min_distance=10, threshold='deprecated', ------- coordinates : (N, 2) array (row, column) coordinates of peaks. - + Notes ----- The peak local maximum function returns the coordinates of local peaks (maxima) in a image. A maximum filter is used for finding local maxima. This operation - dilates the original image. After comparison between dilated and original image, + dilates the original image. After comparison between dilated and original image, peak_local_max function returns the coordinates of peaks where dilated image = original. - + Examples -------- >>> im = np.zeros((7, 7)) @@ -60,14 +55,14 @@ def peak_local_max(image, min_distance=10, threshold='deprecated', [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ]]) - + >>> peak_local_max(im, min_distance=1) array([[3, 2], [3, 4]]) - + >>> peak_local_max(im, min_distance=2) array([[3, 2]]) - + """ if np.all(image == image.flat[0]): return [] @@ -95,10 +90,9 @@ def peak_local_max(image, min_distance=10, threshold='deprecated', # get coordinates of peaks coordinates = np.transpose(image_t.nonzero()) - if len(coordinates) > num_peaks: - intensities = image[tuple(coordinates.T)] + if coordinates.shape[0] > num_peaks: + intensities = image[coordinates[:, 0], coordinates[:, 1]] idx_maxsort = np.argsort(intensities)[::-1] - coordinates = coordinates[idx_maxsort][:2] + coordinates = coordinates[idx_maxsort][:num_peaks] return coordinates - diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index 13d4fae5..6f820163 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -5,29 +5,30 @@ from skimage._build import cython base_path = os.path.abspath(os.path.dirname(__file__)) + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs config = Configuration('feature', parent_package, top_path) config.add_data_dir('tests') - cython(['_greycomatrix.pyx'], working_path=base_path) + cython(['_texture.pyx'], working_path=base_path) cython(['_template.pyx'], working_path=base_path) - config.add_extension('_greycomatrix', sources=['_greycomatrix.c'], - include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_texture', sources=['_texture.c'], + include_dirs=[get_numpy_include_dirs(), '../_shared']) config.add_extension('_template', sources=['_template.c'], - include_dirs=[get_numpy_include_dirs()]) + include_dirs=[get_numpy_include_dirs(), '../_shared']) return config if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - author = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Features', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='scikit-image Developers', + author='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Features', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) diff --git a/skimage/feature/template.py b/skimage/feature/template.py index 4f3449bd..51ca90b4 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -3,8 +3,6 @@ import numpy as np from . import _template -from skimage.util.dtype import convert - def match_template(image, template, pad_input=False): """Match a template to an image using normalized correlation. @@ -67,8 +65,8 @@ def match_template(image, template, pad_input=False): """ if np.any(np.less(image.shape, template.shape)): raise ValueError("Image must be larger than template.") - image = convert(image, np.float32) - template = convert(template, np.float32) + image = np.ascontiguousarray(image, dtype=np.float32) + template = np.ascontiguousarray(template, dtype=np.float32) if pad_input: pad_size = tuple(np.array(image.shape) + np.array(template.shape) - 1) @@ -77,8 +75,7 @@ def match_template(image, template, pad_input=False): i0, j0 = template.shape i0 /= 2 j0 /= 2 - pad_image[i0:i0+h, j0:j0+w] = image + pad_image[i0:i0 + h, j0:j0 + w] = image image = pad_image result = _template.match_template(image, template) return result - diff --git a/skimage/feature/tests/test_harris.py b/skimage/feature/tests/test_harris.py index 758bfa5e..43bf28a3 100644 --- a/skimage/feature/tests/test_harris.py +++ b/skimage/feature/tests/test_harris.py @@ -13,6 +13,7 @@ def test_square_image(): assert results.any() assert len(results) == 1 + def test_noisy_square_image(): im = np.zeros((50, 50)).astype(float) im[:25, :25] = 1. @@ -21,6 +22,7 @@ def test_noisy_square_image(): assert results.any() assert len(results) == 1 + def test_squared_dot(): im = np.zeros((50, 50)) im[4:8, 4:8] = 1 @@ -28,6 +30,7 @@ def test_squared_dot(): results = harris(im, min_distance=3) assert (results == np.array([[6, 6]])).all() + def test_rotated_lena(): """ The harris filter should yield the same results with an image and it's @@ -44,4 +47,3 @@ def test_rotated_lena(): if __name__ == '__main__': from numpy import testing testing.run_module_suite() - diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index e4b8fda8..6f2d4cdf 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -1,17 +1,134 @@ import numpy as np -import scipy - -from skimage.feature import hog +from scipy import ndimage +from skimage import data +from skimage import feature +from skimage import img_as_float +from skimage import draw +from numpy.testing import * def test_histogram_of_oriented_gradients(): - # Replace with skimage.data.lena() after merge - img = scipy.misc.lena()[:256,:].astype(np.int8) - - fd = hog(img, orientations=9, pixels_per_cell=(8, 8), - cells_per_block=(1, 1)) + img = img_as_float(data.lena()[:256, :].mean(axis=2)) + + fd = feature.hog(img, orientations=9, pixels_per_cell=(8, 8), + cells_per_block=(1, 1)) + + assert len(fd) == 9 * (256 // 8) * (512 // 8) + +def test_hog_image_size_cell_size_mismatch(): + image = data.camera()[:150, :200] + fd = feature.hog(image, orientations=9, pixels_per_cell=(8, 8), + cells_per_block=(1, 1)) + assert len(fd) == 9 * (150 // 8) * (200 // 8) + +def test_hog_color_image_unsupported_error(): + image = np.zeros((20, 20, 3)) + assert_raises(ValueError, feature.hog, image) + +def test_hog_basic_orientations_and_data_types(): + # scenario: + # 1) create image (with float values) where upper half is filled by zeros, bottom half by 100 + # 2) create unsigned integer version of this image + # 3) calculate feature.hog() for both images, both with 'normalise' option enabled and disabled + # 4) verify that all results are equal where expected + # 5) verify that computed feature vector is as expected + # 6) repeat the scenario for 90, 180 and 270 degrees rotated images + + # size of testing image + width = height = 35 + + image0 = np.zeros((height, width), dtype='float') + image0[height / 2:] = 100 + + for rot in range(4): + # rotate by 0, 90, 180 and 270 degrees + image_float = np.rot90(image0, rot) + + # create uint8 image from image_float + image_uint8 = image_float.astype('uint8') + + (hog_float, hog_img_float) = feature.hog(image_float, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=False) + (hog_uint8, hog_img_uint8) = feature.hog(image_uint8, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=False) + (hog_float_norm, hog_img_float_norm) = feature.hog(image_float, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=True) + (hog_uint8_norm, hog_img_uint8_norm) = feature.hog(image_uint8, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=True) + + # set to True to enable manual debugging with graphical output, + # must be False for automatic testing + if False: + import matplotlib.pyplot as plt + plt.figure() + plt.subplot(2, 3, 1); plt.imshow(image_float); plt.colorbar(); plt.title('image') + plt.subplot(2, 3, 2); plt.imshow(hog_img_float); plt.colorbar(); plt.title('HOG result visualisation (float img)') + plt.subplot(2, 3, 5); plt.imshow(hog_img_uint8); plt.colorbar(); plt.title('HOG result visualisation (uint8 img)') + plt.subplot(2, 3, 3); plt.imshow(hog_img_float_norm); plt.colorbar(); plt.title('HOG result (normalise) visualisation (float img)') + plt.subplot(2, 3, 6); plt.imshow(hog_img_uint8_norm); plt.colorbar(); plt.title('HOG result (normalise) visualisation (uint8 img)') + plt.show() + + # results (features and visualisation) for float and uint8 images must be almost equal + assert_almost_equal(hog_float, hog_uint8) + assert_almost_equal(hog_img_float, hog_img_uint8) + + # resulting features should be almost equal when 'normalise' is enabled or disabled (for current simple testing image) + assert_almost_equal(hog_float, hog_float_norm, decimal=4) + assert_almost_equal(hog_float, hog_uint8_norm, decimal=4) + + # reshape resulting feature vector to matrix with 4 columns (each corresponding to one of 4 directions), + # only one direction should contain nonzero values (this is manually determined for testing image) + actual = np.max(hog_float.reshape(-1, 4), axis=0) + + if rot in [0, 2]: + # image is rotated by 0 and 180 degrees + desired = [0, 0, 1, 0] + elif rot in [1, 3]: + # image is rotated by 90 and 270 degrees + desired = [1, 0, 0, 0] + else: + raise Exception('Result is not determined for this rotation.') + + assert_almost_equal(actual, desired, decimal=2) + +def test_hog_orientations_circle(): + # scenario: + # 1) create image with blurred circle in the middle + # 2) calculate feature.hog() + # 3) verify that the resulting feature vector contains uniformly distributed values for all orientations, + # i.e. no orientation is lost or emphasized + # 4) repeat the scenario for other 'orientations' option + + # size of testing image + width = height = 100 + + image = np.zeros((height, width)) + rr, cc = draw.circle(height/2, width/2, width/3) + image[rr, cc] = 100 + image = ndimage.gaussian_filter(image, 2) + + for orientations in range(2, 15): + (hog, hog_img) = feature.hog(image, orientations=orientations, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=False) + + # set to True to enable manual debugging with graphical output, + # must be False for automatic testing + if False: + import matplotlib.pyplot as plt + plt.figure() + plt.subplot(1, 2, 1); plt.imshow(image); plt.colorbar(); plt.title('image_float') + plt.subplot(1, 2, 2); plt.imshow(hog_img); plt.colorbar(); plt.title('HOG result visualisation, orientations=%d' % (orientations)) + plt.show() + + # reshape resulting feature vector to matrix with N columns (each column corresponds to one direction), + hog_matrix = hog.reshape(-1, orientations) + + # compute mean values in the resulting feature vector for each direction, + # these values should be almost equal to the global mean value (since the image contains a circle), + # i.e. all directions have same contribution to the result + actual = np.mean(hog_matrix, axis=0) + desired = np.mean(hog_matrix) + assert_almost_equal(actual, desired, decimal=1) - assert len(fd) == 9 * (256//8) * (512//8) - if __name__ == '__main__': from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/feature/tests/test_peak.py b/skimage/feature/tests/test_peak.py index 91d38d99..13457781 100644 --- a/skimage/feature/tests/test_peak.py +++ b/skimage/feature/tests/test_peak.py @@ -51,18 +51,25 @@ def test_flat_peak(): def test_num_peaks(): - image = np.zeros((3, 7), dtype=np.uint8) + image = np.zeros((7, 7), dtype=np.uint8) image[1, 1] = 10 image[1, 3] = 11 image[1, 5] = 12 - assert len(peak.peak_local_max(image, min_distance=1)) == 3 + image[3, 5] = 8 + image[5, 3] = 7 + assert len(peak.peak_local_max(image, min_distance=1)) == 5 peaks_limited = peak.peak_local_max(image, min_distance=1, num_peaks=2) assert len(peaks_limited) == 2 assert (1, 3) in peaks_limited assert (1, 5) in peaks_limited + peaks_limited = peak.peak_local_max(image, min_distance=1, num_peaks=4) + assert len(peaks_limited) == 4 + assert (1, 3) in peaks_limited + assert (1, 5) in peaks_limited + assert (1, 1) in peaks_limited + assert (3, 5) in peaks_limited if __name__ == '__main__': from numpy import testing testing.run_module_suite() - diff --git a/skimage/feature/tests/test_template.py b/skimage/feature/tests/test_template.py index 7097a70a..1b9ff213 100644 --- a/skimage/feature/tests/test_template.py +++ b/skimage/feature/tests/test_template.py @@ -50,8 +50,8 @@ def test_normalization(): image[ineg:ineg + n, jneg:jneg + n] = 0 # white square with a black border - template = np.zeros((n+2, n+2)) - template[1:1+n, 1:1+n] = 1 + template = np.zeros((n + 2, n + 2)) + template[1:1 + n, 1:1 + n] = 1 result = match_template(image, template) @@ -121,4 +121,3 @@ def test_pad_input(): if __name__ == "__main__": from numpy import testing testing.run_module_suite() - diff --git a/skimage/feature/tests/test_glcm.py b/skimage/feature/tests/test_texture.py similarity index 61% rename from skimage/feature/tests/test_glcm.py rename to skimage/feature/tests/test_texture.py index 3f321e55..d48a14f7 100644 --- a/skimage/feature/tests/test_glcm.py +++ b/skimage/feature/tests/test_texture.py @@ -1,14 +1,15 @@ import numpy as np -from skimage.feature import greycomatrix, greycoprops +from skimage.feature import greycomatrix, greycoprops, local_binary_pattern class TestGLCM(): + def setup(self): self.image = np.array([[0, 0, 1, 1], [0, 0, 1, 1], [0, 2, 2, 2], - [2, 2, 3, 3]], dtype=np.uint8) - + [2, 2, 3, 3]], dtype=np.uint8) + def test_output_angles(self): result = greycomatrix(self.image, [1], [0, np.pi / 2], 4) assert result.shape == (4, 4, 1, 2) @@ -21,23 +22,23 @@ class TestGLCM(): [0, 2, 2, 0], [0, 0, 1, 2], [0, 0, 0, 0]], dtype=np.uint32) - np.testing.assert_array_equal(result[:, :, 0, 1], expected2) - - def test_output_symmetric_1(self): - result = greycomatrix(self.image, [1], [np.pi / 2], 4, + np.testing.assert_array_equal(result[:, :, 0, 1], expected2) + + def test_output_symmetric_1(self): + result = greycomatrix(self.image, [1], [np.pi / 2], 4, symmetric=True) assert result.shape == (4, 4, 1, 1) expected = np.array([[6, 0, 2, 0], [0, 4, 2, 0], [2, 2, 2, 2], [0, 0, 2, 0]], dtype=np.uint32) - np.testing.assert_array_equal(result[:, :, 0, 0], expected) + np.testing.assert_array_equal(result[:, :, 0, 0], expected) def test_output_distance(self): im = np.array([[0, 0, 0, 0], [1, 0, 0, 1], [2, 0, 0, 2], - [3, 0, 0, 3]], dtype=np.uint8) + [3, 0, 0, 3]], dtype=np.uint8) result = greycomatrix(im, [3], [0], 4, symmetric=False) expected = np.array([[1, 0, 0, 0], [0, 1, 0, 0], @@ -52,7 +53,7 @@ class TestGLCM(): [3]], dtype=np.uint8) result = greycomatrix(im, [1, 2], [0, np.pi / 2], 4) assert result.shape == (4, 4, 2, 2) - + z = np.zeros((4, 4), dtype=np.uint32) e1 = np.array([[0, 1, 0, 0], [0, 0, 1, 0], @@ -62,7 +63,7 @@ class TestGLCM(): [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], dtype=np.uint32) - + np.testing.assert_array_equal(result[:, :, 0, 0], z) np.testing.assert_array_equal(result[:, :, 1, 0], z) np.testing.assert_array_equal(result[:, :, 0, 1], e1) @@ -70,39 +71,39 @@ class TestGLCM(): def test_output_empty(self): result = greycomatrix(self.image, [10], [0], 4) - np.testing.assert_array_equal(result[:, :, 0, 0], - np.zeros((4, 4), dtype=np.uint32)) + np.testing.assert_array_equal(result[:, :, 0, 0], + np.zeros((4, 4), dtype=np.uint32)) result = greycomatrix(self.image, [10], [0], 4, normed=True) - np.testing.assert_array_equal(result[:, :, 0, 0], - np.zeros((4, 4), dtype=np.uint32)) + np.testing.assert_array_equal(result[:, :, 0, 0], + np.zeros((4, 4), dtype=np.uint32)) - def test_normed_symmetric(self): - result = greycomatrix(self.image, [1, 2, 3], - [0, np.pi / 2, np.pi], 4, + def test_normed_symmetric(self): + result = greycomatrix(self.image, [1, 2, 3], + [0, np.pi / 2, np.pi], 4, normed=True, symmetric=True) for d in range(result.shape[2]): for a in range(result.shape[3]): - np.testing.assert_almost_equal(result[:, :, d, a].sum(), + np.testing.assert_almost_equal(result[:, :, d, a].sum(), 1.0) - np.testing.assert_array_equal(result[:, :, d, a], + np.testing.assert_array_equal(result[:, :, d, a], result[:, :, d, a].transpose()) def test_contrast(self): - result = greycomatrix(self.image, [1, 2], [0], 4, + result = greycomatrix(self.image, [1, 2], [0], 4, normed=True, symmetric=True) result = np.round(result, 3) contrast = greycoprops(result, 'contrast') np.testing.assert_almost_equal(contrast[0, 0], 0.586) - + def test_dissimilarity(self): - result = greycomatrix(self.image, [1], [0, np.pi / 2], 4, + result = greycomatrix(self.image, [1], [0, np.pi / 2], 4, normed=True, symmetric=True) result = np.round(result, 3) dissimilarity = greycoprops(result, 'dissimilarity') np.testing.assert_almost_equal(dissimilarity[0, 0], 0.418) def test_dissimilarity_2(self): - result = greycomatrix(self.image, [1, 3], [np.pi/2], 4, + result = greycomatrix(self.image, [1, 3], [np.pi / 2], 4, normed=True, symmetric=True) result = np.round(result, 3) dissimilarity = greycoprops(result, 'dissimilarity')[0, 0] @@ -110,23 +111,23 @@ class TestGLCM(): def test_invalid_property(self): result = greycomatrix(self.image, [1], [0], 4) - np.testing.assert_raises(ValueError, greycoprops, + np.testing.assert_raises(ValueError, greycoprops, result, 'ABC') - + def test_homogeneity(self): - result = greycomatrix(self.image, [1], [0, 6], 4, normed=True, + result = greycomatrix(self.image, [1], [0, 6], 4, normed=True, symmetric=True) homogeneity = greycoprops(result, 'homogeneity')[0, 0] np.testing.assert_almost_equal(homogeneity, 0.80833333) def test_energy(self): - result = greycomatrix(self.image, [1], [0, 4], 4, normed=True, + result = greycomatrix(self.image, [1], [0, 4], 4, normed=True, symmetric=True) energy = greycoprops(result, 'energy')[0, 0] np.testing.assert_almost_equal(energy, 0.38188131) - + def test_correlation(self): - result = greycomatrix(self.image, [1, 2], [0], 4, normed=True, + result = greycomatrix(self.image, [1, 2], [0], 4, normed=True, symmetric=True) energy = greycoprops(result, 'correlation') np.testing.assert_almost_equal(energy[0, 0], 0.71953255) @@ -134,11 +135,69 @@ class TestGLCM(): def test_uniform_properties(self): im = np.ones((4, 4), dtype=np.uint8) - result = greycomatrix(im, [1, 2, 8], [0, np.pi / 2], 4, normed=True, + result = greycomatrix(im, [1, 2, 8], [0, np.pi / 2], 4, normed=True, symmetric=True) - for prop in ['contrast', 'dissimilarity', 'homogeneity', + for prop in ['contrast', 'dissimilarity', 'homogeneity', 'energy', 'correlation', 'ASM']: greycoprops(result, prop) + +class TestLBP(): + + def setup(self): + self.image = np.array([[255, 6, 255, 0, 141, 0], + [ 48, 250, 204, 166, 223, 63], + [ 8, 0, 159, 50, 255, 30], + [167, 255, 63, 40, 128, 255], + [ 0, 255, 30, 34, 255, 24], + [146, 241, 255, 0, 189, 126]], dtype='double') + + def test_default(self): + lbp = local_binary_pattern(self.image, 8, 1, 'default') + ref = np.array([[ 0, 251, 0, 255, 96, 255], + [143, 0, 20, 153, 64, 56], + [238, 255, 12, 191, 0, 252], + [129, 64., 62, 159, 199, 0], + [255, 4, 255, 175, 0, 254], + [ 3, 5, 0, 255, 4, 24]]) + np.testing.assert_array_equal(lbp, ref) + + def test_ror(self): + lbp = local_binary_pattern(self.image, 8, 1, 'ror') + ref = np.array([[ 0, 127, 0, 255, 3, 255], + [ 31, 0, 5, 51, 1, 7], + [119, 255, 3, 127, 0, 63], + [ 3, 1, 31, 63, 31, 0], + [255, 1, 255, 95, 0, 127], + [ 3, 5, 0, 255, 1, 3]]) + np.testing.assert_array_equal(lbp, ref) + + def test_uniform(self): + lbp = local_binary_pattern(self.image, 8, 1, 'uniform') + ref = np.array([[0, 7, 0, 8, 2, 8], + [5, 0, 9, 9, 1, 3], + [9, 8, 2, 7, 0, 6], + [2, 1, 5, 6, 5, 0], + [8, 1, 8, 9, 0, 7], + [2, 9, 0, 8, 1, 2]]) + np.testing.assert_array_equal(lbp, ref) + + def test_var(self): + lbp = local_binary_pattern(self.image, 8, 1, 'var') + ref = np.array([[0. , 0.00072786, 0. , 0.00115377, + 0.00032355, 0.00224467], + [0.00051758, 0. , 0.0026383 , 0.00163246, + 0.00027414, 0.00041124], + [0.00192834, 0.00130368, 0.00042095, 0.00171894, + 0. , 0.00063726], + [0.00023048, 0.00019464 , 0.00082291, 0.00225386, + 0.00076696, 0. ], + [0.00097253, 0.00013236, 0.0009134 , 0.0014467 , + 0. , 0.00082472], + [0.00024701, 0.0012277 , 0. , 0.00109869, + 0.00015445, 0.00035881]]) + np.testing.assert_array_almost_equal(lbp, ref) + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py new file mode 100644 index 00000000..7655b82a --- /dev/null +++ b/skimage/feature/texture.py @@ -0,0 +1,276 @@ +""" +Methods to characterize image textures. +""" + +import numpy as np + +from ._texture import _glcm_loop, _local_binary_pattern + + +def greycomatrix(image, distances, angles, levels=256, symmetric=False, + normed=False): + """Calculate the grey-level co-occurrence matrix. + + A grey level co-occurence matrix is a histogram of co-occuring + greyscale values at a given offset over an image. + + Parameters + ---------- + image : array_like of uint8 + Integer typed input image. The image will be cast to uint8, so + the maximum value must be less than 256. + distances : array_like + List of pixel pair distance offsets. + angles : array_like + List of pixel pair angles in radians. + levels : int, optional + The input image should contain integers in [0, levels-1], + where levels indicate the number of grey-levels counted + (typically 256 for an 8-bit image). The maximum value is + 256. + symmetric : bool, optional + If True, the output matrix `P[:, :, d, theta]` is symmetric. This + is accomplished by ignoring the order of value pairs, so both + (i, j) and (j, i) are accumulated when (i, j) is encountered + for a given offset. The default is False. + normed : bool, optional + If True, normalize each matrix `P[:, :, d, theta]` by dividing + by the total number of accumulated co-occurrences for the given + offset. The elements of the resulting matrix sum to 1. The + default is False. + + Returns + ------- + P : 4-D ndarray + The grey-level co-occurrence histogram. The value + `P[i,j,d,theta]` is the number of times that grey-level `j` + occurs at a distance `d` and at an angle `theta` from + grey-level `i`. If `normed` is `False`, the output is of + type uint32, otherwise it is float64. + + References + ---------- + .. [1] The GLCM Tutorial Home Page, + http://www.fp.ucalgary.ca/mhallbey/tutorial.htm + .. [2] Pattern Recognition Engineering, Morton Nadler & Eric P. + Smith + .. [3] Wikipedia, http://en.wikipedia.org/wiki/Co-occurrence_matrix + + + Examples + -------- + Compute 2 GLCMs: One for a 1-pixel offset to the right, and one + for a 1-pixel offset upwards. + + >>> image = np.array([[0, 0, 1, 1], + ... [0, 0, 1, 1], + ... [0, 2, 2, 2], + ... [2, 2, 3, 3]], dtype=np.uint8) + >>> result = greycomatrix(image, [1], [0, np.pi/2], levels=4) + >>> result[:, :, 0, 0] + array([[2, 2, 1, 0], + [0, 2, 0, 0], + [0, 0, 3, 1], + [0, 0, 0, 1]], dtype=uint32) + >>> result[:, :, 0, 1] + array([[3, 0, 2, 0], + [0, 2, 2, 0], + [0, 0, 1, 2], + [0, 0, 0, 0]], dtype=uint32) + + """ + + assert levels <= 256 + image = np.ascontiguousarray(image) + assert image.ndim == 2 + assert image.min() >= 0 + assert image.max() < levels + image = image.astype(np.uint8) + distances = np.ascontiguousarray(distances, dtype=np.float64) + angles = np.ascontiguousarray(angles, dtype=np.float64) + assert distances.ndim == 1 + assert angles.ndim == 1 + + P = np.zeros((levels, levels, len(distances), len(angles)), + dtype=np.uint32, order='C') + + # count co-occurences + _glcm_loop(image, distances, angles, levels, P) + + # make each GLMC symmetric + if symmetric: + Pt = np.transpose(P, (1, 0, 2, 3)) + P = P + Pt + + # normalize each GLMC + if normed: + P = P.astype(np.float64) + glcm_sums = np.apply_over_axes(np.sum, P, axes=(0, 1)) + glcm_sums[glcm_sums == 0] = 1 + P /= glcm_sums + + return P + + +def greycoprops(P, prop='contrast'): + """Calculate texture properties of a GLCM. + + Compute a feature of a grey level co-occurrence matrix to serve as + a compact summary of the matrix. The properties are computed as + follows: + + - 'contrast': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}(i-j)^2` + - 'dissimilarity': :math:`\\sum_{i,j=0}^{levels-1}P_{i,j}|i-j|` + - 'homogeneity': :math:`\\sum_{i,j=0}^{levels-1}\\frac{P_{i,j}}{1+(i-j)^2}` + - 'ASM': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}^2` + - 'energy': :math:`\\sqrt{ASM}` + - 'correlation': + .. math:: \\sum_{i,j=0}^{levels-1} P_{i,j}\\left[\\frac{(i-\\mu_i) \\ + (j-\\mu_j)}{\\sqrt{(\\sigma_i^2)(\\sigma_j^2)}}\\right] + + + Parameters + ---------- + P : ndarray + Input array. `P` is the grey-level co-occurrence histogram + for which to compute the specified property. The value + `P[i,j,d,theta]` is the number of times that grey-level j + occurs at a distance d and at an angle theta from + grey-level i. + prop : {'contrast', 'dissimilarity', 'homogeneity', 'energy', \ + 'correlation', 'ASM'}, optional + The property of the GLCM to compute. The default is 'contrast'. + + Returns + ------- + results : 2-D ndarray + 2-dimensional array. `results[d, a]` is the property 'prop' for + the d'th distance and the a'th angle. + + References + ---------- + .. [1] The GLCM Tutorial Home Page, + http://www.fp.ucalgary.ca/mhallbey/tutorial.htm + + Examples + -------- + Compute the contrast for GLCMs with distances [1, 2] and angles + [0 degrees, 90 degrees] + + >>> image = np.array([[0, 0, 1, 1], + ... [0, 0, 1, 1], + ... [0, 2, 2, 2], + ... [2, 2, 3, 3]], dtype=np.uint8) + >>> g = greycomatrix(image, [1, 2], [0, np.pi/2], levels=4, + ... normed=True, symmetric=True) + >>> contrast = greycoprops(g, 'contrast') + >>> contrast + array([[ 0.58333333, 1. ], + [ 1.25 , 2.75 ]]) + + """ + + assert P.ndim == 4 + (num_level, num_level2, num_dist, num_angle) = P.shape + assert num_level == num_level2 + assert num_dist > 0 + assert num_angle > 0 + + # create weights for specified property + I, J = np.ogrid[0:num_level, 0:num_level] + if prop == 'contrast': + weights = (I - J) ** 2 + elif prop == 'dissimilarity': + weights = np.abs(I - J) + elif prop == 'homogeneity': + weights = 1. / (1. + (I - J) ** 2) + elif prop in ['ASM', 'energy', 'correlation']: + pass + else: + raise ValueError('%s is an invalid property' % (prop)) + + # compute property for each GLCM + if prop == 'energy': + asm = np.apply_over_axes(np.sum, (P ** 2), axes=(0, 1))[0, 0] + results = np.sqrt(asm) + elif prop == 'ASM': + results = np.apply_over_axes(np.sum, (P ** 2), axes=(0, 1))[0, 0] + elif prop == 'correlation': + results = np.zeros((num_dist, num_angle), dtype=np.float64) + I = np.array(range(num_level)).reshape((num_level, 1, 1, 1)) + J = np.array(range(num_level)).reshape((1, num_level, 1, 1)) + diff_i = I - np.apply_over_axes(np.sum, (I * P), axes=(0, 1))[0, 0] + diff_j = J - np.apply_over_axes(np.sum, (J * P), axes=(0, 1))[0, 0] + + std_i = np.sqrt(np.apply_over_axes(np.sum, (P * (diff_i) ** 2), + axes=(0, 1))[0, 0]) + std_j = np.sqrt(np.apply_over_axes(np.sum, (P * (diff_j) ** 2), + axes=(0, 1))[0, 0]) + cov = np.apply_over_axes(np.sum, (P * (diff_i * diff_j)), + axes=(0, 1))[0, 0] + + # handle the special case of standard deviations near zero + mask_0 = std_i < 1e-15 + mask_0[std_j < 1e-15] = True + results[mask_0] = 1 + + # handle the standard case + mask_1 = mask_0 == False + results[mask_1] = cov[mask_1] / (std_i[mask_1] * std_j[mask_1]) + elif prop in ['contrast', 'dissimilarity', 'homogeneity']: + weights = weights.reshape((num_level, num_level, 1, 1)) + results = np.apply_over_axes(np.sum, (P * weights), axes=(0, 1))[0, 0] + + return results + + +def local_binary_pattern(image, P, R, method='default'): + """Gray scale and rotation invariant LBP (Local Binary Patterns). + + LBP is an invariant descriptor that can be used for texture classification. + + Parameters + ---------- + image : (N, M) array + Graylevel image. + P : int + Number of circularly symmetric neighbour set points (quantization of + the angular space). + R : float + Radius of circle (spatial resolution of the operator). + method : {'default', 'ror', 'uniform', 'var'} + Method to determine the pattern. + + * 'default': original local binary pattern which is gray scale but not + rotation invariant. + * 'ror': extension of default implementation which is gray scale and + rotation invariant. + * 'uniform': improved rotation invariance with uniform patterns and + finer quantization of the angular space which is gray scale and + rotation invariant. + * 'var': rotation invariant variance measures of the contrast of local + image texture which is rotation but not gray scale invariant. + + Returns + ------- + output : (N, M) array + LBP image. + + References + ---------- + .. [1] Multiresolution Gray-Scale and Rotation Invariant Texture + Classification with Local Binary Patterns. + Timo Ojala, Matti Pietikainen, Topi Maenpaa. + http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ + Articoliriferimento/LBP.pdf, 2002. + """ + + methods = { + 'default': ord('D'), + 'ror': ord('R'), + 'uniform': ord('U'), + 'var': ord('V') + } + image = np.array(image, dtype='double', copy=True) + output = _local_binary_pattern(image, P, R, methods[method.lower()]) + return output diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 04c972cc..bdbbb531 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,7 +1,7 @@ from .lpi_filter import * from .ctmf import median_filter -from .canny import canny +from ._canny import canny from .edges import sobel, hsobel, vsobel, hprewitt, vprewitt, prewitt -from .tv_denoise import tv_denoise -from .rank_order import rank_order +from ._tv_denoise import tv_denoise +from ._rank_order import rank_order from .thresholding import threshold_otsu, threshold_adaptive diff --git a/skimage/filter/canny.py b/skimage/filter/_canny.py similarity index 97% rename from skimage/filter/canny.py rename to skimage/filter/_canny.py index 4818916c..8be77894 100644 --- a/skimage/filter/canny.py +++ b/skimage/filter/_canny.py @@ -56,7 +56,7 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): Parameters ----------- image : array_like, dtype=float - The greyscale input image to detect edges on; should be normalized to + The greyscale input image to detect edges on; should be normalized to 0.0 to 1.0. sigma : float @@ -85,21 +85,21 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): The steps of the algorithm are as follows: * Smooth the image using a Gaussian with ``sigma`` width. - + * Apply the horizontal and vertical Sobel operators to get the gradients within the image. The edge strength is the norm of the gradient. - - * Thin potential edges to 1-pixel wide curves. First, find the normal - to the edge at each point. This is done by looking at the + + * Thin potential edges to 1-pixel wide curves. First, find the normal + to the edge at each point. This is done by looking at the signs and the relative magnitude of the X-Sobel and Y-Sobel to sort the points into 4 categories: horizontal, vertical, - diagonal and antidiagonal. Then look in the normal and reverse - directions to see if the values in either of those directions are - greater than the point in question. Use interpolation to get a mix of - points instead of picking the one that's the closest to the normal. - - * Perform a hysteresis thresholding: first label all points above the - high threshold as edges. Then recursively label any point above the + diagonal and antidiagonal. Then look in the normal and reverse + directions to see if the values in either of those directions are + greater than the point in question. Use interpolation to get a mix of + points instead of picking the one that's the closest to the normal. + + * Perform a hysteresis thresholding: first label all points above the + high threshold as edges. Then recursively label any point above the low threshold that is 8-connected to a labeled point as an edge. References @@ -120,7 +120,7 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): >>> # First trial with the Canny filter, with the default smoothing >>> edges1 = filter.canny(im) >>> # Increase the smoothing for better results - >>> edges2 = filter.canny(im, sigma=3) + >>> edges2 = filter.canny(im, sigma=3) ''' # diff --git a/skimage/filter/rank_order.py b/skimage/filter/_rank_order.py similarity index 91% rename from skimage/filter/rank_order.py rename to skimage/filter/_rank_order.py index 3da98974..f878702f 100644 --- a/skimage/filter/rank_order.py +++ b/skimage/filter/_rank_order.py @@ -10,9 +10,10 @@ Original author: Lee Kamentstky """ import numpy + def rank_order(image): """Return an image of the same shape where each pixel is the - index of the pixel value in the ascending order of the unique + index of the pixel value in the ascending order of the unique values of `image`, aka the rank-order value. Parameters @@ -48,14 +49,12 @@ def rank_order(image): flat_image = image.ravel() sort_order = flat_image.argsort().astype(numpy.uint32) flat_image = flat_image[sort_order] - sort_rank = numpy.zeros_like(sort_order) + sort_rank = numpy.zeros_like(sort_order) is_different = flat_image[:-1] != flat_image[1:] numpy.cumsum(is_different, out=sort_rank[1:]) - original_values = numpy.zeros((sort_rank[-1]+1,),image.dtype) + original_values = numpy.zeros((sort_rank[-1] + 1,), image.dtype) original_values[0] = flat_image[0] - original_values[1:] = flat_image[1:][is_different] + original_values[1:] = flat_image[1:][is_different] int_image = numpy.zeros_like(sort_order) int_image[sort_order] = sort_rank return (int_image.reshape(image.shape), original_values) - - diff --git a/skimage/filter/tv_denoise.py b/skimage/filter/_tv_denoise.py similarity index 81% rename from skimage/filter/tv_denoise.py rename to skimage/filter/_tv_denoise.py index bb74a4bc..3302319c 100644 --- a/skimage/filter/tv_denoise.py +++ b/skimage/filter/_tv_denoise.py @@ -1,4 +1,6 @@ import numpy as np +from skimage import img_as_float + def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): """ @@ -10,8 +12,8 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): 3-D input data to be denoised weight: float, optional - denoising weight. The greater ``weight``, the more denoising (at - the expense of fidelity to ``input``) + denoising weight. The greater ``weight``, the more denoising (at + the expense of fidelity to ``input``) eps: float, optional relative difference of the value of the cost function that determines @@ -25,11 +27,11 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): Returns ------- out: ndarray - denoised array + denoised array of floats Notes ----- - Rudin, Osher and Fatemi algorithm + Rudin, Osher and Fatemi algorithm Examples --------- @@ -50,25 +52,25 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): i = 0 while i < n_iter_max: d = - px - py - pz - d[1:] += px[:-1] - d[:, 1:] += py[:, :-1] - d[:, :, 1:] += pz[:, :, :-1] - + d[1:] += px[:-1] + d[:, 1:] += py[:, :-1] + d[:, :, 1:] += pz[:, :, :-1] + out = im + d E = (d**2).sum() - gx[:-1] = np.diff(out, axis=0) - gy[:, :-1] = np.diff(out, axis=1) - gz[:, :, :-1] = np.diff(out, axis=2) + gx[:-1] = np.diff(out, axis=0) + gy[:, :-1] = np.diff(out, axis=1) + gz[:, :, :-1] = np.diff(out, axis=2) norm = np.sqrt(gx**2 + gy**2 + gz**2) E += weight * norm.sum() norm *= 0.5 / weight norm += 1. - px -= 1./6.*gx + px -= 1. / 6. * gx px /= norm - py -= 1./6.*gy + py -= 1. / 6. * gy py /= norm - pz -= 1/6.*gz + pz -= 1 / 6. * gz pz /= norm E /= float(im.size) if i == 0: @@ -81,7 +83,8 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): E_previous = E i += 1 return out - + + def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): """ Perform total-variation denoising @@ -92,8 +95,8 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): input data to be denoised weight: float, optional - denoising weight. The greater ``weight``, the more denoising (at - the expense of fidelity to ``input``) + denoising weight. The greater ``weight``, the more denoising (at + the expense of fidelity to ``input``) eps: float, optional relative difference of the value of the cost function that determines @@ -107,21 +110,21 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): Returns ------- out: ndarray - denoised array + denoised array of floats Notes ----- The principle of total variation denoising is explained in http://en.wikipedia.org/wiki/Total_variation_denoising - This code is an implementation of the algorithm of Rudin, Fatemi and Osher + This code is an implementation of the algorithm of Rudin, Fatemi and Osher that was proposed by Chambolle in [1]_. References ---------- - .. [1] A. Chambolle, An algorithm for total variation minimization and - applications, Journal of Mathematical Imaging and Vision, + .. [1] A. Chambolle, An algorithm for total variation minimization and + applications, Journal of Mathematical Imaging and Vision, Springer, 2004, 20, 89-97. Examples @@ -140,21 +143,21 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): d = np.zeros_like(im) i = 0 while i < n_iter_max: - d = -px -py - d[1:] += px[:-1] - d[:, 1:] += py[:, :-1] - + d = -px - py + d[1:] += px[:-1] + d[:, 1:] += py[:, :-1] + out = im + d E = (d**2).sum() - gx[:-1] = np.diff(out, axis=0) - gy[:, :-1] = np.diff(out, axis=1) + gx[:-1] = np.diff(out, axis=0) + gy[:, :-1] = np.diff(out, axis=1) norm = np.sqrt(gx**2 + gy**2) E += weight * norm.sum() norm *= 0.5 / weight norm += 1 - px -= 0.25*gx + px -= 0.25 * gx px /= norm - py -= 0.25*gy + py -= 0.25 * gy py /= norm E /= float(im.size) if i == 0: @@ -168,7 +171,8 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): i += 1 return out -def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): + +def tv_denoise(im, weight=50, eps=2.e-4, n_iter_max=200): """ Perform total-variation denoising on 2-d and 3-d images @@ -176,32 +180,26 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): ---------- im: ndarray (2d or 3d) of ints, uints or floats input data to be denoised. `im` can be of any numeric type, - but it is cast into an ndarray of floats for the computation + but it is cast into an ndarray of floats for the computation of the denoised image. weight: float, optional - denoising weight. The greater ``weight``, the more denoising (at - the expense of fidelity to ``input``) + denoising weight. The greater ``weight``, the more denoising (at + the expense of fidelity to ``input``) eps: float, optional - relative difference of the value of the cost function that + relative difference of the value of the cost function that determines the stop criterion. The algorithm stops when: (E_(n-1) - E_n) < eps * E_0 - keep_type: bool, optional (False) - whether the output has the same dtype as the input array. - keep_type is False by default, and the dtype of the output - is np.float - n_iter_max: int, optional maximal number of iterations used for the optimization. Returns ------- out: ndarray - denoised array - + denoised array of floats Notes ----- @@ -209,19 +207,19 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): http://en.wikipedia.org/wiki/Total_variation_denoising The principle of total variation denoising is to minimize the - total variation of the image, which can be roughly described as - the integral of the norm of the image gradient. Total variation - denoising tends to produce "cartoon-like" images, that is, + total variation of the image, which can be roughly described as + the integral of the norm of the image gradient. Total variation + denoising tends to produce "cartoon-like" images, that is, piecewise-constant images. - This code is an implementation of the algorithm of Rudin, Fatemi and Osher + This code is an implementation of the algorithm of Rudin, Fatemi and Osher that was proposed by Chambolle in [1]_. References ---------- - .. [1] A. Chambolle, An algorithm for total variation minimization and - applications, Journal of Mathematical Imaging and Vision, + .. [1] A. Chambolle, An algorithm for total variation minimization and + applications, Journal of Mathematical Imaging and Vision, Springer, 2004, 20, 89-97. Examples @@ -242,16 +240,13 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): """ im_type = im.dtype if not im_type.kind == 'f': - im = im.astype(np.float) + im = img_as_float(im) if im.ndim == 2: out = _tv_denoise_2d(im, weight, eps, n_iter_max) elif im.ndim == 3: out = _tv_denoise_3d(im, weight, eps, n_iter_max) else: - raise ValueError('only 2-d and 3-d images may be denoised with this function') - if keep_type: - return out.astype(im_type) - else: - return out - + raise ValueError('only 2-d and 3-d images may be denoised with this ' + 'function') + return out diff --git a/skimage/filter/ctmf.py b/skimage/filter/ctmf.py index 286932be..94f273e1 100644 --- a/skimage/filter/ctmf.py +++ b/skimage/filter/ctmf.py @@ -13,7 +13,7 @@ Original author: Lee Kamentsky import numpy as np from . import _ctmf -from .rank_order import rank_order +from ._rank_order import rank_order def median_filter(image, radius=2, mask=None, percent=50): diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index f6bf7031..134aa796 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -12,6 +12,27 @@ import numpy as np from skimage import img_as_float from scipy.ndimage import convolve, binary_erosion, generate_binary_structure + +EROSION_SELEM = generate_binary_structure(2, 2) + + +def _mask_filter_result(result, mask): + """Return result after masking. + + Input masks are eroded so that mask areas in the original image don't + affect values in the result. + """ + if mask is None: + result[0, :] = 0 + result[-1, :] = 0 + result[:, 0] = 0 + result[:, -1] = 0 + return result + else: + mask = binary_erosion(mask, EROSION_SELEM, border_value=0) + return result * mask + + def sobel(image, mask=None): """Calculate the absolute magnitude Sobel to find edges. @@ -21,6 +42,8 @@ def sobel(image, mask=None): Image to process. mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -38,6 +61,7 @@ def sobel(image, mask=None): """ return np.sqrt(hsobel(image, mask)**2 + vsobel(image, mask)**2) + def hsobel(image, mask=None): """Find the horizontal edges of an image using the Sobel transform. @@ -47,6 +71,8 @@ def hsobel(image, mask=None): Image to process. mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -64,17 +90,12 @@ def hsobel(image, mask=None): """ image = img_as_float(image) - if mask is None: - mask = np.ones(image.shape, bool) - big_mask = binary_erosion(mask, - generate_binary_structure(2, 2), - border_value = 0) result = np.abs(convolve(image, np.array([[ 1, 2, 1], [ 0, 0, 0], [-1,-2,-1]]).astype(float) / 4.0)) - result[big_mask == False] = 0 - return result + return _mask_filter_result(result, mask) + def vsobel(image, mask=None): """Find the vertical edges of an image using the Sobel transform. @@ -85,6 +106,8 @@ def vsobel(image, mask=None): Image to process mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -102,17 +125,12 @@ def vsobel(image, mask=None): """ image = img_as_float(image) - if mask is None: - mask = np.ones(image.shape, bool) - big_mask = binary_erosion(mask, - generate_binary_structure(2, 2), - border_value=0) result = np.abs(convolve(image, np.array([[1, 0, -1], [2, 0, -2], [1, 0, -1]]).astype(float) / 4.0)) - result[big_mask == False] = 0 - return result + return _mask_filter_result(result, mask) + def prewitt(image, mask=None): """Find the edge magnitude using the Prewitt transform. @@ -123,6 +141,8 @@ def prewitt(image, mask=None): Image to process. mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -134,7 +154,8 @@ def prewitt(image, mask=None): Return the square root of the sum of squares of the horizontal and vertical Prewitt transforms. """ - return np.sqrt(hprewitt(image, mask) ** 2 + vprewitt(image, mask) ** 2) + return np.sqrt(hprewitt(image, mask)**2 + vprewitt(image, mask)**2) + def hprewitt(image, mask=None): """Find the horizontal edges of an image using the Prewitt transform. @@ -145,6 +166,8 @@ def hprewitt(image, mask=None): Image to process. mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -162,17 +185,12 @@ def hprewitt(image, mask=None): """ image = img_as_float(image) - if mask is None: - mask = np.ones(image.shape, bool) - big_mask = binary_erosion(mask, - generate_binary_structure(2, 2), - border_value=0) result = np.abs(convolve(image, np.array([[ 1, 1, 1], [ 0, 0, 0], [-1,-1,-1]]).astype(float) / 3.0)) - result[big_mask == False] = 0 - return result + return _mask_filter_result(result, mask) + def vprewitt(image, mask=None): """Find the vertical edges of an image using the Prewitt transform. @@ -183,6 +201,8 @@ def vprewitt(image, mask=None): Image to process. mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -200,14 +220,8 @@ def vprewitt(image, mask=None): """ image = img_as_float(image) - if mask is None: - mask = np.ones(image.shape, bool) - big_mask = binary_erosion(mask, - generate_binary_structure(2, 2), - border_value=0) result = np.abs(convolve(image, np.array([[1, 0, -1], [1, 0, -1], [1, 0, -1]]).astype(float) / 3.0)) - result[big_mask == False] = 0 - return result + return _mask_filter_result(result, mask) diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index 50d2de17..2af74d7a 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -7,14 +7,16 @@ __all__ = ['inverse', 'wiener', 'LPIFilter2D'] __docformat__ = 'restructuredtext en' import numpy as np -from scipy.fftpack import fftshift, ifftshift +from scipy.fftpack import ifftshift eps = np.finfo(float).eps + def _min_limit(x, val=eps): mask = np.abs(x) < eps x[mask] = np.sign(x[mask]) * eps + def _centre(x, oshape): """Return an array of oshape from the centre of x. @@ -23,6 +25,7 @@ def _centre(x, oshape): out = x[[slice(s, s + n) for s, n in zip(start, oshape)]] return out + def _pad(data, shape): """Pad the data to the given shape with zeros. @@ -38,7 +41,6 @@ def _pad(data, shape): return out - class LPIFilter2D(object): """Linear Position-Invariant Filter (2-dimensional) @@ -48,16 +50,19 @@ class LPIFilter2D(object): Parameters ---------- impulse_response : callable `f(r, c, **filter_params)` - Function that yields the impulse response. `r` and - `c` are 1-dimensional vectors that represent row and - column positions, in other words coordinates are - (r[0],c[0]),(r[0],c[1]) etc. `**filter_params` are - passed through. + Function that yields the impulse response. `r` and `c` are + 1-dimensional vectors that represent row and column positions, in + other words coordinates are (r[0],c[0]),(r[0],c[1]) etc. + `**filter_params` are passed through. - In other words, example would be called like this: + In other words, `impulse_response` would be called like this: + >>> def impulse_response(r, c, **filter_params): + ... pass + >>> >>> r = [0,0,0,1,1,1,2,2,2] >>> c = [0,1,2,0,1,2,0,1,2] + >>> filter_params = {'kw1': 1, 'kw2': 2, 'kw3': 3} >>> impulse_response(r, c, **filter_params) Examples @@ -66,12 +71,11 @@ class LPIFilter2D(object): Gaussian filter: >>> def filt_func(r, c): - return np.exp(-np.hypot(r, c)/1) - + ... return np.exp(-np.hypot(r, c)/1) >>> filter = LPIFilter2D(filt_func) """ - if impulse_response is None: + if not callable(impulse_response): raise ValueError("Impulse response must be a callable.") self.impulse_response = impulse_response @@ -83,21 +87,21 @@ class LPIFilter2D(object): """ dshape = np.array(data.shape) - dshape += (dshape % 2 == 0) # all filter dimensions must be uneven + dshape += (dshape % 2 == 0) # all filter dimensions must be uneven oshape = np.array(data.shape) * 2 - 1 if self._cache is None or np.any(self._cache.shape != oshape): coords = np.mgrid[[slice(0, float(n)) for n in dshape]] # this steps over two sets of coordinates, # not over the coordinates individually - for k,coord in enumerate(coords): - coord -= (dshape[k] - 1)/2. - coords = coords.reshape(2, -1).T # coordinate pairs (r,c) + for k, coord in enumerate(coords): + coord -= (dshape[k] - 1) / 2. + coords = coords.reshape(2, -1).T # coordinate pairs (r,c) - f = self.impulse_response(coords[:,0],coords[:,1], + f = self.impulse_response(coords[:, 0], coords[:, 1], **self.filter_params).reshape(dshape) - f = _pad(f,oshape) + f = _pad(f, oshape) F = np.dual.fftn(f) self._cache = F else: @@ -108,11 +112,12 @@ class LPIFilter2D(object): return F, G - def __call__(self,data): + def __call__(self, data): """Apply the filter to the given data. - *Parameters*: - data : (M,N) ndarray + Parameters + ---------- + data : (M,N) ndarray """ F, G = self._prepare(data) @@ -120,6 +125,7 @@ class LPIFilter2D(object): out = np.abs(_centre(out, data.shape)) return out + def forward(data, impulse_response=None, filter_params={}, predefined_filter=None): """Apply the given filter to data. @@ -136,9 +142,8 @@ def forward(data, impulse_response=None, filter_params={}, Other Parameters ---------------- predefined_filter : LPIFilter2D - If you need to apply the same filter multiple times over - different images, construct the LPIFilter2D and specify - it here. + If you need to apply the same filter multiple times over different + images, construct the LPIFilter2D and specify it here. Examples -------- @@ -146,15 +151,17 @@ def forward(data, impulse_response=None, filter_params={}, Gaussian filter: >>> def filt_func(r, c): - return np.exp(-np.hypot(r, c)/1) - - >>> forward(data, filt_func) + ... return np.exp(-np.hypot(r, c)/1) + >>> + >>> from skimage import data + >>> filtered = forward(data.coins(), filt_func) """ if predefined_filter is None: predefined_filter = LPIFilter2D(impulse_response, **filter_params) return predefined_filter(data) + def inverse(data, impulse_response=None, filter_params={}, max_gain=2, predefined_filter=None): """Apply the filter in reverse to the given data. @@ -168,17 +175,15 @@ def inverse(data, impulse_response=None, filter_params={}, max_gain=2, filter_params : dict Additional keyword parameters to the impulse_response function. max_gain : float - Limit the filter gain. Often, the filter contains - zeros, which would cause the inverse filter to have - infinite gain. High gain causes amplification of - artefacts, so a conservative limit is recommended. + Limit the filter gain. Often, the filter contains zeros, which would + cause the inverse filter to have infinite gain. High gain causes + amplification of artefacts, so a conservative limit is recommended. Other Parameters ---------------- predefined_filter : LPIFilter2D - If you need to apply the same filter multiple times over - different images, construct the LPIFilter2D and specify - it here. + If you need to apply the same filter multiple times over different + images, construct the LPIFilter2D and specify it here. """ if predefined_filter is None: @@ -189,12 +194,13 @@ def inverse(data, impulse_response=None, filter_params={}, max_gain=2, F, G = filt._prepare(data) _min_limit(F) - F = 1/F + F = 1 / F mask = np.abs(F) > max_gain F[mask] = np.sign(F[mask]) * max_gain return _centre(np.abs(ifftshift(np.dual.ifftn(G * F))), data.shape) + def wiener(data, impulse_response=None, filter_params={}, K=0.25, predefined_filter=None): """Minimum Mean Square Error (Wiener) inverse filter. @@ -214,9 +220,8 @@ def wiener(data, impulse_response=None, filter_params={}, K=0.25, Other Parameters ---------------- predefined_filter : LPIFilter2D - If you need to apply the same filter multiple times over - different images, construct the LPIFilter2D and specify - it here. + If you need to apply the same filter multiple times over different + images, construct the LPIFilter2D and specify it here. """ if predefined_filter is None: @@ -228,11 +233,11 @@ def wiener(data, impulse_response=None, filter_params={}, K=0.25, _min_limit(F) H_mag_sqr = np.abs(F)**2 - F = 1/F * H_mag_sqr / (H_mag_sqr + K) + F = 1 / F * H_mag_sqr / (H_mag_sqr + K) return _centre(np.abs(ifftshift(np.dual.ifftn(G * F))), data.shape) + def constrained_least_squares(data, lam, impulse_response=None, filter_params={}): raise NotImplementedError - diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index 12cb84a7..3e573aec 100644 --- a/skimage/filter/setup.py +++ b/skimage/filter/setup.py @@ -5,6 +5,7 @@ from skimage._build import cython base_path = os.path.abspath(os.path.dirname(__file__)) + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs @@ -20,11 +21,11 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - author = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Filters', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='scikit-image Developers', + author='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Filters', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) diff --git a/skimage/filter/tests/test_canny.py b/skimage/filter/tests/test_canny.py index 1e406008..2c758edf 100644 --- a/skimage/filter/tests/test_canny.py +++ b/skimage/filter/tests/test_canny.py @@ -61,3 +61,8 @@ class TestCanny(unittest.TestCase): def test_image_shape(self): self.assertRaises(TypeError, F.canny, np.zeros((20, 20, 20)), 4, 0, 0) + + def test_mask_none(self): + result1 = F.canny(np.zeros((20, 20)), 4, 0, 0, np.ones((20, 20), bool)) + result2 = F.canny(np.zeros((20, 20)), 4, 0, 0) + self.assertTrue(np.all(result1 == result2)) diff --git a/skimage/filter/tests/test_ctmf.py b/skimage/filter/tests/test_ctmf.py index fd820469..c4f6d10e 100644 --- a/skimage/filter/tests/test_ctmf.py +++ b/skimage/filter/tests/test_ctmf.py @@ -117,5 +117,11 @@ def test_insufficient_size(): median_filter(img, radius=1) +@raises(TypeError) +def test_wrong_shape(): + img = np.empty((10, 10, 3)) + median_filter(img) + + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/filter/tests/test_edges.py b/skimage/filter/tests/test_edges.py index 0c306273..a5d52aa5 100644 --- a/skimage/filter/tests/test_edges.py +++ b/skimage/filter/tests/test_edges.py @@ -1,206 +1,236 @@ -import os - -from numpy.testing import * import numpy as np -from scipy.ndimage import binary_dilation, binary_erosion +from numpy.testing import assert_array_almost_equal as assert_close import skimage.filter as F -from skimage import data_dir, img_as_float - -class TestSobel(): - def test_00_00_zeros(self): - """Sobel on an array of all zeros""" - result = F.sobel(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) - - def test_00_01_mask(self): - """Sobel on a masked array should be zero""" - np.random.seed(0) - result = F.sobel(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) - - def test_01_01_horizontal(self): - """Sobel on an edge should be a horizontal line""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.sobel(image) - # Fudge the eroded points - i[np.abs(j) == 5] = 10000 - assert (np.all(result[i == 0] == 1)) - assert (np.all(result[np.abs(i) > 1] == 0)) - - def test_01_02_vertical(self): - """Sobel on a vertical edge should be a vertical line""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.sobel(image) - j[np.abs(i) == 5] = 10000 - assert (np.all(result[j == 0] == 1)) - assert (np.all(result[np.abs(j) > 1] == 0)) -class TestHSobel(): - def test_00_00_zeros(self): - """Horizontal sobel on an array of all zeros""" - result = F.hsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) +def test_sobel_zeros(): + """Sobel on an array of all zeros""" + result = F.sobel(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) - def test_00_01_mask(self): - """Horizontal Sobel on a masked array should be zero""" - np.random.seed(0) - result = F.hsobel(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) +def test_sobel_mask(): + """Sobel on a masked array should be zero""" + np.random.seed(0) + result = F.sobel(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) - def test_01_01_horizontal(self): - """Horizontal Sobel on an edge should be a horizontal line""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.hsobel(image) - # Fudge the eroded points - i[np.abs(j) == 5] = 10000 - assert (np.all(result[i == 0] == 1)) - assert (np.all(result[np.abs(i) > 1] == 0)) +def test_sobel_horizontal(): + """Sobel on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.sobel(image) + # Fudge the eroded points + i[np.abs(j) == 5] = 10000 + assert (np.all(result[i == 0] == 1)) + assert (np.all(result[np.abs(i) > 1] == 0)) - def test_01_02_vertical(self): - """Horizontal Sobel on a vertical edge should be zero""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.hsobel(image) - assert (np.all(result == 0)) +def test_sobel_vertical(): + """Sobel on a vertical edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.sobel(image) + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + assert (np.all(result[np.abs(j) > 1] == 0)) -class TestVSobel(): - def test_00_00_zeros(self): - """Vertical sobel on an array of all zeros""" - result = F.vsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) - def test_00_01_mask(self): - """Vertical Sobel on a masked array should be zero""" - np.random.seed(0) - result = F.vsobel(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) +def test_hsobel_zeros(): + """Horizontal sobel on an array of all zeros""" + result = F.hsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) - def test_01_01_vertical(self): - """Vertical Sobel on an edge should be a vertical line""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.vsobel(image) - # Fudge the eroded points - j[np.abs(i) == 5] = 10000 - assert (np.all(result[j == 0] == 1)) - assert (np.all(result[np.abs(j) > 1] == 0)) +def test_hsobel_mask(): + """Horizontal Sobel on a masked array should be zero""" + np.random.seed(0) + result = F.hsobel(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) - def test_01_02_horizontal(self): - """vertical Sobel on a horizontal edge should be zero""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.vsobel(image) - eps = .000001 - assert (np.all(np.abs(result) < eps)) +def test_hsobel_horizontal(): + """Horizontal Sobel on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.hsobel(image) + # Fudge the eroded points + i[np.abs(j) == 5] = 10000 + assert (np.all(result[i == 0] == 1)) + assert (np.all(result[np.abs(i) > 1] == 0)) -class TestPrewitt(): - def test_00_00_zeros(self): - """Prewitt on an array of all zeros""" - result = F.prewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) +def test_hsobel_vertical(): + """Horizontal Sobel on a vertical edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.hsobel(image) + assert (np.all(result == 0)) - def test_00_01_mask(self): - """Prewitt on a masked array should be zero""" - np.random.seed(0) - result = F.prewitt(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - eps = .000001 - assert (np.all(np.abs(result) < eps)) - def test_01_01_horizontal(self): - """Prewitt on an edge should be a horizontal line""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.prewitt(image) - # Fudge the eroded points - i[np.abs(j) == 5] = 10000 - eps = .000001 - assert (np.all(result[i == 0] == 1)) - assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) +def test_vsobel_zeros(): + """Vertical sobel on an array of all zeros""" + result = F.vsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) - def test_01_02_vertical(self): - """Prewitt on a vertical edge should be a vertical line""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.prewitt(image) - eps = .000001 - j[np.abs(i)==5] = 10000 - assert (np.all(result[j == 0] == 1)) - assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) +def test_vsobel_mask(): + """Vertical Sobel on a masked array should be zero""" + np.random.seed(0) + result = F.vsobel(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) -class TestHPrewitt(): - def test_00_00_zeros(self): - """Horizontal sobel on an array of all zeros""" - result = F.hprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) +def test_vsobel_vertical(): + """Vertical Sobel on an edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.vsobel(image) + # Fudge the eroded points + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + assert (np.all(result[np.abs(j) > 1] == 0)) - def test_00_01_mask(self): - """Horizontal prewitt on a masked array should be zero""" - np.random.seed(0) - result = F.hprewitt(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - eps = .000001 - assert (np.all(np.abs(result) < eps)) +def test_vsobel_horizontal(): + """vertical Sobel on a horizontal edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.vsobel(image) + eps = .000001 + assert (np.all(np.abs(result) < eps)) - def test_01_01_horizontal(self): - """Horizontal prewitt on an edge should be a horizontal line""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.hprewitt(image) - # Fudge the eroded points - i[np.abs(j) == 5] = 10000 - eps = .000001 - assert (np.all(result[i == 0] == 1)) - assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) - def test_01_02_vertical(self): - """Horizontal prewitt on a vertical edge should be zero""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.hprewitt(image) - eps = .000001 - assert (np.all(np.abs(result) < eps)) +def test_prewitt_zeros(): + """Prewitt on an array of all zeros""" + result = F.prewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) -class TestVPrewitt(): - def test_00_00_zeros(self): - """Vertical prewitt on an array of all zeros""" - result = F.vprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) +def test_prewitt_mask(): + """Prewitt on a masked array should be zero""" + np.random.seed(0) + result = F.prewitt(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + eps = .000001 + assert (np.all(np.abs(result) < eps)) - def test_00_01_mask(self): - """Vertical prewitt on a masked array should be zero""" - np.random.seed(0) - result = F.vprewitt(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) +def test_prewitt_horizontal(): + """Prewitt on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.prewitt(image) + # Fudge the eroded points + i[np.abs(j) == 5] = 10000 + eps = .000001 + assert (np.all(result[i == 0] == 1)) + assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) - def test_01_01_vertical(self): - """Vertical prewitt on an edge should be a vertical line""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.vprewitt(image) - # Fudge the eroded points - j[np.abs(i) == 5] = 10000 - assert (np.all(result[j == 0] == 1)) - eps = .000001 - assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) +def test_prewitt_vertical(): + """Prewitt on a vertical edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.prewitt(image) + eps = .000001 + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) - def test_01_02_horizontal(self): - """Vertical prewitt on a horizontal edge should be zero""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.vprewitt(image) - eps = .000001 - assert (np.all(np.abs(result) < eps)) + +def test_hprewitt_zeros(): + """Horizontal prewitt on an array of all zeros""" + result = F.hprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) + +def test_hprewitt_mask(): + """Horizontal prewitt on a masked array should be zero""" + np.random.seed(0) + result = F.hprewitt(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + eps = .000001 + assert (np.all(np.abs(result) < eps)) + +def test_hprewitt_horizontal(): + """Horizontal prewitt on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.hprewitt(image) + # Fudge the eroded points + i[np.abs(j) == 5] = 10000 + eps = .000001 + assert (np.all(result[i == 0] == 1)) + assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) + +def test_hprewitt_vertical(): + """Horizontal prewitt on a vertical edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.hprewitt(image) + eps = .000001 + assert (np.all(np.abs(result) < eps)) + + +def test_vprewitt_zeros(): + """Vertical prewitt on an array of all zeros""" + result = F.vprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) + +def test_vprewitt_mask(): + """Vertical prewitt on a masked array should be zero""" + np.random.seed(0) + result = F.vprewitt(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) + +def test_vprewitt_vertical(): + """Vertical prewitt on an edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.vprewitt(image) + # Fudge the eroded points + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + eps = .000001 + assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) + +def test_vprewitt_horizontal(): + """Vertical prewitt on a horizontal edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.vprewitt(image) + eps = .000001 + assert (np.all(np.abs(result) < eps)) + + +def test_horizontal_mask_line(): + """Horizontal edge filters mask pixels surrounding input mask.""" + vgrad, _ = np.mgrid[:1:11j, :1:11j] # vertical gradient with spacing 0.1 + vgrad[5, :] = 1 # bad horizontal line + + mask = np.ones_like(vgrad) + mask[5, :] = 0 # mask bad line + + expected = np.zeros_like(vgrad) + expected[1:-1, 1:-1] = 0.2 # constant gradient for most of image, + expected[4:7, 1:-1] = 0 # but line and neighbors masked + + for grad_func in (F.hprewitt, F.hsobel): + result = grad_func(vgrad, mask) + yield assert_close, result, expected + + +def test_vertical_mask_line(): + """Vertical edge filters mask pixels surrounding input mask.""" + _, hgrad = np.mgrid[:1:11j, :1:11j] # horizontal gradient with spacing 0.1 + hgrad[:, 5] = 1 # bad vertical line + + mask = np.ones_like(hgrad) + mask[:, 5] = 0 # mask bad line + + expected = np.zeros_like(hgrad) + expected[1:-1, 1:-1] = 0.2 # constant gradient for most of image, + expected[1:-1, 4:7] = 0 # but line and neighbors masked + + for grad_func in (F.vprewitt, F.vsobel): + result = grad_func(hgrad, mask) + yield assert_close, result, expected if __name__ == "__main__": - run_module_suite() + from numpy import testing + testing.run_module_suite() diff --git a/skimage/filter/tests/test_lpi_filter.py b/skimage/filter/tests/test_lpi_filter.py index 1cf7b3ca..f6176691 100644 --- a/skimage/filter/tests/test_lpi_filter.py +++ b/skimage/filter/tests/test_lpi_filter.py @@ -7,12 +7,14 @@ from skimage import data_dir from skimage.io import * from skimage.filter import * -class TestLPIFilter2D(): - img = imread(os.path.join(data_dir, 'camera.png'), - flatten=True)[:50,:50] - def filt_func(self,r,c): - return np.exp(-np.hypot(r,c)/1) +class TestLPIFilter2D(object): + + img = imread(os.path.join(data_dir, 'camera.png'), + flatten=True)[:50, :50] + + def filt_func(self, r, c): + return np.exp(-np.hypot(r, c) / 1) def setUp(self): self.f = LPIFilter2D(self.filt_func) @@ -33,27 +35,30 @@ class TestLPIFilter2D(): g = inverse(F, predefined_filter=self.f) assert_equal(g.shape, self.img.shape) - g1 = inverse(F[::-1,::-1], predefined_filter=self.f) - assert ((g - g1[::-1,::-1]).sum() < 55) + g1 = inverse(F[::-1, ::-1], predefined_filter=self.f) + assert ((g - g1[::-1, ::-1]).sum() < 55) # test cache - g1 = inverse(F[::-1,::-1], predefined_filter=self.f) - assert ((g - g1[::-1,::-1]).sum() < 55) + g1 = inverse(F[::-1, ::-1], predefined_filter=self.f) + assert ((g - g1[::-1, ::-1]).sum() < 55) g1 = inverse(F[::-1, ::-1], self.filt_func) - assert ((g - g1[::-1,::-1]).sum() < 55) + assert ((g - g1[::-1, ::-1]).sum() < 55) def test_wiener(self): F = self.f(self.img) g = wiener(F, predefined_filter=self.f) assert_equal(g.shape, self.img.shape) - g1 = wiener(F[::-1,::-1], predefined_filter=self.f) - assert ((g - g1[::-1,::-1]).sum() < 1) + g1 = wiener(F[::-1, ::-1], predefined_filter=self.f) + assert ((g - g1[::-1, ::-1]).sum() < 1) + + g1 = wiener(F[::-1, ::-1], self.filt_func) + assert ((g - g1[::-1, ::-1]).sum() < 1) + + def test_non_callable(self): + assert_raises(ValueError, LPIFilter2D, None) - g1 = wiener(F[::-1,::-1], self.filt_func) - assert ((g - g1[::-1,::-1]).sum() < 1) if __name__ == "__main__": run_module_suite() - diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 6177b9f5..97d3d9e3 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -73,19 +73,24 @@ class TestSimpleImage(): def test_otsu_camera_image(): - assert threshold_otsu(data.camera()) == 87 + camera = skimage.img_as_ubyte(data.camera()) + assert 86 < threshold_otsu(camera) < 88 + def test_otsu_coins_image(): - assert threshold_otsu(data.coins()) == 107 + coins = skimage.img_as_ubyte(data.coins()) + assert 106 < threshold_otsu(coins) < 108 + def test_otsu_coins_image_as_float(): coins = skimage.img_as_float(data.coins()) assert 0.41 < threshold_otsu(coins) < 0.42 + def test_otsu_lena_image(): - assert threshold_otsu(data.lena()) == 141 + lena = skimage.img_as_ubyte(data.lena()) + assert 140 < threshold_otsu(lena) < 142 if __name__ == '__main__': np.testing.run_module_suite() - diff --git a/skimage/filter/tests/test_tv_denoise.py b/skimage/filter/tests/test_tv_denoise.py index f851f4f1..cc4fae7e 100644 --- a/skimage/filter/tests/test_tv_denoise.py +++ b/skimage/filter/tests/test_tv_denoise.py @@ -2,7 +2,7 @@ import numpy as np from numpy.testing import run_module_suite from skimage import filter, data, color -from skimage import img_as_uint + class TestTvDenoise(): @@ -14,7 +14,7 @@ class TestTvDenoise(): # lena image lena = color.rgb2gray(data.lena())[:256, :256] # add noise to lena - lena += 0.5 * lena.std()*np.random.randn(*lena.shape) + lena += 0.5 * lena.std() * np.random.randn(*lena.shape) # clip noise so that it does not exceed allowed range for float images. lena = np.clip(lena, 0, 1) # denoise @@ -22,14 +22,24 @@ class TestTvDenoise(): # which dtype? assert denoised_lena.dtype in [np.float, np.float32, np.float64] from scipy import ndimage - grad = ndimage.morphological_gradient(lena, size=((3,3))) - grad_denoised = ndimage.morphological_gradient(denoised_lena, size=((3,3))) + grad = ndimage.morphological_gradient(lena, size=((3, 3))) + grad_denoised = ndimage.morphological_gradient( + denoised_lena, size=((3, 3))) # test if the total variation has decreased - assert np.sqrt((grad_denoised**2).sum()) < np.sqrt((grad**2).sum()) / 2 - denoised_lena_int = filter.tv_denoise(img_as_uint(lena), - weight=60.0, keep_type=True) - assert denoised_lena_int.dtype is np.dtype('uint16') + assert grad_denoised.dtype == np.float + assert (np.sqrt((grad_denoised**2).sum()) + < np.sqrt((grad**2).sum()) / 2) + def test_tv_denoise_float_result_range(self): + # lena image + lena = color.rgb2gray(data.lena())[:256, :256] + int_lena = np.multiply(lena, 255).astype(np.uint8) + assert np.max(int_lena) > 1 + denoised_int_lena = filter.tv_denoise(int_lena, weight=60.0) + # test if the value range of output float data is within [0.0:1.0] + assert denoised_int_lena.dtype == np.float + assert np.max(denoised_int_lena) <= 1.0 + assert np.min(denoised_int_lena) >= 0.0 def test_tv_denoise_3d(self): """ @@ -37,19 +47,16 @@ class TestTvDenoise(): a sphere. """ x, y, z = np.ogrid[0:40, 0:40, 0:40] - mask = (x -22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 + mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 mask = 100 * mask.astype(np.float) mask += 60 - mask += 20*np.random.randn(*mask.shape) + mask += 20 * np.random.randn(*mask.shape) mask[mask < 0] = 0 mask[mask > 255] = 255 - res = filter.tv_denoise(mask.astype(np.uint8), - weight=100, keep_type=True) - assert res.std() < mask.std() - assert res.dtype is np.dtype('uint8') res = filter.tv_denoise(mask.astype(np.uint8), weight=100) - assert res.std() < mask.std() - assert res.dtype is not np.dtype('uint8') + assert res.dtype == np.float + assert res.std() * 255 < mask.std() + # test wrong number of dimensions a = np.random.random((8, 8, 8, 8)) try: diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 4e3ebca3..77f244fc 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -16,7 +16,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, Parameters ---------- - image : NxM ndarray + image : (N, M) ndarray Input image. block_size : int Uneven size of pixel neighborhood which is used to calculate the @@ -24,11 +24,13 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, method : {'generic', 'gaussian', 'mean', 'median'}, optional Method used to determine adaptive threshold for local neighbourhood in weighted mean image. - * 'generic': use custom function (see `param` parameter) - * 'gaussian': apply gaussian filter (see `param` parameter for custom - sigma value) - * 'mean': apply arithmetic mean filter - * 'median' apply median rank filter + + * 'generic': use custom function (see `param` parameter) + * 'gaussian': apply gaussian filter (see `param` parameter for custom + sigma value) + * 'mean': apply arithmetic mean filter + * 'median' apply median rank filter + By default the 'gaussian' method is used. offset : float, optional Constant subtracted from weighted mean of neighborhood to calculate @@ -45,7 +47,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, Returns ------- - threshold : NxM ndarray + threshold : (N, M) ndarray Thresholded binary image References @@ -86,6 +88,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, return image > (thresh_image - offset) + def threshold_otsu(image, nbins=256): """Return threshold value based on Otsu's method. diff --git a/skimage/graph/_mcp.pyx b/skimage/graph/_mcp.pyx index beb814fe..320493c2 100644 --- a/skimage/graph/_mcp.pyx +++ b/skimage/graph/_mcp.pyx @@ -102,7 +102,7 @@ def _offset_edge_map(shape, offsets): """ indices = np.indices(shape) # indices.shape = (n,)+shape - + #get the distance from each index to the upper or lower edge in each dim pos_edges = (shape - indices.T).T neg_edges = -1 - indices @@ -112,7 +112,7 @@ def _offset_edge_map(shape, offsets): mins = offsets.min(axis=0) for pos, neg, mx, mn in zip(pos_edges, neg_edges, maxes, mins): pos[pos > mx] = 0 - neg[neg < mn] = 0 + neg[neg < mn] = 0 return pos_edges.astype(EDGE_D), neg_edges.astype(EDGE_D) def make_offsets(d, fully_connected): @@ -130,8 +130,8 @@ def make_offsets(d, fully_connected): ------- offsets : list of tuples of length `d` - Example - ------- + Examples + -------- The singly-connected 2-d neighborhood is four offsets: @@ -216,7 +216,7 @@ cdef class MCP: `costs` array at each point on the path. The class MCP_Geometric, on the other hand, accounts for the fact that diagonal vs. axial moves are of different lengths, and weights the path cost accordingly. - + Array elements with infinite or negative costs will simply be ignored, as will paths whose cumulative cost overflows to infinite. @@ -295,7 +295,7 @@ cdef class MCP: pos, neg = _offset_edge_map(costs.shape, self.offsets) self.flat_pos_edge_map = pos.reshape((self.dim, size), order='F') self.flat_neg_edge_map = neg.reshape((self.dim, size), order='F') - + # The offset lengths are the distances traveled along each offset self.offset_lengths = np.sqrt( @@ -449,7 +449,7 @@ cdef class MCP: # edge along any axis is_at_edge = 0 for d in range(dim): - if (flat_pos_edge_map[d, index] != 0 or + if (flat_pos_edge_map[d, index] != 0 or flat_neg_edge_map[d, index] != 0): is_at_edge = 1 break @@ -490,7 +490,7 @@ cdef class MCP: new_cost = flat_costs[new_index] if new_cost < 0 or new_cost == inf: continue - + # Now we ask the heap to append or update the cost to # this new point, but only if that point isn't already # in the heap, or it is but the new cost is lower. diff --git a/skimage/graph/_spath.pyx b/skimage/graph/_spath.pyx index f342021d..1624bc7d 100644 --- a/skimage/graph/_spath.pyx +++ b/skimage/graph/_spath.pyx @@ -2,9 +2,8 @@ import _mcp cimport _mcp +from libc.math cimport fabs -cdef extern from "math.h": - double fabs(double f) cdef class MCP_Diff(_mcp.MCP): """MCP_Diff(costs, offsets=None, fully_connected=True) diff --git a/skimage/graph/mcp.py b/skimage/graph/mcp.py index a803f58c..dc584226 100644 --- a/skimage/graph/mcp.py +++ b/skimage/graph/mcp.py @@ -1,6 +1,8 @@ -from ._mcp import MCP, MCP_Geometric, make_offsets +from ._mcp import MCP, MCP_Geometric -def route_through_array(array, start, end, fully_connected=True, geometric=True): + +def route_through_array(array, start, end, fully_connected=True, + geometric=True): """Simple example of how to use the MCP and MCP_Geometric classes. See the MCP and MCP_Geometric class documentation for explanation of the @@ -26,7 +28,56 @@ def route_through_array(array, start, end, fully_connected=True, geometric=True) path : list List of n-d index tuples defining the path from `start` to `end`. cost : float - Cost of the path. + Cost of the path. If `geometric` is False, the cost of the path is + the sum of the values of `array` along the path. If `geometric` is + True, a finer computation is made (see the documentation of the + MCP_Geometric class). + + See Also + -------- + MCP, MCP_Geometric + + Examples + -------- + >>> import numpy as np + >>> from skimage.graph import route_through_array + >>> + >>> image = np.array([[1, 3], [10, 12]]) + >>> image + array([[ 1, 3], + [10, 12]]) + >>> # Forbid diagonal steps + >>> route_through_array(image, [0, 0], [1, 1], fully_connected=False) + ([(0, 0), (0, 1), (1, 1)], 9.5) + >>> # Now allow diagonal steps: the path goes directly from start to end + >>> route_through_array(image, [0, 0], [1, 1]) + ([(0, 0), (1, 1)], 9.1923881554251192) + >>> # Cost is the sum of array values along the path (16 = 1 + 3 + 12) + >>> route_through_array(image, [0, 0], [1, 1], fully_connected=False, + ... geometric=False) + ([(0, 0), (0, 1), (1, 1)], 16.0) + >>> # Larger array where we display the path that is selected + >>> image = np.arange((36)).reshape((6, 6)) + >>> image + array([[ 0, 1, 2, 3, 4, 5], + [ 6, 7, 8, 9, 10, 11], + [12, 13, 14, 15, 16, 17], + [18, 19, 20, 21, 22, 23], + [24, 25, 26, 27, 28, 29], + [30, 31, 32, 33, 34, 35]]) + >>> # Find the path with lowest cost + >>> indices, weight = route_through_array(image, (0, 0), (5, 5)) + >>> indices = np.array(indices).T + >>> path = np.zeros_like(image) + >>> path[indices[0], indices[1]] = 1 + >>> path + array([[1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1]]) + """ start, end = tuple(start), tuple(end) if geometric: diff --git a/skimage/graph/setup.py b/skimage/graph/setup.py index 5e0a2f06..463d2739 100644 --- a/skimage/graph/setup.py +++ b/skimage/graph/setup.py @@ -5,6 +5,7 @@ import os.path base_path = os.path.abspath(os.path.dirname(__file__)) + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs @@ -28,10 +29,10 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Graph-based Image-processing Algorithms', - url = 'https://github.com/scikits-image/scikits-image', - license = 'Modified BSD', + setup(maintainer='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Graph-based Image-processing Algorithms', + url='https://github.com/scikit-image/scikit-image', + license='Modified BSD', **(configuration(top_path='').todict()) ) diff --git a/skimage/graph/spath.py b/skimage/graph/spath.py index a912a693..d8ec3526 100644 --- a/skimage/graph/spath.py +++ b/skimage/graph/spath.py @@ -1,6 +1,7 @@ import numpy as np from . import _spath + def shortest_path(arr, reach=1, axis=-1, output_indexlist=False): """Find the shortest path through an n-d array from one side to another. @@ -39,7 +40,7 @@ def shortest_path(arr, reach=1, axis=-1, output_indexlist=False): # a grid defined by the reach. if axis < 0: axis += arr.ndim - offset_ind_shape = (2*reach + 1,) * (arr.ndim - 1) + offset_ind_shape = (2 * reach + 1,) * (arr.ndim - 1) offset_indices = np.indices(offset_ind_shape) - reach offset_indices = np.insert(offset_indices, axis, np.ones(offset_ind_shape), axis=0) @@ -49,7 +50,7 @@ def shortest_path(arr, reach=1, axis=-1, output_indexlist=False): # Valid starting positions are anywhere on the hyperplane defined by # position 0 on the given axis. Ending positions are anywhere on the # hyperplane at position -1 along the same. - non_axis_shape = arr.shape[:axis] + arr.shape[axis+1:] + non_axis_shape = arr.shape[:axis] + arr.shape[axis + 1:] non_axis_indices = np.indices(non_axis_shape) non_axis_size = np.multiply.reduce(non_axis_shape) start_indices = np.insert(non_axis_indices, axis, @@ -72,7 +73,7 @@ def shortest_path(arr, reach=1, axis=-1, output_indexlist=False): if not output_indexlist: traceback = np.array(traceback) - traceback = np.concatenate([traceback[:,:axis], traceback[:,axis+1:]], + traceback = np.concatenate([traceback[:, :axis], traceback[:, axis + 1:]], axis=1) traceback = np.squeeze(traceback) diff --git a/skimage/graph/tests/test_heap.py b/skimage/graph/tests/test_heap.py index 0cc41c92..8322fd4e 100644 --- a/skimage/graph/tests/test_heap.py +++ b/skimage/graph/tests/test_heap.py @@ -1,22 +1,23 @@ -import numpy as np from numpy.testing import * import time import random import skimage.graph.heap as heap + def test_heap(): _test_heap(100000, True) _test_heap(100000, False) + def _test_heap(n, fast_update): # generate random numbers with duplicates random.seed(0) - a = [random.uniform(1.0,100.0) for i in range(n//2)] - a = a+a - + a = [random.uniform(1.0, 100.0) for i in range(n // 2)] + a = a + a + t0 = time.clock() - + # insert in heap with random removals if fast_update: h = heap.FastUpdateBinaryHeap(128, n) @@ -25,12 +26,12 @@ def _test_heap(n, fast_update): for i in range(len(a)): h.push(a[i], i) if a[i] < 25: - # double-push same ref sometimes to test fast update codepaths - h.push(2*a[i], i) + # double-push same ref sometimes to test fast update codepaths + h.push(2 * a[i], i) if 25 < a[i] < 50: - # pop some to test random removal - h.pop() - + # pop some to test random removal + h.pop() + # pop from heap b = [] while True: @@ -38,14 +39,14 @@ def _test_heap(n, fast_update): b.append(h.pop()[0]) except IndexError: break - + t1 = time.clock() - + # verify - for i in range(1,len(b)): - assert(b[i] >= b[i-1]) - - return t1-t0 + for i in range(1, len(b)): + assert(b[i] >= b[i - 1]) + + return t1 - t0 if __name__ == "__main__": run_module_suite() diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index 9d36a6ec..e3fd45a0 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -3,7 +3,7 @@ from numpy.testing import * import skimage.graph.mcp as mcp -a = np.ones((8,8), dtype=np.float32) +a = np.ones((8, 8), dtype=np.float32) a[1:-1, 1] = 0 a[1, 1:-1] = 0 @@ -16,19 +16,20 @@ a[1, 1:-1] = 0 ## [ 1., 0., 1., 1., 1., 1., 1., 1.], ## [ 1., 1., 1., 1., 1., 1., 1., 1.]], dtype=float32) + def test_basic(): m = mcp.MCP(a, fully_connected=True) - costs, traceback = m.find_costs([(1,6)]) + costs, traceback = m.find_costs([(1, 6)]) return_path = m.traceback((7, 2)) assert_array_equal(costs, - [[ 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 0., 0., 0., 0., 0., 0., 1.], - [ 1., 0., 1., 1., 1., 1., 1., 1.], - [ 1., 0., 1., 2., 2., 2., 2., 2.], - [ 1., 0., 1., 2., 3., 3., 3., 3.], - [ 1., 0., 1., 2., 3., 4., 4., 4.], - [ 1., 0., 1., 2., 3., 4., 5., 5.], - [ 1., 1., 1., 2., 3., 4., 5., 6.]]) + [[1., 1., 1., 1., 1., 1., 1., 1.], + [1., 0., 0., 0., 0., 0., 0., 1.], + [1., 0., 1., 1., 1., 1., 1., 1.], + [1., 0., 1., 2., 2., 2., 2., 2.], + [1., 0., 1., 2., 3., 3., 3., 3.], + [1., 0., 1., 2., 3., 4., 4., 4.], + [1., 0., 1., 2., 3., 4., 5., 5.], + [1., 1., 1., 2., 3., 4., 5., 6.]]) assert_array_equal(return_path, [(1, 6), @@ -43,8 +44,9 @@ def test_basic(): (6, 1), (7, 2)]) + def test_neg_inf(): - expected_costs = np.where(a==1, np.inf, 0) + expected_costs = np.where(a == 1, np.inf, 0) expected_path = [(1, 6), (1, 5), (1, 4), @@ -55,8 +57,8 @@ def test_neg_inf(): (4, 1), (5, 1), (6, 1)] - test_neg = np.where(a==1, -1, 0) - test_inf = np.where(a==1, np.inf, 0) + test_neg = np.where(a == 1, -1, 0) + test_inf = np.where(a == 1, np.inf, 0) m = mcp.MCP(test_neg, fully_connected=True) costs, traceback = m.find_costs([(1, 6)]) return_path = m.traceback((6, 1)) @@ -67,11 +69,12 @@ def test_neg_inf(): return_path = m.traceback((6, 1)) assert_array_equal(costs, expected_costs) assert_array_equal(return_path, expected_path) - + def test_route(): - return_path, cost = mcp.route_through_array(a, (1,6), (7,2), geometric=True) - assert_almost_equal(cost, np.sqrt(2)/2) + return_path, cost = mcp.route_through_array(a, (1, 6), (7, 2), + geometric=True) + assert_almost_equal(cost, np.sqrt(2) / 2) assert_array_equal(return_path, [(1, 6), (1, 5), @@ -85,19 +88,20 @@ def test_route(): (6, 1), (7, 2)]) + def test_no_diagonal(): m = mcp.MCP(a, fully_connected=False) - costs, traceback = m.find_costs([(1,6)]) + costs, traceback = m.find_costs([(1, 6)]) return_path = m.traceback((7, 2)) assert_array_equal(costs, - [[ 2., 1., 1., 1., 1., 1., 1., 2.], - [ 1., 0., 0., 0., 0., 0., 0., 1.], - [ 1., 0., 1., 1., 1., 1., 1., 2.], - [ 1., 0., 1., 2., 2., 2., 2., 3.], - [ 1., 0., 1., 2., 3., 3., 3., 4.], - [ 1., 0., 1., 2., 3., 4., 4., 5.], - [ 1., 0., 1., 2., 3., 4., 5., 6.], - [ 2., 1., 2., 3., 4., 5., 6., 7.]]) + [[2., 1., 1., 1., 1., 1., 1., 2.], + [1., 0., 0., 0., 0., 0., 0., 1.], + [1., 0., 1., 1., 1., 1., 1., 2.], + [1., 0., 1., 2., 2., 2., 2., 3.], + [1., 0., 1., 2., 3., 3., 3., 4.], + [1., 0., 1., 2., 3., 4., 4., 5.], + [1., 0., 1., 2., 3., 4., 5., 6.], + [2., 1., 2., 3., 4., 5., 6., 7.]]) assert_array_equal(return_path, [(1, 6), (1, 5), @@ -115,34 +119,36 @@ def test_no_diagonal(): def test_offsets(): - offsets = [(1,i) for i in range(10)] + [(1, -i) for i in range(1,10)] - m = mcp.MCP(a, offsets=offsets) - costs, traceback = m.find_costs([(1,6)]) - assert_array_equal(traceback, - [[-1, -1, -1, -1, -1, -1, -1, -1], - [-1, -1, -1, -1, -1, -1, -1, -1], - [15, 14, 13, 12, 11, 10, 0, 1], - [10, 0, 1, 2, 3, 4, 5, 6], - [10, 0, 1, 2, 3, 4, 5, 6], - [10, 0, 1, 2, 3, 4, 5, 6], - [10, 0, 1, 2, 3, 4, 5, 6], - [10, 0, 1, 2, 3, 4, 5, 6]]) - + offsets = [(1, i) for i in range(10)] + [(1, -i) for i in range(1, 10)] + m = mcp.MCP(a, offsets=offsets) + costs, traceback = m.find_costs([(1, 6)]) + assert_array_equal(traceback, + [[-1, -1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1, -1], + [15, 14, 13, 12, 11, 10, 0, 1], + [10, 0, 1, 2, 3, 4, 5, 6], + [10, 0, 1, 2, 3, 4, 5, 6], + [10, 0, 1, 2, 3, 4, 5, 6], + [10, 0, 1, 2, 3, 4, 5, 6], + [10, 0, 1, 2, 3, 4, 5, 6]]) + def test_crashing(): - for shape in [(100, 100), (5, 8, 13, 17)]*5: + for shape in [(100, 100), (5, 8, 13, 17)] * 5: yield _test_random, shape + def _test_random(shape): # Just tests for crashing -- not for correctness. np.random.seed(0) a = np.random.random(shape).astype(np.float32) - starts = [[0]*len(shape), [-1]*len(shape), - (np.random.random(len(shape))*shape).astype(int)] - ends = [(np.random.random(len(shape))*shape).astype(int) for i in range(4)] + starts = [[0] * len(shape), [-1] * len(shape), + (np.random.random(len(shape)) * shape).astype(int)] + ends = [(np.random.random(len(shape)) * shape).astype(int) + for i in range(4)] m = mcp.MCP(a, fully_connected=True) costs, offsets = m.find_costs(starts) - for point in [(np.random.random(len(shape))*shape).astype(int) + for point in [(np.random.random(len(shape)) * shape).astype(int) for i in range(4)]: m.traceback(point) m._reset() diff --git a/skimage/graph/tests/test_spath.py b/skimage/graph/tests/test_spath.py index a9f6b274..62f9f303 100644 --- a/skimage/graph/tests/test_spath.py +++ b/skimage/graph/tests/test_spath.py @@ -3,6 +3,7 @@ from numpy.testing import * import skimage.graph.spath as spath + def test_basic(): x = np.array([[1, 1, 3], [0, 2, 0], @@ -11,6 +12,7 @@ def test_basic(): assert_array_equal(path, [0, 0, 1]) assert_equal(cost, 1) + def test_reach(): x = np.array([[1, 1, 3], [0, 2, 0], @@ -19,6 +21,7 @@ def test_reach(): assert_array_equal(path, [0, 0, 2]) assert_equal(cost, 0) + def test_non_square(): x = np.array([[1, 1, 1, 1, 5, 5, 5], [5, 0, 0, 5, 9, 1, 1], diff --git a/skimage/io/__init__.py b/skimage/io/__init__.py index c5dd0b77..5e701a51 100644 --- a/skimage/io/__init__.py +++ b/skimage/io/__init__.py @@ -31,7 +31,7 @@ def _load_preferred_plugins(): try: use_plugin(plugin, kind=func) break - except (ImportError, RuntimeError): + except (ImportError, RuntimeError, OSError): pass # Use PIL as the default imread plugin, since matplotlib (1.2.x) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 05a7a8b7..eea7e509 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,13 +1,59 @@ -__all__ = ['imread', 'imread_collection', 'imsave', 'imshow', 'show', +__all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] +from io import BytesIO + +import numpy as np + from skimage.io._plugins import call as call_plugin from skimage.color import rgb2grey -import numpy as np + # Shared image queue _image_stack = [] + +class Image(np.ndarray): + """Class representing Image data. + + These objects have tags for image metadata and IPython display protocol + methods for image display. + """ + + tags = {'filename': '', + 'EXIF': {}, + 'info': {}} + + def __new__(cls, arr, **kwargs): + """Set the image data and tags according to given parameters. + + Parameters + ---------- + arr : ndarray + Image data. + kwargs : Image tags as keywords + Specified in the form ``tag0=value``, ``tag1=value``. + + """ + x = np.asarray(arr).view(cls) + for tag, value in Image.tags.items(): + setattr(x, tag, kwargs.get(tag, getattr(arr, tag, value))) + return x + + def _repr_png_(self): + return self._repr_image_format('png') + + def _repr_jpeg_(self): + return self._repr_image_format('jpeg') + + def _repr_image_format(self, format_str): + str_buffer = BytesIO() + imsave(str_buffer, self, format_str=format_str) + return_str = str_buffer.getvalue() + str_buffer.close() + return return_str + + def push(img): """Push an image onto the shared image stack. @@ -22,6 +68,7 @@ def push(img): _image_stack.append(img) + def pop(): """Pop an image from the shared image stack. @@ -33,6 +80,7 @@ def pop(): """ return _image_stack.pop() + def imread(fname, as_grey=False, plugin=None, flatten=None, **plugin_args): """Load an image from file. @@ -74,7 +122,8 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, if as_grey and getattr(img, 'ndim', 0) >= 3: img = rgb2grey(img) - return img + return Image(img) + def imread_collection(load_pattern, conserve_memory=True, plugin=None, **plugin_args): @@ -128,13 +177,14 @@ def imsave(fname, arr, plugin=None, **plugin_args): """ return call_plugin('imsave', fname, arr, plugin=plugin, **plugin_args) + def imshow(arr, plugin=None, **plugin_args): """Display an image. Parameters ---------- - arr : ndarray - Image data. + arr : ndarray or str + Image data or name of image file. plugin : str Name of plugin to use. By default, the different plugins are tried (starting with the Python Imaging Library) until a suitable @@ -146,8 +196,11 @@ def imshow(arr, plugin=None, **plugin_args): Passed to the given plugin. """ + if isinstance(arr, basestring): + arr = call_plugin('imread', arr, plugin=plugin) return call_plugin('imshow', arr, plugin=plugin, **plugin_args) + def show(): '''Display pending images. diff --git a/skimage/io/_plugins/_colormixer.pyx b/skimage/io/_plugins/_colormixer.pyx index 5d0dd1b9..ace45c94 100644 --- a/skimage/io/_plugins/_colormixer.pyx +++ b/skimage/io/_plugins/_colormixer.pyx @@ -8,15 +8,10 @@ integers, so currently the only way to clip results efficiently one. """ - +import cython import numpy as np cimport numpy as np - -import cython - -cdef extern from "math.h": - float exp(float) nogil - float pow(float, float) nogil +from libc.math cimport exp, pow @cython.boundscheck(False) @@ -189,7 +184,6 @@ def sigmoid_gamma(np.ndarray[np.uint8_t, ndim=3] img, img[i,j,2] = lut[stateimg[i,j,2]] - @cython.boundscheck(False) def gamma(np.ndarray[np.uint8_t, ndim=3] img, np.ndarray[np.uint8_t, ndim=3] stateimg, @@ -219,7 +213,6 @@ def gamma(np.ndarray[np.uint8_t, ndim=3] img, img[i,j,2] = lut[stateimg[i,j,2]] - @cython.cdivision(True) cdef void rgb_2_hsv(float* RGB, float* HSV) nogil: cdef float R, G, B, H, S, V, MAX, MIN @@ -283,6 +276,7 @@ cdef void rgb_2_hsv(float* RGB, float* HSV) nogil: HSV[1] = S HSV[2] = V + @cython.cdivision(True) cdef void hsv_2_rgb(float* HSV, float* RGB) nogil: cdef float H, S, V @@ -388,6 +382,7 @@ def py_hsv_2_rgb(H, S, V): return (R, G, B) + def py_rgb_2_hsv(R, G, B): '''Convert an HSV value to RGB. @@ -561,9 +556,3 @@ def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img, img[i, j, 0] = RGB[0] img[i, j, 1] = RGB[1] img[i, j, 2] = RGB[2] - - - - - - diff --git a/skimage/io/_plugins/fits_plugin.py b/skimage/io/_plugins/fits_plugin.py index b6627032..52785814 100644 --- a/skimage/io/_plugins/fits_plugin.py +++ b/skimage/io/_plugins/fits_plugin.py @@ -1,6 +1,5 @@ __all__ = ['imread', 'imread_collection'] -import numpy as np import skimage.io as io try: @@ -49,9 +48,9 @@ def imread(fname, dtype=None): for hdu in hdulist: if isinstance(hdu, pyfits.ImageHDU) or \ isinstance(hdu, pyfits.PrimaryHDU): - if hdu.data is not None: - img_array = hdu.data - break + if hdu.data is not None: + img_array = hdu.data + break hdulist.close() return img_array @@ -109,7 +108,7 @@ def imread_collection(load_pattern, conserve_memory=True): def FITSFactory(image_ext): """Load an image extension from a FITS file and return a NumPy array - + Parameters ---------- @@ -136,7 +135,7 @@ def FITSFactory(image_ext): raise ValueError("Expected a (filename, extension) tuple") hdulist = pyfits.open(filename) - + data = hdulist[extnum].data hdulist.close() @@ -146,4 +145,3 @@ def FITSFactory(image_ext): (extnum, filename)) return data - diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index 5d023909..d2a9d4f9 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -20,7 +20,7 @@ def _generate_candidate_libs(): lib_dirs.append(os.path.join(os.environ['HOME'], 'lib')) lib_dirs = [ld for ld in lib_dirs if os.path.exists(ld)] - lib_names = ['libfreeimage', 'freeimage'] # should be lower-case! + lib_names = ['libfreeimage', 'freeimage'] # should be lower-case! # Now attempt to find libraries of that name in the given directory # (case-insensitive and without regard for extension) lib_paths = [] @@ -34,6 +34,7 @@ def _generate_candidate_libs(): return lib_dirs, lib_paths + def load_freeimage(): if sys.platform == 'win32': loader = ctypes.windll @@ -70,14 +71,14 @@ def load_freeimage(): if errors: # No freeimage library loaded, and load-errors reported for some # candidate libs - err_txt = ['%s:\n%s'%(l, str(e.message)) for l, e in errors] - raise OSError('One or more FreeImage libraries were found, but ' - 'could not be loaded due to the following errors:\n'+ - '\n\n'.join(err_txt)) + err_txt = ['%s:\n%s' % (l, str(e.message)) for l, e in errors] + raise RuntimeError('One or more FreeImage libraries were found, but ' + 'could not be loaded due to the following errors:\n' + '\n\n'.join(err_txt)) else: # No errors, because no potential libraries found at all! - raise OSError('Could not find a FreeImage library in any of:\n'+ - '\n'.join(lib_dirs)) + raise RuntimeError('Could not find a FreeImage library in any of:\n' + + '\n'.join(lib_dirs)) # FreeImage found @functype(None, ctypes.c_int, ctypes.c_char_p) @@ -113,7 +114,9 @@ API = { } # Albert's ctypes pattern -def register_api(lib,api): + + +def register_api(lib, api): for f, (restype, argtypes) in api.items(): func = getattr(lib, f) func.restype = restype @@ -153,20 +156,20 @@ class FI_TYPES(object): } fi_types = { - (numpy.uint8, 1): FIT_BITMAP, - (numpy.uint8, 3): FIT_BITMAP, - (numpy.uint8, 4): FIT_BITMAP, - (numpy.uint16, 1): FIT_UINT16, - (numpy.int16, 1): FIT_INT16, - (numpy.uint32, 1): FIT_UINT32, - (numpy.int32, 1): FIT_INT32, - (numpy.float32, 1): FIT_FLOAT, - (numpy.float64, 1): FIT_DOUBLE, - (numpy.complex128, 1): FIT_COMPLEX, - (numpy.uint16, 3): FIT_RGB16, - (numpy.uint16, 4): FIT_RGBA16, - (numpy.float32, 3): FIT_RGBF, - (numpy.float32, 4): FIT_RGBAF + (numpy.dtype('uint8'), 1): FIT_BITMAP, + (numpy.dtype('uint8'), 3): FIT_BITMAP, + (numpy.dtype('uint8'), 4): FIT_BITMAP, + (numpy.dtype('uint16'), 1): FIT_UINT16, + (numpy.dtype('int16'), 1): FIT_INT16, + (numpy.dtype('uint32'), 1): FIT_UINT32, + (numpy.dtype('int32'), 1): FIT_INT32, + (numpy.dtype('float32'), 1): FIT_FLOAT, + (numpy.dtype('float64'), 1): FIT_DOUBLE, + (numpy.dtype('complex128'), 1): FIT_COMPLEX, + (numpy.dtype('uint16'), 3): FIT_RGB16, + (numpy.dtype('uint16'), 4): FIT_RGBA16, + (numpy.dtype('float32'), 3): FIT_RGBF, + (numpy.dtype('float32'), 4): FIT_RGBAF } extra_dims = { @@ -205,111 +208,112 @@ class FI_TYPES(object): extra_dims = cls.extra_dims[fi_type] return numpy.dtype(dtype), extra_dims + [w, h] + class IO_FLAGS(object): - FIF_LOAD_NOPIXELS = 0x8000 # loading: load the image header only - # (not supported by all plugins) + FIF_LOAD_NOPIXELS = 0x8000 # loading: load the image header only + # (not supported by all plugins) BMP_DEFAULT = 0 BMP_SAVE_RLE = 1 CUT_DEFAULT = 0 DDS_DEFAULT = 0 - EXR_DEFAULT = 0 # save data as half with piz-based wavelet compression - EXR_FLOAT = 0x0001 # save data as float instead of as half (not recommended) - EXR_NONE = 0x0002 # save with no compression - EXR_ZIP = 0x0004 # save with zlib compression, in blocks of 16 scan lines - EXR_PIZ = 0x0008 # save with piz-based wavelet compression - EXR_PXR24 = 0x0010 # save with lossy 24-bit float compression - EXR_B44 = 0x0020 # save with lossy 44% float compression + EXR_DEFAULT = 0 # save data as half with piz-based wavelet compression + EXR_FLOAT = 0x0001 # save data as float instead of as half (not recommended) + EXR_NONE = 0x0002 # save with no compression + EXR_ZIP = 0x0004 # save with zlib compression, in blocks of 16 scan lines + EXR_PIZ = 0x0008 # save with piz-based wavelet compression + EXR_PXR24 = 0x0010 # save with lossy 24-bit float compression + EXR_B44 = 0x0020 # save with lossy 44% float compression # - goes to 22% when combined with EXR_LC - EXR_LC = 0x0040 # save images with one luminance and two chroma channels, + EXR_LC = 0x0040 # save images with one luminance and two chroma channels, # rather than as RGB (lossy compression) FAXG3_DEFAULT = 0 GIF_DEFAULT = 0 - GIF_LOAD256 = 1 # Load the image as a 256 color image with ununsed - # palette entries, if it's 16 or 2 color - GIF_PLAYBACK = 2 # 'Play' the GIF to generate each frame (as 32bpp) - # instead of returning raw frame data when loading + GIF_LOAD256 = 1 # Load the image as a 256 color image with ununsed + # palette entries, if it's 16 or 2 color + GIF_PLAYBACK = 2 # 'Play' the GIF to generate each frame (as 32bpp) + # instead of returning raw frame data when loading HDR_DEFAULT = 0 ICO_DEFAULT = 0 - ICO_MAKEALPHA = 1 # convert to 32bpp and create an alpha channel from the - # AND-mask when loading + ICO_MAKEALPHA = 1 # convert to 32bpp and create an alpha channel from the + # AND-mask when loading IFF_DEFAULT = 0 - J2K_DEFAULT = 0 # save with a 16:1 rate - JP2_DEFAULT = 0 # save with a 16:1 rate - JPEG_DEFAULT = 0 # loading (see JPEG_FAST); - # saving (see JPEG_QUALITYGOOD|JPEG_SUBSAMPLING_420) - JPEG_FAST = 0x0001 # load the file as fast as possible, - # sacrificing some quality - JPEG_ACCURATE = 0x0002 # load the file with the best quality, - # sacrificing some speed - JPEG_CMYK = 0x0004 # load separated CMYK "as is" - # (use | to combine with other load flags) - JPEG_EXIFROTATE = 0x0008 # load and rotate according to - # Exif 'Orientation' tag if available - JPEG_QUALITYSUPERB = 0x80 # save with superb quality (100:1) - JPEG_QUALITYGOOD = 0x0100 # save with good quality (75:1) - JPEG_QUALITYNORMAL = 0x0200 # save with normal quality (50:1) - JPEG_QUALITYAVERAGE = 0x0400 # save with average quality (25:1) - JPEG_QUALITYBAD = 0x0800 # save with bad quality (10:1) - JPEG_PROGRESSIVE = 0x2000 # save as a progressive-JPEG - # (use | to combine with other save flags) - JPEG_SUBSAMPLING_411 = 0x1000 # save with high 4x1 chroma - # subsampling (4:1:1) - JPEG_SUBSAMPLING_420 = 0x4000 # save with medium 2x2 medium chroma - # subsampling (4:2:0) - default value - JPEG_SUBSAMPLING_422 = 0x8000 # save with low 2x1 chroma subsampling (4:2:2) - JPEG_SUBSAMPLING_444 = 0x10000 # save with no chroma subsampling (4:4:4) - JPEG_OPTIMIZE = 0x20000 # on saving, compute optimal Huffman coding tables - # (can reduce a few percent of file size) - JPEG_BASELINE = 0x40000 # save basic JPEG, without metadata or any markers + J2K_DEFAULT = 0 # save with a 16:1 rate + JP2_DEFAULT = 0 # save with a 16:1 rate + JPEG_DEFAULT = 0 # loading (see JPEG_FAST); + # saving (see JPEG_QUALITYGOOD|JPEG_SUBSAMPLING_420) + JPEG_FAST = 0x0001 # load the file as fast as possible, + # sacrificing some quality + JPEG_ACCURATE = 0x0002 # load the file with the best quality, + # sacrificing some speed + JPEG_CMYK = 0x0004 # load separated CMYK "as is" + # (use | to combine with other load flags) + JPEG_EXIFROTATE = 0x0008 # load and rotate according to + # Exif 'Orientation' tag if available + JPEG_QUALITYSUPERB = 0x80 # save with superb quality (100:1) + JPEG_QUALITYGOOD = 0x0100 # save with good quality (75:1) + JPEG_QUALITYNORMAL = 0x0200 # save with normal quality (50:1) + JPEG_QUALITYAVERAGE = 0x0400 # save with average quality (25:1) + JPEG_QUALITYBAD = 0x0800 # save with bad quality (10:1) + JPEG_PROGRESSIVE = 0x2000 # save as a progressive-JPEG + # (use | to combine with other save flags) + JPEG_SUBSAMPLING_411 = 0x1000 # save with high 4x1 chroma + # subsampling (4:1:1) + JPEG_SUBSAMPLING_420 = 0x4000 # save with medium 2x2 medium chroma + # subsampling (4:2:0) - default value + JPEG_SUBSAMPLING_422 = 0x8000 # save with low 2x1 chroma subsampling (4:2:2) + JPEG_SUBSAMPLING_444 = 0x10000 # save with no chroma subsampling (4:4:4) + JPEG_OPTIMIZE = 0x20000 # on saving, compute optimal Huffman coding tables + # (can reduce a few percent of file size) + JPEG_BASELINE = 0x40000 # save basic JPEG, without metadata or any markers KOALA_DEFAULT = 0 LBM_DEFAULT = 0 MNG_DEFAULT = 0 PCD_DEFAULT = 0 - PCD_BASE = 1 # load the bitmap sized 768 x 512 - PCD_BASEDIV4 = 2 # load the bitmap sized 384 x 256 - PCD_BASEDIV16 = 3 # load the bitmap sized 192 x 128 + PCD_BASE = 1 # load the bitmap sized 768 x 512 + PCD_BASEDIV4 = 2 # load the bitmap sized 384 x 256 + PCD_BASEDIV16 = 3 # load the bitmap sized 192 x 128 PCX_DEFAULT = 0 PFM_DEFAULT = 0 PICT_DEFAULT = 0 PNG_DEFAULT = 0 - PNG_IGNOREGAMMA = 1 # loading: avoid gamma correction - PNG_Z_BEST_SPEED = 0x0001 # save using ZLib level 1 compression flag - # (default value is 6) - PNG_Z_DEFAULT_COMPRESSION = 0x0006 # save using ZLib level 6 compression - # flag (default recommended value) - PNG_Z_BEST_COMPRESSION = 0x0009 # save using ZLib level 9 compression flag - # (default value is 6) - PNG_Z_NO_COMPRESSION = 0x0100 # save without ZLib compression - PNG_INTERLACED = 0x0200 # save using Adam7 interlacing (use | to combine - # with other save flags) + PNG_IGNOREGAMMA = 1 # loading: avoid gamma correction + PNG_Z_BEST_SPEED = 0x0001 # save using ZLib level 1 compression flag + # (default value is 6) + PNG_Z_DEFAULT_COMPRESSION = 0x0006 # save using ZLib level 6 compression + # flag (default recommended value) + PNG_Z_BEST_COMPRESSION = 0x0009 # save using ZLib level 9 compression flag + # (default value is 6) + PNG_Z_NO_COMPRESSION = 0x0100 # save without ZLib compression + PNG_INTERLACED = 0x0200 # save using Adam7 interlacing (use | to combine + # with other save flags) PNM_DEFAULT = 0 - PNM_SAVE_RAW = 0 # Writer saves in RAW format (i.e. P4, P5 or P6) - PNM_SAVE_ASCII = 1 # Writer saves in ASCII format (i.e. P1, P2 or P3) + PNM_SAVE_RAW = 0 # Writer saves in RAW format (i.e. P4, P5 or P6) + PNM_SAVE_ASCII = 1 # Writer saves in ASCII format (i.e. P1, P2 or P3) PSD_DEFAULT = 0 - PSD_CMYK = 1 # reads tags for separated CMYK (default is conversion to RGB) - PSD_LAB = 2 # reads tags for CIELab (default is conversion to RGB) + PSD_CMYK = 1 # reads tags for separated CMYK (default is conversion to RGB) + PSD_LAB = 2 # reads tags for CIELab (default is conversion to RGB) RAS_DEFAULT = 0 - RAW_DEFAULT = 0 # load the file as linear RGB 48-bit - RAW_PREVIEW = 1 # try to load the embedded JPEG preview with included - # Exif Data or default to RGB 24-bit - RAW_DISPLAY = 2 # load the file as RGB 24-bit + RAW_DEFAULT = 0 # load the file as linear RGB 48-bit + RAW_PREVIEW = 1 # try to load the embedded JPEG preview with included + # Exif Data or default to RGB 24-bit + RAW_DISPLAY = 2 # load the file as RGB 24-bit SGI_DEFAULT = 0 TARGA_DEFAULT = 0 - TARGA_LOAD_RGB888 = 1 # Convert RGB555 and ARGB8888 -> RGB888. - TARGA_SAVE_RLE = 2 # Save with RLE compression + TARGA_LOAD_RGB888 = 1 # Convert RGB555 and ARGB8888 -> RGB888. + TARGA_SAVE_RLE = 2 # Save with RLE compression TIFF_DEFAULT = 0 - TIFF_CMYK = 0x0001 # reads/stores tags for separated CMYK - # (use | to combine with compression flags) - TIFF_PACKBITS = 0x0100 # save using PACKBITS compression - TIFF_DEFLATE = 0x0200 # save using DEFLATE (a.k.a. ZLIB) compression - TIFF_ADOBE_DEFLATE = 0x0400 # save using ADOBE DEFLATE compression - TIFF_NONE = 0x0800 # save without any compression - TIFF_CCITTFAX3 = 0x1000 # save using CCITT Group 3 fax encoding - TIFF_CCITTFAX4 = 0x2000 # save using CCITT Group 4 fax encoding - TIFF_LZW = 0x4000 # save using LZW compression - TIFF_JPEG = 0x8000 # save using JPEG compression - TIFF_LOGLUV = 0x10000 # save using LogLuv compression + TIFF_CMYK = 0x0001 # reads/stores tags for separated CMYK + # (use | to combine with compression flags) + TIFF_PACKBITS = 0x0100 # save using PACKBITS compression + TIFF_DEFLATE = 0x0200 # save using DEFLATE (a.k.a. ZLIB) compression + TIFF_ADOBE_DEFLATE = 0x0400 # save using ADOBE DEFLATE compression + TIFF_NONE = 0x0800 # save without any compression + TIFF_CCITTFAX3 = 0x1000 # save using CCITT Group 3 fax encoding + TIFF_CCITTFAX4 = 0x2000 # save using CCITT Group 4 fax encoding + TIFF_LZW = 0x4000 # save using LZW compression + TIFF_JPEG = 0x8000 # save using JPEG compression + TIFF_LOGLUV = 0x10000 # save using LogLuv compression WBMP_DEFAULT = 0 XBM_DEFAULT = 0 XPM_DEFAULT = 0 @@ -329,23 +333,23 @@ class METADATA_MODELS(object): class METADATA_DATATYPE(object): - FIDT_BYTE = 1 # 8-bit unsigned integer - FIDT_ASCII = 2 # 8-bit bytes w/ last byte null - FIDT_SHORT = 3 # 16-bit unsigned integer - FIDT_LONG = 4 # 32-bit unsigned integer - FIDT_RATIONAL = 5 # 64-bit unsigned fraction - FIDT_SBYTE = 6 # 8-bit signed integer - FIDT_UNDEFINED = 7 # 8-bit untyped data - FIDT_SSHORT = 8 # 16-bit signed integer - FIDT_SLONG = 9 # 32-bit signed integer - FIDT_SRATIONAL = 10 # 64-bit signed fraction - FIDT_FLOAT = 11 # 32-bit IEEE floating point - FIDT_DOUBLE = 12 # 64-bit IEEE floating point - FIDT_IFD = 13 # 32-bit unsigned integer (offset) - FIDT_PALETTE = 14 # 32-bit RGBQUAD - FIDT_LONG8 = 16 # 64-bit unsigned integer - FIDT_SLONG8 = 17 # 64-bit signed integer - FIDT_IFD8 = 18 # 64-bit unsigned integer (offset) + FIDT_BYTE = 1 # 8-bit unsigned integer + FIDT_ASCII = 2 # 8-bit bytes w/ last byte null + FIDT_SHORT = 3 # 16-bit unsigned integer + FIDT_LONG = 4 # 32-bit unsigned integer + FIDT_RATIONAL = 5 # 64-bit unsigned fraction + FIDT_SBYTE = 6 # 8-bit signed integer + FIDT_UNDEFINED = 7 # 8-bit untyped data + FIDT_SSHORT = 8 # 16-bit signed integer + FIDT_SLONG = 9 # 32-bit signed integer + FIDT_SRATIONAL = 10 # 64-bit signed fraction + FIDT_FLOAT = 11 # 32-bit IEEE floating point + FIDT_DOUBLE = 12 # 64-bit IEEE floating point + FIDT_IFD = 13 # 32-bit unsigned integer (offset) + FIDT_PALETTE = 14 # 32-bit RGBQUAD + FIDT_LONG8 = 16 # 64-bit unsigned integer + FIDT_SLONG8 = 17 # 64-bit signed integer + FIDT_IFD8 = 18 # 64-bit unsigned integer (offset) dtypes = { FIDT_BYTE: numpy.uint8, @@ -384,6 +388,7 @@ def _process_bitmap(filename, flags, process_func): finally: _FI.FreeImage_Unload(bitmap) + def read(filename, flags=0): """Read an image to a numpy array of shape (height, width) for greyscale images, or shape (height, width, nchannels) for RGB or @@ -394,6 +399,7 @@ def read(filename, flags=0): """ return _process_bitmap(filename, flags, _array_from_bitmap) + def read_metadata(filename): """Return a dict containing all image metadata. @@ -404,6 +410,7 @@ def read_metadata(filename): flags = IO_FLAGS.FIF_LOAD_NOPIXELS return _process_bitmap(filename, flags, _read_metadata) + def _process_multipage(filename, flags, process_func): filename = asbytes(filename) ftype = _FI.FreeImage_GetFileType(filename, 0) @@ -435,6 +442,7 @@ def _process_multipage(filename, flags, process_func): finally: _FI.FreeImage_CloseMultiBitmap(multibitmap, 0) + def read_multipage(filename, flags=0): """Read a multipage image to a list of numpy arrays, where each array is of shape (height, width) for greyscale images, or shape @@ -445,6 +453,7 @@ def read_multipage(filename, flags=0): """ return _process_multipage(filename, flags, _array_from_bitmap) + def read_multipage_metadata(filename): """Read a multipage image to a list of metadata dicts, one dict for each page. The dict format is as in read_metadata(). @@ -452,26 +461,28 @@ def read_multipage_metadata(filename): flags = IO_FLAGS.FIF_LOAD_NOPIXELS return _process_multipage(filename, flags, _read_metadata) + def _wrap_bitmap_bits_in_array(bitmap, shape, dtype): - """Return an ndarray view on the data in a FreeImage bitmap. Only - valid for as long as the bitmap is loaded (if single page) / locked - in memory (if multipage). + """Return an ndarray view on the data in a FreeImage bitmap. Only + valid for as long as the bitmap is loaded (if single page) / locked + in memory (if multipage). - """ - pitch = _FI.FreeImage_GetPitch(bitmap) - height = shape[-1] - byte_size = height * pitch - itemsize = dtype.itemsize + """ + pitch = _FI.FreeImage_GetPitch(bitmap) + height = shape[-1] + byte_size = height * pitch + itemsize = dtype.itemsize + + if len(shape) == 3: + strides = (itemsize, shape[0] * itemsize, pitch) + else: + strides = (itemsize, pitch) + bits = _FI.FreeImage_GetBits(bitmap) + array = numpy.ndarray(shape, dtype=dtype, + buffer=(ctypes.c_char * byte_size).from_address(bits), + strides=strides) + return array - if len(shape) == 3: - strides = (itemsize, shape[0]*itemsize, pitch) - else: - strides = (itemsize, pitch) - bits = _FI.FreeImage_GetBits(bitmap) - array = numpy.ndarray(shape, dtype=dtype, - buffer=(ctypes.c_char*byte_size).from_address(bits), - strides=strides) - return array def _array_from_bitmap(bitmap): """Convert a FreeImage bitmap pointer to a numpy array. @@ -482,6 +493,7 @@ def _array_from_bitmap(bitmap): # swizzle the color components and flip the scanlines to go from # FreeImage's BGR[A] and upside-down internal memory format to something # more normal + def n(arr): return arr[..., ::-1].T if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ @@ -490,10 +502,10 @@ def _array_from_bitmap(bitmap): g = n(array[1]) r = n(array[2]) if shape[0] == 3: - return numpy.dstack( (r, g, b) ) + return numpy.dstack((r, g, b)) elif shape[0] == 4: a = n(array[3]) - return numpy.dstack( (r, g, b, a) ) + return numpy.dstack((r, g, b, a)) else: raise ValueError('Cannot handle images of shape %s' % shape) @@ -501,6 +513,7 @@ def _array_from_bitmap(bitmap): # after bitmap is freed. return n(array).copy() + def _read_metadata(bitmap): metadata = {} models = [(name[5:], number) for name, number in @@ -531,6 +544,7 @@ def _read_metadata(bitmap): _FI.FreeImage_FindCloseMetadata(mdhandle) return metadata + def write(array, filename, flags=0): """Write a (height, width) or (height, width, nchannels) array to a greyscale, RGB, or RGBA image, with file type deduced from the @@ -558,7 +572,8 @@ def write(array, filename, flags=0): if not res: raise RuntimeError('Could not save image properly.') finally: - _FI.FreeImage_Unload(bitmap) + _FI.FreeImage_Unload(bitmap) + def write_multipage(arrays, filename, flags=0): """Write a list of (height, width) or (height, width, nchannels) @@ -592,23 +607,24 @@ def write_multipage(arrays, filename, flags=0): # 4-byte quads of 0,v,v,v from 0,0,0,0 to 0,255,255,255 _GREY_PALETTE = numpy.arange(0, 0x01000000, 0x00010101, dtype=numpy.uint32) + def _array_to_bitmap(array): """Allocate a FreeImage bitmap and copy a numpy array into it. """ shape = array.shape dtype = array.dtype - r,c = shape[:2] + r, c = shape[:2] if len(shape) == 2: n_channels = 1 - w_shape = (c,r) + w_shape = (c, r) elif len(shape) == 3: n_channels = shape[2] - w_shape = (n_channels,c,r) + w_shape = (n_channels, c, r) else: n_channels = shape[0] try: - fi_type = FI_TYPES.fi_types[(dtype.type, n_channels)] + fi_type = FI_TYPES.fi_types[(dtype, n_channels)] except KeyError: raise ValueError('Cannot write arrays of given type and shape.') @@ -619,18 +635,18 @@ def _array_to_bitmap(array): if not bitmap: raise RuntimeError('Could not allocate image for storage') try: - def n(arr): # normalise to freeimage's in-memory format - return arr.T[:,::-1] + def n(arr): # normalise to freeimage's in-memory format + return arr.T[:, ::-1] wrapped_array = _wrap_bitmap_bits_in_array(bitmap, w_shape, dtype) # swizzle the color components and flip the scanlines to go to # FreeImage's BGR[A] and upside-down internal memory format if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ dtype.type == numpy.uint8: - wrapped_array[0] = n(array[:,:,2]) - wrapped_array[1] = n(array[:,:,1]) - wrapped_array[2] = n(array[:,:,0]) + wrapped_array[0] = n(array[:, :, 2]) + wrapped_array[1] = n(array[:, :, 1]) + wrapped_array[2] = n(array[:, :, 0]) if shape[2] == 4: - wrapped_array[3] = n(array[:,:,3]) + wrapped_array[3] = n(array[:, :, 3]) else: wrapped_array[:] = n(array) if len(shape) == 2 and dtype.type == numpy.uint8: @@ -641,8 +657,8 @@ def _array_to_bitmap(array): ctypes.memmove(palette, _GREY_PALETTE.ctypes.data, 1024) return bitmap, fi_type except: - _FI.FreeImage_Unload(bitmap) - raise + _FI.FreeImage_Unload(bitmap) + raise def imread(filename): @@ -661,6 +677,7 @@ def imread(filename): img = read(filename) return img + def imsave(filename, img): ''' imsave(filename, img) diff --git a/skimage/io/_plugins/gdal_plugin.py b/skimage/io/_plugins/gdal_plugin.py index acc6db36..6749420f 100644 --- a/skimage/io/_plugins/gdal_plugin.py +++ b/skimage/io/_plugins/gdal_plugin.py @@ -1,7 +1,5 @@ __all__ = ['imread'] -import numpy as np - try: import osgeo.gdal as gdal except ImportError: @@ -9,6 +7,7 @@ except ImportError: "Please refer to http://www.gdal.org/ " "for further instructions.") + def imread(fname, dtype=None): """Load an image from file. @@ -16,4 +15,3 @@ def imread(fname, dtype=None): ds = gdal.Open(fname) return ds.ReadAsArray().astype(dtype) - diff --git a/skimage/io/_plugins/matplotlib_plugin.py b/skimage/io/_plugins/matplotlib_plugin.py index 652c8be1..ece44d93 100644 --- a/skimage/io/_plugins/matplotlib_plugin.py +++ b/skimage/io/_plugins/matplotlib_plugin.py @@ -1,5 +1,6 @@ import matplotlib.pyplot as plt + def imshow(*args, **kwargs): kwargs.setdefault('interpolation', 'nearest') kwargs.setdefault('cmap', 'gray') @@ -8,5 +9,6 @@ def imshow(*args, **kwargs): imread = plt.imread show = plt.show + def _app_show(): show() diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 914f7e9f..2247f3a1 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -11,6 +11,7 @@ except ImportError: from skimage.util import img_as_ubyte + def imread(fname, dtype=None): """Load an image from file. @@ -33,6 +34,7 @@ def imread(fname, dtype=None): return np.array(im, dtype=dtype) + def _palette_is_grayscale(pil_image): """Return True if PIL image in palette mode is grayscale. @@ -56,17 +58,22 @@ def _palette_is_grayscale(pil_image): # are all zero. return np.allclose(np.diff(valid_palette), 0) -def imsave(fname, arr): + +def imsave(fname, arr, format_str=None): """Save an image to disk. Parameters ---------- - fname : str + fname : str or file-like object Name of destination file. arr : ndarray of uint8 or float Array (image) to save. Arrays of data-type uint8 should have values in [0, 255], whereas floating-point arrays must be in [0, 1]. + format_str: str + Format to save as, this is required if using a file-like object; + this is optional if fname is a string and the format can be + derived from the extension. Notes ----- @@ -98,7 +105,8 @@ def imsave(fname, arr): arr = arr.astype(np.uint8) img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) - img.save(fname) + img.save(fname, format=format_str) + def imshow(arr): """Display an image, using PIL's default display command. @@ -112,5 +120,6 @@ def imshow(arr): """ Image.fromarray(img_as_ubyte(arr)).show() + def _app_show(): pass diff --git a/skimage/io/_plugins/plugin.py b/skimage/io/_plugins/plugin.py index 66ac3911..da69363d 100644 --- a/skimage/io/_plugins/plugin.py +++ b/skimage/io/_plugins/plugin.py @@ -4,7 +4,6 @@ __all__ = ['use', 'available', 'call', 'info', 'configuration', 'reset_plugins'] -import warnings from ConfigParser import ConfigParser import os.path from glob import glob @@ -15,8 +14,9 @@ plugin_provides = {} plugin_module_name = {} plugin_meta_data = {} + def reset_plugins(): - """Clear the plugin state to the default, i.e., where no plugins are loaded. + """Clear the plugin state to the default, i.e., where no plugins are loaded """ global plugin_store @@ -28,6 +28,7 @@ def reset_plugins(): reset_plugins() + def _scan_plugins(): """Scan the plugins directory for .ini files and parse them to gather plugin meta-data. @@ -59,6 +60,7 @@ def _scan_plugins(): _scan_plugins() + def call(kind, *args, **kwargs): """Find the appropriate plugin of 'kind' and execute it. @@ -90,13 +92,14 @@ command. A list of all available plugins can be found using else: _load(plugin) try: - func = [f for (p,f) in plugin_funcs if p == plugin][0] + func = [f for (p, f) in plugin_funcs if p == plugin][0] except IndexError: raise RuntimeError('Could not find the plugin "%s" for %s.' % \ (plugin, kind)) return func(*args, **kwargs) + def use(name, kind=None): """Set the default plugin for a specified operation. The plugin will be loaded if it hasn't been already. @@ -149,6 +152,7 @@ def use(name, kind=None): plugin_store[k] = funcs + def available(loaded=False): """List available plugins. @@ -178,6 +182,7 @@ def available(loaded=False): return d + def _load(plugin): """Load the given plugin. @@ -211,6 +216,7 @@ def _load(plugin): if not (plugin, func) in store: store.append((plugin, func)) + def info(plugin): """Return plugin meta-data. @@ -230,6 +236,7 @@ def info(plugin): except KeyError: raise ValueError('No information on plugin "%s"' % plugin) + def configuration(): """Return the currently preferred plugin order. diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 5a60b92b..3fe9e29c 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -1,7 +1,6 @@ # the module for the qt color_mixer plugin from PyQt4 import QtGui, QtCore -from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QVBoxLayout, - QGridLayout, QLabel) +from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QGridLayout, QLabel) from util import ColorMixer @@ -36,7 +35,8 @@ class IntelligentSlider(QWidget): self.name_label.setAlignment(QtCore.Qt.AlignCenter) self.value_label = QLabel() - self.value_label.setText('%2.2f' % (self.slider.value() * self.a + self.b)) + self.value_label.setText('%2.2f' % (self.slider.value() * self.a + + self.b)) self.value_label.setAlignment(QtCore.Qt.AlignCenter) self.layout = QGridLayout(self) @@ -120,11 +120,9 @@ class MixerPanel(QtGui.QFrame): self.rgb_widget.layout.addWidget(self.gs, 2, 1) self.rgb_widget.layout.addWidget(self.bs, 2, 2) - #--------------------------------------------------------------- # HSV sliders #--------------------------------------------------------------- - # radio buttons self.hsv_add = QtGui.QRadioButton('Additive') self.hsv_mul = QtGui.QRadioButton('Multiplicative') @@ -147,11 +145,9 @@ class MixerPanel(QtGui.QFrame): self.hsv_widget.layout.addWidget(self.ss, 2, 1) self.hsv_widget.layout.addWidget(self.vs, 2, 2) - #--------------------------------------------------------------- # Brightness/Contrast sliders #--------------------------------------------------------------- - # sliders cont = IntelligentSlider('x', 0.002, 0, self.bright_changed) bright = IntelligentSlider('+', 0.51, -255, self.bright_changed) @@ -164,10 +160,9 @@ class MixerPanel(QtGui.QFrame): self.bright_widget.layout.addWidget(self.cont, 0, 0) self.bright_widget.layout.addWidget(self.bright, 0, 1) - - #----------------------------------------------------------------------- + #---------------------------------------------------------------------- # Gamma Slider - #----------------------------------------------------------------------- + #---------------------------------------------------------------------- gamma = IntelligentSlider('gamma', 0.005, 0, self.gamma_changed) self.gamma = gamma @@ -176,11 +171,9 @@ class MixerPanel(QtGui.QFrame): self.gamma_widget.layout = QtGui.QGridLayout(self.gamma_widget) self.gamma_widget.layout.addWidget(self.gamma, 0, 0) - #--------------------------------------------------------------- # Sigmoid Gamma sliders #--------------------------------------------------------------- - # sliders alpha = IntelligentSlider('alpha', 0.011, 1, self.sig_gamma_changed) beta = IntelligentSlider('beta', 0.012, 0, self.sig_gamma_changed) @@ -225,7 +218,6 @@ class MixerPanel(QtGui.QFrame): self.rgb_mul.setChecked(True) self.hsv_mul.setChecked(True) - def set_callback(self, callback): self.callback = callback @@ -281,7 +273,6 @@ class MixerPanel(QtGui.QFrame): self.a_gamma.set_value(1) self.b_gamma.set_value(0.5) - def rgb_changed(self, name, val): if name == 'R': channel = self.mixer.RED @@ -346,4 +337,4 @@ class MixerPanel(QtGui.QFrame): self.reset_sliders() if self.callback: - self.callback() \ No newline at end of file + self.callback() diff --git a/skimage/io/_plugins/q_histogram.py b/skimage/io/_plugins/q_histogram.py index 36edb30d..0a60ce9f 100644 --- a/skimage/io/_plugins/q_histogram.py +++ b/skimage/io/_plugins/q_histogram.py @@ -39,7 +39,7 @@ class ColorHistogram(QWidget): orig_height = self.height() # fill perc % of the widget - perc = 1 + perc = 1 width = int(orig_width * perc) height = int(orig_height * perc) @@ -60,14 +60,14 @@ class ColorHistogram(QWidget): remainder = width % nbars bar_width = [int(width / nbars)] * nbars for i in range(remainder): - bar_width[i]+=1 + bar_width[i] += 1 paint = QPainter() paint.begin(self) # determine the scaling factor max_val = np.max(self.counts) - scale = 1. * height / max_val + scale = 1. * height / max_val # determine if we have a colormap and drop into the appopriate # loop. @@ -95,7 +95,6 @@ class ColorHistogram(QWidget): paint.end() - def update_hist(self, counts, cmap): self._validate_input(counts, cmap) self.counts = counts @@ -121,17 +120,17 @@ class QuadHistogram(QFrame): self.b_hist = ColorHistogram(b, (0, 0, 255)) self.v_hist = ColorHistogram(v, (0, 0, 0)) - self.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken) + self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.layout = QGridLayout(self) self.layout.setMargin(0) order_map = {'R': self.r_hist, 'G': self.g_hist, 'B': self.b_hist, 'V': self.v_hist} - if layout=='vertical': + if layout == 'vertical': for i in range(len(order)): self.layout.addWidget(order_map[order[i]], i, 0) - elif layout=='horizontal': + elif layout == 'horizontal': for i in range(len(order)): self.layout.addWidget(order_map[order[i]], 0, i) @@ -140,4 +139,4 @@ class QuadHistogram(QFrame): self.r_hist.update_hist(r, (255, 0, 0)) self.g_hist.update_hist(g, (0, 255, 0)) self.b_hist.update_hist(b, (0, 0, 255)) - self.v_hist.update_hist(v, (0, 0, 0)) \ No newline at end of file + self.v_hist.update_hist(v, (0, 0, 0)) diff --git a/skimage/io/_plugins/qt_plugin.py b/skimage/io/_plugins/qt_plugin.py index dd4e4a8f..24cf472c 100644 --- a/skimage/io/_plugins/qt_plugin.py +++ b/skimage/io/_plugins/qt_plugin.py @@ -1,14 +1,13 @@ -from .util import prepare_for_display, window_manager, GuiLockError +from .util import prepare_for_display, window_manager import numpy as np -import sys # We try to aquire the gui lock first or else the gui import might # trample another GUI's PyOS_InputHook. window_manager.acquire('qt') try: - from PyQt4.QtGui import (QApplication, QMainWindow, QImage, QPixmap, - QLabel, QWidget) + from PyQt4.QtGui import (QApplication, QImage, + QLabel, QMainWindow, QPixmap, QWidget) from PyQt4 import QtCore, QtGui import sip import warnings @@ -148,12 +147,21 @@ def _app_show(): print 'No images to show. See `imshow`.' -def imsave(filename, img): +def imsave(filename, img, format_str=None): # we can add support for other than 3D uint8 here... img = prepare_for_display(img) qimg = QImage(img.data, img.shape[1], img.shape[0], img.strides[0], QImage.Format_RGB888) - saved = qimg.save(filename) + if _is_filelike(filename): + byte_array = QtCore.QByteArray() + qbuffer = QtCore.QBuffer(byte_array) + qbuffer.open(QtCore.QIODevice.ReadWrite) + saved = qimg.save(qbuffer, format_str.upper()) + qbuffer.seek(0) + filename.write(qbuffer.readAll()) + qbuffer.close() + else: + saved = qimg.save(filename) if not saved: from textwrap import dedent msg = dedent( @@ -161,3 +169,7 @@ def imsave(filename, img): for the QT imsave plugin are: BMP, JPG, JPEG, PNG, PPM, TIFF, XBM, XPM''') raise RuntimeError(msg) + + +def _is_filelike(possible_filelike): + return callable(getattr(possible_filelike, 'write', None)) diff --git a/skimage/io/_plugins/skivi.py b/skimage/io/_plugins/skivi.py index 440e3c2c..fcfe8bc6 100644 --- a/skimage/io/_plugins/skivi.py +++ b/skimage/io/_plugins/skivi.py @@ -3,9 +3,9 @@ Skivi is written/maintained/developed by: S. Chris Colbert - sccolbert@gmail.com -Skivi is free software and is part of the scikits-image project. +Skivi is free software and is part of the scikit-image project. -Skivi is governed by the licenses of the scikits-image project. +Skivi is governed by the licenses of the scikit-image project. Please report any bugs to the author. @@ -14,13 +14,9 @@ The skivi module is not meant to be used directly. Use skimage.io.imshow(img, fancy=True)''' from textwrap import dedent -import numpy as np -import sys from PyQt4 import QtCore, QtGui -from PyQt4.QtGui import (QApplication, QMainWindow, QImage, QPixmap, - QLabel, QWidget, QVBoxLayout, QSlider, - QPainter, QColor, QFrame, QLayoutItem) +from PyQt4.QtGui import QMainWindow, QImage, QPixmap, QLabel, QWidget, QFrame from .q_color_mixer import MixerPanel from .q_histogram import QuadHistogram @@ -69,7 +65,7 @@ class ImageLabel(QLabel): class RGBHSVDisplay(QFrame): def __init__(self): QFrame.__init__(self) - self.setFrameStyle(QtGui.QFrame.Box|QtGui.QFrame.Sunken) + self.setFrameStyle(QtGui.QFrame.Box | QtGui.QFrame.Sunken) self.posx_label = QLabel('X-pos:') self.posx_value = QLabel() @@ -118,7 +114,6 @@ class RGBHSVDisplay(QFrame): self.v_value.setText(str(v)[:5]) - class SkiviImageWindow(QMainWindow): def __init__(self, arr, mgr): QMainWindow.__init__(self) @@ -132,7 +127,8 @@ class SkiviImageWindow(QMainWindow): self.label = ImageLabel(self, arr) self.label_container = QFrame() - self.label_container.setFrameShape(QtGui.QFrame.StyledPanel|QtGui.QFrame.Sunken) + self.label_container.setFrameShape( + QtGui.QFrame.StyledPanel | QtGui.QFrame.Sunken) self.label_container.setLineWidth(1) self.label_container.layout = QtGui.QGridLayout(self.label_container) @@ -171,7 +167,6 @@ class SkiviImageWindow(QMainWindow): self.layout.addWidget(self.save_stack, 1, 1) self.layout.addWidget(self.save_file, 1, 2) - def closeEvent(self, event): # Allow window to be destroyed by removing any # references to it @@ -206,14 +201,13 @@ class SkiviImageWindow(QMainWindow): if x >= maxw or y >= maxh or x < 0 or y < 0: r = g = b = h = s = v = '' else: - r = self.arr[y,x,0] - g = self.arr[y,x,1] - b = self.arr[y,x,2] + r = self.arr[y, x, 0] + g = self.arr[y, x, 1] + b = self.arr[y, x, 2] h, s, v = self.mixer_panel.mixer.rgb_2_hsv_pixel(r, g, b) self.rgb_hsv_disp.update_vals((x, y, r, g, b, h, s, v)) - def save_to_stack(self): from skimage import io img = self.arr.copy() @@ -238,5 +232,3 @@ class SkiviImageWindow(QMainWindow): if len(filename) == 0: return io.imsave(filename, self.arr) - - diff --git a/skimage/io/_plugins/test_plugin.py b/skimage/io/_plugins/test_plugin.py index 2956f14f..18f750f3 100644 --- a/skimage/io/_plugins/test_plugin.py +++ b/skimage/io/_plugins/test_plugin.py @@ -1,18 +1,22 @@ # This mock-up is called by ../tests/test_plugin.py # to verify the behaviour of the plugin infrastructure + def imread(fname, dtype=None): assert fname == 'test.png' assert dtype == 'i4' + def imsave(fname, arr): assert fname == 'test.png' assert arr == [1, 2, 3] + def imshow(arr, plugin_arg=None): assert arr == [1, 2, 3] assert plugin_arg == (1, 2) + def imread_collection(x, conserve_memory=True): assert conserve_memory == False assert x == '*.png' diff --git a/skimage/io/_plugins/tifffile_plugin.ini b/skimage/io/_plugins/tifffile_plugin.ini index 4666e503..bf83fce2 100644 --- a/skimage/io/_plugins/tifffile_plugin.ini +++ b/skimage/io/_plugins/tifffile_plugin.ini @@ -1,3 +1,3 @@ [tifffile] -description = Open and save TIFF and TIFF-based (LSM, STK, etc.) images using tifffile.py +description = Load and save TIFF and TIFF-based images using tifffile.py provides = imread, imsave diff --git a/skimage/io/_plugins/util.py b/skimage/io/_plugins/util.py index 7eb109b7..ed547222 100644 --- a/skimage/io/_plugins/util.py +++ b/skimage/io/_plugins/util.py @@ -12,6 +12,7 @@ try: except: CPU_COUNT = 2 + class GuiLockError(Exception): def __init__(self, msg): self.msg = msg @@ -19,6 +20,7 @@ class GuiLockError(Exception): def __str__(self): return self.msg + class WindowManager(object): ''' A class to keep track of spawned windows, and make any needed callback once all the windows, @@ -62,7 +64,8 @@ class WindowManager(object): self._gui_lock = False self._guikit = '' else: - raise RuntimeError('Only the toolkit that owns the lock may release it') + raise RuntimeError('Only the toolkit that owns the lock may ' + 'release it') def add_window(self, win): self._check_locked() @@ -138,13 +141,13 @@ def prepare_for_display(npy_img): if npy_img.ndim == 2 or \ (npy_img.ndim == 3 and npy_img.shape[2] == 1): npy_plane = npy_img.reshape((height, width)) - out[:,:,0] = npy_plane - out[:,:,1] = npy_plane - out[:,:,2] = npy_plane + out[:, :, 0] = npy_plane + out[:, :, 1] = npy_plane + out[:, :, 2] = npy_plane elif npy_img.ndim == 3: if npy_img.shape[2] == 3 or npy_img.shape[2] == 4: - out[:,:,:3] = npy_img[:,:,:3] + out[:, :, :3] = npy_img[:, :, :3] else: raise ValueError('Image must have 1, 3, or 4 channels') @@ -184,10 +187,10 @@ class ImgThread(threading.Thread): def run(self): self.func(*self.args) + class ThreadDispatch(object): def __init__(self, img, stateimg, func, *args): - width = img.shape[1] height = img.shape[0] self.cores = CPU_COUNT self.threads = [] @@ -197,21 +200,21 @@ class ThreadDispatch(object): self.chunks.append((img, stateimg)) elif self.cores >= 4: - self.chunks.append((img[:(height/4), :, :], - stateimg[:(height/4), :, :])) - self.chunks.append((img[(height/4):(height/2), :, :], - stateimg[(height/4):(height/2), :, :])) - self.chunks.append((img[(height/2):(3*height/4), :, :], - stateimg[(height/2):(3*height/4), :, :])) - self.chunks.append((img[(3*height/4):, :, :], - stateimg[(3*height/4):, :, :])) + self.chunks.append((img[:(height / 4), :, :], + stateimg[:(height / 4), :, :])) + self.chunks.append((img[(height / 4):(height / 2), :, :], + stateimg[(height / 4):(height / 2), :, :])) + self.chunks.append((img[(height / 2):(3 * height / 4), :, :], + stateimg[(height / 2):(3 * height / 4), :, :])) + self.chunks.append((img[(3 * height / 4):, :, :], + stateimg[(3 * height / 4):, :, :])) # if they dont have 1, or 4 or more, 2 is good. else: - self.chunks.append((img[:(height/2), :, :], - stateimg[:(height/2), :, :])) - self.chunks.append((img[(height/2):, :, :], - stateimg[(height/2):, :, :])) + self.chunks.append((img[:(height / 2), :, :], + stateimg[:(height / 2), :, :])) + self.chunks.append((img[(height / 2):, :, :], + stateimg[(height / 2):, :, :])) for i in range(len(self.chunks)): self.threads.append(ImgThread(func, self.chunks[i][0], @@ -224,7 +227,6 @@ class ThreadDispatch(object): t.join() - class ColorMixer(object): ''' a class to manage mixing colors in an image. The input array must be an RGB uint8 image. @@ -300,8 +302,6 @@ class ColorMixer(object): _colormixer.add, channel, ammount) pool.run() - - def multiply(self, channel, ammount): '''Mutliply the indicated channel by the specified value. @@ -320,7 +320,6 @@ class ColorMixer(object): _colormixer.multiply, channel, ammount) pool.run() - def brightness(self, factor, offset): '''Adjust the brightness off an image with an offset and factor. @@ -338,13 +337,11 @@ class ColorMixer(object): _colormixer.brightness, factor, offset) pool.run() - def sigmoid_gamma(self, alpha, beta): pool = ThreadDispatch(self.img, self.stateimg, _colormixer.sigmoid_gamma, alpha, beta) pool.run() - def gamma(self, gamma): pool = ThreadDispatch(self.img, self.stateimg, _colormixer.gamma, gamma) @@ -435,4 +432,3 @@ class ColorMixer(object): ''' R, G, B = _colormixer.py_hsv_2_rgb(H, S, V) return (R, G, B) - diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 19d3ab57..5a5dfff6 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -2,14 +2,72 @@ from __future__ import with_statement -__all__ = ['MultiImage', 'ImageCollection', 'imread'] +__all__ = ['MultiImage', 'ImageCollection', 'imread', 'concatenate_images'] from glob import glob +import re +from copy import copy import numpy as np from ._io import imread +def concatenate_images(ic): + """Concatenate all images in the image collection into an array. + + Parameters + ---------- + ic: an iterable of images (including ImageCollection and MultiImage) + The images to be concatenated. + + Returns + ------- + ar : np.ndarray + An array having one more dimension than the images in `ic`. + + See Also + -------- + ImageCollection.concatenate, MultiImage.concatenate + + Raises + ------ + ValueError + If images in `ic` don't have identical shapes. + """ + all_images = [img[np.newaxis, ...] for img in ic] + try: + ar = np.concatenate(all_images) + except ValueError: + raise ValueError('Image dimensions must agree.') + return ar + + +def alphanumeric_key(s): + """Convert string to list of strings and ints that gives intuitive sorting. + + Parameters + ---------- + s: string + + Returns + ------- + k: a list of strings and ints + + Examples + -------- + >>> alphanumeric_key('z23a') + ['z', 23, 'a'] + >>> filenames = ['f9.10.png', 'e10.png', 'f9.9.png', 'f10.10.png', + ... 'f10.9.png'] + >>> sorted(filenames) + ['e10.png', 'f10.10.png', 'f10.9.png', 'f9.10.png', 'f9.9.png'] + >>> sorted(filenames, key=alphanumeric_key) + ['e10.png', 'f9.9.png', 'f9.10.png', 'f10.9.png', 'f10.10.png'] + """ + k = [int(c) if c.isdigit() else c for c in re.split('([0-9]+)', s)] + return k + + class MultiImage(object): """A class containing a single multi-frame image. @@ -118,7 +176,8 @@ class MultiImage(object): if -numframes <= n < numframes: n = n % numframes else: - raise IndexError("There are only %s frames in the image"%numframes) + raise IndexError("There are only %s frames in the image" + % numframes) if self.conserve_memory: if not self._cached == n: @@ -141,11 +200,31 @@ class MultiImage(object): def __str__(self): return str(self.filename) + ' [%s frames]' % self._numframes + def concatenate(self): + """Concatenate all images in the multi-image into an array. + + Returns + ------- + ar : np.ndarray + An array having one more dimension than the images in `self`. + + See Also + -------- + concatenate_images + + Raises + ------ + ValueError + If images in the `MultiImage` don't have identical shapes. + """ + return concatenate_images(self) + class ImageCollection(object): """Load and manage a collection of image files. - Note that files are always stored in alphabetical order. + Note that files are always stored in alphabetical order. Also note that + slicing returns a new ImageCollection, *not* a view into the data. Parameters ---------- @@ -209,7 +288,7 @@ class ImageCollection(object): >>> len(coll) 2 >>> coll[0].shape - (128, 128, 3) + (512, 512, 3) >>> ic = io.ImageCollection('/tmp/work/*.png:/tmp/other/*.jpg') @@ -221,7 +300,7 @@ class ImageCollection(object): self._files = [] for pattern in load_pattern: self._files.extend(glob(pattern)) - self._files.sort() + self._files = sorted(self._files, key=alphanumeric_key) else: self._files = load_pattern @@ -249,29 +328,56 @@ class ImageCollection(object): return self._conserve_memory def __getitem__(self, n): - """Return image n in the collection. + """Return selected image(s) in the collection. Loading is done on demand. Parameters ---------- - n : int - The image number to be returned. + n : int or slice + The image number to be returned, or a slice selecting the images + and ordering to be returned in a new ImageCollection. Returns ------- - img : ndarray - The `n`-th image in the collection. + img : ndarray or ImageCollection. + The `n`-th image in the collection, or a new ImageCollection with + the selected images. + """ - n = self._check_imgnum(n) - idx = n % len(self.data) + if hasattr(n, '__index__'): + n = n.__index__() - if (self.conserve_memory and n != self._cached) or \ - (self.data[idx] is None): - self.data[idx] = self.load_func(self.files[n]) - self._cached = n + if type(n) not in [int, slice]: + raise TypeError('slicing must be with an int or slice object') - return self.data[idx] + if type(n) is int: + n = self._check_imgnum(n) + idx = n % len(self.data) + + if (self.conserve_memory and n != self._cached) or \ + (self.data[idx] is None): + self.data[idx] = self.load_func(self.files[n]) + self._cached = n + + return self.data[idx] + else: + # A slice object was provided, so create a new ImageCollection + # object. Any loaded image data in the original ImageCollection + # will be copied by reference to the new object. Image data + # loaded after this creation is not linked. + fidx = range(len(self.files))[n] + new_ic = copy(self) + new_ic._files = [self.files[i] for i in fidx] + if self.conserve_memory: + if self._cached in fidx: + new_ic._cached = fidx.index(self._cached) + new_ic.data = np.copy(self.data) + else: + new_ic.data = np.empty(1, dtype=object) + else: + new_ic.data = self.data[fidx] + return new_ic def _check_imgnum(self, n): """Check that the given image number is valid.""" @@ -279,7 +385,8 @@ class ImageCollection(object): if -num <= n < num: n = n % num else: - raise IndexError("There are only %s images in the collection"%num) + raise IndexError("There are only %s images in the collection" + % num) return n def __iter__(self): @@ -305,3 +412,22 @@ class ImageCollection(object): """ self.data = np.empty_like(self.data) + + def concatenate(self): + """Concatenate all images in the collection into an array. + + Returns + ------- + ar : np.ndarray + An array having one more dimension than the images in `self`. + + See Also + -------- + concatenate_images + + Raises + ------ + ValueError + If images in the `ImageCollection` don't have identical shapes. + """ + return concatenate_images(self) diff --git a/skimage/io/setup.py b/skimage/io/setup.py index d8db9e66..4e3c09c3 100644 --- a/skimage/io/setup.py +++ b/skimage/io/setup.py @@ -6,6 +6,7 @@ import os.path base_path = os.path.abspath(os.path.dirname(__file__)) + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs @@ -30,10 +31,10 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Image I/O Routines', - url = 'https://github.com/scikits-image/scikits-image', - license = 'Modified BSD', + setup(maintainer='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Image I/O Routines', + url='https://github.com/scikit-image/scikit-image', + license='Modified BSD', **(configuration(top_path='').todict()) ) diff --git a/skimage/io/sift.py b/skimage/io/sift.py index e594da4b..d80ba427 100644 --- a/skimage/io/sift.py +++ b/skimage/io/sift.py @@ -10,6 +10,7 @@ __all__ = ['load_sift', 'load_surf'] import numpy as np + def _sift_read(f, mode='SIFT'): """Read SIFT or SURF features from a file. @@ -56,9 +57,11 @@ def _sift_read(f, mode='SIFT'): return data.view(datatype) + def load_sift(f): return _sift_read(f, mode='SIFT') + def load_surf(f): return _sift_read(f, mode='SURF') diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index fa311e3e..2bbfefa7 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -7,6 +7,8 @@ from numpy.testing.decorators import skipif from skimage import data_dir from skimage.io import ImageCollection, MultiImage +from skimage.io.collection import alphanumeric_key +from skimage.io import Image as ioImage try: @@ -19,13 +21,33 @@ else: if sys.version_info[0] > 2: basestring = str +class TestAlphanumericKey(): + def setUp(self): + self.test_string = 'z23a' + self.test_str_result = ['z', 23, 'a'] + self.filenames = ['f9.10.png', 'f9.9.png', 'f10.10.png', 'f10.9.png', + 'e9.png', 'e10.png', 'em.png'] + self.sorted_filenames = \ + ['e9.png', 'e10.png', 'em.png', 'f9.9.png', 'f9.10.png', + 'f10.9.png', 'f10.10.png'] + + def test_string_split(self): + assert_equal(alphanumeric_key(self.test_string), self.test_str_result) + + def test_string_sort(self): + sorted_filenames = sorted(self.filenames, key=alphanumeric_key) + assert_equal(sorted_filenames, self.sorted_filenames) + class TestImageCollection(): pattern = [os.path.join(data_dir, pic) for pic in ['camera.png', 'color.png']] + pattern_matched = [os.path.join(data_dir, pic) for pic in + ['camera.png', 'moon.png']] def setUp(self): self.collection = ImageCollection(self.pattern) + self.collection_matched = ImageCollection(self.pattern_matched) def test_len(self): assert len(self.collection) == 2 @@ -33,7 +55,7 @@ class TestImageCollection(): def test_getitem(self): num = len(self.collection) for i in range(-num, num): - assert type(self.collection[i]) is np.ndarray + assert type(self.collection[i]) is ioImage assert_array_almost_equal(self.collection[0], self.collection[-num]) @@ -41,7 +63,17 @@ class TestImageCollection(): def return_img(n): return self.collection[n] assert_raises(IndexError, return_img, num) - assert_raises(IndexError, return_img, -num-1) + assert_raises(IndexError, return_img, -num - 1) + + def test_slicing(self): + assert type(self.collection[:]) is ImageCollection + assert len(self.collection[:]) == 2 + assert len(self.collection[:1]) == 1 + assert len(self.collection[1:]) == 1 + assert_array_almost_equal(self.collection[0], self.collection[:1][0]) + assert_array_almost_equal(self.collection[1], self.collection[1:][0]) + assert_array_almost_equal(self.collection[1], self.collection[::-1][0]) + assert_array_almost_equal(self.collection[0], self.collection[::-1][1]) def test_files_property(self): assert isinstance(self.collection.files, list) @@ -52,12 +84,19 @@ class TestImageCollection(): def test_custom_load(self): load_pattern = [(1, 'one'), (2, 'two')] + def load_fn(x): return x ic = ImageCollection(load_pattern, load_func=load_fn) assert_equal(ic[1], (2, 'two')) + def test_concatenate(self): + ar = self.collection_matched.concatenate() + assert_equal(ar.shape, (len(self.collection_matched),) + + self.collection[0].shape) + assert_raises(ValueError, self.collection.concatenate) + class TestMultiImage(): @@ -83,7 +122,7 @@ class TestMultiImage(): def return_img(n): return self.img[n] assert_raises(IndexError, return_img, num) - assert_raises(IndexError, return_img, -num-1) + assert_raises(IndexError, return_img, -num - 1) @skipif(not PIL_available) def test_files_property(self): @@ -101,11 +140,12 @@ class TestMultiImage(): self.img.conserve_memory = val assert_raises(AttributeError, set_mem, True) - - - + @skipif(not PIL_available) + def test_concatenate(self): + ar = self.img.concatenate() + assert_equal(ar.shape, (len(self.img),) + + self.img[0].shape) if __name__ == "__main__": run_module_suite() - diff --git a/skimage/io/tests/test_colormixer.py b/skimage/io/tests/test_colormixer.py index bf0363c5..b9f362a2 100644 --- a/skimage/io/tests/test_colormixer.py +++ b/skimage/io/tests/test_colormixer.py @@ -3,6 +3,7 @@ import numpy as np import skimage.io._plugins._colormixer as cm + class ColorMixerTest(object): def setup(self): self.state = np.ones((18, 33, 3), dtype=np.uint8) * 200 @@ -91,7 +92,8 @@ class TestColorMixer(object): def test_gamma(self): gamma = 1.5 cm.gamma(self.img, self.state, gamma) - img = np.asarray(((self.state/255.)**(1/gamma))*255, dtype='uint8') + img = np.asarray(((self.state / 255.)**(1 / gamma)) * 255, + dtype='uint8') assert_array_almost_equal(img, self.img) def test_rgb_2_hsv(self): @@ -112,7 +114,6 @@ class TestColorMixer(object): assert_almost_equal(np.array([g]), np.array([0])) assert_almost_equal(np.array([b]), np.array([0])) - def test_hsv_add(self): cm.hsv_add(self.img, self.state, 360, 0, 0) assert_almost_equal(self.img, self.state) @@ -123,7 +124,7 @@ class TestColorMixer(object): def test_hsv_add_clip_pos(self): cm.hsv_add(self.img, self.state, 0, 0, 1) - assert_equal(self.img, np.ones_like(self.state)*255) + assert_equal(self.img, np.ones_like(self.state) * 255) def test_hsv_mul(self): cm.hsv_multiply(self.img, self.state, 360, 1, 1) @@ -134,7 +135,5 @@ class TestColorMixer(object): assert_equal(self.img, np.zeros_like(self.state)) - - if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/test_fits.py b/skimage/io/tests/test_fits.py index bf918882..d432b611 100644 --- a/skimage/io/tests/test_fits.py +++ b/skimage/io/tests/test_fits.py @@ -15,6 +15,7 @@ except ImportError: else: import skimage.io._plugins.fits_plugin as fplug + def test_fits_plugin_import(): # Make sure we get an import exception if PyFITS isn't there # (not sure how useful this is, but it ensures there isn't some other @@ -36,14 +37,16 @@ def test_imread_MEF(): io.use_plugin('fits') testfile = os.path.join(data_dir, 'multi.fits') img = io.imread(testfile) - assert np.all(img==pyfits.getdata(testfile, 1)) + assert np.all(img == pyfits.getdata(testfile, 1)) + @skipif(not pyfits_available) def test_imread_simple(): io.use_plugin('fits') testfile = os.path.join(data_dir, 'simple.fits') img = io.imread(testfile) - assert np.all(img==pyfits.getdata(testfile, 0)) + assert np.all(img == pyfits.getdata(testfile, 0)) + @skipif(not pyfits_available) def test_imread_collection_single_MEF(): @@ -54,6 +57,7 @@ def test_imread_collection_single_MEF(): load_func=fplug.FITSFactory) assert _same_ImageCollection(ic1, ic2) + @skipif(not pyfits_available) def test_imread_collection_MEF_and_simple(): io.use_plugin('fits') @@ -65,6 +69,7 @@ def test_imread_collection_MEF_and_simple(): load_func=fplug.FITSFactory) assert _same_ImageCollection(ic1, ic2) + def _same_ImageCollection(collection1, collection2): """Ancillary function to compare two ImageCollection objects, checking that their constituent arrays are equal. @@ -79,4 +84,3 @@ def _same_ImageCollection(collection1, collection2): if __name__ == '__main__': run_module_suite() - diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index 3d8d16f4..7a294f9e 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -11,7 +11,7 @@ try: import skimage.io._plugins.freeimage_plugin as fi FI_available = True sio.use_plugin('freeimage') -except OSError: +except RuntimeError: FI_available = False @@ -23,7 +23,7 @@ def setup_module(self): """ try: sio.use_plugin('freeimage') - except OSError: + except RuntimeError: pass @@ -35,7 +35,8 @@ def teardown(): def test_imread(): img = sio.imread(os.path.join(si.data_dir, 'color.png')) assert img.shape == (370, 371, 3) - assert all(img[274,135] == [0, 130, 253]) + assert all(img[274, 135] == [0, 130, 253]) + @skipif(not FI_available) def test_imread_uint16(): @@ -44,6 +45,7 @@ def test_imread_uint16(): assert img.dtype == np.uint16 assert_array_almost_equal(img, expected) + @skipif(not FI_available) def test_imread_uint16_big_endian(): expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) @@ -54,7 +56,7 @@ def test_imread_uint16_big_endian(): class TestSave: def roundtrip(self, dtype, x, suffix): - f = NamedTemporaryFile(suffix='.'+suffix) + f = NamedTemporaryFile(suffix='.' + suffix) fname = f.name f.close() sio.imsave(fname, x) @@ -64,12 +66,12 @@ class TestSave: @skipif(not FI_available) def test_imsave_roundtrip(self): for shape, dtype, format in [ - [(10, 10), (np.uint8, np.uint16), ('tif', 'png')], - [(10, 10), (np.float32,), ('tif',)], - [(10, 10, 3), (np.uint8,), ('png',)], + [(10, 10), (np.uint8, np.uint16), ('tif', 'png')], + [(10, 10), (np.float32,), ('tif',)], + [(10, 10, 3), (np.uint8,), ('png',)], [(10, 10, 4), (np.uint8,), ('png',)] ]: - tests = [(d,f) for d in dtype for f in format] + tests = [(d, f) for d in dtype for f in format] for d, f in tests: x = np.ones(shape, dtype=d) * np.random.random(shape) if not np.issubdtype(d, float): @@ -83,7 +85,8 @@ def test_metadata(): assert meta[('EXIF_MAIN', 'Orientation')] == 1 assert meta[('EXIF_MAIN', 'Software')].startswith('ImageMagick') - meta = fi.read_multipage_metadata(os.path.join(si.data_dir, 'multipage.tif')) + meta = fi.read_multipage_metadata(os.path.join(si.data_dir, + 'multipage.tif')) assert len(meta) == 2 assert meta[0][('EXIF_MAIN', 'Orientation')] == 1 assert meta[1][('EXIF_MAIN', 'Software')].startswith('ImageMagick') diff --git a/skimage/io/tests/test_histograms.py b/skimage/io/tests/test_histograms.py index 11602f3a..bf972b17 100644 --- a/skimage/io/tests/test_histograms.py +++ b/skimage/io/tests/test_histograms.py @@ -1,23 +1,23 @@ from numpy.testing import * import numpy as np -import skimage.io._plugins._colormixer as cm from skimage.io._plugins._histograms import histograms + class TestHistogram: def test_basic(self): img = np.ones((50, 50, 3), dtype=np.uint8) r, g, b, v = histograms(img, 255) for band in (r, g, b, v): - yield assert_equal, band.sum(), 50*50 + yield assert_equal, band.sum(), 50 * 50 def test_counts(self): channel = np.arange(255).reshape(51, 5) img = np.empty((51, 5, 3), dtype='uint8') - img[:,:,0] = channel - img[:,:,1] = channel - img[:,:,2] = channel + img[:, :, 0] = channel + img[:, :, 1] = channel + img[:, :, 2] = channel r, g, b, v = histograms(img, 255) assert_array_equal(r, g) assert_array_equal(r, b) diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index ba05d2d3..72f8496a 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -3,12 +3,14 @@ import numpy as np import skimage.io as io + def test_stack_basic(): x = np.arange(12).reshape(3, 4) io.push(x) assert_array_equal(io.pop(), x) + @raises(ValueError) def test_stack_non_array(): io.push([[1, 2, 3]]) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 47126fce..a9d986d6 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -32,6 +32,8 @@ def setup_module(self): except ImportError: pass + + @skipif(not PIL_available) def test_imread_flatten(): # a color image is flattened @@ -42,6 +44,7 @@ def test_imread_flatten(): # check that flattening does not occur for an image that is grey already. assert np.sctype2char(img.dtype) in np.typecodes['AllInteger'] + @skipif(not PIL_available) def test_imread_palette(): img = imread(os.path.join(data_dir, 'palette_gray.png')) @@ -49,6 +52,7 @@ def test_imread_palette(): img = imread(os.path.join(data_dir, 'palette_color.png')) assert img.ndim == 3 + @skipif(not PIL_available) def test_palette_is_gray(): from PIL import Image @@ -57,6 +61,7 @@ def test_palette_is_gray(): color = Image.open(os.path.join(data_dir, 'palette_color.png')) assert not _palette_is_grayscale(color) + @skipif(not PIL_available) def test_bilevel(): expected = np.zeros((10, 10)) @@ -65,6 +70,7 @@ def test_bilevel(): img = imread(os.path.join(data_dir, 'checker_bilevel.png')) assert_array_equal(img, expected) + @skipif(not PIL_available) def test_imread_uint16(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) @@ -72,6 +78,21 @@ def test_imread_uint16(): assert np.issubdtype(img.dtype, np.uint16) assert_array_almost_equal(img, expected) + +@skipif(not PIL_available) +def test_repr_png(): + img_path = os.path.join(data_dir, 'camera.png') + original_img = imread(img_path) + original_img_str = original_img._repr_png_() + + with NamedTemporaryFile(suffix='.png') as temp_png: + temp_png.write(original_img_str) + temp_png.seek(0) + round_trip = imread(temp_png) + + assert np.all(original_img == round_trip) + + # Big endian images not correctly loaded for PIL < 1.1.7 # Renable test when PIL 1.1.7 is more common. @skipif(True) diff --git a/skimage/io/tests/test_plugin.py b/skimage/io/tests/test_plugin.py index 6480c922..5d1febe4 100644 --- a/skimage/io/tests/test_plugin.py +++ b/skimage/io/tests/test_plugin.py @@ -15,16 +15,18 @@ try: io.use_plugin('freeimage') FI_available = True priority_plugin = 'freeimage' -except OSError: +except RuntimeError: FI_available = False def setup_module(self): - plugin.use('test') # see ../_plugins/test_plugin.py + plugin.use('test') # see ../_plugins/test_plugin.py + def teardown_module(self): io.reset_plugins() + class TestPlugin: def test_read(self): io.imread('test.png', as_grey=True, dtype='i4', plugin='test') diff --git a/skimage/io/tests/test_plugin_util.py b/skimage/io/tests/test_plugin_util.py index 0170fcb1..41f28339 100644 --- a/skimage/io/tests/test_plugin_util.py +++ b/skimage/io/tests/test_plugin_util.py @@ -3,6 +3,7 @@ from skimage.io._plugins.util import prepare_for_display, WindowManager from numpy.testing import * import numpy as np + class TestPrepareForDisplay: def test_basic(self): prepare_for_display(np.random.random((10, 10))) @@ -12,24 +13,25 @@ class TestPrepareForDisplay: assert x.dtype == np.dtype(np.uint8) def test_grey(self): - x = prepare_for_display(np.arange(12, dtype=float).reshape((4,3))/11.) + x = prepare_for_display(np.arange(12, dtype=float).reshape((4, 3)) / 11) assert_array_equal(x[..., 0], x[..., 2]) assert x[0, 0, 0] == 0 assert x[3, 2, 0] == 255 def test_colour(self): - x = prepare_for_display(np.random.random((10, 10, 3))) + prepare_for_display(np.random.random((10, 10, 3))) def test_alpha(self): - x = prepare_for_display(np.random.random((10, 10, 4))) + prepare_for_display(np.random.random((10, 10, 4))) @raises(ValueError) def test_wrong_dimensionality(self): - x = prepare_for_display(np.random.random((10, 10, 1, 1))) + prepare_for_display(np.random.random((10, 10, 1, 1))) @raises(ValueError) def test_wrong_depth(self): - x = prepare_for_display(np.random.random((10, 10, 5))) + prepare_for_display(np.random.random((10, 10, 5))) + class TestWindowManager: callback_called = False @@ -46,7 +48,6 @@ class TestWindowManager: self.callback_called = True def test_callback(self): - cb = lambda x: x self.wm.register_callback(self.callback) self.wm.add_window('window') self.wm.remove_window('window') diff --git a/skimage/io/tests/test_sift.py b/skimage/io/tests/test_sift.py index b6029461..cabc8984 100644 --- a/skimage/io/tests/test_sift.py +++ b/skimage/io/tests/test_sift.py @@ -1,12 +1,11 @@ -import numpy as np from nose.tools import * -from numpy.testing import assert_array_equal, assert_array_almost_equal, \ - assert_equal, run_module_suite +from numpy.testing import assert_equal, run_module_suite from tempfile import NamedTemporaryFile import os from skimage.io import load_sift, load_surf + def test_load_sift(): f = NamedTemporaryFile(delete=False) fname = f.name @@ -40,6 +39,7 @@ def test_load_sift(): assert_equal(features['row'][0], 133.92) assert_equal(features['column'][1], 99.75) + def test_load_surf(): f = NamedTemporaryFile(delete=False) fname = f.name diff --git a/skimage/io/video.py b/skimage/io/video.py index 365589c1..6cb1a8e9 100644 --- a/skimage/io/video.py +++ b/skimage/io/video.py @@ -1,11 +1,12 @@ import numpy as np -import os, time +import os from skimage.io import ImageCollection try: import pygst pygst.require("0.10") - import gst, gobject + import gst + import gobject gobject.threads_init() from gst.extend.discoverer import Discoverer gstreamer_available = True @@ -36,11 +37,11 @@ class CvVideo(object): self.source = source self.capture = cv.CreateFileCapture(self.source) self.size = size - + def get(self): """ Retrieve a video frame as a numpy array. - + Returns ------- output : array (image) @@ -55,31 +56,34 @@ class CvVideo(object): else: cv.Resize(img, cv.fromarray(img_mat)) # opencv stores images in BGR format - cv.CvtColor(cv.fromarray(img_mat), cv.fromarray(img_mat), cv.CV_BGR2RGB) + cv.CvtColor(cv.fromarray(img_mat), cv.fromarray(img_mat), + cv.CV_BGR2RGB) return img_mat - + def seek_frame(self, frame_number): """ Seek to specified frame in video. - + Parameters ---------- frame_number : int Frame position """ - cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_FRAMES, frame_number) - + cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_FRAMES, + frame_number) + def seek_time(self, milliseconds): """ Seek to specified time in video. - + Parameters ---------- milliseconds : int Time position """ - cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_MSEC, milliseconds) - + cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_MSEC, + milliseconds) + def frame_count(self): """ Returns frame count of video. @@ -90,7 +94,7 @@ class CvVideo(object): Frame count. """ return cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FRAME_COUNT) - + def duration(self): """ Returns time length of video in milliseconds. @@ -102,8 +106,8 @@ class CvVideo(object): """ return cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FPS) * \ cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FRAME_COUNT) - - + + class GstVideo(object): """ GStreamer-based video loader. @@ -115,9 +119,9 @@ class GstVideo(object): size: tuple, optional Size of returned array. sync: bool, optional (default False) - Frames are extracted per frame or per time basis. - If enabled the video time step continues onward according to the play rate. - Useful for ip cameras and other real time video feeds. + Frames are extracted per frame or per time basis. If enabled the video + time step continues onward according to the play rate. Useful for ip + cameras and other real time video feeds. """ def __init__(self, source=None, size=None, sync=False): if not gstreamer_available: @@ -127,7 +131,7 @@ class GstVideo(object): self.video_length = 0 self.video_rate = 0 # extract video size - if not size: + if not size: gobject.idle_add(self._discover_one) self.mainloop = gobject.MainLoop() self.mainloop.run() @@ -143,7 +147,7 @@ class GstVideo(object): """ discoverer = Discoverer(self.source) discoverer.connect('discovered', self._discovered) - discoverer.discover() + discoverer.discover() return False def _discovered(self, d, is_media): @@ -152,13 +156,13 @@ class GstVideo(object): """ if is_media: self.size = (d.videowidth, d.videoheight) - self.video_length = d.videolength / gst.MSECOND + self.video_length = d.videolength / gst.MSECOND self.video_rate = d.videorate.num self.mainloop.quit() return False - + def _create_main_pipeline(self, source, size, sync): - """ + """ Create the frame extraction pipeline. """ pipeline_string = "uridecodebin name=decoder uri=%s ! ffmpegcolorspace ! videoscale ! appsink name=play_sink" % self.source @@ -173,42 +177,43 @@ class GstVideo(object): self.appsink.set_property('caps', gst.caps_from_string(caps)) if self.pipeline.set_state(gst.STATE_PLAYING) == gst.STATE_CHANGE_FAILURE: raise NameError("Failed to load video source %s" % self.source) - buff = self.appsink.emit('pull-preroll') - + self.appsink.emit('pull-preroll') + def get(self): """ Retrieve a video frame as a numpy array. - + Returns ------- output : array (image) Retrieved image. """ buff = self.appsink.emit('pull-buffer') - img_mat = np.ndarray(shape=(self.size[1], self.size[0], 3), dtype=np.uint8, buffer=buff.data) + img_mat = np.ndarray(shape=(self.size[1], self.size[0], 3), + dtype=np.uint8, buffer=buff.data) return img_mat def seek_frame(self, frame_number): """ Seek to specified frame in video. - + Parameters ---------- frame_number : int Frame position """ self.pipeline.seek_simple(gst.FORMAT_DEFAULT, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, frame_number) - + def seek_time(self, milliseconds): """ Seek to specified time in video. - + Parameters ---------- milliseconds : int Time position """ - self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, milliseconds/1000.0 * gst.SECOND) + self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, milliseconds / 1000.0 * gst.SECOND) def frame_count(self): """ @@ -219,8 +224,8 @@ class GstVideo(object): output : int Frame count. """ - return self.video_length/1000*self.video_rate - + return self.video_length / 1000 * self.video_rate + def duration(self): """ Returns time length of video in milliseconds. @@ -236,7 +241,7 @@ class GstVideo(object): class Video(object): """ Video loader. Supports Opencv and Gstreamer backends. - + Parameters ---------- source : str @@ -248,7 +253,7 @@ class Video(object): If enabled the video time step continues onward according to the play rate. Useful for IP cameras and other real time video feeds. backend: str, 'gstreamer' or 'opencv' - Backend to use. + Backend to use. """ def __init__(self, source=None, size=None, sync=False, backend=None): if backend == None: @@ -270,29 +275,29 @@ class Video(object): def get(self): """ Retrieve the next video frame as a numpy array. - + Returns ------- output : array (image) Retrieved image. """ return self.video.get() - + def seek_frame(self, frame_number): """ Seek to specified frame in video. - + Parameters ---------- frame_number : int Frame position """ self.video.seek_frame(frame_number) - + def seek_time(self, milliseconds): """ Seek to specified time in video. - + Parameters ---------- milliseconds : int @@ -310,7 +315,7 @@ class Video(object): Frame count. """ return self.video.frame_count() - + def duration(self): """ Returns time length of video in milliseconds. @@ -321,11 +326,11 @@ class Video(object): Time length [ms]. """ return self.video.duration() - + def get_index_frame(self, frame_number): """ Retrieve a specified video frame as a numpy array. - + Parameters ---------- frame_number : int @@ -335,28 +340,27 @@ class Video(object): ------- output : array (image) Retrieved image. - """ + """ self.video.seek_frame(frame_number) return self.video.get() - + def get_collection(self, time_range=None): """ Returns an ImageCollection object. - + Parameters ---------- time_range: range (int), optional Time steps to extract, defaults to the entire length of video. - + Returns ------- - output: ImageCollection + output: ImageCollection Collection of images iterator. """ if not time_range: time_range = range(int(self.frame_count())) return ImageCollection(time_range, load_func=self.get_index_frame) - - -__all__ = ["Video"] + +__all__ = ["Video"] diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 588ec74d..1c92d9ee 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -1,3 +1,4 @@ from .find_contours import find_contours -from ._regionprops import regionprops +from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity +from ._polygon import approximate_polygon, subdivide_polygon \ No newline at end of file diff --git a/skimage/measure/_find_contours.pyx b/skimage/measure/_find_contours.pyx index 17702f95..4f1b3cee 100644 --- a/skimage/measure/_find_contours.pyx +++ b/skimage/measure/_find_contours.pyx @@ -6,14 +6,14 @@ cimport numpy as np np.import_array() -cdef inline double _get_fraction(double from_value, double to_value, +cdef inline double _get_fraction(double from_value, double to_value, double level): if (to_value == from_value): return 0 return ((level - from_value) / (to_value - from_value)) -def iterate_and_store(np.ndarray[double, ndim=2, mode='c'] array, +def iterate_and_store(np.ndarray[double, ndim=2] array, double level, int vertex_connect_high): """Iterate across the given array in a marching-squares fashion, looking for segments that cross 'level'. If such a segment is @@ -92,7 +92,7 @@ def iterate_and_store(np.ndarray[double, ndim=2, mode='c'] array, else: coords[0] += 1 coords[1] = 0 - + square_case = 0 if (ul > level): square_case += 1 @@ -103,7 +103,7 @@ def iterate_and_store(np.ndarray[double, ndim=2, mode='c'] array, if (square_case != 0 and square_case != 15): # only do anything if there's a line passing through the # square. Cases 0 and 15 are entirely below/above the contour. - + top = r0, c0 + _get_fraction(ul, ur, level) bottom = r1, c0 + _get_fraction(ll, lr, level) left = r0 + _get_fraction(ul, ll, level), c0 diff --git a/skimage/measure/_polygon.py b/skimage/measure/_polygon.py new file mode 100644 index 00000000..7b9b920b --- /dev/null +++ b/skimage/measure/_polygon.py @@ -0,0 +1,168 @@ +import numpy as np +from scipy import signal + + +def approximate_polygon(coords, tolerance): + """Approximate a polygonal chain with the specified tolerance. + + It is based on the Douglas-Peucker algorithm. + + Note that the approximated polygon is always within the convex hull of the + original polygon. + + Parameters + ---------- + coords : (N, 2) array + Coordinate array. + tolerance : float + Maximum distance from original points of polygon to approximated + polygonal chain. If tolerance is 0, the original coordinate array + is returned. + + Returns + ------- + coords : (M, 2) array + Approximated polygonal chain where M <= N. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm + """ + if tolerance <= 0: + return coords + + chain = np.zeros(coords.shape[0], 'bool') + # pre-allocate distance array for all points + dists = np.zeros(coords.shape[0]) + chain[0] = True + chain[-1] = True + pos_stack = [(0, chain.shape[0] - 1)] + end_of_chain = False + + while not end_of_chain: + start, end = pos_stack.pop() + # determine properties of current line segment + r0, c0 = coords[start, :] + r1, c1 = coords[end, :] + dr = r1 - r0 + dc = c1 - c0 + segment_angle = - np.arctan2(dr, dc) + segment_dist = c0 * np.sin(segment_angle) + r0 * np.cos(segment_angle) + + # select points in-between line segment + segment_coords = coords[start + 1:end, :] + segment_dists = dists[start + 1:end] + + # check whether to take perpendicular or euclidean distance with + # inner product of vectors + + # vectors from points -> start and end + dr0 = segment_coords[:, 0] - r0 + dc0 = segment_coords[:, 1] - c0 + dr1 = segment_coords[:, 0] - r1 + dc1 = segment_coords[:, 1] - c1 + # vectors points -> start and end projected on start -> end vector + projected_lengths0 = dr0 * dr + dc0 * dc + projected_lengths1 = - dr1 * dr - dc1 * dc + perp = np.logical_and(projected_lengths0 > 0, + projected_lengths1 > 0) + eucl = np.logical_not(perp) + segment_dists[perp] = np.abs( + segment_coords[perp, 0] * np.cos(segment_angle) + + segment_coords[perp, 1] * np.sin(segment_angle) + - segment_dist + ) + segment_dists[eucl] = np.minimum( + # distance to start point + np.sqrt(dc0[eucl] ** 2 + dr0[eucl] ** 2), + # distance to end point + np.sqrt(dc1[eucl] ** 2 + dr1[eucl] ** 2) + ) + + if np.any(segment_dists > tolerance): + # select point with maximum distance to line + new_end = start + np.argmax(segment_dists) + 1 + pos_stack.append((new_end, end)) + pos_stack.append((start, new_end)) + chain[new_end] = True + + if len(pos_stack) == 0: + end_of_chain = True + + return coords[chain, :] + + +# B-Spline subdivision +_SUBDIVISION_MASKS = { + # degree: (mask_even, mask_odd) + # extracted from (degree + 2)th row of Pascal's triangle + 1: ([1, 1], [1, 1]), + 2: ([3, 1], [1, 3]), + 3: ([1, 6, 1], [0, 4, 4]), + 4: ([5, 10, 1], [1, 10, 5]), + 5: ([1, 15, 15, 1], [0, 6, 20, 6]), + 6: ([7, 35, 21, 1], [1, 21, 35, 7]), + 7: ([1, 28, 70, 28, 1], [0, 8, 56, 56, 8]), +} + + +def subdivide_polygon(coords, degree=2, preserve_ends=False): + """Subdivision of polygonal curves using B-Splines. + + Note that the resulting curve is always within the convex hull of the + original polygon. Circular polygons stay closed after subdivision. + + Parameters + ---------- + coords : (N, 2) array + Coordinate array. + degree : {1, 2, 3, 4, 5, 6, 7}, optional + Degree of B-Spline. Default is 2. + preserve_ends : bool, optional + Preserve first and last coordinate of non-circular polygon. Default is + False. + + Returns + ------- + coords : (M, 2) array + Subdivided coordinate array. + + References + ---------- + .. [1] http://mrl.nyu.edu/publications/subdiv-course2000/coursenotes00.pdf + """ + if degree not in _SUBDIVISION_MASKS: + raise ValueError("Invalid B-Spline degree. Only degree 1 - 7 is " + "supported.") + + circular = np.all(coords[0, :] == coords[-1, :]) + + method = 'valid' + if circular: + # remove last coordinate because of wrapping + coords = coords[:-1, :] + # circular convolution by wrapping boundaries + method = 'same' + + mask_even, mask_odd = _SUBDIVISION_MASKS[degree] + # divide by total weight + mask_even = np.array(mask_even, np.float) / (2 ** degree) + mask_odd = np.array(mask_odd, np.float) / (2 ** degree) + + even = signal.convolve2d(coords.T, np.atleast_2d(mask_even), mode=method, + boundary='wrap') + odd = signal.convolve2d(coords.T, np.atleast_2d(mask_odd), mode=method, + boundary='wrap') + + out = np.zeros((even.shape[1] + odd.shape[1], 2)) + out[1::2] = even.T + out[::2] = odd.T + + if circular: + # close polygon + out = np.vstack([out, out[0, :]]) + + if preserve_ends and not circular: + out = np.vstack([coords[0, :], out, coords[-1, :]]) + + return out diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 937b80f5..d285d453 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -1,5 +1,5 @@ # coding: utf-8 -from math import sqrt, atan, pi as PI +from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage @@ -10,6 +10,9 @@ from . import _moments __all__ = ['regionprops'] +STREL_4 = np.array([[0, 1, 0], + [1, 1, 1], + [0, 1, 0]]) STREL_8 = np.ones((3, 3), 'int8') PROPS = ( 'Area', @@ -19,6 +22,7 @@ PROPS = ( 'ConvexArea', # 'ConvexHull', 'ConvexImage', + 'Coordinates', 'Eccentricity', 'EquivDiameter', 'EulerNumber', @@ -36,7 +40,7 @@ PROPS = ( 'Moments', 'NormalizedMoments', 'Orientation', -# 'Perimeter', + 'Perimeter', # 'PixelIdxList', # 'PixelList', 'Solidity', @@ -55,98 +59,146 @@ def regionprops(label_image, properties=['Area', 'Centroid'], Parameters ---------- - label_image : N x M ndarray + label_image : (N, M) ndarray Labelled input image. properties : {'all', list} Shape measurements to be determined for each labelled image region. Default is `['Area', 'Centroid']`. The following properties can be determined: + * Area : int Number of pixels of region. + * BoundingBox : tuple Bounding box `(min_row, min_col, max_row, max_col)` - * CentralMoments : 3 x 3 ndarray + + * CentralMoments : (3, 3) ndarray Central moments (translation invariant) up to 3rd order. + mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + where the sum is over the `x`, `y` coordinates of the region, and `x_c` and `y_c` are the coordinates of the region's centroid. + * Centroid : array Centroid coordinate tuple `(row, col)`. + * ConvexArea : int Number of pixels of convex hull image. - * ConvexImage : H x J ndarray + + * ConvexImage : (H, J) ndarray Binary convex hull image which has the same size as bounding box. + + * Coordinates : (N, 2) ndarray + Coordinate list `(row, col)` of the region. + * Eccentricity : float Eccentricity of the ellipse that has the same second-moments as the region. The eccentricity is the ratio of the distance between its minor and major axis length. The value is between 0 and 1. + * EquivDiameter : float The diameter of a circle with the same area as the region. + * EulerNumber : int Euler number of region. Computed as number of objects (= 1) subtracted by number of holes (8-connectivity). + * Extent : float Ratio of pixels in the region to pixels in the total bounding box. Computed as `Area / (rows*cols)` + * FilledArea : int Number of pixels of filled region. - * FilledImage : H x J ndarray + + * FilledImage : (H, J) ndarray Binary region image with filled holes which has the same size as bounding box. + * HuMoments : tuple Hu moments (translation, scale and rotation invariant). - * Image : H x J ndarray + + * Image : (H, J) ndarray Sliced binary region image which has the same size as bounding box. + * MajorAxisLength : float The length of the major axis of the ellipse that has the same normalized second central moments as the region. + * MaxIntensity: float Value with the greatest intensity in the region. + * MeanIntensity: float Value with the mean intensity in the region. + * MinIntensity: float Value with the least intensity in the region. + * MinorAxisLength : float The length of the minor axis of the ellipse that has the same normalized second central moments as the region. - * Moments : 3 x 3 ndarray + + * Moments : (3, 3) ndarray Spatial moments up to 3rd order. + m_ji = sum{ array(x, y) * x^j * y^i } + where the sum is over the `x`, `y` coordinates of the region. - * NormalizedMoments : 3 x 3 ndarray + + * NormalizedMoments : (3, 3) ndarray Normalized moments (translation and scale invariant) up to 3rd order. + nu_ji = mu_ji / m_00^[(i+j)/2 + 1] + where `m_00` is the zeroth spatial moment. + * Orientation : float Angle between the X-axis and the major axis of the ellipse that has the same second-moments as the region. Ranging from `-pi/2` to - `-pi/2` in counter-clockwise direction. + `pi/2` in counter-clockwise direction. + + * Perimeter : float + Perimeter of object which approximates the contour as a line + through the centers of border pixels using a 4-connectivity. + * Solidity : float Ratio of pixels in the region to pixels of the convex hull image. - * WeightedCentralMoments : 3 x 3 ndarray - Central moments (translation invariant) of intensity image up to 3rd - order. + + * WeightedCentralMoments : (3, 3) ndarray + Central moments (translation invariant) of intensity image up to + 3rd order. + wmu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + where the sum is over the `x`, `y` coordinates of the region, and `x_c` and `y_c` are the coordinates of the region's centroid. + * WeightedCentroid : array Centroid coordinate tuple `(row, col)` weighted with intensity image. + * WeightedHuMoments : tuple Hu moments (translation, scale and rotation invariant) of intensity image. - * WeightedMoments : 3 x 3 ndarray + + * WeightedMoments : (3, 3) ndarray Spatial moments of intensity image up to 3rd order. + wm_ji = sum{ array(x, y) * x^j * y^i } + where the sum is over the `x`, `y` coordinates of the region. - * WeightedNormalizedMoments : 3 x 3 ndarray + + * WeightedNormalizedMoments : (3, 3) ndarray Normalized moments (translation and scale invariant) of intensity image up to 3rd order. + wnu_ji = wmu_ji / wm_00^[(i+j)/2 + 1] + where `wm_00` is the zeroth spatial moment (intensity-weighted area). - intensity_image : N x M ndarray, optional + + intensity_image : (N, M) ndarray, optional Intensity image with same size as labelled image. Default is None. Returns @@ -157,13 +209,14 @@ def regionprops(label_image, properties=['Area', 'Centroid'], References ---------- - Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: Core - Algorithms. Springer-Verlag, London, 2009. - B. Jähne. Digital Image Processing. Springer-Verlag, - Berlin-Heidelberg, 6. edition, 2005. - T. H. Reiss. Recognizing Planar Objects Using Invariant Image Features, - from Lecture notes in computer science, p. 676. Springer, Berlin, 1993. - http://en.wikipedia.org/wiki/Image_moment + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] http://en.wikipedia.org/wiki/Image_moment Examples -------- @@ -201,15 +254,15 @@ def regionprops(label_image, properties=['Area', 'Centroid'], m = _moments.central_moments(array, 0, 0, 3) # centroid - cr = m[0,1] / m[0,0] - cc = m[1,0] / m[0,0] + cr = m[0, 1] / m[0, 0] + cc = m[1, 0] / m[0, 0] mu = _moments.central_moments(array, cr, cc, 3) - #: elements of the inertia tensor [a b; b c] - a = mu[2,0] / mu[0,0] - b = mu[1,1] / mu[0,0] - c = mu[0,2] / mu[0,0] - #: eigen values of inertia tensor + # elements of the inertia tensor [a b; b c] + a = mu[2, 0] / mu[0, 0] + b = mu[1, 1] / mu[0, 0] + c = mu[0, 2] / mu[0, 0] + # eigen values of inertia tensor l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 @@ -219,7 +272,7 @@ def regionprops(label_image, properties=['Area', 'Centroid'], _nu = None if 'Area' in properties: - obj_props['Area'] = m[0,0] + obj_props['Area'] = m[0, 0] if 'BoundingBox' in properties: obj_props['BoundingBox'] = (r0, c0, sl[0].stop, sl[1].stop) @@ -240,6 +293,10 @@ def regionprops(label_image, properties=['Area', 'Centroid'], _convex_image = convex_hull_image(array) obj_props['ConvexImage'] = _convex_image + if 'Coordinates' in properties: + rr, cc = np.nonzero(array) + obj_props['Coordinates'] = np.vstack((rr + r0, cc + c0)).T + if 'Eccentricity' in properties: if l1 == 0: obj_props['Eccentricity'] = 0 @@ -247,17 +304,17 @@ def regionprops(label_image, properties=['Area', 'Centroid'], obj_props['Eccentricity'] = sqrt(1 - l2 / l1) if 'EquivDiameter' in properties: - obj_props['EquivDiameter'] = sqrt(4 * m[0,0] / PI) + obj_props['EquivDiameter'] = sqrt(4 * m[0, 0] / PI) if 'EulerNumber' in properties: if _filled_image is None: _filled_image = ndimage.binary_fill_holes(array, STREL_8) euler_array = _filled_image != array _, num = ndimage.label(euler_array, STREL_8) - obj_props['EulerNumber'] = - num + obj_props['EulerNumber'] = - num if 'Extent' in properties: - obj_props['Extent'] = m[0,0] / (array.shape[0] * array.shape[1]) + obj_props['Extent'] = m[0, 0] / (array.shape[0] * array.shape[1]) if 'HuMoments' in properties: if _nu is None: @@ -293,23 +350,28 @@ def regionprops(label_image, properties=['Area', 'Centroid'], if 'Orientation' in properties: if a - c == 0: - obj_props['Orientation'] = PI / 2 + if b > 0: + obj_props['Orientation'] = -PI / 4. + else: + obj_props['Orientation'] = PI / 4. else: - obj_props['Orientation'] = - 0.5 * atan(2 * b / (a - c)) + obj_props['Orientation'] = - 0.5 * atan2(2 * b, (a - c)) + + if 'Perimeter' in properties: + obj_props['Perimeter'] = perimeter(array, 4) if 'Solidity' in properties: if _convex_image is None: _convex_image = convex_hull_image(array) - obj_props['Solidity'] = m[0,0] / np.sum(_convex_image) - + obj_props['Solidity'] = m[0, 0] / np.sum(_convex_image) if intensity_image is not None: weighted_array = array * intensity_image[sl] wm = _moments.central_moments(weighted_array, 0, 0, 3) # weighted centroid - wcr = wm[0,1] / wm[0,0] - wcc = wm[1,0] / wm[0,0] + wcr = wm[0, 1] / wm[0, 0] + wcc = wm[1, 0] / wm[0, 0] wmu = _moments.central_moments(weighted_array, wcr, wcc, 3) # cached results which are used by several properties @@ -351,3 +413,50 @@ def regionprops(label_image, properties=['Area', 'Centroid'], obj_props['WeightedNormalizedMoments'] = _wnu return props + + +def perimeter(image, neighbourhood=4): + """Calculate total perimeter of all objects in binary image. + + Parameters + ---------- + image : array + binary image + neighbourhood : 4 or 8, optional + neighbourhood connectivity for border pixel determination, default 4 + + Returns + ------- + perimeter : float + total perimeter of all objects in binary image + + References + ---------- + .. [1] K. Benkrid, D. Crookes. Design and FPGA Implementation of + a Perimeter Estimator. The Queen's University of Belfast. + http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc + """ + if neighbourhood == 4: + strel = STREL_4 + else: + strel = STREL_8 + eroded_image = ndimage.binary_erosion(image, strel) + border_image = image - eroded_image + + # perimeter contribution: corresponding values in convolved image + perimeter_weights = { + 1: (5, 7, 15, 17, 25, 27), + sqrt(2): (21, 33), + 1 + sqrt(2) / 2: (13, 23) + } + perimeter_image = ndimage.convolve(border_image, np.array([[10, 2, 10], + [ 2, 1, 2], + [10, 2, 10]])) + total_perimeter = 0 + for weight, values in perimeter_weights.items(): + num_values = 0 + for value in values: + num_values += np.sum(perimeter_image == value) + total_perimeter += num_values * weight + + return total_perimeter diff --git a/skimage/measure/find_contours.py b/skimage/measure/find_contours.py index 92f848d2..3a546ccf 100755 --- a/skimage/measure/find_contours.py +++ b/skimage/measure/find_contours.py @@ -5,6 +5,7 @@ from collections import deque _param_options = ('high', 'low') + def find_contours(array, level, fully_connected='low', positive_orientation='low'): """Find iso-valued contours in a 2D array for a given level value. @@ -83,10 +84,10 @@ def find_contours(array, level, This means that to find reasonable contours, it is best to find contours midway between the expected "light" and "dark" values. In particular, - given a binarized array, *do not* choose to find contours at the low or high - value of the array. This will often yield degenerate contours, especially - around structures that are a single array element wide. Instead choose - a middle value, as above. + given a binarized array, *do not* choose to find contours at the low or + high value of the array. This will often yield degenerate contours, + especially around structures that are a single array element wide. Instead + choose a middle value, as above. References ---------- @@ -97,12 +98,12 @@ def find_contours(array, level, """ array = np.asarray(array, dtype=np.double) if array.ndim != 2: - raise RuntimeError('Only 2D arrays are supported.') + raise ValueError('Only 2D arrays are supported.') level = float(level) if (fully_connected not in _param_options or positive_orientation not in _param_options): - raise ValueError('Parameters "fully_connected" and' - ' "positive_orientation" must be either "high" or "low".') + raise ValueError('Parameters "fully_connected" and' + ' "positive_orientation" must be either "high" or "low".') point_list = _find_contours.iterate_and_store(array, level, fully_connected == 'high') contours = _assemble_contours(_take_2(point_list)) @@ -110,12 +111,14 @@ def find_contours(array, level, contours = [c[::-1] for c in contours] return contours + def _take_2(seq): - iterator = iter(seq) - while(True): - n1 = iterator.next() - n2 = iterator.next() - yield (n1, n2) + iterator = iter(seq) + while(True): + n1 = iterator.next() + n2 = iterator.next() + yield (n1, n2) + def _assemble_contours(points_iterator): current_index = 0 @@ -127,7 +130,8 @@ def _assemble_contours(points_iterator): # This happens when (and only when) one vertex of the square is # exactly the contour level, and the rest are above or below. # This degnerate vertex will be picked up later by neighboring squares. - if from_point == to_point: continue + if from_point == to_point: + continue tail_data = starts.get(to_point) head_data = ends.get(from_point) @@ -143,7 +147,7 @@ def _assemble_contours(points_iterator): head.append(to_point) del starts[to_point] del ends[from_point] - else: # tail is not head + else: # tail is not head # We need to join two distinct contours. # We want to keep the first contour segment created, so that # the final contours are ordered left->right, top->bottom. @@ -157,7 +161,7 @@ def _assemble_contours(points_iterator): # remove the old end of head and add the new end. del ends[from_point] ends[head[-1]] = (head, head_num) - else: # tail_num <= head_num + else: # tail_num <= head_num # head was created second. Prepend head to tail. tail.extendleft(reversed(head)) # remove all traces of head: diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index 4b02a1d1..21d9964e 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -5,6 +5,7 @@ from skimage._build import cython import os base_path = os.path.abspath(os.path.dirname(__file__)) + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs @@ -23,10 +24,10 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Graph-based Image-processing Algorithms', - url = 'https://github.com/scikits-image/scikits-image', - license = 'Modified BSD', + setup(maintainer='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Graph-based Image-processing Algorithms', + url='https://github.com/scikit-image/scikit-image', + license='Modified BSD', **(configuration(top_path='').todict()) ) diff --git a/skimage/measure/tests/test_find_contours.py b/skimage/measure/tests/test_find_contours.py index 8d705878..11b9b443 100644 --- a/skimage/measure/tests/test_find_contours.py +++ b/skimage/measure/tests/test_find_contours.py @@ -1,9 +1,9 @@ import numpy as np from numpy.testing import * -from skimage.measure import find_contours +from skimage.measure import find_contours -a = np.ones((8,8), dtype=np.float32) +a = np.ones((8, 8), dtype=np.float32) a[1:-1, 1] = 0 a[1, 1:-1] = 0 @@ -16,43 +16,48 @@ a[1, 1:-1] = 0 ## [ 1., 0., 1., 1., 1., 1., 1., 1.], ## [ 1., 1., 1., 1., 1., 1., 1., 1.]], dtype=float32) -x,y = np.mgrid[-1:1:5j,-1:1:5j] +x, y = np.mgrid[-1:1:5j, -1:1:5j] r = np.sqrt(x**2 + y**2) + def test_binary(): - contours = find_contours(a, 0.5) - assert len(contours) == 1 - assert_array_equal(contours[0], - [[ 6. , 1.5], - [ 5. , 1.5], - [ 4. , 1.5], - [ 3. , 1.5], - [ 2. , 1.5], - [ 1.5, 2. ], - [ 1.5, 3. ], - [ 1.5, 4. ], - [ 1.5, 5. ], - [ 1.5, 6. ], - [ 1. , 6.5], - [ 0.5, 6. ], - [ 0.5, 5. ], - [ 0.5, 4. ], - [ 0.5, 3. ], - [ 0.5, 2. ], - [ 0.5, 1. ], - [ 1. , 0.5], - [ 2. , 0.5], - [ 3. , 0.5], - [ 4. , 0.5], - [ 5. , 0.5], - [ 6. , 0.5], - [ 6.5, 1. ], - [ 6. , 1.5]]) + ref = [[6. , 1.5], + [5. , 1.5], + [4. , 1.5], + [3. , 1.5], + [2. , 1.5], + [1.5, 2. ], + [1.5, 3. ], + [1.5, 4. ], + [1.5, 5. ], + [1.5, 6. ], + [1. , 6.5], + [0.5, 6. ], + [0.5, 5. ], + [0.5, 4. ], + [0.5, 3. ], + [0.5, 2. ], + [0.5, 1. ], + [1. , 0.5], + [2. , 0.5], + [3. , 0.5], + [4. , 0.5], + [5. , 0.5], + [6. , 0.5], + [6.5, 1. ], + [6. , 1.5]] + + contours = find_contours(a, 0.5, positive_orientation='high') + assert len(contours) == 1 + assert_array_equal(contours[0][::-1], ref) + + + def test_float(): - contours = find_contours(r, 0.5) - assert len(contours) == 1 - assert_array_equal(contours[0], + contours = find_contours(r, 0.5) + assert len(contours) == 1 + assert_array_equal(contours[0], [[ 2., 3.], [ 1., 2.], [ 2., 1.], @@ -60,7 +65,19 @@ def test_float(): [ 2., 3.]]) - +def test_memory_order(): + contours = find_contours(np.ascontiguousarray(r), 0.5) + assert len(contours) == 1 + + contours = find_contours(np.asfortranarray(r), 0.5) + assert len(contours) == 1 + + +def test_invalid_input(): + assert_raises(ValueError, find_contours, r, 0.5, 'foo', 'bar') + assert_raises(ValueError, find_contours, r[..., None], 0.5) + + if __name__ == '__main__': from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/measure/tests/test_polygon.py b/skimage/measure/tests/test_polygon.py new file mode 100644 index 00000000..c96aed88 --- /dev/null +++ b/skimage/measure/tests/test_polygon.py @@ -0,0 +1,62 @@ +import numpy as np +from skimage.measure import approximate_polygon, subdivide_polygon +from skimage.measure._polygon import _SUBDIVISION_MASKS + +square = np.array([ + [0, 0], [0, 1], [0, 2], [0, 3], + [1, 3], [2, 3], [3, 3], + [3, 2], [3, 1], [3, 0], + [2, 0], [1, 0], [0, 0] +]) + + +def test_approximate_polygon(): + out = approximate_polygon(square, 0.1) + np.testing.assert_array_equal(out, square[(0, 3, 6, 9, 12), :]) + + out = approximate_polygon(square, 2.2) + np.testing.assert_array_equal(out, square[(0, 6, 12), :]) + + out = approximate_polygon(square[(0, 1, 3, 4, 5, 6, 7, 9, 11, 12), :], 0.1) + np.testing.assert_array_equal(out, square[(0, 3, 6, 9, 12), :]) + + out = approximate_polygon(square, -1) + np.testing.assert_array_equal(out, square) + out = approximate_polygon(square, 0) + np.testing.assert_array_equal(out, square) + + +def test_subdivide_polygon(): + new_square1 = square + new_square2 = square[:-1] + new_square3 = square[:-1] + # test iterative subdvision + for _ in range(10): + square1, square2, square3 = new_square1, new_square2, new_square3 + # test different B-Spline degrees + for degree in range(1, 7): + mask_len = len(_SUBDIVISION_MASKS[degree][0]) + # test circular + new_square1 = subdivide_polygon(square1, degree) + np.testing.assert_array_equal(new_square1[-1], new_square1[0]) + np.testing.assert_equal(new_square1.shape[0], + 2 * square1.shape[0] - 1) + # test non-circular + new_square2 = subdivide_polygon(square2, degree) + np.testing.assert_equal(new_square2.shape[0], + 2 * (square2.shape[0] - mask_len + 1)) + # test non-circular, preserve_ends + new_square3 = subdivide_polygon(square3, degree, True) + np.testing.assert_equal(new_square3[0], square3[0]) + np.testing.assert_equal(new_square3[-1], square3[-1]) + + np.testing.assert_equal(new_square3.shape[0], + 2 * (square3.shape[0] - mask_len + 2)) + + # not supported B-Spline degree + np.testing.assert_raises(ValueError, subdivide_polygon, square, 0) + np.testing.assert_raises(ValueError, subdivide_polygon, square, 8) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index 4d040b5c..bd23d8b3 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -1,8 +1,9 @@ from numpy.testing import assert_array_equal, assert_almost_equal, \ - assert_array_almost_equal + assert_array_almost_equal, assert_raises import numpy as np +import math -from skimage.measure import regionprops +from skimage.measure._regionprops import regionprops, PROPS, perimeter SAMPLE = np.array( @@ -18,22 +19,34 @@ SAMPLE = np.array( [0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]] ) INTENSITY_SAMPLE = SAMPLE.copy() -INTENSITY_SAMPLE[1,9:11] = 2 +INTENSITY_SAMPLE[1, 9:11] = 2 + + +def test_unsupported_dtype(): + assert_raises(TypeError, regionprops, np.zeros((10, 10), dtype=np.double)) + + +def test_all_props(): + props = regionprops(SAMPLE, 'all', INTENSITY_SAMPLE)[0] + for prop in PROPS: + assert prop in props def test_area(): area = regionprops(SAMPLE, ['Area'])[0]['Area'] assert area == np.sum(SAMPLE) + def test_bbox(): bbox = regionprops(SAMPLE, ['BoundingBox'])[0]['BoundingBox'] assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1])) SAMPLE_mod = SAMPLE.copy() - SAMPLE_mod[:,-1] = 0 + SAMPLE_mod[:, -1] = 0 bbox = regionprops(SAMPLE_mod, ['BoundingBox'])[0]['BoundingBox'] assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]-1)) + def test_central_moments(): mu = regionprops(SAMPLE, ['CentralMoments'])[0]['CentralMoments'] #: determined with OpenCV @@ -46,16 +59,19 @@ def test_central_moments(): assert_almost_equal(mu[2,1], 2000.296296296291) assert_almost_equal(mu[3,0], -760.0246913580195) + def test_centroid(): centroid = regionprops(SAMPLE, ['Centroid'])[0]['Centroid'] # determined with MATLAB assert_array_almost_equal(centroid, (5.66666666666666, 9.444444444444444)) + def test_convex_area(): area = regionprops(SAMPLE, ['ConvexArea'])[0]['ConvexArea'] # determined with MATLAB assert area == 124 + def test_convex_image(): img = regionprops(SAMPLE, ['ConvexImage'])[0]['ConvexImage'] # determined with MATLAB @@ -73,28 +89,46 @@ def test_convex_image(): ) assert_array_equal(img, ref) + +def test_coordinates(): + sample = np.zeros((10, 10), dtype=np.int8) + coords = np.array([[3, 2], [3, 3], [3, 4]]) + sample[coords[:, 0], coords[:, 1]] = 1 + prop_coords = regionprops(sample, ['Coordinates'])[0]['Coordinates'] + assert_array_equal(prop_coords, coords) + + def test_eccentricity(): eps = regionprops(SAMPLE, ['Eccentricity'])[0]['Eccentricity'] assert_almost_equal(eps, 0.814629313427) + img = np.zeros((5, 5), dtype=np.int) + img[2, 2] = 1 + eps = regionprops(img, ['Eccentricity'])[0]['Eccentricity'] + assert_almost_equal(eps, 0) + + def test_equiv_diameter(): diameter = regionprops(SAMPLE, ['EquivDiameter'])[0]['EquivDiameter'] # determined with MATLAB assert_almost_equal(diameter, 9.57461472963) + def test_euler_number(): en = regionprops(SAMPLE, ['EulerNumber'])[0]['EulerNumber'] assert en == 0 SAMPLE_mod = SAMPLE.copy() - SAMPLE_mod[7,-3] = 0 + SAMPLE_mod[7, -3] = 0 en = regionprops(SAMPLE_mod, ['EulerNumber'])[0]['EulerNumber'] assert en == -1 + def test_extent(): extent = regionprops(SAMPLE, ['Extent'])[0]['Extent'] assert_almost_equal(extent, 0.4) + def test_hu_moments(): hu = regionprops(SAMPLE, ['HuMoments'])[0]['HuMoments'] ref = np.array([ @@ -109,46 +143,59 @@ def test_hu_moments(): # bug in OpenCV caused in Central Moments calculation? assert_array_almost_equal(hu, ref) + def test_image(): img = regionprops(SAMPLE, ['Image'])[0]['Image'] assert_array_equal(img, SAMPLE) + def test_filled_area(): area = regionprops(SAMPLE, ['FilledArea'])[0]['FilledArea'] assert area == np.sum(SAMPLE) SAMPLE_mod = SAMPLE.copy() - SAMPLE_mod[7,-3] = 0 + SAMPLE_mod[7, -3] = 0 area = regionprops(SAMPLE_mod, ['FilledArea'])[0]['FilledArea'] assert area == np.sum(SAMPLE) + +def test_filled_image(): + img = regionprops(SAMPLE, ['FilledImage'])[0]['FilledImage'] + assert_array_equal(img, SAMPLE) + + def test_major_axis_length(): length = regionprops(SAMPLE, ['MajorAxisLength'])[0]['MajorAxisLength'] # MATLAB has different interpretation of ellipse than found in literature, # here implemented as found in literature assert_almost_equal(length, 16.7924234999) + def test_max_intensity(): intensity = regionprops(SAMPLE, ['MaxIntensity'], INTENSITY_SAMPLE )[0]['MaxIntensity'] assert_almost_equal(intensity, 2) + def test_mean_intensity(): intensity = regionprops(SAMPLE, ['MeanIntensity'], INTENSITY_SAMPLE )[0]['MeanIntensity'] assert_almost_equal(intensity, 1.02777777777777) + def test_min_intensity(): intensity = regionprops(SAMPLE, ['MinIntensity'], INTENSITY_SAMPLE )[0]['MinIntensity'] assert_almost_equal(intensity, 1) + def test_minor_axis_length(): length = regionprops(SAMPLE, ['MinorAxisLength'])[0]['MinorAxisLength'] # MATLAB has different interpretation of ellipse than found in literature, # here implemented as found in literature assert_almost_equal(length, 9.739302807263) + def test_moments(): m = regionprops(SAMPLE, ['Moments'])[0]['Moments'] #: determined with OpenCV @@ -163,6 +210,7 @@ def test_moments(): assert_almost_equal(m[2,1], 43882.0) assert_almost_equal(m[3,0], 95588.0) + def test_normalized_moments(): nu = regionprops(SAMPLE, ['NormalizedMoments'])[0]['NormalizedMoments'] #: determined with OpenCV @@ -173,16 +221,43 @@ def test_normalized_moments(): assert_almost_equal(nu[2,1], 0.045473992910668816) assert_almost_equal(nu[3,0], -0.017278118992041805) + def test_orientation(): orientation = regionprops(SAMPLE, ['Orientation'])[0]['Orientation'] # determined with MATLAB assert_almost_equal(orientation, 0.10446844651921) + # test correct quadrant determination + orientation2 = regionprops(SAMPLE.T, ['Orientation'])[0]['Orientation'] + assert_almost_equal(orientation2, math.pi / 2 - orientation) + # test diagonal regions + diag = np.eye(10, dtype=int) + orientation_diag = regionprops(diag, ['Orientation'])[0]['Orientation'] + assert_almost_equal(orientation_diag, -math.pi / 4) + orientation_diag = regionprops(np.flipud(diag), ['Orientation'] + )[0]['Orientation'] + assert_almost_equal(orientation_diag, math.pi / 4) + orientation_diag = regionprops(np.fliplr(diag), ['Orientation'] + )[0]['Orientation'] + assert_almost_equal(orientation_diag, math.pi / 4) + orientation_diag = regionprops(np.fliplr(np.flipud(diag)), ['Orientation'] + )[0]['Orientation'] + assert_almost_equal(orientation_diag, -math.pi / 4) + + +def test_perimeter(): + per = regionprops(SAMPLE, ['Perimeter'])[0]['Perimeter'] + assert_almost_equal(per, 59.2132034355964) + + per = perimeter(SAMPLE.astype('double'), neighbourhood=8) + assert_almost_equal(per, 43.1213203436) + def test_solidity(): solidity = regionprops(SAMPLE, ['Solidity'])[0]['Solidity'] # determined with MATLAB assert_almost_equal(solidity, 0.580645161290323) + def test_weighted_central_moments(): wmu = regionprops(SAMPLE, ['WeightedCentralMoments'], INTENSITY_SAMPLE )[0]['WeightedCentralMoments'] @@ -199,11 +274,13 @@ def test_weighted_central_moments(): np.set_printoptions(precision=10) assert_array_almost_equal(wmu, ref) + def test_weighted_centroid(): centroid = regionprops(SAMPLE, ['WeightedCentroid'], INTENSITY_SAMPLE )[0]['WeightedCentroid'] assert_array_almost_equal(centroid, (5.540540540540, 9.445945945945)) + def test_weighted_hu_moments(): whu = regionprops(SAMPLE, ['WeightedHuMoments'], INTENSITY_SAMPLE )[0]['WeightedHuMoments'] @@ -218,6 +295,7 @@ def test_weighted_hu_moments(): ]) assert_array_almost_equal(whu, ref) + def test_weighted_moments(): wm = regionprops(SAMPLE, ['WeightedMoments'], INTENSITY_SAMPLE )[0]['WeightedMoments'] @@ -233,6 +311,7 @@ def test_weighted_moments(): ) assert_array_almost_equal(wm, ref) + def test_weighted_normalized_moments(): wnu = regionprops(SAMPLE, ['WeightedNormalizedMoments'], INTENSITY_SAMPLE )[0]['WeightedNormalizedMoments'] @@ -244,6 +323,7 @@ def test_weighted_normalized_moments(): ) assert_array_almost_equal(wnu, ref) + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/measure/tests/test_structural_similarity.py b/skimage/measure/tests/test_structural_similarity.py index 87846e6f..84bd7cc6 100644 --- a/skimage/measure/tests/test_structural_similarity.py +++ b/skimage/measure/tests/test_structural_similarity.py @@ -1,8 +1,8 @@ import numpy as np -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_raises from skimage.measure import structural_similarity as ssim -import scipy.optimize as opt + def test_ssim_patch_range(): N = 51 @@ -12,6 +12,7 @@ def test_ssim_patch_range(): assert(ssim(X, Y, win_size=N) < 0.1) assert_equal(ssim(X, X, win_size=N), 1) + def test_ssim_image(): N = 100 X = (np.random.random((N, N)) * 255).astype(np.uint8) @@ -23,20 +24,20 @@ def test_ssim_image(): S1 = ssim(X, Y, win_size=3) assert(S1 < 0.3) -## Come up with a better way of testing the gradient -## + +## # NOTE: This test is known to randomly fail on some systems (Mac OS X 10.6) ## def test_ssim_grad(): ## N = 30 ## X = np.random.random((N, N)) * 255 ## Y = np.random.random((N, N)) * 255 -## def func(Y): -## return ssim(X, Y, dynamic_range=255) +## f = ssim(X, Y, dynamic_range=255) +## g = ssim(X, Y, dynamic_range=255, gradient=True) -## def grad(Y): -## return ssim(X, Y, dynamic_range=255, gradient=True)[1] +## assert f < 0.05 +## assert g[0] < 0.05 +## assert np.all(g[1] < 0.05) -## assert(np.all(opt.check_grad(func, grad, Y) < 0.05)) def test_ssim_dtype(): N = 30 @@ -54,5 +55,15 @@ def test_ssim_dtype(): assert S2 < 0.1 +def test_invalid_input(): + X = np.zeros((3, 3), dtype=np.double) + Y = np.zeros((3, 3), dtype=np.int) + assert_raises(ValueError, ssim, X, Y) + + Y = np.zeros((4, 4), dtype=np.double) + assert_raises(ValueError, ssim, X, Y) + + assert_raises(ValueError, ssim, X, X, win_size=8) + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index d639be1c..d4c775eb 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -1,6 +1,9 @@ +from .binary import (binary_erosion, binary_dilation, binary_opening, + binary_closing) from .grey import * from .selem import * from .ccomp import label from .watershed import watershed, is_local_maximum -from .skeletonize import skeletonize, medial_axis +from ._skeletonize import skeletonize, medial_axis from .convex_hull import convex_hull_image +from .greyreconstruct import reconstruction diff --git a/skimage/morphology/_greyreconstruct.pyx b/skimage/morphology/_greyreconstruct.pyx new file mode 100644 index 00000000..e8a84f3b --- /dev/null +++ b/skimage/morphology/_greyreconstruct.pyx @@ -0,0 +1,94 @@ +""" +`reconstruction_loop` originally part of CellProfiler, code licensed under both GPL and BSD licenses. + +Website: http://www.cellprofiler.org +Copyright (c) 2003-2009 Massachusetts Institute of Technology +Copyright (c) 2009-2011 Broad Institute +All rights reserved. +Original author: Lee Kamentsky + +""" +cimport numpy as cnp +cimport cython + + +@cython.boundscheck(False) +def reconstruction_loop(cnp.ndarray[dtype=cnp.uint32_t, ndim=1, + negative_indices=False, mode='c'] aranks, + cnp.ndarray[dtype=cnp.int32_t, ndim=1, + negative_indices=False, mode='c'] aprev, + cnp.ndarray[dtype=cnp.int32_t, ndim=1, + negative_indices=False, mode='c'] anext, + cnp.ndarray[dtype=cnp.int32_t, ndim=1, + negative_indices=False, mode='c'] astrides, + int current_idx, + int image_stride): + """The inner loop for reconstruction. + + This algorithm uses the rank-order of pixels. If low intensity pixels have + a low rank and high intensity pixels have a high rank, then this loop + performs reconstruction by dilation. If this ranking is reversed, the + result is reconstruction by erosion. + + For each pixel in the seed image, check its neighbors. If its neighbor's + rank is below that of the current pixel, replace the neighbor's rank with + the rank of the current pixel. This dilation is limited by the mask, i.e. + the rank at each pixel cannot exceed the mask as that pixel. + + Parameters + ---------- + aranks : array + The rank order of the flattened seed and mask images. + aprev, anext: arrays + Indices of previous and next pixels in rank sorted order. + astrides : array + Strides to neighbors of the current pixel. + current_idx : int + Index of highest-ranked pixel used as starting point in loop. + image_stride : int + Stride between seed image and mask image in `aranks`. + """ + cdef unsigned int neighbor_rank, current_rank, mask_rank + cdef int i, neighbor_idx, current_link, nprev, nnext + cdef int nstrides = astrides.shape[0] + cdef cnp.uint32_t *ranks = (aranks.data) + cdef cnp.int32_t *prev = (aprev.data) + cdef cnp.int32_t *next = (anext.data) + cdef cnp.int32_t *strides = (astrides.data) + + while current_idx != -1: + if current_idx < image_stride: + current_rank = ranks[current_idx] + if current_rank == 0: + break + for i in range(nstrides): + neighbor_idx = current_idx + strides[i] + neighbor_rank = ranks[neighbor_idx] + # Only propagate neighbors ranked below the current rank + if neighbor_rank < current_rank: + mask_rank = ranks[neighbor_idx + image_stride] + # Only propagate neighbors ranked below the mask rank + if neighbor_rank < mask_rank: + # Raise the neighbor to the mask rank if + # the mask ranked below the current rank + if mask_rank < current_rank: + current_link = neighbor_idx + image_stride + ranks[neighbor_idx] = mask_rank + else: + current_link = current_idx + ranks[neighbor_idx] = current_rank + # unlink the neighbor + nprev = prev[neighbor_idx] + nnext = next[neighbor_idx] + next[nprev] = nnext + if nnext != -1: + prev[nnext] = nprev + # link to the neighbor after the current link + nnext = next[current_link] + next[neighbor_idx] = nnext + prev[neighbor_idx] = current_link + if nnext >= 0: + prev[nnext] = neighbor_idx + next[current_link] = neighbor_idx + current_idx = next[current_idx] + diff --git a/skimage/morphology/_pnpoly.h b/skimage/morphology/_pnpoly.h deleted file mode 100644 index 95c89bcb..00000000 --- a/skimage/morphology/_pnpoly.h +++ /dev/null @@ -1,72 +0,0 @@ -/* `pnpoly` is from - - http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html - - Copyright (c) 1970-2003, Wm. Randolph Franklin - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, copy, - modify, merge, publish, distribute, sublicense, and/or sell copies - of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimers. - 2. Redistributions in binary form must reproduce the above - copyright notice in the documentation and/or other materials - provided with the distribution. - 3. The name of W. Randolph Franklin may not be used to endorse or - promote products derived from this Software without specific - prior written permission. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS - BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN - ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. */ - -#ifdef __cplusplus -extern "C" { -#endif - -unsigned char pnpoly(int nr_verts, double *xp, double *yp, double x, double y) -{ - int i, j; - unsigned char c = 0; - for (i = 0, j = nr_verts-1; i < nr_verts; j = i++) { - if ((((yp[i]<=y) && (yvx.data, vy.data, m, n) + out[m, n] = point_in_polygon(V, vx.data, vy.data, m, n) return out.view(bool) - + def points_inside_poly(points, verts): - """Test whether points lie inside a polygon. + """Test whether points lie inside a polygon. - Parameters - ---------- - points : (N, 2) array - Input points, ``(x, y)``. - verts : (M, 2) array - Vertices of the polygon, sorted either clockwise or anti-clockwise. - The first point may (but does not need to be) duplicated. + Parameters + ---------- + points : (N, 2) array + Input points, ``(x, y)``. + verts : (M, 2) array + Vertices of the polygon, sorted either clockwise or anti-clockwise. + The first point may (but does not need to be) duplicated. - Returns - ------- - mask : (N,) array of bool - True if corresponding point is inside the polygon. + Returns + ------- + mask : (N,) array of bool + True if corresponding point is inside the polygon. - """ - cdef np.ndarray[np.double_t, ndim=1, mode="c"] x, y, vx, vy + """ + cdef np.ndarray[np.double_t, ndim=1, mode="c"] x, y, vx, vy - points = np.asarray(points) - verts = np.asarray(verts) + points = np.asarray(points) + verts = np.asarray(verts) - x = points[:, 0].astype(np.double) - y = points[:, 1].astype(np.double) + x = points[:, 0].astype(np.double) + y = points[:, 1].astype(np.double) - vx = verts[:, 0].astype(np.double) - vy = verts[:, 1].astype(np.double) + vx = verts[:, 0].astype(np.double) + vy = verts[:, 1].astype(np.double) - cdef np.ndarray[np.uint8_t, ndim=1] out = \ - np.zeros(x.shape[0], dtype=np.uint8) - - npnpoly(vx.shape[0], vx.data, vy.data, - x.shape[0], x.data, y.data, - out.data) + cdef np.ndarray[np.uint8_t, ndim=1] out = \ + np.zeros(x.shape[0], dtype=np.uint8) - return out.astype(bool) + points_in_polygon(vx.shape[0], vx.data, vy.data, + x.shape[0], x.data, y.data, + out.data) + + return out.astype(bool) diff --git a/skimage/morphology/skeletonize.py b/skimage/morphology/_skeletonize.py similarity index 90% rename from skimage/morphology/skeletonize.py rename to skimage/morphology/_skeletonize.py index e734d8c6..04a65da6 100644 --- a/skimage/morphology/skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -5,35 +5,36 @@ Algorithms for computing the skeleton of a binary image import numpy as np from scipy import ndimage -from ._skeletonize import _skeletonize_loop, _table_lookup_index +from ._skeletonize_cy import _skeletonize_loop, _table_lookup_index # --------- Skeletonization by morphological thinning --------- + def skeletonize(image): """Return the skeleton of a binary image. - + Thinning is used to reduce each connected component in a binary image - to a single-pixel wide skeleton. - + to a single-pixel wide skeleton. + Parameters ---------- - image : numpy.ndarray - A binary image containing the objects to be skeletonized. '1' - represents foreground, and '0' represents background. It + image : numpy.ndarray + A binary image containing the objects to be skeletonized. '1' + represents foreground, and '0' represents background. It also accepts arrays of boolean values where True is foreground. - + Returns ------- skeleton : ndarray A matrix containing the thinned image. - + See also -------- medial_axis Notes ----- - The algorithm [1] works by making successive passes of the image, + The algorithm [1] works by making successive passes of the image, removing pixels on object borders. This continues until no more pixels can be removed. The image is correlated with a mask that assigns each pixel a number in the range [0...255] @@ -41,18 +42,18 @@ def skeletonize(image): pixels. A look up table is then used to assign the pixels a value of 0, 1, 2 or 3, which are selectively removed during the iterations. - - Note that this algorithm will give different results than a + + Note that this algorithm will give different results than a medial axis transform, which is also often referred to as - "skeletonization". - + "skeletonization". + References ---------- - .. [1] A fast parallel algorithm for thinning digital patterns, + .. [1] A fast parallel algorithm for thinning digital patterns, T. Y. ZHANG and C. Y. SUEN, Communications of the ACM, March 1984, Volume 27, Number 3 - + Examples -------- >>> X, Y = np.ogrid[0:9, 0:9] @@ -78,16 +79,16 @@ def skeletonize(image): [0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) - + """ # look up table - there is one entry for each of the 2^8=256 possible # combinations of 8 binary neighbours. 1's, 2's and 3's are candidates # for removal at each iteration of the algorithm. lut = [ 0,0,0,1,0,0,1,3,0,0,3,1,1,0,1,3,0,0,0,0,0,0,0,0,2,0,2,0,3,0,3,3, - 0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,3,0,2,2, + 0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,3,0,2,2, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 2,0,0,0,0,0,0,0,2,0,0,0,2,0,0,0,3,0,0,0,0,0,0,0,3,0,0,0,3,0,2,0, - 0,1,3,1,0,0,1,3,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 0,0,3,1,0,0,1,3,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 2,3,1,3,0,0,1,3,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 2,3,0,1,0,0,0,1,0,0,0,0,0,0,0,0,3,3,0,1,0,0,0,0,2,2,0,0,2,0,0,0] @@ -97,38 +98,38 @@ def skeletonize(image): # check some properties of the input image: # - 2D - # - binary image with only 0's and 1's + # - binary image with only 0's and 1's if skeleton.ndim != 2: - raise ValueError('Skeletonize requires a 2D array') + raise ValueError('Skeletonize requires a 2D array') if not np.all(np.in1d(skeleton.flat, (0, 1))): raise ValueError('Image contains values other than 0 and 1') - + # create the mask that will assign a unique value based on the # arrangement of neighbouring pixels - mask = np.array([[ 1, 2, 4], + mask = np.array([[1, 2, 4], [128, 0, 8], - [ 64, 32, 16]], np.uint8) + [64, 32, 16]], np.uint8) pixelRemoved = True while pixelRemoved: - pixelRemoved = False; + pixelRemoved = False # assign each pixel a unique value based on its foreground neighbours neighbours = ndimage.correlate(skeleton, mask, mode='constant') - + # ignore background neighbours *= skeleton - + # use LUT to categorize each foreground pixel as a 0, 1, 2 or 3 codes = np.take(lut, neighbours) - + # pass 1 - remove the 1's and 3's code_mask = (codes == 1) - if np.any(code_mask): + if np.any(code_mask): pixelRemoved = True skeleton[code_mask] = 0 code_mask = (codes == 3) - if np.any(code_mask): + if np.any(code_mask): pixelRemoved = True skeleton[code_mask] = 0 @@ -137,11 +138,11 @@ def skeletonize(image): neighbours *= skeleton codes = np.take(lut, neighbours) code_mask = (codes == 2) - if np.any(code_mask): + if np.any(code_mask): pixelRemoved = True skeleton[code_mask] = 0 code_mask = (codes == 3) - if np.any(code_mask): + if np.any(code_mask): pixelRemoved = True skeleton[code_mask] = 0 @@ -159,8 +160,8 @@ def medial_axis(image, mask=None, return_distance=False): Parameters ---------- - image : binary ndarray - + image : binary ndarray + mask : binary ndarray, optional If a mask is given, only those elements with a true value in `mask` are used for computing the medial axis. @@ -177,18 +178,18 @@ def medial_axis(image, mask=None, return_distance=False): dist : ndarray of ints Distance transform of the image (only returned if `return_distance` is True) - + See also -------- - skeletonize + skeletonize Notes ----- This algorithm computes the medial axis transform of an image - as the ridges of its distance transform. - + as the ridges of its distance transform. + The different steps of the algorithm are as follows - * A lookup table is used, that assigns 0 or 1 to each configuration of + * A lookup table is used, that assigns 0 or 1 to each configuration of the 3x3 binary square, whether the central pixel should be removed or kept. We want a point to be removed if it has more than one neighbor and if removing it does not change the number of connected components. @@ -197,12 +198,12 @@ def medial_axis(image, mask=None, return_distance=False): the cornerness of the pixel. * The foreground (value of 1) points are ordered by - the distance transform, then the cornerness. - - * A cython function is called to reduce the image to its skeleton. It - processes pixels in the order determined at the previous step, and - removes or maintains a pixel according to the lookup table. Because - of the ordering, it is possible to process all pixels in only one + the distance transform, then the cornerness. + + * A cython function is called to reduce the image to its skeleton. It + processes pixels in the order determined at the previous step, and + removes or maintains a pixel according to the lookup table. Because + of the ordering, it is possible to process all pixels in only one pass. Examples @@ -234,7 +235,7 @@ def medial_axis(image, mask=None, return_distance=False): masked_image = image.astype(bool).copy() masked_image[~mask] = False # - # Build lookup table - three conditions + # Build lookup table - three conditions # 1. Keep only positive pixels (center_is_foreground array). # AND # 2. Keep if removing the pixel results in a different connectivity @@ -249,30 +250,29 @@ def medial_axis(image, mask=None, return_distance=False): (np.array([ndimage.label(_pattern_of(index), _eight_connect)[1] != ndimage.label(_pattern_of(index & ~ 2**4), _eight_connect)[1] - for index in range(512)]) # condition 2 - | + for index in range(512)]) # condition 2 + | np.array([np.sum(_pattern_of(index)) < 3 for index in range(512)])) # condition 3 ) - - + # Build distance transform distance = ndimage.distance_transform_edt(masked_image) if return_distance: store_distance = distance.copy() - + # Corners # The processing order along the edge is critical to the shape of the # resulting skeleton: if you process a corner first, that corner will # be eroded and the skeleton will miss the arm from that corner. Pixels # with fewer neighbors are more "cornery" and should be processed last. - # We use a cornerness_table lookup table where the score of a + # We use a cornerness_table lookup table where the score of a # configuration is the number of background (0-value) pixels in the # 3x3 neighbourhood cornerness_table = np.array([9 - np.sum(_pattern_of(index)) for index in range(512)]) corner_score = _table_lookup(masked_image, cornerness_table) - + # Define arrays for inner loop i, j = np.mgrid[0:image.shape[0], 0:image.shape[1]] result = masked_image.copy() @@ -280,7 +280,7 @@ def medial_axis(image, mask=None, return_distance=False): i = np.ascontiguousarray(i[result], np.int32) j = np.ascontiguousarray(j[result], np.int32) result = np.ascontiguousarray(result, np.uint8) - + # Determine the order in which pixels are processed. # We use a random # for tiebreaking. Assign each pixel in the image a # predictable, random # so that masking doesn't affect arbitrary choices @@ -305,6 +305,7 @@ def medial_axis(image, mask=None, return_distance=False): else: return result + def _pattern_of(index): """ Return the pattern represented by an index value @@ -317,9 +318,9 @@ def _pattern_of(index): def _table_lookup(image, table): """ - Perform a morphological transform on an image, directed by its + Perform a morphological transform on an image, directed by its neighbors - + Parameters ---------- image : ndarray @@ -329,7 +330,7 @@ def _table_lookup(image, table): the values of that pixel and its 8-connected neighbors. border_value : bool The value of pixels beyond the border of the image. - + Returns ------- result : ndarray of same shape as `image` @@ -338,13 +339,14 @@ def _table_lookup(image, table): Notes ----- The pixels are numbered like this:: - + + 0 1 2 3 4 5 6 7 8 The index at a pixel is the sum of 2** for pixels - that evaluate to true. + that evaluate to true. """ # # We accumulate into the indexer to get the index into the table @@ -356,11 +358,11 @@ def _table_lookup(image, table): indexer[1:, 1:] += image[:-1, :-1] * 2**0 indexer[1:, :] += image[:-1, :] * 2**1 indexer[1:, :-1] += image[:-1, 1:] * 2**2 - + indexer[:, 1:] += image[:, :-1] * 2**3 indexer[:, :] += image[:, :] * 2**4 indexer[:, :-1] += image[:, 1:] * 2**5 - + indexer[:-1, 1:] += image[1:, :-1] * 2**6 indexer[:-1, :] += image[1:, :] * 2**7 indexer[:-1, :-1] += image[1:, 1:] * 2**8 @@ -368,4 +370,3 @@ def _table_lookup(image, table): indexer = _table_lookup_index(np.ascontiguousarray(image, np.uint8)) image = table[indexer] return image - diff --git a/skimage/morphology/_skeletonize.pyx b/skimage/morphology/_skeletonize_cy.pyx similarity index 100% rename from skimage/morphology/_skeletonize.pyx rename to skimage/morphology/_skeletonize_cy.pyx diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py new file mode 100644 index 00000000..e2e0f20b --- /dev/null +++ b/skimage/morphology/binary.py @@ -0,0 +1,137 @@ +import numpy as np +from scipy import ndimage + + +def binary_erosion(image, selem, out=None): + """Return fast binary morphological erosion of an image. + + This function returns the same result as greyscale erosion but performs + faster for binary images. + + Morphological erosion sets a pixel at (i,j) to the minimum over all pixels + in the neighborhood centered at (i,j). Erosion shrinks bright regions and + enlarges dark regions. + + Parameters + ---------- + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None is + passed, a new array will be allocated. + + Returns + ------- + eroded : bool array + The result of the morphological erosion. + + """ + + conv = ndimage.convolve(image > 0, selem, output=out, + mode='constant', cval=1) + if conv is not None: + out = conv + return np.equal(out, np.sum(selem), out=out) + + +def binary_dilation(image, selem, out=None): + """Return fast binary morphological dilation of an image. + + This function returns the same result as greyscale dilation but performs + faster for binary images. + + Morphological dilation sets a pixel at (i,j) to the maximum over all pixels + in the neighborhood centered at (i,j). Dilation enlarges bright regions + and shrinks dark regions. + + Parameters + ---------- + + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None, is + passed, a new array will be allocated. + + Returns + ------- + dilated : bool array + The result of the morphological dilation. + + """ + + conv = ndimage.convolve(image > 0, selem, output=out, + mode='constant', cval=0) + if conv is not None: + out = conv + return np.not_equal(out, 0, out=out) + + +def binary_opening(image, selem, out=None): + """Return fast binary morphological opening of an image. + + This function returns the same result as greyscale opening but performs + faster for binary images. + + The morphological opening on an image is defined as an erosion followed by + a dilation. Opening can remove small bright spots (i.e. "salt") and connect + small dark cracks. This tends to "open" up (dark) gaps between (bright) + features. + + Parameters + ---------- + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None + is passed, a new array will be allocated. + + Returns + ------- + opening : bool array + The result of the morphological opening. + + """ + + eroded = binary_erosion(image, selem) + out = binary_dilation(eroded, selem, out=out) + return out + + +def binary_closing(image, selem, out=None): + """Return fast binary morphological closing of an image. + + This function returns the same result as greyscale closing but performs + faster for binary images. + + The morphological closing on an image is defined as a dilation followed by + an erosion. Closing can remove small dark spots (i.e. "pepper") and connect + small bright cracks. This tends to "close" up (dark) gaps between (bright) + features. + + Parameters + ---------- + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None, + is passed, a new array will be allocated. + + Returns + ------- + closing : bool array + The result of the morphological closing. + + """ + + dilated = binary_dilation(image, selem) + out = binary_erosion(dilated, selem, out=out) + return out diff --git a/skimage/morphology/ccomp.pxd b/skimage/morphology/ccomp.pxd new file mode 100644 index 00000000..0b431832 --- /dev/null +++ b/skimage/morphology/ccomp.pxd @@ -0,0 +1,10 @@ +"""Export fast union find in Cython""" +cimport numpy as np + +DTYPE = np.int +ctypedef np.int_t DTYPE_t + +cdef DTYPE_t find_root(np.int_t *forest, np.int_t n) +cdef set_root(np.int_t *forest, np.int_t n, np.int_t root) +cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m) +cdef link_bg(np.int_t *forest, np.int_t n, np.int_t *background_node) diff --git a/skimage/morphology/ccomp.pyx b/skimage/morphology/ccomp.pyx index a1d5f303..6a4fb1f2 100644 --- a/skimage/morphology/ccomp.pyx +++ b/skimage/morphology/ccomp.pyx @@ -24,7 +24,6 @@ See also: # The term "forest" is used to indicate an array that stores one or more trees DTYPE = np.int -ctypedef np.int_t DTYPE_t cdef DTYPE_t find_root(np.int_t *forest, np.int_t n): """Find the root of node n. @@ -77,8 +76,7 @@ cdef link_bg(np.int_t *forest, np.int_t n, np.int_t *background_node): # Connected components search as described in Fiorio et al. -def label(np.ndarray[DTYPE_t, ndim=2] input, - np.int_t neighbors=8, np.int_t background=-1): +def label(input, np.int_t neighbors=8, np.int_t background=-1): """Label connected regions of an integer array. Two pixels are connected when they are neighbors and have the same value. @@ -89,7 +87,7 @@ def label(np.ndarray[DTYPE_t, ndim=2] input, [ ] [ ] [ ] [ ] | \ | / [ ]--[ ]--[ ] [ ]--[ ]--[ ] - | / | \ + | / | \ [ ] [ ] [ ] [ ] Parameters @@ -139,7 +137,8 @@ def label(np.ndarray[DTYPE_t, ndim=2] input, cdef np.int_t rows = input.shape[0] cdef np.int_t cols = input.shape[1] - cdef np.ndarray[DTYPE_t, ndim=2] data = input.copy() + cdef np.ndarray[DTYPE_t, ndim=2] data = np.array(input, copy=True, + dtype=DTYPE) cdef np.ndarray[DTYPE_t, ndim=2] forest forest = np.arange(data.size, dtype=DTYPE).reshape((rows, cols)) diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 6870b500..9b8b3a27 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -1,116 +1,118 @@ -""" -:author: Damian Eads, 2009 -:license: modified BSD -""" +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False -from __future__ import division import numpy as np - cimport numpy as np -cimport cython -from cpython cimport bool +from libc.stdlib cimport malloc, free -STREL_DTYPE = np.uint8 -ctypedef np.uint8_t STREL_DTYPE_t -IMAGE_DTYPE = np.uint8 -ctypedef np.uint8_t IMAGE_DTYPE_t +def dilate(np.ndarray[np.uint8_t, ndim=2] image, + np.ndarray[np.uint8_t, ndim=2] selem, + np.ndarray[np.uint8_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): -cdef inline int int_max(int a, int b): return a if a >= b else b -cdef inline int int_min(int a, int b): return a if a <= b else b + cdef int rows = image.shape[0] + cdef int cols = image.shape[1] + cdef int srows = selem.shape[0] + cdef int scols = selem.shape[1] -@cython.boundscheck(False) -def dilate(np.ndarray[IMAGE_DTYPE_t, ndim=2] image not None, - np.ndarray[IMAGE_DTYPE_t, ndim=2] selem not None, - np.ndarray[IMAGE_DTYPE_t, ndim=2] out, - bool shift_x, bool shift_y): - cdef int hw = selem.shape[0] // 2 - cdef int hh = selem.shape[1] // 2 - if shift_x: - hh -= 1 - if shift_y: - hw -= 1 + cdef int centre_r = int(selem.shape[0] / 2) - shift_y + cdef int centre_c = int(selem.shape[1] / 2) - shift_x - cdef int width = image.shape[0], height = image.shape[1] + image = np.ascontiguousarray(image) if out is None: - out = np.zeros([width, height], dtype=IMAGE_DTYPE) + out = np.zeros((rows, cols), dtype=np.uint8) + else: + out = np.ascontiguousarray(out) - assert out.shape[0] == image.shape[0] - assert out.shape[1] == image.shape[1] + cdef np.uint8_t* out_data = out.data + cdef np.uint8_t* image_data = image.data - cdef int x, y, ix, iy, cx, cy - cdef IMAGE_DTYPE_t max_so_far + cdef int r, c, rr, cc, s, value, local_max - cdef int sw = selem.shape[0], sh = selem.shape[1] + cdef int selem_num = np.sum(selem != 0) + cdef int* sr = malloc(selem_num * sizeof(int)) + cdef int* sc = malloc(selem_num * sizeof(int)) - cdef np.ndarray[np.int_t, ndim=2] xinc = np.zeros([sw, sh], dtype=np.int) - cdef np.ndarray[np.int_t, ndim=2] yinc = np.zeros([sw, sh], dtype=np.int) + s = 0 + for r in range(srows): + for c in range(scols): + if selem[r, c] != 0: + sr[s] = r - centre_r + sc[s] = c - centre_c + s += 1 - for x in range(sw): - for y in range(sh): - xinc[x, y] = (x - hw) - yinc[x, y] = (y - hh) + for r in range(rows): + for c in range(cols): + local_max = 0 + for s in range(selem_num): + rr = r + sr[s] + cc = c + sc[s] + if 0 <= rr < rows and 0 <= cc < cols: + value = image_data[rr * cols + cc] + if value > local_max: + local_max = value + out_data[r * cols + c] = local_max - for x in range(width): - for y in range(height): - max_so_far = 0 - for cx in range(0, sw): - for cy in range(0, sh): - ix = x + xinc[cx,cy] - iy = y + yinc[cx,cy] - if ix>=0 and iy>=0 and ix < width and iy < height \ - and selem[cx, cy] == 1 \ - and image[ix,iy] > max_so_far: - max_so_far = image[ix,iy] - out[x,y] = max_so_far + free(sr) + free(sc) return out -@cython.boundscheck(False) -def erode(np.ndarray[IMAGE_DTYPE_t, ndim=2] image not None, - np.ndarray[IMAGE_DTYPE_t, ndim=2] selem not None, - np.ndarray[IMAGE_DTYPE_t, ndim=2] out, - bool shift_x, bool shift_y): - cdef int hw = selem.shape[0] // 2 - cdef int hh = selem.shape[1] // 2 - if shift_x: - hh -= 1 - if shift_y: - hw -= 1 +def erode(np.ndarray[np.uint8_t, ndim=2] image, + np.ndarray[np.uint8_t, ndim=2] selem, + np.ndarray[np.uint8_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): - cdef int width = image.shape[0], height = image.shape[1] + cdef int rows = image.shape[0] + cdef int cols = image.shape[1] + cdef int srows = selem.shape[0] + cdef int scols = selem.shape[1] + + cdef int centre_r = int(selem.shape[0] / 2) - shift_y + cdef int centre_c = int(selem.shape[1] / 2) - shift_x + + image = np.ascontiguousarray(image) if out is None: - out = np.zeros([width, height], dtype=IMAGE_DTYPE) + out = np.zeros((rows, cols), dtype=np.uint8) + else: + out = np.ascontiguousarray(out) - assert out.shape[0] == image.shape[0] - assert out.shape[1] == image.shape[1] + cdef np.uint8_t* out_data = out.data + cdef np.uint8_t* image_data = image.data - cdef int x, y, ix, iy, cx, cy - cdef IMAGE_DTYPE_t min_so_far + cdef int r, c, rr, cc, s, value, local_min - cdef int sw = selem.shape[0], sh = selem.shape[1] + cdef int selem_num = np.sum(selem != 0) + cdef int* sr = malloc(selem_num * sizeof(int)) + cdef int* sc = malloc(selem_num * sizeof(int)) - cdef np.ndarray[np.int_t, ndim=2] xinc = np.zeros([sw, sh], dtype=np.int) - cdef np.ndarray[np.int_t, ndim=2] yinc = np.zeros([sw, sh], dtype=np.int) + s = 0 + for r in range(srows): + for c in range(scols): + if selem[r, c] != 0: + sr[s] = r - centre_r + sc[s] = c - centre_c + s += 1 - for x in range(sw): - for y in range(sh): - xinc[x, y] = (x - hw) - yinc[x, y] = (y - hh) + for r in range(rows): + for c in range(cols): + local_min = 255 + for s in range(selem_num): + rr = r + sr[s] + cc = c + sc[s] + if 0 <= rr < rows and 0 <= cc < cols: + value = image_data[rr * cols + cc] + if value < local_min: + local_min = value - for x in range(width): - for y in range(height): - min_so_far = 255 - for cx in range(0, sw): - for cy in range(0, sh): - ix = x + xinc[cx,cy] - iy = y + yinc[cx,cy] - if ix>=0 and iy>=0 and ix < width \ - and iy < height and selem[cx, cy] == 1 \ - and image[ix,iy] < min_so_far: - min_so_far = image[ix,iy] - out[x,y] = min_so_far + out_data[r * cols + c] = local_min + + free(sr) + free(sc) return out diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index f461dc4d..08ff0e04 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -1,9 +1,10 @@ __all__ = ['convex_hull_image'] import numpy as np -from ._pnpoly import points_inside_poly, grid_points_inside_poly +from ._pnpoly import grid_points_inside_poly from ._convex_hull import possible_hull + def convex_hull_image(image): """Compute the convex hull image of a binary image. @@ -34,7 +35,7 @@ def convex_hull_image(image): # hull. coords = possible_hull(image.astype(np.uint8)) N = len(coords) - + # Add a vertex for the middle of each pixel edge coords_corners = np.empty((N * 4, 2)) for i, (x_offset, y_offset) in enumerate(zip((0, 0, -0.5, 0.5), @@ -61,5 +62,5 @@ def convex_hull_image(image): # For each pixel coordinate, check whether that pixel # lies inside the convex hull mask = grid_points_inside_poly(image.shape[:2], v) - + return mask diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index ee8542dd..2cca69b6 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -1,15 +1,7 @@ -""" -:author: Damian Eads, 2009 -:license: modified BSD -""" - -__docformat__ = 'restructuredtext en' - import warnings +from skimage import img_as_ubyte -import numpy as np - -import skimage +from . import cmorph __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', @@ -28,15 +20,12 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None is - passed, a new array will be allocated. - + The array to store the result of the morphology. If None is + passed, a new array will be allocated. shift_x, shift_y : bool shift structuring element about center point. This only affects eccentric structuring elements (i.e. selem with even numbered sides). @@ -44,11 +33,12 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): Returns ------- eroded : uint8 array - The result of the morphological erosion. + The result of the morphological erosion. Examples -------- >>> # Erosion shrinks bright regions + >>> import numpy as np >>> from skimage.morphology import square >>> bright_square = np.array([[0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], @@ -60,20 +50,16 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): [0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ + if image is out: raise NotImplementedError("In-place erosion not supported!") - image = skimage.img_as_ubyte(image) - - try: - import skimage.morphology.cmorph as cmorph - out = cmorph.erode(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) - return out; - except ImportError: - raise ImportError("cmorph extension not available.") + image = img_as_ubyte(image) + selem = img_as_ubyte(selem) + return cmorph.erode(image, selem, out=out, + shift_x=shift_x, shift_y=shift_y) def dilation(image, selem, out=None, shift_x=False, shift_y=False): @@ -87,15 +73,12 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None, is - passed, a new array will be allocated. - + The array to store the result of the morphology. If None, is + passed, a new array will be allocated. shift_x, shift_y : bool shift structuring element about center point. This only affects eccentric structuring elements (i.e. selem with even numbered sides). @@ -103,11 +86,12 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): Returns ------- dilated : uint8 array - The result of the morphological dilation. + The result of the morphological dilation. Examples -------- >>> # Dilation enlarges bright regions + >>> import numpy as np >>> from skimage.morphology import square >>> bright_pixel = np.array([[0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0], @@ -119,20 +103,16 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): [0, 1, 1, 1, 0], [0, 1, 1, 1, 0], [0, 1, 1, 1, 0], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ + if image is out: raise NotImplementedError("In-place dilation not supported!") - image = skimage.img_as_ubyte(image) - - try: - from . import cmorph - out = cmorph.dilate(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) - return out; - except ImportError: - raise ImportError("cmorph extension not available.") + image = img_as_ubyte(image) + selem = img_as_ubyte(selem) + return cmorph.dilate(image, selem, out=out, + shift_x=shift_x, shift_y=shift_y) def opening(image, selem, out=None): @@ -146,23 +126,22 @@ def opening(image, selem, out=None): Parameters ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None - is passed, a new array will be allocated. + The array to store the result of the morphology. If None + is passed, a new array will be allocated. Returns ------- opening : uint8 array - The result of the morphological opening. + The result of the morphological opening. Examples -------- >>> # Open up gap between two bright regions (but also shrink regions) + >>> import numpy as np >>> from skimage.morphology import square >>> bad_connection = np.array([[1, 0, 0, 0, 1], ... [1, 1, 0, 1, 1], @@ -174,9 +153,10 @@ def opening(image, selem, out=None): [1, 1, 0, 1, 1], [1, 1, 0, 1, 1], [1, 1, 0, 1, 1], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ + h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -197,23 +177,22 @@ def closing(image, selem, out=None): Parameters ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None, - is passed, a new array will be allocated. + The array to store the result of the morphology. If None, + is passed, a new array will be allocated. Returns ------- closing : uint8 array - The result of the morphological closing. + The result of the morphological closing. Examples -------- >>> # Close a gap between two bright lines + >>> import numpy as np >>> from skimage.morphology import square >>> broken_line = np.array([[0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0], @@ -225,9 +204,10 @@ def closing(image, selem, out=None): [0, 0, 0, 0, 0], [1, 1, 1, 1, 1], [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ + h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -247,23 +227,22 @@ def white_tophat(image, selem, out=None): Parameters ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None - is passed, a new array will be allocated. + The array to store the result of the morphology. If None + is passed, a new array will be allocated. Returns ------- opening : uint8 array - The result of the morphological white top hat. + The result of the morphological white top hat. Examples -------- >>> # Subtract grey background from bright peak + >>> import numpy as np >>> from skimage.morphology import square >>> bright_on_grey = np.array([[2, 3, 3, 3, 2], ... [3, 4, 5, 4, 3], @@ -275,12 +254,11 @@ def white_tophat(image, selem, out=None): [0, 0, 1, 0, 0], [0, 1, 5, 1, 0], [0, 0, 1, 0, 0], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ if image is out: raise NotImplementedError("Cannot perform white top hat in place.") - image = skimage.img_as_ubyte(image) out = opening(image, selem, out=out) out = image - out @@ -298,14 +276,12 @@ def black_tophat(image, selem, out=None): Parameters ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None - is passed, a new array will be allocated. + The array to store the result of the morphology. If None + is passed, a new array will be allocated. Returns ------- @@ -315,6 +291,7 @@ def black_tophat(image, selem, out=None): Examples -------- >>> # Change dark peak to bright peak and subtract background + >>> import numpy as np >>> from skimage.morphology import square >>> dark_on_grey = np.array([[7, 6, 6, 6, 7], ... [6, 5, 4, 5, 6], @@ -326,12 +303,12 @@ def black_tophat(image, selem, out=None): [0, 0, 1, 0, 0], [0, 1, 5, 1, 0], [0, 0, 1, 0, 0], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ + if image is out: raise NotImplementedError("Cannot perform white top hat in place.") - image = skimage.img_as_ubyte(image) out = closing(image, selem, out=out) out = out - image @@ -342,23 +319,27 @@ def greyscale_erode(*args, **kwargs): warnings.warn("`greyscale_erode` renamed `erosion`.") return erosion(*args, **kwargs) + def greyscale_dilate(*args, **kwargs): warnings.warn("`greyscale_dilate` renamed `dilation`.") return dilation(*args, **kwargs) + def greyscale_open(*args, **kwargs): warnings.warn("`greyscale_open` renamed `opening`.") return opening(*args, **kwargs) + def greyscale_close(*args, **kwargs): warnings.warn("`greyscale_close` renamed `closing`.") return closing(*args, **kwargs) + def greyscale_white_top_hat(*args, **kwargs): warnings.warn("`greyscale_white_top_hat` renamed `white_tophat`.") return white_tophat(*args, **kwargs) + def greyscale_black_top_hat(*args, **kwargs): warnings.warn("`greyscale_black_top_hat` renamed `black_tophat`.") return black_tophat(*args, **kwargs) - diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py new file mode 100644 index 00000000..09d3c9e6 --- /dev/null +++ b/skimage/morphology/greyreconstruct.py @@ -0,0 +1,193 @@ +""" +This morphological reconstruction routine was adapted from CellProfiler, code +licensed under both GPL and BSD licenses. + +Website: http://www.cellprofiler.org +Copyright (c) 2003-2009 Massachusetts Institute of Technology +Copyright (c) 2009-2011 Broad Institute +All rights reserved. +Original author: Lee Kamentsky + +""" +import numpy as np + +from skimage.filter._rank_order import rank_order + + +def reconstruction(seed, mask, method='dilation', selem=None, offset=None): + """Perform a morphological reconstruction of an image. + + Morphological reconstruction by dilation is similar to basic morphological + dilation: high-intensity values will replace nearby low-intensity values. + The basic dilation operator, however, uses a structuring element to + determine how far a value in the input image can spread. In contrast, + reconstruction uses two images: a "seed" image, which specifies the values + that spread, and a "mask" image, which gives the maximum allowed value at + each pixel. The mask image, like the structuring element, limits the spread + of high-intensity values. Reconstruction by erosion is simply the inverse: + low-intensity values spread from the seed image and are limited by the mask + image, which represents the minimum allowed value. + + Alternatively, you can think of reconstruction as a way to isolate the + connected regions of an image. For dilation, reconstruction connects + regions marked by local maxima in the seed image: neighboring pixels + less-than-or-equal-to those seeds are connected to the seeded region. + Local maxima with values larger than the seed image will get truncated to + the seed value. + + Parameters + ---------- + seed : ndarray + The seed image (a.k.a. marker image), which specifies the values that + are dilated or eroded. + mask : ndarray + The maximum (dilation) / minimum (erosion) allowed value at each pixel. + method : {'dilation'|'erosion'} + Perform reconstruction by dilation or erosion. In dilation (or + erosion), the seed image is dilated (or eroded) until limited by the + mask image. For dilation, each seed value must be less than or equal + to the corresponding mask value; for erosion, the reverse is true. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + + Returns + ------- + reconstructed : ndarray + The result of morphological reconstruction. + + Examples + -------- + >>> import numpy as np + >>> from skimage.morphology import reconstruction + + First, we create a sinusoidal mask image w/ peaks at middle and ends. + >>> x = np.linspace(0, 4 * np.pi) + >>> y_mask = np.cos(x) + + Then, we create a seed image initialized to the minimum mask value (for + reconstruction by dilation, min-intensity values don't spread) and add + "seeds" to the left and right peak, but at a fraction of peak value (1). + >>> y_seed = y_mask.min() * np.ones_like(x) + >>> y_seed[0] = 0.5 + >>> y_seed[-1] = 0 + >>> y_rec = reconstruction(y_seed, y_mask) + + The reconstructed image (or curve, in this case) is exactly the same as the + mask image, except that the peaks are truncated to 0.5 and 0. The middle + peak disappears completely: Since there were no seed values in this peak + region, its reconstructed value is truncated to the surrounding value (-1). + + As a more practical example, we try to extract the bright features of an + image by subtracting a background image created by reconstruction. + + >>> y, x = np.mgrid[:20:0.5, :20:0.5] + >>> bumps = np.sin(x) + np.sin(y) + + To create the background image, set the mask image to the original image, + and the seed image to the original image with an intensity offset, `h`. + + >>> h = 0.3 + >>> seed = bumps - h + >>> background = reconstruction(seed, bumps) + + The resulting reconstructed image looks exactly like the original image, + but with the peaks of the bumps cut off. Subtracting this reconstructed + image from the original image leaves just the peaks of the bumps + + >>> hdome = bumps - background + + This operation is known as the h-dome of the image and leaves features + of height `h` in the subtracted image. + + Notes + ----- + The algorithm is taken from [1]_. Applications for greyscale reconstruction + are discussed in [2]_ and [3]_. + + References + ---------- + .. [1] Robinson, "Efficient morphological reconstruction: a downhill + filter", Pattern Recognition Letters 25 (2004) 1759-1767. + .. [2] Vincent, L., "Morphological Grayscale Reconstruction in Image + Analysis: Applications and Efficient Algorithms", IEEE Transactions + on Image Processing (1993) + .. [3] Soille, P., "Morphological Image Analysis: Principles and + Applications", Chapter 6, 2nd edition (2003), ISBN 3540429883. + """ + assert tuple(seed.shape) == tuple(mask.shape) + if method == 'dilation' and np.any(seed > mask): + raise ValueError("Intensity of seed image must be less than that " + "of the mask image for reconstruction by dilation.") + elif method == 'erosion' and np.any(seed < mask): + raise ValueError("Intensity of seed image must be greater than that " + "of the mask image for reconstruction by erosion.") + try: + from ._greyreconstruct import reconstruction_loop + except ImportError: + raise ImportError("_greyreconstruct extension not available.") + + if selem is None: + selem = np.ones([3] * seed.ndim, dtype=bool) + else: + selem = selem.copy() + + if offset == None: + if not all([d % 2 == 1 for d in selem.shape]): + ValueError("Footprint dimensions must all be odd") + offset = np.array([d // 2 for d in selem.shape]) + # Cross out the center of the selem + selem[[slice(d, d + 1) for d in offset]] = False + + # Make padding for edges of reconstructed image so we can ignore boundaries + padding = (np.array(selem.shape) / 2).astype(int) + dims = np.zeros(seed.ndim + 1, dtype=int) + dims[1:] = np.array(seed.shape) + 2 * padding + dims[0] = 2 + inside_slices = [slice(p, -p) for p in padding] + # Set padded region to minimum image intensity and mask along first axis so + # we can interleave image and mask pixels when sorting. + if method == 'dilation': + pad_value = np.min(seed) + elif method == 'erosion': + pad_value = np.max(seed) + images = np.ones(dims) * pad_value + images[[0] + inside_slices] = seed + images[[1] + inside_slices] = mask + + # Create a list of strides across the array to get the neighbors within + # a flattened array + value_stride = np.array(images.strides[1:]) / images.dtype.itemsize + image_stride = images.strides[0] / images.dtype.itemsize + selem_mgrid = np.mgrid[[slice(-o, d - o) + for d, o in zip(selem.shape, offset)]] + selem_offsets = selem_mgrid[:, selem].transpose() + nb_strides = np.array([np.sum(value_stride * selem_offset) + for selem_offset in selem_offsets], np.int32) + + images = images.flatten() + + # Erosion goes smallest to largest; dilation goes largest to smallest. + index_sorted = np.argsort(images).astype(np.int32) + if method == 'dilation': + index_sorted = index_sorted[::-1] + + # Make a linked list of pixels sorted by value. -1 is the list terminator. + prev = -np.ones(len(images), np.int32) + next = -np.ones(len(images), np.int32) + prev[index_sorted[1:]] = index_sorted[:-1] + next[index_sorted[:-1]] = index_sorted[1:] + + # Cython inner-loop compares the rank of pixel values. + if method == 'dilation': + value_rank, value_map = rank_order(images) + elif method == 'erosion': + value_rank, value_map = rank_order(-images) + value_map = -value_map + + start = index_sorted[0] + reconstruction_loop(value_rank, prev, next, nb_strides, start, image_stride) + + # Reshape reconstructed image to original image shape and remove padding. + rec_img = value_map[value_rank[:image_stride]] + rec_img.shape = np.array(seed.shape) + 2 * padding + return rec_img[inside_slices] diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index ce58fbfa..41be0737 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -5,6 +5,7 @@ import numpy as np + def square(width, dtype=np.uint8): """ Generates a flat, square-shaped structuring element. Every pixel @@ -30,6 +31,7 @@ def square(width, dtype=np.uint8): """ return np.ones((width, width), dtype=dtype) + def rectangle(width, height, dtype=np.uint8): """ Generates a flat, rectangular-shaped structuring element of a @@ -58,6 +60,7 @@ def rectangle(width, height, dtype=np.uint8): """ return np.ones((width, height), dtype=dtype) + def diamond(radius, dtype=np.uint8): """ Generates a flat, diamond-shaped structuring element of a given @@ -75,22 +78,23 @@ def diamond(radius, dtype=np.uint8): Returns ------- - + selem : ndarray The structuring element where elements of the neighborhood are 1 and 0 otherwise. """ half = radius - (I, J) = np.meshgrid(xrange(0, radius*2+1), xrange(0, radius*2+1)) - s = np.abs(I-half)+np.abs(J-half) + (I, J) = np.meshgrid(range(0, radius * 2 + 1), range(0, radius * 2 + 1)) + s = np.abs(I - half) + np.abs(J - half) return np.array(s <= radius, dtype=dtype) - + + def disk(radius, dtype=np.uint8): """ Generates a flat, disk-shaped structuring element of a given radius. A pixel is within the neighborhood if the euclidean distance between it and the origin is no greater than a radius. - + Parameters ---------- radius : int @@ -103,9 +107,9 @@ def disk(radius, dtype=np.uint8): ------- selem : ndarray The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + are 1 and 0 otherwise. """ - L = np.linspace(-radius, radius, 2*radius+1) + L = np.linspace(-radius, radius, 2 * radius + 1) (X, Y) = np.meshgrid(L, L) s = X**2 s += Y**2 diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index 6f227a71..b8564ce4 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -5,6 +5,7 @@ from skimage._build import cython base_path = os.path.abspath(os.path.dirname(__file__)) + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs @@ -14,9 +15,10 @@ def configuration(parent_package='', top_path=None): cython(['ccomp.pyx'], working_path=base_path) cython(['cmorph.pyx'], working_path=base_path) cython(['_watershed.pyx'], working_path=base_path) - cython(['_skeletonize.pyx'], working_path=base_path) + cython(['_skeletonize_cy.pyx'], working_path=base_path) cython(['_pnpoly.pyx'], working_path=base_path) cython(['_convex_hull.pyx'], working_path=base_path) + cython(['_greyreconstruct.pyx'], working_path=base_path) config.add_extension('ccomp', sources=['ccomp.c'], include_dirs=[get_numpy_include_dirs()]) @@ -24,22 +26,24 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_watershed', sources=['_watershed.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_skeletonize', sources=['_skeletonize.c'], + config.add_extension('_skeletonize_cy', sources=['_skeletonize_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_pnpoly', sources=['_pnpoly.c'], - include_dirs=[get_numpy_include_dirs()]) + include_dirs=[get_numpy_include_dirs(), '../shared']) config.add_extension('_convex_hull', sources=['_convex_hull.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_greyreconstruct', sources=['_greyreconstruct.c'], + include_dirs=[get_numpy_include_dirs()]) return config if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - author = 'Damian Eads', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Morphology Wrapper', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='scikit-image Developers', + author='Damian Eads', + maintainer_email='scikit-image@googlegroups.com', + description='Morphology Wrapper', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index cb092b4c..1e0829ca 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -3,6 +3,7 @@ from numpy.testing import assert_array_equal, run_module_suite from skimage.morphology import label + class TestConnectedComponents: def setup(self): self.x = np.array([[0, 0, 3, 2, 1, 9], diff --git a/skimage/morphology/tests/test_convex_hull.py b/skimage/morphology/tests/test_convex_hull.py index 21f45cd7..9ab514d0 100644 --- a/skimage/morphology/tests/test_convex_hull.py +++ b/skimage/morphology/tests/test_convex_hull.py @@ -10,6 +10,7 @@ try: except ImportError: scipy_spatial = False + @skipif(not scipy_spatial) def test_basic(): image = np.array( @@ -30,6 +31,7 @@ def test_basic(): assert_array_equal(convex_hull_image(image), expected) + @skipif(not scipy_spatial) def test_possible_hull(): image = np.array( diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 1103b20d..244ec566 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -5,11 +5,13 @@ from numpy import testing import skimage from skimage import data_dir -from skimage.morphology import grey -from skimage.morphology import selem +from skimage.util import img_as_bool +from skimage.morphology import binary, grey, selem lena = np.load(os.path.join(data_dir, 'lena_GRAY_U8.npy')) +bw_lena = lena > 100 + class TestMorphology(): @@ -153,6 +155,40 @@ class TestDTypes(): self._test_image(image) +def test_non_square_image(): + strel = selem.square(3) + binary_res = binary.binary_erosion(bw_lena[:100, :200], strel) + grey_res = img_as_bool(grey.erosion(bw_lena[:100, :200], strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_erosion(): + strel = selem.square(3) + binary_res = binary.binary_erosion(bw_lena, strel) + grey_res = img_as_bool(grey.erosion(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_dilation(): + strel = selem.square(3) + binary_res = binary.binary_dilation(bw_lena, strel) + grey_res = img_as_bool(grey.dilation(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_closing(): + strel = selem.square(3) + binary_res = binary.binary_closing(bw_lena, strel) + grey_res = img_as_bool(grey.closing(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_opening(): + strel = selem.square(3) + binary_res = binary.binary_opening(bw_lena, strel) + grey_res = img_as_bool(grey.opening(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + if __name__ == '__main__': testing.run_module_suite() - diff --git a/skimage/morphology/tests/test_pnpoly.py b/skimage/morphology/tests/test_pnpoly.py index 33efe15f..da468b08 100644 --- a/skimage/morphology/tests/test_pnpoly.py +++ b/skimage/morphology/tests/test_pnpoly.py @@ -4,6 +4,7 @@ from numpy.testing import assert_array_equal from skimage.morphology._pnpoly import points_inside_poly, \ grid_points_inside_poly + class test_npnpoly(): def test_square(self): v = np.array([[0, 0], @@ -24,13 +25,14 @@ class test_npnpoly(): def test_type(self): assert(points_inside_poly([[0, 0]], [[0, 0]]).dtype == np.bool) + def test_grid_points_inside_poly(): v = np.array([[0, 0], [5, 0], [5, 5]]) expected = np.tril(np.ones((5, 5), dtype=bool)) - + assert_array_equal(grid_points_inside_poly((5, 5), v), expected) diff --git a/skimage/morphology/tests/test_reconstruction.py b/skimage/morphology/tests/test_reconstruction.py new file mode 100644 index 00000000..8e40ac67 --- /dev/null +++ b/skimage/morphology/tests/test_reconstruction.py @@ -0,0 +1,82 @@ +""" +These tests are originally part of CellProfiler, code licensed under both GPL and BSD licenses. + +Website: http://www.cellprofiler.org +Copyright (c) 2003-2009 Massachusetts Institute of Technology +Copyright (c) 2009-2011 Broad Institute +All rights reserved. +Original author: Lee Kamentsky +""" +import numpy as np +from numpy.testing import assert_array_almost_equal as assert_close + +from skimage.morphology.greyreconstruct import reconstruction + + +def test_zeros(): + """Test reconstruction with image and mask of zeros""" + assert_close(reconstruction(np.zeros((5, 7)), np.zeros((5, 7))), 0) + + +def test_image_equals_mask(): + """Test reconstruction where the image and mask are the same""" + assert_close(reconstruction(np.ones((7, 5)), np.ones((7, 5))), 1) + + +def test_image_less_than_mask(): + """Test reconstruction where the image is uniform and less than mask""" + image = np.ones((5, 5)) + mask = np.ones((5, 5)) * 2 + assert_close(reconstruction(image, mask), 1) + + +def test_one_image_peak(): + """Test reconstruction with one peak pixel""" + image = np.ones((5, 5)) + image[2, 2] = 2 + mask = np.ones((5, 5)) * 3 + assert_close(reconstruction(image, mask), 2) + + +def test_two_image_peaks(): + """Test reconstruction with two peak pixels isolated by the mask""" + image = np.array([[1, 1, 1, 1, 1, 1, 1, 1], + [1, 2, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 3, 1], + [1, 1, 1, 1, 1, 1, 1, 1]]) + + mask = np.array([[4, 4, 4, 1, 1, 1, 1, 1], + [4, 4, 4, 1, 1, 1, 1, 1], + [4, 4, 4, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 4, 4, 4], + [1, 1, 1, 1, 1, 4, 4, 4], + [1, 1, 1, 1, 1, 4, 4, 4]]) + + expected = np.array([[2, 2, 2, 1, 1, 1, 1, 1], + [2, 2, 2, 1, 1, 1, 1, 1], + [2, 2, 2, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 3, 3, 3], + [1, 1, 1, 1, 1, 3, 3, 3], + [1, 1, 1, 1, 1, 3, 3, 3]]) + assert_close(reconstruction(image, mask), expected) + + +def test_zero_image_one_mask(): + """Test reconstruction with an image of all zeros and a mask that's not""" + result = reconstruction(np.zeros((10, 10)), np.ones((10, 10))) + assert_close(result, 0) + + +def test_fill_hole(): + """Test reconstruction by erosion, which should fill holes in mask.""" + seed = np.array([0, 8, 8, 8, 8, 8, 8, 8, 8, 0]) + mask = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0]) + result = reconstruction(seed, mask, method='erosion') + assert_close(result, np.array([0, 3, 6, 4, 4, 4, 4, 4, 2, 0])) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/morphology/tests/test_selem.py b/skimage/morphology/tests/test_selem.py index 639cdf4e..eec6cd5b 100644 --- a/skimage/morphology/tests/test_selem.py +++ b/skimage/morphology/tests/test_selem.py @@ -10,6 +10,7 @@ from skimage.io import * from skimage import data_dir from skimage.morphology import * + class TestSElem(): def test_square_selem(self): @@ -32,13 +33,12 @@ class TestSElem(): expected_mask = matlab_masks[arrname] actual_mask = func(k) if (expected_mask.shape == (1,)): - expected_mask = expected_mask[:,np.newaxis] + expected_mask = expected_mask[:, np.newaxis] assert_equal(expected_mask, actual_mask) k = k + 1 - + def test_selem_disk(self): self.strel_worker("disk-matlab-output.npz", selem.disk) def test_selem_diamond(self): self.strel_worker("diamond-matlab-output.npz", selem.diamond) - diff --git a/skimage/morphology/tests/test_skeletonize.py b/skimage/morphology/tests/test_skeletonize.py index 539e0ad8..9c8fe249 100644 --- a/skimage/morphology/tests/test_skeletonize.py +++ b/skimage/morphology/tests/test_skeletonize.py @@ -1,81 +1,83 @@ import numpy as np from skimage.morphology import skeletonize, medial_axis import numpy.testing -from skimage.draw import draw +from skimage import draw from scipy.ndimage import correlate from skimage.io import imread from skimage import data_dir import os.path + class TestSkeletonize(): def test_skeletonize_no_foreground(self): - im = np.zeros((5,5)) + im = np.zeros((5, 5)) result = skeletonize(im) - numpy.testing.assert_array_equal(result, np.zeros((5,5))) - + numpy.testing.assert_array_equal(result, np.zeros((5, 5))) + def test_skeletonize_wrong_dim1(self): im = np.zeros((5)) - numpy.testing.assert_raises(ValueError, skeletonize, im) + numpy.testing.assert_raises(ValueError, skeletonize, im) def test_skeletonize_wrong_dim2(self): im = np.zeros((5, 5, 5)) - numpy.testing.assert_raises(ValueError, skeletonize, im) + numpy.testing.assert_raises(ValueError, skeletonize, im) def test_skeletonize_not_binary(self): im = np.zeros((5, 5)) im[0, 0] = 1 im[0, 1] = 2 - numpy.testing.assert_raises(ValueError, skeletonize, im) - + numpy.testing.assert_raises(ValueError, skeletonize, im) + def test_skeletonize_unexpected_value(self): im = np.zeros((5, 5)) im[0, 0] = 2 - numpy.testing.assert_raises(ValueError, skeletonize, im) - + numpy.testing.assert_raises(ValueError, skeletonize, im) + def test_skeletonize_all_foreground(self): - im = np.ones((3,4)) - result = skeletonize(im) - + im = np.ones((3, 4)) + skeletonize(im) + def test_skeletonize_single_point(self): im = np.zeros((5, 5), np.uint8) im[3, 3] = 1 result = skeletonize(im) numpy.testing.assert_array_equal(result, im) - + def test_skeletonize_already_thinned(self): im = np.zeros((5, 5), np.uint8) - im[3,1:-1] = 1 + im[3, 1:-1] = 1 im[2, -1] = 1 im[4, 0] = 1 result = skeletonize(im) numpy.testing.assert_array_equal(result, im) - + def test_skeletonize_output(self): im = imread(os.path.join(data_dir, "bw_text.png"), as_grey=True) - + # make black the foreground - im = (im==0) + im = (im == 0) result = skeletonize(im) - + expected = np.load(os.path.join(data_dir, "bw_text_skeleton.npy")) numpy.testing.assert_array_equal(result, expected) - - + def test_skeletonize_num_neighbours(self): # an empty image image = np.zeros((300, 300)) - + # foreground object 1 image[10:-10, 10:100] = 1 image[-100:-10, 10:-10] = 1 image[10:-10, -100:-10] = 1 - + # foreground object 2 rs, cs = draw.bresenham(250, 150, 10, 280) - for i in range(10): image[rs+i, cs] = 1 + for i in range(10): + image[rs + i, cs] = 1 rs, cs = draw.bresenham(10, 150, 250, 280) - for i in range(20): image[rs+i, cs] = 1 - + for i in range(20): + image[rs + i, cs] = 1 + # foreground object 3 ir, ic = np.indices(image.shape) circle1 = (ic - 135)**2 + (ir - 150)**2 < 30**2 @@ -83,13 +85,32 @@ class TestSkeletonize(): image[circle1] = 1 image[circle2] = 0 result = skeletonize(image) - + # there should never be a 2x2 block of foreground pixels in a skeleton mask = np.array([[1, 1], - [1, 1]], np.uint8) + [1, 1]], np.uint8) blocks = correlate(result, mask, mode='constant') assert not numpy.any(blocks == 4) - + + def test_lut_fix(self): + im = np.zeros((6, 6), np.uint8) + im[1, 2] = 1 + im[2, 2] = 1 + im[2, 3] = 1 + im[3, 3] = 1 + im[3, 4] = 1 + im[4, 4] = 1 + im[4, 5] = 1 + result = skeletonize(im) + expected = np.array([[0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0]], dtype=np.uint8) + assert np.all(result == expected) + + class TestMedialAxis(): def test_00_00_zeros(self): '''Test skeletonize on an array of all zeros''' @@ -101,7 +122,7 @@ class TestMedialAxis(): result = medial_axis(np.zeros((10, 10), bool), np.zeros((10, 10), bool)) assert np.all(result == False) - + def test_01_01_rectangle(self): '''Test skeletonize on a rectangle''' image = np.zeros((9, 15), bool) @@ -110,15 +131,16 @@ class TestMedialAxis(): # The result should be four diagonals from the # corners, meeting in a horizontal line # - expected = np.array([[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], - [0,1,0,0,0,0,0,0,0,0,0,0,0,1,0], - [0,0,1,0,0,0,0,0,0,0,0,0,1,0,0], - [0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], - [0,0,0,0,1,1,1,1,1,1,1,0,0,0,0], - [0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], - [0,0,1,0,0,0,0,0,0,0,0,0,1,0,0], - [0,1,0,0,0,0,0,0,0,0,0,0,0,1,0], - [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], bool) + expected = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], + bool) result = medial_axis(image) assert np.all(result == expected) result, distance = medial_axis(image, return_distance=True) @@ -129,15 +151,16 @@ class TestMedialAxis(): image = np.zeros((9, 15), bool) image[1:-1, 1:-1] = True image[4, 4:-4] = False - expected = np.array([[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], - [0,1,0,0,0,0,0,0,0,0,0,0,0,1,0], - [0,0,1,1,1,1,1,1,1,1,1,1,1,0,0], - [0,0,1,0,0,0,0,0,0,0,0,0,1,0,0], - [0,0,1,0,0,0,0,0,0,0,0,0,1,0,0], - [0,0,1,0,0,0,0,0,0,0,0,0,1,0,0], - [0,0,1,1,1,1,1,1,1,1,1,1,1,0,0], - [0,1,0,0,0,0,0,0,0,0,0,0,0,1,0], - [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],bool) + expected = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], + bool) result = medial_axis(image) assert np.all(result == expected) diff --git a/skimage/morphology/tests/test_watershed.py b/skimage/morphology/tests/test_watershed.py index 82ba6917..46298ae5 100644 --- a/skimage/morphology/tests/test_watershed.py +++ b/skimage/morphology/tests/test_watershed.py @@ -43,7 +43,6 @@ Original author: Lee Kamentsky import math -import time import unittest import numpy as np @@ -54,6 +53,7 @@ from skimage.morphology.watershed import watershed, \ eps = 1e-12 + def diff(a, b): if not isinstance(a, np.ndarray): a = np.asarray(a) @@ -61,7 +61,7 @@ def diff(a, b): b = np.asarray(b) if (0 in a.shape) and (0 in b.shape): return 0.0 - b[a==0] = 0 + b[a == 0] = 0 if (a.dtype in [np.complex64, np.complex128] or b.dtype in [np.complex64, np.complex128]): a = np.asarray(a, np.complex128) @@ -75,8 +75,10 @@ def diff(a, b): t = ((a - b)**2).sum() return math.sqrt(t) + class TestWatershed(unittest.TestCase): - eight = np.ones((3, 3),bool) + eight = np.ones((3, 3), bool) + def test_watershed01(self): "watershed 1" data = np.array([[0, 0, 0, 0, 0, 0, 0], @@ -100,7 +102,7 @@ class TestWatershed(unittest.TestCase): [ 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0]], np.int8) - out = watershed(data, markers,self.eight) + out = watershed(data, markers, self.eight) expected = np.array([[-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], @@ -120,28 +122,27 @@ class TestWatershed(unittest.TestCase): def test_watershed02(self): "watershed 2" data = np.array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], np.uint8) + markers = np.array([[-1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 1, 0, 0, 0, 1, 0], - [0, 1, 0, 0, 0, 1, 0], - [0, 1, 0, 0, 0, 1, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0]], np.uint8) - markers = np.array([[ -1, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 1, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0]], - np.int8) + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], np.int8) out = watershed(data, markers) error = diff([[-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], @@ -159,26 +160,25 @@ class TestWatershed(unittest.TestCase): def test_watershed03(self): "watershed 3" data = np.array([[0, 0, 0, 0, 0, 0, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0]], np.uint8) - markers = np.array([[ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 2, 0, 3, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, -1]], - np.int8) + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], np.uint8) + markers = np.array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 2, 0, 3, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, -1]], np.int8) out = watershed(data, markers) error = diff([[-1, -1, -1, -1, -1, -1, -1], [-1, 0, 2, 0, 3, 0, -1], @@ -200,21 +200,20 @@ class TestWatershed(unittest.TestCase): [0, 1, 0, 1, 0, 1, 0], [0, 1, 0, 1, 0, 1, 0], [0, 1, 1, 1, 1, 1, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) - markers = np.array([[ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 2, 0, 3, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, -1]], - np.int8) + markers = np.array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 2, 0, 3, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, -1]], np.int8) out = watershed(data, markers, self.eight) error = diff([[-1, -1, -1, -1, -1, -1, -1], [-1, 2, 2, 0, 3, 3, -1], @@ -222,35 +221,34 @@ class TestWatershed(unittest.TestCase): [-1, 2, 2, 0, 3, 3, -1], [-1, 2, 2, 0, 3, 3, -1], [-1, 2, 2, 0, 3, 3, -1], - [-1, -1, -1, -1, -1, -1, -1], - [-1, -1, -1, -1, -1, -1, -1], - [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]], out) self.failUnless(error < eps) def test_watershed05(self): "watershed 5" data = np.array([[0, 0, 0, 0, 0, 0, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 1, 1, 1, 1, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0]], np.uint8) - markers = np.array([[ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 3, 0, 2, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, -1]], - np.int8) + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], np.uint8) + markers = np.array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 3, 0, 2, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, -1]], np.int8) out = watershed(data, markers, self.eight) error = diff([[-1, -1, -1, -1, -1, -1, -1], [-1, 3, 3, 0, 2, 2, -1], @@ -267,24 +265,23 @@ class TestWatershed(unittest.TestCase): def test_watershed06(self): "watershed 6" data = np.array([[0, 1, 0, 0, 0, 1, 0], - [0, 1, 0, 0, 0, 1, 0], - [0, 1, 0, 0, 0, 1, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0]], np.uint8) - markers = np.array([[ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 1, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ -1, 0, 0, 0, 0, 0, 0]], - np.int8) + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], np.uint8) + markers = np.array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [-1, 0, 0, 0, 0, 0, 0]], np.int8) out = watershed(data, markers, self.eight) error = diff([[-1, 1, 1, 1, 1, 1, -1], [-1, 1, 1, 1, 1, 1, -1], @@ -300,28 +297,28 @@ class TestWatershed(unittest.TestCase): def test_watershed07(self): "A regression test of a competitive case that failed" data = np.array([[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], - [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], - [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], - [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], - [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], - [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], - [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], - [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], - [255,255,255,255,255,204,153,103,103,153,204,255,255,255,255,255], - [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], - [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], - [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], - [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], - [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], - [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], - [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], - [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]]) - mask = (data!=255) - markers = np.zeros(data.shape,int) + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], + [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], + [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], + [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], + [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], + [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], + [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], + [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], + [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], + [255,255,255,255,255,204,153,103,103,153,204,255,255,255,255,255], + [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], + [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], + [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], + [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], + [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], + [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], + [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], + [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]]) + mask = (data != 255) + markers = np.zeros(data.shape, int) markers[6, 7] = 1 markers[14, 7] = 2 out = watershed(data, markers, self.eight, mask=mask) @@ -331,35 +328,35 @@ class TestWatershed(unittest.TestCase): # size1 = np.sum(out == 1) size2 = np.sum(out == 2) - self.assertTrue(abs(size1-size2) <= 6) - + self.assertTrue(abs(size1 - size2) <= 6) + def test_watershed08(self): "The border pixels + an edge are all the same value" data = np.array([[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], - [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], - [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], - [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], - [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], - [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], - [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], - [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], - [255,255,255,255,255,204,153,141,141,153,204,255,255,255,255,255], - [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], - [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], - [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], - [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], - [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], - [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], - [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], - [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]]) + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], + [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], + [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], + [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], + [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], + [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], + [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], + [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], + [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], + [255,255,255,255,255,204,153,141,141,153,204,255,255,255,255,255], + [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], + [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], + [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], + [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], + [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], + [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], + [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], + [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]]) mask = (data != 255) - markers = np.zeros(data.shape,int) - markers[6,7] = 1 - markers[14,7] = 2 + markers = np.zeros(data.shape, int) + markers[6, 7] = 1 + markers[14, 7] = 2 out = watershed(data, markers, self.eight, mask=mask) # # The two objects should be the same size, except possibly for the @@ -367,30 +364,27 @@ class TestWatershed(unittest.TestCase): # size1 = np.sum(out == 1) size2 = np.sum(out == 2) - self.assertTrue(abs(size1-size2) <= 6) - + self.assertTrue(abs(size1 - size2) <= 6) + def test_watershed09(self): """Test on an image of reasonable size - + This is here both for timing (does it take forever?) and to ensure that the memory constraints are reasonable """ image = np.zeros((1000, 1000)) coords = np.random.uniform(0, 1000, (100, 2)).astype(int) - markers = np.zeros((1000, 1000),int) + markers = np.zeros((1000, 1000), int) idx = 1 - for x,y in coords: - image[x,y] = 1 + for x, y in coords: + image[x, y] = 1 markers[x, y] = idx idx += 1 - + image = scipy.ndimage.gaussian_filter(image, 4) - before = time.clock() - out = watershed(image, markers, self.eight) - elapsed = time.clock() - before - before = time.clock() - out = scipy.ndimage.watershed_ift(image.astype(np.uint16), markers, self.eight) - elapsed = time.clock() - before + watershed(image, markers, self.eight) + scipy.ndimage.watershed_ift(image.astype(np.uint16), markers, + self.eight) class TestIsLocalMaximum(unittest.TestCase): @@ -399,7 +393,7 @@ class TestIsLocalMaximum(unittest.TestCase): labels = np.zeros((10, 20), int) result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(~ result)) - + def test_01_01_one_point(self): image = np.zeros((10, 20)) labels = np.zeros((10, 20), int) @@ -407,7 +401,7 @@ class TestIsLocalMaximum(unittest.TestCase): labels[5, 5] = 1 result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(result == (labels == 1))) - + def test_01_02_adjacent_and_same(self): image = np.zeros((10, 20)) labels = np.zeros((10, 20), int) @@ -415,7 +409,7 @@ class TestIsLocalMaximum(unittest.TestCase): labels[5, 5:6] = 1 result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(result == (labels == 1))) - + def test_01_03_adjacent_and_different(self): image = np.zeros((10, 20)) labels = np.zeros((10, 20), int) @@ -427,67 +421,67 @@ class TestIsLocalMaximum(unittest.TestCase): self.assertTrue(np.all(result == expected)) result = is_local_maximum(image, labels) self.assertTrue(np.all(result == expected)) - + def test_01_04_not_adjacent_and_different(self): - image = np.zeros((10,20)) - labels = np.zeros((10,20), int) - image[5,5] = 1 - image[5,8] = .5 + image = np.zeros((10, 20)) + labels = np.zeros((10, 20), int) + image[5, 5] = 1 + image[5, 8] = .5 labels[image > 0] = 1 expected = (labels == 1) - result = is_local_maximum(image, labels, np.ones((3,3), bool)) + result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(result == expected)) - + def test_01_05_two_objects(self): - image = np.zeros((10,20)) - labels = np.zeros((10,20), int) - image[5,5] = 1 - image[5,15] = .5 - labels[5,5] = 1 - labels[5,15] = 2 + image = np.zeros((10, 20)) + labels = np.zeros((10, 20), int) + image[5, 5] = 1 + image[5, 15] = .5 + labels[5, 5] = 1 + labels[5, 15] = 2 expected = (labels > 0) - result = is_local_maximum(image, labels, np.ones((3,3), bool)) + result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(result == expected)) def test_01_06_adjacent_different_objects(self): - image = np.zeros((10,20)) - labels = np.zeros((10,20), int) - image[5,5] = 1 - image[5,6] = .5 - labels[5,5] = 1 - labels[5,6] = 2 + image = np.zeros((10, 20)) + labels = np.zeros((10, 20), int) + image[5, 5] = 1 + image[5, 6] = .5 + labels[5, 5] = 1 + labels[5, 6] = 2 expected = (labels > 0) - result = is_local_maximum(image, labels, np.ones((3,3), bool)) + result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(result == expected)) - + def test_02_01_four_quadrants(self): np.random.seed(21) - image = np.random.uniform(size=(40,60)) - i,j = np.mgrid[0:40,0:60] + image = np.random.uniform(size=(40, 60)) + i, j = np.mgrid[0:40, 0:60] labels = 1 + (i >= 20) + (j >= 30) * 2 - i,j = np.mgrid[-3:4,-3:4] - footprint = (i*i + j*j <=9) + i, j = np.mgrid[-3:4, -3:4] + footprint = (i * i + j * j <= 9) expected = np.zeros(image.shape, float) for imin, imax in ((0, 20), (20, 40)): for jmin, jmax in ((0, 30), (30, 60)): - expected[imin:imax,jmin:jmax] = scipy.ndimage.maximum_filter( - image[imin:imax, jmin:jmax], footprint = footprint) + expected[imin:imax, jmin:jmax] = scipy.ndimage.maximum_filter( + image[imin:imax, jmin:jmax], footprint=footprint) expected = (expected == image) result = is_local_maximum(image, labels, footprint) self.assertTrue(np.all(result == expected)) - + def test_03_01_disk_1(self): '''regression test of img-1194, footprint = [1] - + Test is_local_maximum when every point is a local maximum ''' np.random.seed(31) - image = np.random.uniform(size=(10,20)) + image = np.random.uniform(size=(10, 20)) footprint = np.array([[1]]) - result = is_local_maximum(image, np.ones((10,20)), footprint) + result = is_local_maximum(image, np.ones((10, 20)), footprint) self.assertTrue(np.all(result)) result = is_local_maximum(image, footprint=footprint) self.assertTrue(np.all(result)) - + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py index 1966a1a8..8a08fafc 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -24,13 +24,13 @@ All rights reserved. Original author: Lee Kamentsky """ -from _heapq import heapify, heappush, heappop +from _heapq import heappush, heappop import numpy as np import scipy.ndimage from ..filter import rank_order from . import _watershed -import warnings + def watershed(image, markers, connectivity=None, offset=None, mask=None): """ @@ -87,8 +87,8 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): This implementation converts all arguments to specific, lowest common denominator types, then passes these to a C algorithm. - - Markers can be determined manually, or automatically using for example + + Markers can be determined manually, or automatically using for example the local minima of the gradient of the image, or the local maxima of the distance function to the background for separating overlapping objects (see example). @@ -122,28 +122,28 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): The algorithm works also for 3-D images, and can be used for example to separate overlapping spheres. """ - + if connectivity == None: c_connectivity = scipy.ndimage.generate_binary_structure(image.ndim, 1) else: c_connectivity = np.array(connectivity, bool) if c_connectivity.ndim != image.ndim: - raise ValueError,"Connectivity dimension must be same as image" + raise ValueError("Connectivity dimension must be same as image") if offset == None: - if any([x%2==0 for x in c_connectivity.shape]): - raise ValueError,"Connectivity array must have an unambiguous \ - center" + if any([x % 2 == 0 for x in c_connectivity.shape]): + raise ValueError("Connectivity array must have an unambiguous " + "center") # # offset to center of connectivity array # offset = np.array(c_connectivity.shape) // 2 - # pad the image, markers, and mask so that we can use the mask to + # pad the image, markers, and mask so that we can use the mask to # keep from running off the edges pads = offset def pad(im): - new_im = np.zeros([i + 2*p for i, p in zip(im.shape, pads)], im.dtype) + new_im = np.zeros([i + 2 * p for i, p in zip(im.shape, pads)], im.dtype) new_im[[slice(p, -p, None) for p in pads]] = im return new_im @@ -157,18 +157,17 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): c_image = rank_order(image)[0].astype(np.int32) c_markers = np.ascontiguousarray(markers, dtype=np.int32) if c_markers.ndim != c_image.ndim: - raise ValueError,\ - "markers (ndim=%d) must have same # of dimensions "\ - "as image (ndim=%d)"%(c_markers.ndim, cimage.ndim) + raise ValueError("markers (ndim=%d) must have same # of dimensions " + "as image (ndim=%d)" % (c_markers.ndim, c_image.ndim)) if c_markers.shape != c_image.shape: raise ValueError("image and markers must have the same shape") if mask != None: c_mask = np.ascontiguousarray(mask, dtype=bool) if c_mask.ndim != c_markers.ndim: - raise ValueError, "mask must have same # of dimensions as image" + raise ValueError("mask must have same # of dimensions as image") if c_markers.shape != c_mask.shape: - raise ValueError, "mask must have same shape as image" - c_markers[np.logical_not(mask)]=0 + raise ValueError("mask must have same shape as image") + c_markers[np.logical_not(mask)] = 0 else: c_mask = None c_output = c_markers.copy() @@ -189,9 +188,8 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): indexes = [] ignore = True for j in range(len(c_connectivity.shape)): - elems = c_image.shape[j] - idx = (i // multiplier) % c_connectivity.shape[j] - off = idx - offset[j] + idx = (i // multiplier) % c_connectivity.shape[j] + off = idx - offset[j] if off: ignore = False offs.append(off) @@ -214,10 +212,10 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): else: c_mask = c_mask.astype(np.int8).flatten() _watershed.watershed(c_image.flatten(), - pq, age, c, - c_image.ndim, + pq, age, c, + c_image.ndim, c_mask, - np.array(c_image.shape,np.int32), + np.array(c_image.shape, np.int32), c_output) c_output = c_output.reshape(c_image.shape)[[slice(1, -1, None)] * image.ndim] @@ -230,18 +228,18 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): def is_local_maximum(image, labels=None, footprint=None): """ Return a boolean array of points that are local maxima - + Parameters ---------- image: ndarray (2-D, 3-D, ...) intensity image - - labels: ndarray, optional + + labels: ndarray, optional find maxima only within labels. Zero is reserved for background. - + footprint: ndarray of bools, optional binary mask indicating the neighborhood to be examined - `footprint` must be a matrix with odd dimensions, the center is taken + `footprint` must be a matrix with odd dimensions, the center is taken to be the point in question. Returns @@ -263,7 +261,7 @@ def is_local_maximum(image, labels=None, footprint=None): array([[ True, False, False, False], [ True, False, True, False], [ True, False, False, False], - [ True, True, False, True]], dtype='bool') + [ True, True, False, True]], dtype=bool) >>> image = np.arange(16).reshape((4, 4)) >>> labels = np.array([[1, 2], [3, 4]]) >>> labels = np.repeat(np.repeat(labels, 2, axis=0), 2, axis=1) @@ -281,7 +279,7 @@ def is_local_maximum(image, labels=None, footprint=None): array([[False, False, False, False], [False, True, False, True], [False, False, False, False], - [False, True, False, True]], dtype='bool') + [False, True, False, True]], dtype=bool) """ if labels is None: labels = np.ones(image.shape, dtype=np.uint8) @@ -289,7 +287,7 @@ def is_local_maximum(image, labels=None, footprint=None): footprint = np.ones([3] * image.ndim, dtype=np.uint8) assert((np.all(footprint.shape) & 1) == 1) footprint = (footprint != 0) - footprint_extent = (np.array(footprint.shape)-1) // 2 + footprint_extent = (np.array(footprint.shape) - 1) // 2 if np.all(footprint_extent == 0): return labels > 0 result = (labels > 0).copy() @@ -297,17 +295,17 @@ def is_local_maximum(image, labels=None, footprint=None): # Create a labels matrix with zeros at the borders that might be # hit by the footprint. # - big_labels = np.zeros(np.array(labels.shape) + footprint_extent*2, + big_labels = np.zeros(np.array(labels.shape) + footprint_extent * 2, labels.dtype) - big_labels[[slice(fe,-fe) for fe in footprint_extent]] = labels + big_labels[[slice(fe, -fe) for fe in footprint_extent]] = labels # # Find the relative indexes of each footprint element # image_strides = np.array(image.strides) // image.dtype.itemsize big_strides = np.array(big_labels.strides) // big_labels.dtype.itemsize result_strides = np.array(result.strides) // result.dtype.itemsize - footprint_offsets = np.mgrid[[slice(-fe,fe+1) for fe in footprint_extent]] - + footprint_offsets = np.mgrid[[slice(-fe, fe + 1) for fe in footprint_extent]] + fp_image_offsets = np.sum(image_strides[:, np.newaxis] * footprint_offsets[:, footprint], 0) fp_big_offsets = np.sum(big_strides[:, np.newaxis] * @@ -315,9 +313,9 @@ def is_local_maximum(image, labels=None, footprint=None): # # Get the index of each labeled pixel in the image and big_labels arrays # - indexes = np.mgrid[[slice(0,x) for x in labels.shape]][:, labels > 0] + indexes = np.mgrid[[slice(0, x) for x in labels.shape]][:, labels > 0] image_indexes = np.sum(image_strides[:, np.newaxis] * indexes, 0) - big_indexes = np.sum(big_strides[:, np.newaxis] * + big_indexes = np.sum(big_strides[:, np.newaxis] * (indexes + footprint_extent[:, np.newaxis]), 0) result_indexes = np.sum(result_strides[:, np.newaxis] * indexes, 0) # @@ -335,18 +333,15 @@ def is_local_maximum(image, labels=None, footprint=None): same_label = (big_labels_raveled[big_indexes + fp_big_offset] == big_labels_raveled[big_indexes]) less_than = (image_raveled[image_indexes[same_label]] < - image_raveled[image_indexes[same_label]+ fp_image_offset]) + image_raveled[image_indexes[same_label] + fp_image_offset]) result_raveled[result_indexes[same_label][less_than]] = False - + return result - # ---------------------- deprecated ------------------------------ -# Deprecate slower pure-Python code, that we keep only for +# Deprecate slower pure-Python code, that we keep only for # pedagogical purposes - - def __heapify_markers(markers, image): """Create a priority queue heap with the markers on it""" stride = np.array(image.strides) // image.itemsize @@ -354,24 +349,25 @@ def __heapify_markers(markers, image): ncoords = coords.shape[0] if ncoords > 0: pixels = image[markers != 0] - age = np.arange(ncoords) + age = np.arange(ncoords) offset = np.zeros(coords.shape[0], int) for i in range(image.ndim): offset = offset + stride[i] * coords[:, i] pq = np.column_stack((pixels, age, offset, coords)) # pixels = top priority, age=second - ordering = np.lexsort((age, pixels)) + ordering = np.lexsort((age, pixels)) pq = pq[ordering, :] else: pq = np.zeros((0, markers.ndim + 3), int) return (pq, ncoords) - + + def _slow_watershed(image, markers, connectivity=8, mask=None): """Return a matrix labeled using the watershed algorithm - + Use the `watershed` function for a faster execution. This pure Python function is solely for pedagogical purposes. - + Parameters ---------- image: 2-d ndarray of integers @@ -380,16 +376,16 @@ def _slow_watershed(image, markers, connectivity=8, mask=None): markers: 2-d ndarray of integers a two-dimensional matrix marking the basins with the values to be assigned in the label matrix. Zero means not a marker. - connectivity: {4, 8}, optional + connectivity: {4, 8}, optional either 4 for four-connected or 8 (default) for eight-connected - mask: 2-d ndarray of bools, optional + mask: 2-d ndarray of bools, optional don't label points in the mask - Returns + Returns ------- out: ndarray A labeled matrix of the same type and shape as markers - + Notes ----- @@ -409,20 +405,20 @@ def _slow_watershed(image, markers, connectivity=8, mask=None): This implementation converts all arguments to specific, lowest common denominator types, then passes these to a C algorithm. - - Markers can be determined manually, or automatically using for example + + Markers can be determined manually, or automatically using for example the local minima of the gradient of the image, or the local maxima of the distance function to the background for separating overlapping objects. """ if connectivity not in (4, 8): raise ValueError("Connectivity was %d: it should be either \ - four or eight" %(connectivity)) - + four or eight" % (connectivity)) + image = np.array(image) markers = np.array(markers) labels = markers.copy() - max_x = markers.shape[0] - max_y = markers.shape[1] + max_x = markers.shape[0] + max_y = markers.shape[1] if connectivity == 4: connect_increments = ((1, 0), (0, 1), (-1, 0), (0, -1)) else: diff --git a/skimage/scripts/skivi b/skimage/scripts/skivi index 4469f015..e3108f64 100755 --- a/skimage/scripts/skivi +++ b/skimage/scripts/skivi @@ -3,4 +3,3 @@ if __name__ == "__main__": from skimage.scripts import skivi skivi.main() - diff --git a/skimage/scripts/skivi.py b/skimage/scripts/skivi.py index 610013cc..2e907bbe 100644 --- a/skimage/scripts/skivi.py +++ b/skimage/scripts/skivi.py @@ -1,4 +1,6 @@ """skimage viewer""" + + def main(): import skimage.io as io import sys @@ -10,4 +12,3 @@ def main(): io.use_plugin('qt') io.imshow(io.imread(sys.argv[1]), fancy=True) io.show() - diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 118c3ec2..9dca2bc3 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1 +1,6 @@ from .random_walker_segmentation import random_walker +from ._felzenszwalb import felzenszwalb +from ._slic import slic +from ._quickshift import quickshift +from .boundaries import find_boundaries, visualize_boundaries +from ._clear_border import clear_border diff --git a/skimage/segmentation/_clear_border.py b/skimage/segmentation/_clear_border.py new file mode 100644 index 00000000..266e17e2 --- /dev/null +++ b/skimage/segmentation/_clear_border.py @@ -0,0 +1,69 @@ +import numpy as np +from scipy.ndimage import label + + +def clear_border(image, buffer_size=0, bgval=0): + """Clear objects connected to image border. + + The changes will be applied to the input image. + + Parameters + ---------- + image : (N, M) array + Binary image. + buffer_size : int, optional + Define additional buffer around image border. + bgval : float or int, optional + Value for cleared objects. + + Returns + ------- + image : (N, M) array + Cleared binary image. + + Examples + -------- + >>> import numpy as np + >>> from skimage.segmentation import clear_border + >>> image = np.array([[0, 0, 0, 0, 0, 0, 0, 1, 0], + ... [0, 0, 0, 0, 1, 0, 0, 0, 0], + ... [1, 0, 0, 1, 0, 1, 0, 0, 0], + ... [0, 0, 1, 1, 1, 1, 1, 0, 0], + ... [0, 1, 1, 1, 1, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> clear_border(image) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + + """ + + rows, cols = image.shape + if buffer_size >= rows or buffer_size >= cols: + raise ValueError("buffer size may not be greater than image size") + + # create borders with buffer_size + borders = np.zeros_like(image, dtype=np.bool_) + ext = buffer_size + 1 + borders[:ext] = True + borders[- ext:] = True + borders[:, :ext] = True + borders[:, - ext:] = True + + labels, number = label(image) + + # determine all objects that are connected to borders + borders_indices = np.unique(labels[borders]) + indices = np.arange(number + 1) + # mask all label indices that are connected to borders + label_mask = np.in1d(indices, borders_indices) + # create mask for pixels to clear + mask = label_mask[labels.ravel()].reshape(labels.shape) + + # clear border pixels + image[mask] = bgval + + return image diff --git a/skimage/segmentation/_felzenszwalb.py b/skimage/segmentation/_felzenszwalb.py new file mode 100644 index 00000000..67971a96 --- /dev/null +++ b/skimage/segmentation/_felzenszwalb.py @@ -0,0 +1,77 @@ +import warnings +import numpy as np + +from ._felzenszwalb_cy import _felzenszwalb_grey + + +def felzenszwalb(image, scale=1, sigma=0.8, min_size=20): + """Computes Felsenszwalb's efficient graph based image segmentation. + + Produces an oversegmentation of a multichannel (i.e. RGB) image + using a fast, minimum spanning tree based clustering on the image grid. + The parameter ``scale`` sets an observation level. Higher scale means + less and larger segments. ``sigma`` is the diameter of a Gaussian kernel, + used for smoothing the image prior to segmentation. + + The number of produced segments as well as their size can only be + controlled indirectly through ``scale``. Segment size within an image can + vary greatly depending on local contrast. + + For RGB images, the algorithm computes a separate segmentation for each + channel and then combines these. The combined segmentation is the + intersection of the separate segmentations on the color channels. + + Parameters + ---------- + image : (width, height, 3) or (width, height) ndarray + Input image. + scale : float + Free parameter. Higher means larger clusters. + sigma : float + Width of Gaussian kernel used in preprocessing. + min_size : int + Minimum component size. Enforced using postprocessing. + + Returns + ------- + segment_mask : (width, height) ndarray + Integer mask indicating segment labels. + + References + ---------- + .. [1] Efficient graph-based image segmentation, Felzenszwalb, P.F. and + Huttenlocher, D.P. International Journal of Computer Vision, 2004 + """ + + #image = img_as_float(image) + if image.ndim == 2: + # assume single channel image + return _felzenszwalb_grey(image, scale=scale, sigma=sigma) + + elif image.ndim != 3: + raise ValueError("Felzenswalb segmentation can only operate on RGB and" + " grey images, but input array of ndim %d given." + % image.ndim) + + # assume we got 2d image with multiple channels + n_channels = image.shape[2] + if n_channels != 3: + warnings.warn("Got image with %d channels. Is that really what you" + " wanted?" % image.shape[2]) + segmentations = [] + # compute quickshift for each channel + for c in range(n_channels): + channel = np.ascontiguousarray(image[:, :, c]) + s = _felzenszwalb_grey(channel, scale=scale, sigma=sigma, + min_size=min_size) + segmentations.append(s) + + # put pixels in same segment only if in the same segment in all images + # we do this by combining the channels to one number + n0 = segmentations[0].max() + 1 + n1 = segmentations[1].max() + 1 + segmentation = (segmentations[0] + segmentations[1] * n0 + + segmentations[2] * n0 * n1) + # make segment labels consecutive numbers starting at 0 + labels = np.unique(segmentation, return_inverse=True)[1] + return labels.reshape(image.shape[:2]) diff --git a/skimage/segmentation/_felzenszwalb_cy.pyx b/skimage/segmentation/_felzenszwalb_cy.pyx new file mode 100644 index 00000000..efae45a4 --- /dev/null +++ b/skimage/segmentation/_felzenszwalb_cy.pyx @@ -0,0 +1,114 @@ +import numpy as np +cimport numpy as np +import scipy +cimport cython + +from skimage.morphology.ccomp cimport find_root, join_trees + +from ..util import img_as_float + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def _felzenszwalb_grey(image, double scale=1, sigma=0.8, int min_size=20): + """Felzenszwalb's efficient graph based segmentation for a single channel. + + Produces an oversegmentation of a 2d image using a fast, minimum spanning + tree based clustering on the image grid. + The number of produced segments as well as their size can only be + controlled indirectly through ``scale``. Segment size within an image can + vary greatly depending on local contrast. + + Parameters + ---------- + image: ndarray + Input image. + scale: float + Sets the obervation level. Higher means larger clusters. + sigma: float + Width of Gaussian smoothing kernel used in preprocessing. + Larger sigma gives smother segment boundaries. + min_size: int + Minimum component size. Enforced using postprocessing. + + Returns + ------- + segment_mask: (height, width) ndarray + Integer mask indicating segment labels. + """ + if image.ndim != 2: + raise ValueError("This algorithm works only on single-channel 2d" + "images. Got image of shape %s" % str(image.shape)) + image = img_as_float(image) + # rescale scale to behave like in reference implementation + scale = float(scale) / 255. + image = scipy.ndimage.gaussian_filter(image, sigma=sigma) + + # compute edge weights in 8 connectivity: + right_cost = np.abs((image[1:, :] - image[:-1, :])) + down_cost = np.abs((image[:, 1:] - image[:, :-1])) + dright_cost = np.abs((image[1:, 1:] - image[:-1, :-1])) + uright_cost = np.abs((image[1:, :-1] - image[:-1, 1:])) + cdef np.ndarray[np.float_t, ndim=1] costs = np.hstack([right_cost.ravel(), + down_cost.ravel(), dright_cost.ravel(), + uright_cost.ravel()]).astype(np.float) + # compute edges between pixels: + height, width = image.shape[:2] + cdef np.ndarray[np.int_t, ndim=2] segments \ + = np.arange(width * height, dtype=np.int).reshape(height, width) + right_edges = np.c_[segments[1:, :].ravel(), segments[:-1, :].ravel()] + down_edges = np.c_[segments[:, 1:].ravel(), segments[:, :-1].ravel()] + dright_edges = np.c_[segments[1:, 1:].ravel(), segments[:-1, :-1].ravel()] + uright_edges = np.c_[segments[:-1, 1:].ravel(), segments[1:, :-1].ravel()] + cdef np.ndarray[np.int_t, ndim=2] edges \ + = np.vstack([right_edges, down_edges, dright_edges, uright_edges]) + # initialize data structures for segment size + # and inner cost, then start greedy iteration over edges. + edge_queue = np.argsort(costs) + edges = np.ascontiguousarray(edges[edge_queue]) + costs = np.ascontiguousarray(costs[edge_queue]) + cdef np.int_t *segments_p = segments.data + cdef np.int_t *edges_p = edges.data + cdef np.float_t *costs_p = costs.data + cdef np.ndarray[np.int_t, ndim=1] segment_size \ + = np.ones(width * height, dtype=np.int) + # inner cost of segments + cdef np.ndarray[np.float_t, ndim=1] cint = np.zeros(width * height) + cdef int seg0, seg1, seg_new, e + cdef float cost, inner_cost0, inner_cost1 + # set costs_p back one. we increase it before we use it + # since we might continue before that. + costs_p -= 1 + for e in range(costs.size): + seg0 = find_root(segments_p, edges_p[0]) + seg1 = find_root(segments_p, edges_p[1]) + edges_p += 2 + costs_p += 1 + if seg0 == seg1: + continue + inner_cost0 = cint[seg0] + scale / segment_size[seg0] + inner_cost1 = cint[seg1] + scale / segment_size[seg1] + if costs_p[0] < min(inner_cost0, inner_cost1): + # update size and cost + join_trees(segments_p, seg0, seg1) + seg_new = find_root(segments_p, seg0) + segment_size[seg_new] = segment_size[seg0] + segment_size[seg1] + cint[seg_new] = costs_p[0] + + # postprocessing to remove small segments + edges_p = edges.data + for e in range(costs.size): + seg0 = find_root(segments_p, edges_p[0]) + seg1 = find_root(segments_p, edges_p[1]) + edges_p += 2 + if segment_size[seg0] < min_size or segment_size[seg1] < min_size: + join_trees(segments_p, seg0, seg1) + + # unravel the union find tree + flat = segments.ravel() + old = np.zeros_like(flat) + while (old != flat).any(): + old = flat + flat = flat[flat] + flat = np.unique(flat, return_inverse=True)[1] + return flat.reshape((height, width)) diff --git a/skimage/segmentation/_quickshift.pyx b/skimage/segmentation/_quickshift.pyx new file mode 100644 index 00000000..b465eb08 --- /dev/null +++ b/skimage/segmentation/_quickshift.pyx @@ -0,0 +1,162 @@ +import numpy as np +cimport numpy as np +cimport cython +from libc.math cimport exp, sqrt + +from itertools import product +from scipy import ndimage + +from ..util import img_as_float +from ..color import rgb2lab + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, + return_tree=False, sigma=0, convert2lab=True, random_seed=None): + """Segments image using quickshift clustering in Color-(x,y) space. + + Produces an oversegmentation of the image using the quickshift mode-seeking + algorithm. + + Parameters + ---------- + image : (width, height, channels) ndarray + Input image. + ratio : float, between 0 and 1. + Balances color-space proximity and image-space proximity. + Higher values give more weight to color-space. + kernel_size : float + Width of Gaussian kernel used in smoothing the + sample density. Higher means fewer clusters. + max_dist : float + Cut-off point for data distances. + Higher means fewer clusters. + return_tree : bool + Whether to return the full segmentation hierarchy tree and distances. + sigma : float + Width for Gaussian smoothing as preprocessing. Zero means no smoothing. + convert2lab : bool + Whether the input should be converted to Lab colorspace prior to + segmentation. For this purpose, the input is assumed to be RGB. + random_seed : None or int + Random seed used for breaking ties. + + Returns + ------- + segment_mask : (width, height) ndarray + Integer mask indicating segment labels. + + Notes + ----- + The authors advocate to convert the image to Lab color space prior to + segmentation, though this is not strictly necessary. For this to work, the + image must be given in RGB format. + + References + ---------- + .. [1] Quick shift and kernel methods for mode seeking, + Vedaldi, A. and Soatto, S. + European Conference on Computer Vision, 2008 + + + """ + image = img_as_float(np.atleast_3d(image)) + if convert2lab: + if image.shape[2] != 3: + ValueError("Only RGB images can be converted to Lab space.") + image = rgb2lab(image) + + image = ndimage.gaussian_filter(img_as_float(image), [sigma, sigma, 0]) + cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c \ + = np.ascontiguousarray(image) * ratio + + random_state = np.random.RandomState(random_seed) + + # TODO join orphaned roots? + # Some nodes might not have a point of higher density within the + # search window. We could do a global search over these in the end. + # Reference implementation doesn't do that, though, and it only has + # an effect for very high max_dist. + + # window size for neighboring pixels to consider + if kernel_size < 1: + raise ValueError("Sigma should be >= 1") + cdef int w = int(3 * kernel_size) + + cdef int height = image_c.shape[0] + cdef int width = image_c.shape[1] + cdef int channels = image_c.shape[2] + cdef double current_density, closest, dist + + cdef int r, c, r_, c_, channel + + cdef np.float_t* image_p = image_c.data + cdef np.float_t* current_pixel_p = image_p + + cdef np.ndarray[dtype=np.float_t, ndim=2] densities \ + = np.zeros((height, width)) + # compute densities + for r in range(height): + for c in range(width): + r_min, r_max = max(r - w, 0), min(r + w + 1, height) + c_min, c_max = max(c - w, 0), min(c + w + 1, width) + for r_ in range(r_min, r_max): + for c_ in range(c_min, c_max): + dist = 0 + for channel in range(channels): + dist += (current_pixel_p[channel] - + image_c[r_, c_, channel])**2 + dist += (r - r_)**2 + (c - c_)**2 + densities[r, c] += exp(-dist / (2 * kernel_size**2)) + current_pixel_p += channels + + # this will break ties that otherwise would give us headache + densities += random_state.normal(scale=0.00001, size=(height, width)) + + # default parent to self: + cdef np.ndarray[dtype=np.int_t, ndim=2] parent \ + = np.arange(width * height).reshape(height, width) + cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent \ + = np.zeros((height, width)) + # find nearest node with higher density + current_pixel_p = image_p + for r in range(height): + for c in range(width): + current_density = densities[r, c] + closest = np.inf + r_min, r_max = max(r - w, 0), min(r + w + 1, height) + c_min, c_max = max(c - w, 0), min(c + w + 1, width) + for r_ in range(r_min, r_max): + for c_ in range(c_min, c_max): + if densities[r_, c_] > current_density: + dist = 0 + # We compute the distances twice since otherwise + # we get crazy memory overhead + # (width * height * windowsize**2) + for channel in range(channels): + dist += (current_pixel_p[channel] - + image_c[r_, c_, channel])**2 + dist += (r - r_)**2 + (c - c_)**2 + if dist < closest: + closest = dist + parent[r, c] = r_ * width + c_ + dist_parent[r, c] = sqrt(closest) + current_pixel_p += channels + + dist_parent_flat = dist_parent.ravel() + flat = parent.ravel() + # remove parents with distance > max_dist + too_far = dist_parent_flat > max_dist + flat[too_far] = np.arange(width * height)[too_far] + old = np.zeros_like(flat) + # flatten forest (mark each pixel with root of corresponding tree) + while (old != flat).any(): + old = flat + flat = flat[flat] + flat = np.unique(flat, return_inverse=True)[1] + flat = flat.reshape(height, width) + if return_tree: + return flat, parent, dist_parent + return flat diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx new file mode 100644 index 00000000..d0e4c2a3 --- /dev/null +++ b/skimage/segmentation/_slic.pyx @@ -0,0 +1,134 @@ +import numpy as np +cimport numpy as np +from time import time +from scipy import ndimage +from ..util import img_as_float +from ..color import rgb2lab + + +def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, + convert2lab=True): + """Segments image using k-means clustering in Color-(x,y) space. + + Parameters + ---------- + image : (width, height, 3) ndarray + Input image. + n_segments : int + The (approximate) number of labels in the segmented output image. + ratio: float + Balances color-space proximity and image-space proximity. + Higher values give more weight to color-space. + max_iter : int + Maximum number of iterations of k-means. + sigma : float + Width of Gaussian smoothing kernel for preprocessing. Zero means no + smoothing. + convert2lab : bool + Whether the input should be converted to Lab colorspace prior to + segmentation. For this purpose, the input is assumed to be RGB. Highly + recommended. + + Returns + ------- + segment_mask : (width, height) ndarray + Integer mask indicating segment labels. + + Notes + ----- + The image is smoothed using a Gaussian kernel prior to segmentation. + + References + ---------- + .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, + Pascal Fua, and Sabine SĂĽsstrunk, SLIC Superpixels Compared to + State-of-the-art Superpixel Methods, TPAMI, May 2012. + + Examples + -------- + >>> from skimage.segmentation import slic + >>> from skimage.data import lena + >>> img = lena() + >>> segments = slic(img, n_segments=100, ratio=10) + >>> # Increasing the ratio parameter yields more square regions + >>> segments = slic(img, n_segments=100, ratio=20) + """ + image = np.atleast_3d(image) + if image.shape[2] != 3: + ValueError("Only 3-channel 2D images are supported.") + image = ndimage.gaussian_filter(img_as_float(image), [sigma, sigma, 0]) + if convert2lab: + image = rgb2lab(image) + + # initialize on grid: + cdef int height, width + height, width = image.shape[:2] + # approximate grid size for desired n_segments + cdef int step = np.ceil(np.sqrt(height * width / n_segments)) + grid_y, grid_x = np.mgrid[:height, :width] + means_y = grid_y[::step, ::step] + means_x = grid_x[::step, ::step] + + means_color = np.zeros((means_y.shape[0], means_y.shape[1], 3)) + cdef np.ndarray[dtype=np.float_t, ndim=2] means \ + = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) + cdef np.float_t* current_mean + cdef np.float_t* mean_entry + n_means = means.shape[0] + # we do the scaling of ratio in the same way as in the SLIC paper + # so the values have the same meaning + ratio = (ratio / float(step)) ** 2 + cdef np.ndarray[dtype=np.float_t, ndim=3] image_yx \ + = np.dstack([grid_y, grid_x, image / ratio]).copy("C") + cdef int i, k, x, y, x_min, x_max, y_min, y_max, changes + cdef double dist_mean + + cdef np.ndarray[dtype=np.int_t, ndim=2] nearest_mean \ + = np.zeros((height, width), dtype=np.int) + cdef np.ndarray[dtype=np.float_t, ndim=2] distance \ + = np.empty((height, width)) + cdef np.float_t* image_p = image_yx.data + cdef np.float_t* distance_p = distance.data + cdef np.float_t* current_distance + cdef np.float_t* current_pixel + cdef double tmp + for i in range(max_iter): + distance.fill(np.inf) + changes = 0 + current_mean = means.data + # assign pixels to means + for k in range(n_means): + # compute windows: + y_min = int(max(current_mean[0] - 2 * step, 0)) + y_max = int(min(current_mean[0] + 2 * step, height)) + x_min = int(max(current_mean[1] - 2 * step, 0)) + x_max = int(min(current_mean[1] + 2 * step, width)) + for y in range(y_min, y_max): + current_pixel = &image_p[5 * (y * width + x_min)] + current_distance = &distance_p[y * width + x_min] + for x in range(x_min, x_max): + mean_entry = current_mean + dist_mean = 0 + for c in range(5): + # you would think the compiler can optimize the squaring + # itself. mine can't (with O2) + tmp = current_pixel[0] - mean_entry[0] + dist_mean += tmp * tmp + current_pixel += 1 + mean_entry += 1 + # some precision issue here. Doesnt work if testing ">" + if current_distance[0] - dist_mean > 1e-10: + nearest_mean[y, x] = k + current_distance[0] = dist_mean + changes += 1 + current_distance += 1 + current_mean += 5 + if changes == 0: + break + # recompute means: + means_list = [np.bincount(nearest_mean.ravel(), + image_yx[:, :, j].ravel()) for j in range(5)] + in_mean = np.bincount(nearest_mean.ravel()) + in_mean[in_mean == 0] = 1 + means = (np.vstack(means_list) / in_mean).T.copy("C") + return nearest_mean diff --git a/skimage/segmentation/boundaries.py b/skimage/segmentation/boundaries.py new file mode 100644 index 00000000..b40ecb01 --- /dev/null +++ b/skimage/segmentation/boundaries.py @@ -0,0 +1,19 @@ +import numpy as np +from ..morphology import dilation, square +from ..util import img_as_float + + +def find_boundaries(label_img): + boundaries = np.zeros(label_img.shape, dtype=np.bool) + boundaries[1:, :] += label_img[1:, :] != label_img[:-1, :] + boundaries[:, 1:] += label_img[:, 1:] != label_img[:, :-1] + return boundaries + + +def visualize_boundaries(img, label_img): + img = img_as_float(img, force_copy=True) + boundaries = find_boundaries(label_img) + outer_boundaries = dilation(boundaries.astype(np.uint8), square(2)) + img[outer_boundaries != 0, :] = np.array([0, 0, 0]) # black + img[boundaries, :] = np.array([1, 1, 0]) # yellow + return img diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 130be926..6eb7def8 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -27,6 +27,8 @@ try: except ImportError: amg_loaded = False from scipy.sparse.linalg import cg +from ..util import img_as_float +from ..filter import rank_order #-----------Laplacian-------------------- @@ -62,17 +64,28 @@ def _make_graph_edges_3d(n_x, n_y, n_z): return edges -def _compute_weights_3d(data, beta=130, eps=1.e-6): - gradients = _compute_gradients_3d(data) ** 2 +def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1., + multichannel=False): + # Weight calculation is main difference in multispectral version + # Original gradient**2 replaced with sum of gradients ** 2 + gradients = 0 + for channel in range(0, data.shape[-1]): + gradients += _compute_gradients_3d(data[..., channel], + depth=depth) ** 2 + # All channels considered together in this standard deviation beta /= 10 * data.std() + if multichannel: + # New final term in beta to give == results in trivial case where + # multiple identical spectra are passed. + beta /= np.sqrt(data.shape[-1]) gradients *= beta weights = np.exp(- gradients) weights += eps return weights -def _compute_gradients_3d(data): - gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel() +def _compute_gradients_3d(data, depth=1.): + gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel() / depth gr_right = np.abs(data[:, :-1] - data[:, 1:]).ravel() gr_down = np.abs(data[:-1] - data[1:]).ravel() return np.r_[gr_deep, gr_right, gr_down] @@ -96,7 +109,10 @@ def _make_laplacian_sparse(edges, weights): return lap.tocsr() -def _clean_labels_ar(X, labels): +def _clean_labels_ar(X, labels, copy=False): + X = X.astype(labels.dtype) + if copy: + labels = np.copy(labels) labels = np.ravel(labels) labels[labels == 0] = X return labels @@ -130,23 +146,24 @@ def _mask_edges_weights(edges, weights, mask): corresponding weights of the edges. """ mask0 = np.hstack((mask[:, :, :-1].ravel(), mask[:, :-1].ravel(), - mask[:-1].ravel())) + mask[:-1].ravel())) mask1 = np.hstack((mask[:, :, 1:].ravel(), mask[:, 1:].ravel(), - mask[1:].ravel())) + mask[1:].ravel())) ind_mask = np.logical_and(mask0, mask1) edges, weights = edges[:, ind_mask], weights[ind_mask] max_node_index = edges.max() # Reassign edges labels to 0, 1, ... edges_number - 1 order = np.searchsorted(np.unique(edges.ravel()), - np.arange(max_node_index + 1)) + np.arange(max_node_index + 1)) edges = order[edges] return edges, weights -def _build_laplacian(data, mask=None, beta=50): - l_x, l_y, l_z = data.shape +def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): + l_x, l_y, l_z = data.shape[:3] edges = _make_graph_edges_3d(l_x, l_y, l_z) - weights = _compute_weights_3d(data, beta=beta, eps=1.e-10) + weights = _compute_weights_3d(data, beta=beta, eps=1.e-10, depth=depth, + multichannel=multichannel) if mask is not None: edges, weights = _mask_edges_weights(edges, weights, mask) lap = _make_laplacian_sparse(edges, weights) @@ -157,22 +174,31 @@ def _build_laplacian(data, mask=None, beta=50): #----------- Random walker algorithm -------------------------------- -def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): +def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, + multichannel=False, return_full_prob=False, depth=1.): """ - Random walker algorithm for segmentation from markers. + Random walker algorithm for segmentation from markers, for gray-level or + multichannel images. Parameters ---------- data : array_like - Image to be segmented in phases. `data` can be two- or - three-dimensional. + Image to be segmented in phases. Gray-level `data` can be two- or + three-dimensional; multichannel data can be three- or four- + dimensional (multichannel=True) with the highest dimension denoting + channels. Data spacing is assumed isotropic unless depth keyword + argument is used. - labels : array of ints, of same shape as `data` + labels : array of ints, of same shape as `data` without channels dimension Array of seed markers labeled with different positive integers for different phases. Zero-labeled pixels are unlabeled pixels. Negative labels correspond to inactive pixels that are not taken - into account (they are removed from the graph). + into account (they are removed from the graph). If labels are not + consecutive integers, the labels array will be transformed so that + labels are consecutive. In the multichannel case, `labels` should have + the same shape as a single channel of `data`, i.e. without the final + dimension denoting channels. beta : float Penalization coefficient for the random walker motion @@ -208,12 +234,31 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): the result of the segmentation. Use copy=False if you want to save on memory. + multichannel : bool, default False + If True, input data is parsed as multichannel data (see 'data' above + for proper input format in this case) + + return_full_prob : bool, default False + If True, the probability that a pixel belongs to each of the labels + will be returned, instead of only the most likely label. + + depth : float, default 1. + Correction for non-isotropic voxel depths in 3D volumes. + Default (1.) implies isotropy. This factor is derived as follows: + depth = (out-of-plane voxel spacing) / (in-plane voxel spacing), where + in-plane voxel spacing represents the first two spatial dimensions and + out-of-plane voxel spacing represents the third spatial dimension. + Returns ------- - output : ndarray of ints - Array in which each pixel has been labeled according to the marker - that reached the pixel first by anisotropic diffusion. + output : ndarray + If `return_full_prob` is False, array of ints of same shape as `data`, + in which each pixel has been labeled according to the marker that + reached the pixel first by anisotropic diffusion. + If `return_full_prob` is True, array of floats of shape + `(nlabels, data.shape)`. `output[label_nb, i, j]` is the probability + that label `label_nb` reaches the pixel `(i, j)` first. See also -------- @@ -225,6 +270,16 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): Notes ----- + Multichannel inputs are scaled with all channel data combined. Ensure all + channels are separately normalized prior to running this algorithm. + + The `depth` argument is specifically for certain types of 3-dimensional + volumes which, due to how they were acquired, have different spacing + along in-plane and out-of-plane dimensions. This is commonly encountered + in medical imaging. The `depth` argument corrects gradients calculated + along the third spatial dimension for the otherwise inherent assumption + that all points are equally spaced. + The algorithm was first proposed in *Random walks for image segmentation*, Leo Grady, IEEE Trans Pattern Anal Mach Intell. 2006 Nov;28(11):1768-83. @@ -247,7 +302,8 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): The weight w_ij is a decreasing function of the norm of the local gradient. This ensures that diffusion is easier between pixels of similar values. - When the Laplacian is decomposed into blocks of marked and unmarked pixels:: + When the Laplacian is decomposed into blocks of marked and unmarked + pixels:: L = M B.T B A @@ -257,7 +313,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): A x = - B x_m - where x_m=1 on markers of the given phase, and 0 on other markers. + where x_m = 1 on markers of the given phase, and 0 on other markers. This linear system is solved in the algorithm using a direct method for small images, and an iterative method for larger images. @@ -270,23 +326,41 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): >>> b[3,3] = 1 #Marker for first phase >>> b[6,6] = 2 #Marker for second phase >>> random_walker(a, b) - array([[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 2., 2., 2., 1., 1.], - [ 1., 1., 1., 1., 1., 2., 2., 2., 1., 1.], - [ 1., 1., 1., 1., 1., 2., 2., 2., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]]) + array([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], + [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], + [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], dtype=int32) """ - # We work with 3-D arrays - data = np.atleast_3d(data) + # Parse input data + if not multichannel: + # We work with 4-D arrays of floats + dims = data.shape + data = np.atleast_3d(img_as_float(data)) + data.shape += (1,) + else: + dims = data[..., 0].shape + assert multichannel and data.ndim > 2, 'For multichannel input, data \ + must have >= 3 dimensions.' + data = img_as_float(data) + if data.ndim == 3: + data.shape += (1,) + data = data.transpose((0, 1, 3, 2)) + if copy: labels = np.copy(labels) - labels = labels.astype(np.intp) + label_values = np.unique(labels) + # Reorder label values to have consecutive integers (no gaps) + if np.any(np.diff(label_values) != 1): + mask = labels >= 0 + labels[mask] = rank_order(labels[mask])[0].astype(labels.dtype) + labels = labels.astype(np.int32) # If the array has pruned zones, be sure that no isolated pixels # exist between pruned zones (they could not be determined) if np.any(labels < 0): @@ -295,33 +369,48 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): del filled labels = np.atleast_3d(labels) if np.any(labels < 0): - lap_sparse = _build_laplacian(data, mask=labels >= 0, beta=beta) + lap_sparse = _build_laplacian(data, mask=labels >= 0, beta=beta, + depth=depth, multichannel=multichannel) else: - lap_sparse = _build_laplacian(data, beta=beta) + lap_sparse = _build_laplacian(data, beta=beta, depth=depth, + multichannel=multichannel) lap_sparse, B = _buildAB(lap_sparse, labels) # We solve the linear system # lap_sparse X = B # where X[i, j] is the probability that a marker of label i arrives # first at pixel j by anisotropic diffusion. if mode == 'cg': - X = _solve_cg(lap_sparse, B, tol=tol) + X = _solve_cg(lap_sparse, B, tol=tol, + return_full_prob=return_full_prob) if mode == 'cg_mg': if not amg_loaded: warnings.warn( """pyamg (http://code.google.com/p/pyamg/)) is needed to use this mode, but is not installed. The 'cg' mode will be used instead.""") - X = _solve_cg(lap_sparse, B, tol=tol) + X = _solve_cg(lap_sparse, B, tol=tol, + return_full_prob=return_full_prob) else: - X = _solve_cg_mg(lap_sparse, B, tol=tol) + X = _solve_cg_mg(lap_sparse, B, tol=tol, + return_full_prob=return_full_prob) if mode == 'bf': - X = _solve_bf(lap_sparse, B) - X = _clean_labels_ar(X + 1, labels) - data = np.squeeze(data) - return X.reshape(data.shape) + X = _solve_bf(lap_sparse, B, + return_full_prob=return_full_prob) + # Clean up results + if return_full_prob: + labels = labels.astype(np.float) + X = np.array([_clean_labels_ar(Xline, labels, + copy=True).reshape(dims) for Xline in X]) + for i in range(1, int(labels.max()) + 1): + mask_i = np.squeeze(labels == i) + X[:, mask_i] = 0 + X[i - 1, mask_i] = 1 + else: + X = _clean_labels_ar(X + 1, labels).reshape(dims) + return X -def _solve_bf(lap_sparse, B): +def _solve_bf(lap_sparse, B, return_full_prob=False): """ solves lap_sparse X_i = B_i for each phase i. An LU decomposition of lap_sparse is computed first. For each pixel, the label i @@ -330,12 +419,13 @@ def _solve_bf(lap_sparse, B): lap_sparse = lap_sparse.tocsc() solver = sparse.linalg.factorized(lap_sparse.astype(np.double)) X = np.array([solver(np.array((-B[i]).todense()).ravel())\ - for i in range(len(B))]) - X = np.argmax(X, axis=0) + for i in range(len(B))]) + if not return_full_prob: + X = np.argmax(X, axis=0) return X -def _solve_cg(lap_sparse, B, tol): +def _solve_cg(lap_sparse, B, tol, return_full_prob=False): """ solves lap_sparse X_i = B_i for each phase i, using the conjugate gradient method. For each pixel, the label i corresponding to the @@ -346,12 +436,13 @@ def _solve_cg(lap_sparse, B, tol): for i in range(len(B)): x0 = cg(lap_sparse, -B[i].todense(), tol=tol)[0] X.append(x0) - X = np.array(X) - X = np.argmax(X, axis=0) + if not return_full_prob: + X = np.array(X) + X = np.argmax(X, axis=0) return X -def _solve_cg_mg(lap_sparse, B, tol): +def _solve_cg_mg(lap_sparse, B, tol, return_full_prob=False): """ solves lap_sparse X_i = B_i for each phase i, using the conjugate gradient method with a multigrid preconditioner (ruge-stuben from @@ -364,6 +455,7 @@ def _solve_cg_mg(lap_sparse, B, tol): for i in range(len(B)): x0 = cg(lap_sparse, -B[i].todense(), tol=tol, M=M, maxiter=30)[0] X.append(x0) - X = np.array(X) - X = np.argmax(X, axis=0) + if not return_full_prob: + X = np.array(X) + X = np.argmax(X, axis=0) return X diff --git a/skimage/segmentation/setup.py b/skimage/segmentation/setup.py new file mode 100644 index 00000000..ec4ac4ff --- /dev/null +++ b/skimage/segmentation/setup.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +import os +from skimage._build import cython + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs + + config = Configuration('segmentation', parent_package, top_path) + + cython(['_felzenszwalb_cy.pyx'], working_path=base_path) + config.add_extension('_felzenszwalb_cy', sources=['_felzenszwalb_cy.c'], + include_dirs=[get_numpy_include_dirs()]) + cython(['_quickshift.pyx'], working_path=base_path) + config.add_extension('_quickshift', sources=['_quickshift.c'], + include_dirs=[get_numpy_include_dirs()]) + cython(['_slic.pyx'], working_path=base_path) + config.add_extension('_slic', sources=['_slic.c'], + include_dirs=[get_numpy_include_dirs()]) + + return config + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Segmentation Algorithms', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/segmentation/tests/test_clear_border.py b/skimage/segmentation/tests/test_clear_border.py new file mode 100644 index 00000000..5d6852cf --- /dev/null +++ b/skimage/segmentation/tests/test_clear_border.py @@ -0,0 +1,32 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.segmentation import clear_border + + +def test_clear_border(): + image = np.array( + [[0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [1, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + + # test default case + result = clear_border(image.copy()) + ref = image.copy() + ref[2, 0] = 0 + ref[0, -2] = 0 + assert_array_equal(result, ref) + + # test buffer + result = clear_border(image.copy(), 1) + assert_array_equal(result, np.zeros(result.shape)) + + # test background value + result = clear_border(image.copy(), 1, 2) + assert_array_equal(result, 2 * np.ones_like(image)) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/segmentation/tests/test_felzenszwalb.py b/skimage/segmentation/tests/test_felzenszwalb.py new file mode 100644 index 00000000..9fd0b018 --- /dev/null +++ b/skimage/segmentation/tests/test_felzenszwalb.py @@ -0,0 +1,39 @@ +import numpy as np +from numpy.testing import assert_equal, assert_array_equal +from skimage._shared.testing import assert_greater +from skimage.segmentation import felzenszwalb + + +def test_grey(): + # very weak tests. This algorithm is pretty unstable. + img = np.zeros((20, 21)) + img[:10, 10:] = 0.2 + img[10:, :10] = 0.4 + img[10:, 10:] = 0.6 + seg = felzenszwalb(img, sigma=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + # that mostly respect the 4 regions: + for i in range(4): + hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0] + assert_greater(hist[i], 40) + + +def test_color(): + # very weak tests. This algorithm is pretty unstable. + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + seg = felzenszwalb(img, sigma=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + assert_array_equal(seg[:10, :10], 0) + assert_array_equal(seg[10:, :10], 2) + assert_array_equal(seg[:10, 10:], 1) + assert_array_equal(seg[10:, 10:], 3) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/segmentation/tests/test_quickshift.py b/skimage/segmentation/tests/test_quickshift.py new file mode 100644 index 00000000..d43d7559 --- /dev/null +++ b/skimage/segmentation/tests/test_quickshift.py @@ -0,0 +1,53 @@ +import numpy as np +from numpy.testing import assert_equal, assert_array_equal +from nose.tools import assert_true +from skimage._shared.testing import assert_greater +from skimage.segmentation import quickshift + + +def test_grey(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21)) + img[:10, 10:] = 0.2 + img[10:, :10] = 0.4 + img[10:, 10:] = 0.6 + img += 0.1 * rnd.normal(size=img.shape) + seg = quickshift(img, kernel_size=2, max_dist=3, random_seed=0, + convert2lab=False, sigma=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + # that mostly respect the 4 regions: + for i in range(4): + hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0] + assert_greater(hist[i], 20) + + +def test_color(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.01 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = quickshift(img, random_seed=0, max_dist=30, kernel_size=10, sigma=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + assert_array_equal(seg[:10, :10], 0) + assert_array_equal(seg[10:, :10], 3) + assert_array_equal(seg[:10, 10:], 1) + assert_array_equal(seg[10:, 10:], 2) + + seg2 = quickshift(img, kernel_size=1, max_dist=2, random_seed=0, + convert2lab=False, sigma=0) + # very oversegmented: + assert_equal(len(np.unique(seg2)), 7) + # still don't cross lines + assert_true((seg2[9, :] != seg2[10, :]).all()) + assert_true((seg2[:, 9] != seg2[:, 10]).all()) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index 41a86dc3..7df0241c 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -54,8 +54,17 @@ def test_2d_bf(): data, labels = make_2d_syntheticdata(lx, ly) labels_bf = random_walker(data, labels, beta=90, mode='bf') assert (labels_bf[25:45, 40:60] == 2).all() - return data, labels_bf - + full_prob_bf = random_walker(data, labels, beta=90, mode='bf', + return_full_prob=True) + assert (full_prob_bf[1, 25:45, 40:60] >= + full_prob_bf[0, 25:45, 40:60]).all() + # Now test with more than two labels + labels[55, 80] = 3 + full_prob_bf = random_walker(data, labels, beta=90, mode='bf', + return_full_prob=True) + assert (full_prob_bf[1, 25:45, 40:60] >= + full_prob_bf[0, 25:45, 40:60]).all() + assert len(full_prob_bf) == 3 def test_2d_cg(): lx = 70 @@ -63,6 +72,10 @@ def test_2d_cg(): data, labels = make_2d_syntheticdata(lx, ly) labels_cg = random_walker(data, labels, beta=90, mode='cg') assert (labels_cg[25:45, 40:60] == 2).all() + full_prob = random_walker(data, labels, beta=90, mode='cg', + return_full_prob=True) + assert (full_prob[1, 25:45, 40:60] >= + full_prob[0, 25:45, 40:60]).all() return data, labels_cg @@ -72,9 +85,34 @@ def test_2d_cg_mg(): data, labels = make_2d_syntheticdata(lx, ly) labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') assert (labels_cg_mg[25:45, 40:60] == 2).all() + full_prob = random_walker(data, labels, beta=90, mode='cg_mg', + return_full_prob=True) + assert (full_prob[1, 25:45, 40:60] >= + full_prob[0, 25:45, 40:60]).all() return data, labels_cg_mg +def test_types(): + lx = 70 + ly = 100 + data, labels = make_2d_syntheticdata(lx, ly) + data = 255 * (data - data.min()) / (data.max() - data.min()) + data = data.astype(np.uint8) + labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') + assert (labels_cg_mg[25:45, 40:60] == 2).all() + return data, labels_cg_mg + + +def test_reorder_labels(): + lx = 70 + ly = 100 + data, labels = make_2d_syntheticdata(lx, ly) + labels[labels == 2] = 4 + labels_bf = random_walker(data, labels, beta=90, mode='bf') + assert (labels_bf[25:45, 40:60] == 2).all() + return data, labels_bf + + def test_2d_inactive(): lx = 70 ly = 100 @@ -106,6 +144,31 @@ def test_3d_inactive(): assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all() return data, labels, old_labels, after_labels + +def test_multispectral_2d(): + lx, ly = 70, 100 + data, labels = make_2d_syntheticdata(lx, ly) + data2 = data.copy() + data.shape += (1,) + data = data.repeat(2, axis=2) # Result should be identical + multi_labels = random_walker(data, labels, mode='cg', multichannel=True) + single_labels = random_walker(data2, labels, mode='cg') + assert (multi_labels.reshape(labels.shape)[25:45, 40:60] == 2).all() + return data, multi_labels, single_labels, labels + + +def test_multispectral_3d(): + n = 30 + lx, ly, lz = n, n, n + data, labels = make_3d_syntheticdata(lx, ly, lz) + data.shape += (1,) + data = data.repeat(2, axis=3) # Result should be identical + multi_labels = random_walker(data, labels, mode='cg', multichannel=True) + single_labels = random_walker(data[..., 0], labels, mode='cg') + assert (multi_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all() + assert (single_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all() + return data, multi_labels, single_labels, labels + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py new file mode 100644 index 00000000..f2d6698d --- /dev/null +++ b/skimage/segmentation/tests/test_slic.py @@ -0,0 +1,27 @@ +import numpy as np +from numpy.testing import assert_equal, assert_array_equal +from skimage.segmentation import slic + + +def test_color(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.01 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, sigma=0, n_segments=4) + # we expect 4 segments: + print(seg) + assert_equal(len(np.unique(seg)), 4) + assert_array_equal(seg[:10, :10], 0) + assert_array_equal(seg[10:, :10], 2) + assert_array_equal(seg[:10, 10:], 1) + assert_array_equal(seg[10:, 10:], 3) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/setup.py b/skimage/setup.py index c26014f8..1082ba07 100644 --- a/skimage/setup.py +++ b/skimage/setup.py @@ -1,21 +1,24 @@ import os + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration config = Configuration('skimage', parent_package, top_path) - config.add_subpackage('graph') - config.add_subpackage('io') - config.add_subpackage('morphology') - config.add_subpackage('filter') - config.add_subpackage('transform') - config.add_subpackage('data') - config.add_subpackage('util') + config.add_subpackage('_shared') config.add_subpackage('color') + config.add_subpackage('data') config.add_subpackage('draw') config.add_subpackage('feature') + config.add_subpackage('filter') + config.add_subpackage('graph') + config.add_subpackage('io') config.add_subpackage('measure') + config.add_subpackage('morphology') + config.add_subpackage('transform') + config.add_subpackage('util') + config.add_subpackage('segmentation') def add_test_directories(arg, dirname, fnames): if dirname.split(os.path.sep)[-1] == 'tests': @@ -37,4 +40,3 @@ if __name__ == "__main__": config = configuration(top_path='').todict() setup(**config) - diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index fa4059ee..a55d5df4 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -1,8 +1,11 @@ from .hough_transform import * from .radon_transform import * from .finite_radon_transform import * -from .project import * -from ._project import homography as fast_homography from .integral import * -from ._warp import warp -from ._warp_zoo import swirl +from ._geometric import (warp, warp_coords, estimate_transform, + SimilarityTransform, AffineTransform, + ProjectiveTransform, PolynomialTransform, + PiecewiseAffineTransform) +from ._warps import swirl, homography, resize, rotate +from .pyramids import (pyramid_reduce, pyramid_expand, + pyramid_gaussian, pyramid_laplacian) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py new file mode 100644 index 00000000..1179811d --- /dev/null +++ b/skimage/transform/_geometric.py @@ -0,0 +1,1022 @@ +import math +import numpy as np +from scipy import ndimage, spatial +from skimage.util import img_as_float +from ._warps_cy import _warp_fast + + +class GeometricTransform(object): + """Perform geometric transformations on a set of coordinates. + + """ + def __call__(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : (N, 2) array + Source coordinates. + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + raise NotImplementedError() + + def inverse(self, coords): + """Apply inverse transformation. + + Parameters + ---------- + coords : (N, 2) array + Source coordinates. + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + raise NotImplementedError() + + def __add__(self, other): + """Combine this transformation with another. + + """ + raise NotImplementedError() + + +class ProjectiveTransform(GeometricTransform): + """Matrix transformation. + + Apply a projective transformation (homography) on coordinates. + + For each homogeneous coordinate :math:`\mathbf{x} = [x, y, 1]^T`, its + target position is calculated by multiplying with the given matrix, + :math:`H`, to give :math:`H \mathbf{x}`:: + + [[a0 a1 a2] + [b0 b1 b2] + [c0 c1 1 ]]. + + E.g., to rotate by theta degrees clockwise, the matrix should be:: + + [[cos(theta) -sin(theta) 0] + [sin(theta) cos(theta) 0] + [0 0 1]] + + or, to translate x by 10 and y by 20:: + + [[1 0 10] + [0 1 20] + [0 0 1 ]]. + + Parameters + ---------- + matrix : (3, 3) array, optional + Homogeneous transformation matrix. + + """ + + _coeffs = range(8) + + def __init__(self, matrix=None): + if matrix is None: + # default to an identity transform + matrix = np.eye(3) + if matrix.shape != (3, 3): + raise ValueError("invalid shape of transformation matrix") + self._matrix = matrix + + @property + def _inv_matrix(self): + return np.linalg.inv(self._matrix) + + def _apply_mat(self, coords, matrix): + coords = np.array(coords, copy=False, ndmin=2) + + x, y = np.transpose(coords) + src = np.vstack((x, y, np.ones_like(x))) + dst = np.dot(src.transpose(), matrix.transpose()) + + # rescale to homogeneous coordinates + dst[:, 0] /= dst[:, 2] + dst[:, 1] /= dst[:, 2] + + return dst[:, :2] + + def __call__(self, coords): + return self._apply_mat(coords, self._matrix) + + def inverse(self, coords): + """Apply inverse transformation. + + Parameters + ---------- + coords : (N, 2) array + Source coordinates. + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + return self._apply_mat(coords, self._inv_matrix) + + def estimate(self, src, dst): + """Set the transformation matrix with the explicit transformation + parameters. + + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source and destination coordinates must match. + + The transformation is defined as:: + + X = (a0*x + a1*y + a2) / (c0*x + c1*y + 1) + Y = (b0*x + b1*y + b2) / (c0*x + c1*y + 1) + + These equations can be transformed to the following form:: + + 0 = a0*x + a1*y + a2 - c0*x*X - c1*y*X - X + 0 = b0*x + b1*y + b2 - c0*x*Y - c1*y*Y - Y + + which exist for each set of corresponding points, so we have a set of + N * 2 equations. The coefficients appear linearly so we can write + A x = 0, where:: + + A = [[x y 1 0 0 0 -x*X -y*X -X] + [0 0 0 x y 1 -x*Y -y*Y -Y] + ... + ... + ] + x.T = [a0 a1 a2 b0 b1 b2 c0 c1 c3] + + In case of total least-squares the solution of this homogeneous system + of equations is the right singular vector of A which corresponds to the + smallest singular value normed by the coefficient c3. + + In case of the affine transformation the coefficients c0 and c1 are 0. + Thus the system of equations is:: + + A = [[x y 1 0 0 0 -X] + [0 0 0 x y 1 -Y] + ... + ... + ] + x.T = [a0 a1 a2 b0 b1 b2 c3] + + Parameters + ---------- + src : (N, 2) array + Source coordinates. + dst : (N, 2) array + Destination coordinates. + + """ + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + # params: a0, a1, a2, b0, b1, b2, c0, c1 + A = np.zeros((rows * 2, 9)) + A[:rows, 0] = xs + A[:rows, 1] = ys + A[:rows, 2] = 1 + A[:rows, 6] = - xd * xs + A[:rows, 7] = - xd * ys + A[rows:, 3] = xs + A[rows:, 4] = ys + A[rows:, 5] = 1 + A[rows:, 6] = - yd * xs + A[rows:, 7] = - yd * ys + A[:rows, 8] = xd + A[rows:, 8] = yd + + # Select relevant columns, depending on params + A = A[:, self._coeffs + [8]] + + _, _, V = np.linalg.svd(A) + + H = np.zeros((3, 3)) + # solution is right singular vector that corresponds to smallest + # singular value + H.flat[self._coeffs + [8]] = - V[-1, :-1] / V[-1, -1] + H[2, 2] = 1 + + self._matrix = H + + def __add__(self, other): + """Combine this transformation with another. + + """ + if isinstance(other, ProjectiveTransform): + # combination of the same types result in a transformation of this + # type again, otherwise use general projective transformation + if type(self) == type(other): + tform = self.__class__ + else: + tform = ProjectiveTransform + return tform(other._matrix.dot(self._matrix)) + else: + raise TypeError("Cannot combine transformations of differing " + "types.") + + +class AffineTransform(ProjectiveTransform): + + """2D affine transformation of the form:: + + X = a0*x + a1*y + a2 = + = sx*x*cos(rotation) - sy*y*sin(rotation + shear) + a2 + + Y = b0*x + b1*y + b2 = + = sx*x*sin(rotation) + sy*y*cos(rotation + shear) + b2 + + where ``sx`` and ``sy`` are zoom factors in the x and y directions, + and the homogeneous transformation matrix is:: + + [[a0 a1 a2] + [b0 b1 b2] + [0 0 1]] + + Parameters + ---------- + matrix : (3, 3) array, optional + Homogeneous transformation matrix. + scale : (sx, sy) as array, list or tuple, optional + Scale factors. + rotation : float, optional + Rotation angle in counter-clockwise direction as radians. + shear : float, optional + Shear angle in counter-clockwise direction as radians. + translation : (tx, ty) as array, list or tuple, optional + Translation parameters. + + """ + + _coeffs = range(6) + + def __init__(self, matrix=None, scale=None, rotation=None, shear=None, + translation=None): + params = any(param is not None + for param in (scale, rotation, shear, translation)) + + if params and matrix is not None: + raise ValueError("You cannot specify the transformation matrix and" + " the implicit parameters at the same time.") + elif matrix is not None: + if matrix.shape != (3, 3): + raise ValueError("Invalid shape of transformation matrix.") + self._matrix = matrix + elif params: + if scale is None: + scale = (1, 1) + if rotation is None: + rotation = 0 + if shear is None: + shear = 0 + if translation is None: + translation = (0, 0) + + sx, sy = scale + self._matrix = np.array([ + [sx * math.cos(rotation), -sy * math.sin(rotation + shear), 0], + [sx * math.sin(rotation), sy * math.cos(rotation + shear), 0], + [ 0, 0, 1] + ]) + self._matrix[0:2, 2] = translation + else: + # default to an identity transform + self._matrix = np.eye(3) + + @property + def scale(self): + sx = math.sqrt(self._matrix[0, 0] ** 2 + self._matrix[1, 0] ** 2) + sy = math.sqrt(self._matrix[0, 1] ** 2 + self._matrix[1, 1] ** 2) + return sx, sy + + @property + def rotation(self): + return math.atan2(self._matrix[1, 0], self._matrix[0, 0]) + + @property + def shear(self): + beta = math.atan2(- self._matrix[0, 1], self._matrix[1, 1]) + return beta - self.rotation + + @property + def translation(self): + return self._matrix[0:2, 2] + + +class PiecewiseAffineTransform(ProjectiveTransform): + + """2D piecewise affine transformation. + + Control points are used to define the mapping. The transform is based on + a Delaunay triangulation of the points to form a mesh. Each triangle is + used to find a local affine transform. + + """ + + def __init__(self): + self._tesselation = None + self._inverse_tesselation = None + self.affines = [] + self.inverse_affines = [] + + def estimate(self, src, dst): + """Set the control points with which to perform the piecewise mapping. + + Number of source and destination coordinates must match. + + Parameters + ---------- + src : (N, 2) array + Source coordinates. + dst : (N, 2) array + Destination coordinates. + + """ + + # forward piecewise affine + # triangulate input positions into mesh + self._tesselation = spatial.Delaunay(src) + # find affine mapping from source positions to destination + self.affines = [] + for tri in self._tesselation.vertices: + affine = AffineTransform() + affine.estimate(src[tri, :], dst[tri, :]) + self.affines.append(affine) + + # inverse piecewise affine + # triangulate input positions into mesh + self._inverse_tesselation = spatial.Delaunay(dst) + # find affine mapping from source positions to destination + self.inverse_affines = [] + for tri in self._inverse_tesselation.vertices: + affine = AffineTransform() + affine.estimate(dst[tri, :], src[tri, :]) + self.inverse_affines.append(affine) + + def __call__(self, coords): + """Apply forward transformation. + + Coordinates outside of the mesh will be set to `- 1`. + + Parameters + ---------- + coords : (N, 2) array + Source coordinates. + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + + out = np.empty_like(coords, np.double) + + # determine triangle index for each coordinate + simplex = self._tesselation.find_simplex(coords) + + # coordinates outside of mesh + out[simplex == -1, :] = -1 + + for index in range(len(self._tesselation.vertices)): + # affine transform for triangle + affine = self.affines[index] + # all coordinates within triangle + index_mask = simplex == index + + out[index_mask, :] = affine(coords[index_mask, :]) + + return out + + def inverse(self, coords): + """Apply inverse transformation. + + Coordinates outside of the mesh will be set to `- 1`. + + Parameters + ---------- + coords : (N, 2) array + Source coordinates. + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + + out = np.empty_like(coords, np.double) + + # determine triangle index for each coordinate + simplex = self._inverse_tesselation.find_simplex(coords) + + # coordinates outside of mesh + out[simplex == -1, :] = -1 + + for index in range(len(self._inverse_tesselation.vertices)): + # affine transform for triangle + affine = self.inverse_affines[index] + # all coordinates within triangle + index_mask = simplex == index + + out[index_mask, :] = affine(coords[index_mask, :]) + + return out + + +class SimilarityTransform(ProjectiveTransform): + """2D similarity transformation of the form:: + + X = a0*x - b0*y + a1 = + = m*x*cos(rotation) + m*y*sin(rotation) + a1 + + Y = b0*x + a0*y + b1 = + = m*x*sin(rotation) + m*y*cos(rotation) + b1 + + where ``m`` is a zoom factor and the homogeneous transformation matrix is:: + + [[a0 b0 a1] + [b0 a0 b1] + [0 0 1]] + + Parameters + ---------- + matrix : (3, 3) array, optional + Homogeneous transformation matrix. + scale : float, optional + Scale factor. + rotation : float, optional + Rotation angle in counter-clockwise direction as radians. + translation : (tx, ty) as array, list or tuple, optional + x, y translation parameters. + + """ + + def __init__(self, matrix=None, scale=None, rotation=None, + translation=None): + params = any(param is not None + for param in (scale, rotation, translation)) + + if params and matrix is not None: + raise ValueError("You cannot specify the transformation matrix and " + "the implicit parameters at the same time.") + elif matrix is not None: + if matrix.shape != (3, 3): + raise ValueError("Invalid shape of transformation matrix.") + self._matrix = matrix + elif params: + if scale is None: + scale = 1 + if rotation is None: + rotation = 0 + if translation is None: + translation = (0, 0) + + self._matrix = np.array([ + [math.cos(rotation), - math.sin(rotation), 0], + [math.sin(rotation), math.cos(rotation), 0], + [ 0, 0, 1] + ]) + self._matrix *= scale + self._matrix[0:2, 2] = translation + else: + # default to an identity transform + self._matrix = np.eye(3) + + def estimate(self, src, dst): + """Set the transformation matrix with the explicit parameters. + + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source and destination coordinates must match. + + The transformation is defined as:: + + X = a0*x - b0*y + a1 + Y = b0*x + a0*y + b1 + + These equations can be transformed to the following form:: + + 0 = a0*x - b0*y + a1 - X + 0 = b0*x + a0*y + b1 - Y + + which exist for each set of corresponding points, so we have a set of + N * 2 equations. The coefficients appear linearly so we can write + A x = 0, where:: + + A = [[x 1 -y 0 -X] + [y 0 x 1 -Y] + ... + ... + ] + x.T = [a0 a1 b0 b1 c3] + + In case of total least-squares the solution of this homogeneous system + of equations is the right singular vector of A which corresponds to the + smallest singular value normed by the coefficient c3. + + Parameters + ---------- + src : (N, 2) array + Source coordinates. + dst : (N, 2) array + Destination coordinates. + + """ + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + # params: a0, a1, b0, b1 + A = np.zeros((rows * 2, 5)) + A[:rows, 0] = xs + A[:rows, 2] = - ys + A[:rows, 1] = 1 + A[rows:, 2] = xs + A[rows:, 0] = ys + A[rows:, 3] = 1 + A[:rows, 4] = xd + A[rows:, 4] = yd + + _, _, V = np.linalg.svd(A) + + # solution is right singular vector that corresponds to smallest + # singular value + a0, a1, b0, b1 = - V[-1, :-1] / V[-1, -1] + + self._matrix = np.array([[a0, -b0, a1], + [b0, a0, b1], + [ 0, 0, 1]]) + + @property + def scale(self): + if math.cos(self.rotation) == 0: + # sin(self.rotation) == 1 + scale = self._matrix[0, 1] + else: + scale = self._matrix[0, 0] / math.cos(self.rotation) + return scale + + @property + def rotation(self): + return math.atan2(self._matrix[1, 0], self._matrix[1, 1]) + + @property + def translation(self): + return self._matrix[0:2, 2] + + +class PolynomialTransform(GeometricTransform): + """2D transformation of the form:: + + X = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) + Y = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + + Parameters + ---------- + params : (2, N) array, optional + Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So, + a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`. + + """ + + def __init__(self, params=None): + if params is None: + # default to transformation which preserves original coordinates + params = np.array([[0, 1, 0], [0, 0, 1]]) + if params.shape[0] != 2: + raise ValueError("invalid shape of transformation parameters") + self._params = params + + def estimate(self, src, dst, order): + """Set the transformation matrix with the explicit transformation + parameters. + + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source and destination coordinates must match. + + The transformation is defined as:: + + X = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) + Y = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + + These equations can be transformed to the following form:: + + 0 = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) - X + 0 = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) - Y + + which exist for each set of corresponding points, so we have a set of + N * 2 equations. The coefficients appear linearly so we can write + A x = 0, where:: + + A = [[1 x y x**2 x*y y**2 ... 0 ... 0 -X] + [0 ... 0 1 x y x**2 x*y y**2 -Y] + ... + ... + ] + x.T = [a00 a10 a11 a20 a21 a22 ... ann + b00 b10 b11 b20 b21 b22 ... bnn c3] + + In case of total least-squares the solution of this homogeneous system + of equations is the right singular vector of A which corresponds to the + smallest singular value normed by the coefficient c3. + + Parameters + ---------- + src : (N, 2) array + Source coordinates. + dst : (N, 2) array + Destination coordinates. + order : int + Polynomial order (number of coefficients is order + 1). + + """ + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + # number of unknown polynomial coefficients + u = (order + 1) * (order + 2) + + A = np.zeros((rows * 2, u + 1)) + pidx = 0 + for j in range(order + 1): + for i in range(j + 1): + A[:rows, pidx] = xs ** (j - i) * ys ** i + A[rows:, pidx + u / 2] = xs ** (j - i) * ys ** i + pidx += 1 + + A[:rows, -1] = xd + A[rows:, -1] = yd + + _, _, V = np.linalg.svd(A) + + # solution is right singular vector that corresponds to smallest + # singular value + params = - V[-1, :-1] / V[-1, -1] + + self._params = params.reshape((2, u / 2)) + + def __call__(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : (N, 2) array + source coordinates + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + x = coords[:, 0] + y = coords[:, 1] + u = len(self._params.ravel()) + # number of coefficients -> u = (order + 1) * (order + 2) + order = int((- 3 + math.sqrt(9 - 4 * (2 - u))) / 2) + dst = np.zeros(coords.shape) + + pidx = 0 + for j in range(order + 1): + for i in range(j + 1): + dst[:, 0] += self._params[0, pidx] * x ** (j - i) * y ** i + dst[:, 1] += self._params[1, pidx] * x ** (j - i) * y ** i + pidx += 1 + + return dst + + def inverse(self, coords): + raise Exception( + 'There is no explicit way to do the inverse polynomial ' + 'transformation. Instead, estimate the inverse transformation ' + 'parameters by exchanging source and destination coordinates,' + 'then apply the forward transformation.') + + +TRANSFORMS = { + 'similarity': SimilarityTransform, + 'affine': AffineTransform, + 'piecewise-affine': PiecewiseAffineTransform, + 'projective': ProjectiveTransform, + 'polynomial': PolynomialTransform, +} +HOMOGRAPHY_TRANSFORMS = ( + SimilarityTransform, + AffineTransform, + ProjectiveTransform +) + + +def estimate_transform(ttype, src, dst, **kwargs): + """Estimate 2D geometric transformation parameters. + + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source and destination coordinates must match. + + Parameters + ---------- + ttype : {'similarity', 'affine', 'piecewise-affine', 'projective', \ + 'polynomial'} + Type of transform. + kwargs : array or int + Function parameters (src, dst, n, angle):: + + NAME / TTYPE FUNCTION PARAMETERS + 'similarity' `src, `dst` + 'affine' `src, `dst` + 'piecewise-affine' `src, `dst` + 'projective' `src, `dst` + 'polynomial' `src, `dst`, `order` (polynomial order) + + Also see examples below. + + Returns + ------- + tform : :class:`GeometricTransform` + Transform object containing the transformation parameters and providing + access to forward and inverse transformation functions. + + Examples + -------- + >>> import numpy as np + >>> from skimage import transform as tf + + >>> # estimate transformation parameters + >>> src = np.array([0, 0, 10, 10]).reshape((2, 2)) + >>> dst = np.array([12, 14, 1, -20]).reshape((2, 2)) + + >>> tform = tf.estimate_transform('similarity', src, dst) + + >>> tform.inverse(tform(src)) # == src + + >>> # warp image using the estimated transformation + >>> from skimage import data + >>> image = data.camera() + + >>> warp(image, inverse_map=tform.inverse) + + >>> # create transformation with explicit parameters + >>> tform2 = tf.SimilarityTransform(scale=1.1, rotation=1, + ... translation=(10, 20)) + + >>> # unite transformations, applied in order from left to right + >>> tform3 = tform + tform2 + >>> tform3(src) # == tform2(tform(src)) + + """ + ttype = ttype.lower() + if ttype not in TRANSFORMS: + raise ValueError('the transformation type \'%s\' is not' + 'implemented' % ttype) + + tform = TRANSFORMS[ttype]() + tform.estimate(src, dst, **kwargs) + + return tform + + +def matrix_transform(coords, matrix): + """Apply 2D matrix transform. + + Parameters + ---------- + coords : (N, 2) array + x, y coordinates to transform + matrix : (3, 3) array + Homogeneous transformation matrix. + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + return ProjectiveTransform(matrix)(coords) + + +def _stackcopy(a, b): + """Copy b into each color layer of a, such that:: + + a[:,:,0] = a[:,:,1] = ... = b + + Parameters + ---------- + a : (M, N) or (M, N, P) ndarray + Target array. + b : (M, N) + Source array. + + Notes + ----- + Color images are stored as an ``(M, N, 3)`` or ``(M, N, 4)`` arrays. + + """ + if a.ndim == 3: + a[:] = b[:, :, np.newaxis] + else: + a[:] = b + + +def warp_coords(coord_map, shape, dtype=np.float64): + """Build the source coordinates for the output pixels of an image warp. + + Parameters + ---------- + coord_map : callable like GeometricTransform.inverse + Return input coordinates for given output coordinates. + shape : tuple + Shape of output image ``(rows, cols[, bands])``. + dtype : np.dtype or string + dtype for return value (sane choices: float32 or float64). + + Returns + ------- + coords : (ndim, rows, cols[, bands]) array of dtype `dtype` + Coordinates for `scipy.ndimage.map_coordinates`, that will yield + an image of shape (orows, ocols, bands) by drawing from source + points according to the `coord_transform_fn`. + + Notes + ----- + This is a lower-level routine that produces the source coordinates used by + `warp()`. + + It is provided separately from `warp` to give additional flexibility to + users who would like, for example, to re-use a particular coordinate + mapping, to use specific dtypes at various points along the the + image-warping process, or to implement different post-processing logic + than `warp` performs after the call to `ndimage.map_coordinates`. + + + Examples + -------- + Produce a coordinate map that Shifts an image to the right: + + >>> from skimage import data + >>> from scipy.ndimage import map_coordinates + >>> + >>> def shift_right(xy): + ... xy[:, 0] -= 10 + ... return xy + >>> + >>> image = data.lena().astype(np.float32) + >>> coords = warp_coords(shift_right, image.shape) + >>> warped_image = map_coordinates(image, coords) + + """ + rows, cols = shape[0], shape[1] + coords_shape = [len(shape), rows, cols] + if len(shape) == 3: + coords_shape.append(shape[2]) + coords = np.empty(coords_shape, dtype=dtype) + + # Reshape grid coordinates into a (P, 2) array of (x, y) pairs + tf_coords = np.indices((cols, rows), dtype=dtype).reshape(2, -1).T + + # Map each (x, y) pair to the source image according to + # the user-provided mapping + tf_coords = coord_map(tf_coords) + + # Reshape back to a (2, M, N) coordinate grid + tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) + + # Place the y-coordinate mapping + _stackcopy(coords[1, ...], tf_coords[0, ...]) + + # Place the x-coordinate mapping + _stackcopy(coords[0, ...], tf_coords[1, ...]) + + if len(shape) == 3: + coords[2, ...] = range(shape[2]) + + return coords + + +def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, + mode='constant', cval=0., reverse_map=None): + """Warp an image according to a given coordinate transformation. + + Parameters + ---------- + image : 2-D array + Input image. + inverse_map : transformation object, callable ``xy = f(xy, **kwargs)`` + Inverse coordinate map. A function that transforms a (N, 2) array of + ``(x, y)`` coordinates in the *output image* into their corresponding + coordinates in the *source image* (e.g. a transformation object or its + inverse). + map_args : dict, optional + Keyword arguments passed to `inverse_map`. + output_shape : tuple (rows, cols) + Shape of the output image generated. + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : float + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Examples + -------- + Shift an image to the right: + + >>> from skimage import data + >>> image = data.camera() + >>> + >>> def shift_right(xy): + ... xy[:, 0] -= 10 + ... return xy + >>> + >>> warp(image, shift_right) + + """ + # Backward API compatibility + if reverse_map is not None: + inverse_map = reverse_map + + if image.ndim < 2: + raise ValueError("Input must have more than 1 dimension.") + + orig_ndim = image.ndim + image = np.atleast_3d(img_as_float(image)) + ishape = np.array(image.shape) + bands = ishape[2] + + out = None + + # use fast Cython version for specific interpolation orders + if order in range(4) and not map_args: + matrix = None + if inverse_map in HOMOGRAPHY_TRANSFORMS: + matrix = inverse_map._matrix + elif hasattr(inverse_map, '__name__') \ + and inverse_map.__name__ == 'inverse' \ + and inverse_map.im_class in HOMOGRAPHY_TRANSFORMS: + matrix = np.linalg.inv(inverse_map.im_self._matrix) + if matrix is not None: + # transform all bands + dims = [] + for dim in range(image.shape[2]): + dims.append(_warp_fast(image[..., dim], matrix, + output_shape=output_shape, + order=order, mode=mode, cval=cval)) + out = np.dstack(dims) + if orig_ndim == 2: + out = out[..., 0] + + if out is None: # use ndimage.map_coordinates + + if output_shape is None: + output_shape = ishape + + rows, cols = output_shape[:2] + + def coord_map(*args): + return inverse_map(*args, **map_args) + + coords = warp_coords(coord_map, (rows, cols, bands)) + + # Prefilter not necessary for order 1 interpolation + prefilter = order > 1 + out = ndimage.map_coordinates(image, coords, prefilter=prefilter, + mode=mode, order=order, cval=cval) + + # The spline filters sometimes return results outside [0, 1], + # so clip to ensure valid data + clipped = np.clip(out, 0, 1) + + if mode == 'constant' and not (0 <= cval <= 1): + clipped[out == cval] = cval + + if clipped.ndim == 3 and orig_ndim == 2: + # remove singleton dim introduced by atleast_3d + return clipped[..., 0] + else: + return clipped diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index b34b28e2..ef1cf700 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -2,27 +2,24 @@ cimport cython import numpy as np cimport numpy as np from random import randint +from libc.math cimport abs, fabs, sqrt, ceil, floor +from libc.stdlib cimport rand + + np.import_array() -cdef extern from "stdlib.h": - int rand() - -cdef extern from "math.h": - int abs(int) - double fabs(double) - double sqrt(double) - double ceil(double) - double floor(double) - -cdef double round(double val): - return floor(val + 0.5); cdef double PI_2 = 1.5707963267948966 cdef double NEG_PI_2 = -PI_2 + +cdef inline int round(double r): + return ((r + 0.5) if (r > 0.0) else (r - 0.5)) + + @cython.boundscheck(False) def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): - + if img.ndim != 2: raise ValueError('The input image must be 2D.') @@ -31,7 +28,7 @@ def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): cdef np.ndarray[ndim=1, dtype=np.double_t] stheta if theta is None: - theta = np.linspace(PI_2, NEG_PI_2, 180) + theta = np.linspace(PI_2, NEG_PI_2, 180) ctheta = np.cos(theta) stheta = np.sin(theta) @@ -39,14 +36,14 @@ def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): # compute the bins and allocate the accumulator array cdef np.ndarray[ndim=2, dtype=np.uint64_t] accum cdef np.ndarray[ndim=1, dtype=np.double_t] bins - cdef int max_distance, offset + cdef int max_distance, offset - max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + + max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + img.shape[1] * img.shape[1]))) accum = np.zeros((max_distance, theta.shape[0]), dtype=np.uint64) bins = np.linspace(-max_distance / 2.0, max_distance / 2.0, max_distance) offset = max_distance / 2 - + # compute the nonzero indexes cdef np.ndarray[ndim=1, dtype=np.npy_intp] x_idxs, y_idxs y_idxs, x_idxs = np.PyArray_Nonzero(img) @@ -58,7 +55,7 @@ def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): nthetas = theta.shape[0] for i in range(nidxs): x = x_idxs[i] - y = y_idxs[i] + y = y_idxs[i] for j in range(nthetas): accum_idx = round((ctheta[j] * x + stheta[j] * y)) + offset accum[accum_idx, j] += 1 @@ -94,7 +91,7 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ # maximum line number cutoff cdef int lines_max = 2 ** 15 cdef int xflag, x0, y0, dx0, dy0, dx, dy, gap, x1, y1, good_line, count - max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + + max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + img.shape[1] * img.shape[1]))) accum = np.zeros((max_distance, theta.shape[0]), dtype=np.int64) offset = max_distance / 2 @@ -114,11 +111,11 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ # select random non-zero point count = len(points) if count == 0: - break + break index = rand() % (count) x = points[index][0] y = points[index][1] - del points[index] + del points[index] # if previously eliminated, skip if not mask[y, x]: continue @@ -147,7 +144,7 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ dx0 = 1 else: dx0 = -1 - dy0 = round(b * (1 << shift) / fabs(a)) + dy0 = round(b * (1 << shift) / fabs(a)) y0 = (y0 << shift) + (1 << (shift - 1)) else: if b > 0: @@ -156,7 +153,7 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ dy0 = -1 dx0 = round(a * (1 << shift) / fabs(b)) x0 = (x0 << shift) + (1 << (shift - 1)) - + # pass 1: walk the line, merging lines less than specified gap length for k in range(2): gap = 0 @@ -208,9 +205,9 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ x1 = px >> shift y1 = py # if non-zero point found, continue the line - if mask[y1, x1]: - if good_line: - accum_idx = round((ctheta[j] * x1 + stheta[j] * y1)) + offset + if mask[y1, x1]: + if good_line: + accum_idx = round((ctheta[j] * x1 + stheta[j] * y1)) + offset accum[accum_idx, max_theta] -= 1 mask[y1, x1] = 0 # exit when the point is the line end diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx deleted file mode 100644 index 734d6bff..00000000 --- a/skimage/transform/_project.pyx +++ /dev/null @@ -1,209 +0,0 @@ -#cython: cdivison=True boundscheck=False - -__all__ = ['homography'] - -cimport cython -cimport numpy as np - -import numpy as np -import cython - -from cython.operator import dereference - -np.import_array() - -cdef extern from "math.h": - double floor(double) - double fmod(double, double) - -cdef double get_pixel(double *image, int rows, int cols, - int r, int c, char mode, double cval=0): - """Get a pixel from the image, taking wrapping mode into consideration. - - Parameters - ---------- - image : *double - Input image. - rows, cols : int - Dimensions of image. - r, c : int - Position at which to get the pixel. - mode : {'C', 'W', 'M'} - Wrapping mode. Constant, Wrap or Mirror. - cval : double - Constant value to use for mode constant. - - """ - if mode == 'C': - if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): - return cval - else: - return image[r * cols + c] - else: - return image[coord_map(rows, r, mode) * cols + - coord_map(cols, c, mode)] - -cdef int coord_map(int dim, int coord, char mode): - """ - Wrap a coordinate, according to a given dimension and mode. - - Parameters - ---------- - dim : int - Maximum coordinate. - coord : int - Coord provided by user. May be < 0 or > dim. - mode : {'W', 'M'} - Whether to wrap or mirror the coordinate if it - falls outside [0, dim). - - """ - dim = dim - 1 - if mode == 'M': # mirror - if (coord < 0): - # How many times times does the coordinate wrap? - if ((-coord / dim) % 2 != 0): - return dim - (-coord % dim) - else: - return (-coord % dim) - elif (coord > dim): - if ((coord / dim) % 2 != 0): - return (dim - (coord % dim)) - else: - return (coord % dim) - elif mode == 'W': # wrap - if (coord < 0): - return (dim - (-coord % dim)) - elif (coord > dim): - return (coord % dim) - - return coord - -cdef tf(double x, double y, double* H, double *x_, double *y_): - """Apply a homography to a coordinate. - - Parameters - ---------- - x, y : double - Input coordinate. - H : (3,3) *double - Transformation matrix. - x_, y_ : *double - Output coordinate. - - """ - cdef double xx, yy, zz - - xx = H[0] * x + H[1] * y + H[2] - yy = H[3] * x + H[4] * y + H[5] - zz = H[6] * x + H[7] * y + H[8] - - xx = xx / zz - yy = yy / zz - - x_[0] = xx - y_[0] = yy - -@cython.boundscheck(False) -def homography(np.ndarray image, np.ndarray H, output_shape=None, - mode='constant', double cval=0): - """ - Projective transformation (homography). - - Perform a projective transformation (homography) of a - floating point image, using bi-linear interpolation. - - For each pixel, given its homogeneous coordinate :math:`\mathbf{x} - = [x, y, 1]^T`, its target position is calculated by multiplying - with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. - E.g., to rotate by theta degrees clockwise, the matrix should be - - :: - - [[cos(theta) -sin(theta) 0] - [sin(theta) cos(theta) 0] - [0 0 1]] - - or, to translate x by 10 and y by 20, - - :: - - [[1 0 10] - [0 1 20] - [0 0 1 ]]. - - Parameters - ---------- - image : 2-D array - Input image. - H : array of shape ``(3, 3)`` - Transformation matrix H that defines the homography. - output_shape : tuple (rows, cols) - Shape of the output image generated. - order : int - Order of splines used in interpolation. - mode : {'constant', 'mirror', 'wrap'} - How to handle values outside the image borders. - cval : string - Used in conjunction with mode 'C' (constant), the value - outside the image boundaries. - - """ - - cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] img = \ - np.ascontiguousarray(image, dtype=np.double) - cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] M = \ - np.ascontiguousarray(np.linalg.inv(H)) - - if mode not in ('constant', 'wrap', 'mirror'): - raise ValueError("Invalid mode specified. Please use " - "`constant`, `wrap` or `mirror`.") - if mode == 'constant': - mode_c = ord('C') - elif mode == 'wrap': - mode_c = ord('W') - elif mode == 'mirror': - mode_c = ord('M') - - cdef int out_r, out_c, columns, rows - if output_shape is None: - out_r = img.shape[0] - out_c = img.shape[1] - else: - out_r = output_shape[0] - out_c = output_shape[1] - - rows = img.shape[0] - columns = img.shape[1] - - cdef np.ndarray[dtype=np.double_t, ndim=2] out = \ - np.zeros((out_r, out_c), dtype=np.double) - - cdef int tfr, tfc, r_int, c_int - cdef double y0, y1, y2, y3 - cdef double r, c, z, t, u - - for tfr in range(out_r): - for tfc in range(out_c): - tf(tfc, tfr, M.data, &c, &r) - r_int = floor(r) - c_int = floor(c) - - t = r - r_int - u = c - c_int - - y0 = get_pixel(img.data, rows, columns, - r_int, c_int, mode_c) - y1 = get_pixel(img.data, rows, columns, - r_int + 1, c_int, mode_c) - y2 = get_pixel(img.data, rows, columns, - r_int + 1, c_int + 1, mode_c) - y3 = get_pixel(img.data, rows, columns, - r_int, c_int + 1, mode_c) - - out[tfr, tfc] = \ - (1 - t) * (1 - u) * y0 + \ - t * (1 - u) * y1 + \ - t * u * y2 + (1 - t) * u * y3; - - return out diff --git a/skimage/transform/_warp.py b/skimage/transform/_warp.py deleted file mode 100644 index 6477c988..00000000 --- a/skimage/transform/_warp.py +++ /dev/null @@ -1,113 +0,0 @@ -__all__ = ['warp'] - -import numpy as np -from scipy import ndimage -from skimage.util import img_as_float - - -def _stackcopy(a, b): - """Copy b into each color layer of a, such that:: - - a[:,:,0] = a[:,:,1] = ... = b - - Parameters - ---------- - a : (M, N) or (M, N, P) ndarray - Target array. - b : (M, N) - Source array. - - Notes - ----- - Color images are stored as an ``MxNx3`` or ``MxNx4`` arrays. - - """ - if a.ndim == 3: - a[:] = b[:, :, np.newaxis] - else: - a[:] = b - - -def warp(image, reverse_map, map_args={}, - output_shape=None, order=1, mode='constant', cval=0.): - """Warp an image according to a given coordinate transformation. - - Parameters - ---------- - image : 2-D array - Input image. - reverse_map : callable xy = f(xy, **kwargs) - Reverse coordinate map. A function that transforms a Px2 array of - ``(x, y)`` coordinates in the *output image* into their corresponding - coordinates in the *source image*. Also see examples below. - map_args : dict, optional - Keyword arguments passed to `reverse_map`. - output_shape : tuple (rows, cols) - Shape of the output image generated. - order : int - Order of splines used in interpolation. See - `scipy.ndimage.map_coordinates` for detail. - mode : string - How to handle values outside the image borders. See - `scipy.ndimage.map_coordinates` for detail. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Examples - -------- - Shift an image to the right: - - >>> from skimage import data - >>> image = data.camera() - >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 - ... return xy - >>> - >>> warp(image, shift_right) - - """ - if image.ndim < 2: - raise ValueError("Input must have more than 1 dimension.") - - image = np.atleast_3d(img_as_float(image)) - ishape = np.array(image.shape) - bands = ishape[2] - - if output_shape is None: - output_shape = ishape - - coords = np.empty(np.r_[3, output_shape], dtype=float) - - ## Construct transformed coordinates - - rows, cols = output_shape[:2] - - # Reshape grid coordinates into a (P, 2) array of (x, y) pairs - tf_coords = np.indices((cols, rows), dtype=float).reshape(2, -1).T - - # Map each (x, y) pair to the source image according to - # the user-provided mapping - tf_coords = reverse_map(tf_coords, **map_args) - - # Reshape back to a (2, M, N) coordinate grid - tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) - - # Place the y-coordinate mapping - _stackcopy(coords[1, ...], tf_coords[0, ...]) - - # Place the x-coordinate mapping - _stackcopy(coords[0, ...], tf_coords[1, ...]) - - # colour-coordinate mapping - coords[2, ...] = range(bands) - - # Prefilter not necessary for order 1 interpolation - prefilter = order > 1 - mapped = ndimage.map_coordinates(image, coords, prefilter=prefilter, - mode=mode, order=order, cval=cval) - - # The spline filters sometimes return results outside [0, 1], - # so clip to ensure valid data - return np.clip(mapped.squeeze(), 0, 1) diff --git a/skimage/transform/_warp_zoo.py b/skimage/transform/_warp_zoo.py deleted file mode 100644 index 6fd92c44..00000000 --- a/skimage/transform/_warp_zoo.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import division -import numpy as np - -from ._warp import warp - - -def _swirl_mapping(xy, center, rotation, strength, radius): - x, y = xy.T - x0, y0 = center - rho = np.sqrt((x - x0)**2 + (y - y0)**2) - - # Ensure that the transformation decays to approximately 1/1000-th - # within the specified radius. - radius = radius / 5 * np.log(2) - - theta = rotation + strength * \ - np.exp(-rho / radius) + \ - np.arctan2(y - y0, x - x0) - - xy[..., 0] = x0 + rho * np.cos(theta) - xy[..., 1] = y0 + rho * np.sin(theta) - - return xy - -def swirl(image, center=None, strength=1, radius=100, rotation=0, - output_shape=None, order=1, mode='constant', cval=0): - """Perform a swirl transformation. - - Parameters - ---------- - image : ndarray - Input image. - center : (x,y) tuple or (2,) ndarray - Center coordinate of transformation. - strength : float - The amount of swirling applied. - radius : float - The extent of the swirl in pixels. The effect dies out - rapidly beyond `radius`. - rotation : float - Additional rotation applied to the image. - - Returns - ------- - swirled : ndarray - Swirled version of the input. - - Other parameters - ---------------- - output_shape : tuple or ndarray - Size of the generated output image. - order : int - Order of splines used in interpolation. See - `scipy.ndimage.map_coordinates` for detail. - mode : string - How to handle values outside the image borders. See - `scipy.ndimage.map_coordinates` for detail. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - """ - - if center is None: - center = np.array(image.shape)[:2] / 2 - - warp_args = {'center': center, - 'rotation': rotation, - 'strength': strength, - 'radius': radius} - - return warp(image, _swirl_mapping, map_args=warp_args, - output_shape=output_shape, - order=order, mode=mode, cval=cval) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py new file mode 100644 index 00000000..72f90050 --- /dev/null +++ b/skimage/transform/_warps.py @@ -0,0 +1,282 @@ +import numpy as np +from ._geometric import (warp, SimilarityTransform, AffineTransform, + ProjectiveTransform) + + +def resize(image, output_shape, order=1, mode='constant', cval=0.): + """Resize image. + + Parameters + ---------- + image : ndarray + Input image. + output_shape : tuple or ndarray + Size of the generated output image `(rows, cols)`. + + Returns + ------- + resized : ndarray + Resized version of the input. + + Other parameters + ---------------- + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + """ + + rows, cols = output_shape + orig_rows, orig_cols = image.shape[0], image.shape[1] + + rscale = float(orig_rows) / rows + cscale = float(orig_cols) / cols + + # 3 control points necessary to estimate exact AffineTransform + src_corners = np.array([[1, 1], [1, rows], [cols, rows]]) - 1 + dst_corners = np.zeros(src_corners.shape, dtype=np.double) + # take into account that 0th pixel is at position (0.5, 0.5) + dst_corners[:, 0] = cscale * (src_corners[:, 0] + 0.5) - 0.5 + dst_corners[:, 1] = rscale * (src_corners[:, 1] + 0.5) - 0.5 + + tform = AffineTransform() + tform.estimate(src_corners, dst_corners) + + return warp(image, tform, output_shape=output_shape, order=order, + mode=mode, cval=cval) + + +def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): + """Rotate image by a certain angle around its center. + + Parameters + ---------- + image : ndarray + Input image. + angle : float + Rotation angle in degrees in counter-clockwise direction. + resize: bool, optional + Determine whether the shape of the output image will be automatically + calculated, so the complete rotated image exactly fits. Default is + False. + + Returns + ------- + rotated : ndarray + Rotated version of the input. + + Other parameters + ---------------- + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + """ + + rows, cols = image.shape[0], image.shape[1] + + # rotation around center + translation = np.array((cols, rows)) / 2. - 0.5 + tform1 = SimilarityTransform(translation=-translation) + tform2 = SimilarityTransform(rotation=np.deg2rad(angle)) + tform3 = SimilarityTransform(translation=translation) + tform = tform1 + tform2 + tform3 + + output_shape = None + if not resize: + # determine shape of output image + corners = np.array([[1, 1], [1, rows], [cols, rows], [cols, 1]]) + corners = tform(corners - 1) + minc = corners[:, 0].min() + minr = corners[:, 1].min() + maxc = corners[:, 0].max() + maxr = corners[:, 1].max() + out_rows = maxr - minr + 1 + out_cols = maxc - minc + 1 + output_shape = np.ceil((out_rows, out_cols)) + + # fit output image in new shape + translation = ((cols - out_cols) / 2., (rows - out_rows) / 2.) + tform4 = SimilarityTransform(translation=translation) + tform = tform4 + tform + + return warp(image, tform, output_shape=output_shape, order=order, + mode=mode, cval=cval) + + +def _swirl_mapping(xy, center, rotation, strength, radius): + x, y = xy.T + x0, y0 = center + rho = np.sqrt((x - x0) ** 2 + (y - y0) ** 2) + + # Ensure that the transformation decays to approximately 1/1000-th + # within the specified radius. + radius = radius / 5 * np.log(2) + + theta = rotation + strength * \ + np.exp(-rho / radius) + \ + np.arctan2(y - y0, x - x0) + + xy[..., 0] = x0 + rho * np.cos(theta) + xy[..., 1] = y0 + rho * np.sin(theta) + + return xy + + +def swirl(image, center=None, strength=1, radius=100, rotation=0, + output_shape=None, order=1, mode='constant', cval=0): + """Perform a swirl transformation. + + Parameters + ---------- + image : ndarray + Input image. + center : (x,y) tuple or (2,) ndarray + Center coordinate of transformation. + strength : float + The amount of swirling applied. + radius : float + The extent of the swirl in pixels. The effect dies out + rapidly beyond `radius`. + rotation : float + Additional rotation applied to the image. + + Returns + ------- + swirled : ndarray + Swirled version of the input. + + Other parameters + ---------------- + output_shape : tuple or ndarray + Size of the generated output image. + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + """ + + if center is None: + center = np.array(image.shape)[:2] / 2 + + warp_args = {'center': center, + 'rotation': rotation, + 'strength': strength, + 'radius': radius} + + return warp(image, _swirl_mapping, map_args=warp_args, + output_shape=output_shape, + order=order, mode=mode, cval=cval) + + +def homography(image, H, output_shape=None, order=1, + mode='constant', cval=0.): + """ + .. note:: Deprecated in skimage 0.7 + `homography` will be removed in skimage 0.8, it is replaced by + `warp` because the latter provides the same functionality:: + + warp(image, ProjectiveTransform(H)) + + Perform a projective transformation (homography) on an image. + + For each pixel, given its homogeneous coordinate :math:`\mathbf{x} + = [x, y, 1]^T`, its target position is calculated by multiplying + with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. + E.g., to rotate by theta degrees clockwise, the matrix should be + + :: + + [[cos(theta) -sin(theta) 0] + [sin(theta) cos(theta) 0] + [0 0 1]] + + or, to translate x by 10 and y by 20, + + :: + + [[1 0 10] + [0 1 20] + [0 0 1 ]]. + + Parameters + ---------- + image : 2-D array + Input image. + H : array of shape ``(3, 3)`` + Transformation matrix H that defines the homography. + output_shape : tuple (rows, cols) + Shape of the output image generated. + order : int + Order of splines used in interpolation. + mode : string + How to handle values outside the image borders. Passed as-is + to ndimage. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Examples + -------- + >>> # rotate by 90 degrees around origin and shift down by 2 + >>> x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 + >>> x + array([[1, 2, 3], + [4, 5, 6], + [7, 8, 9]], dtype=uint8) + >>> theta = -np.pi/2 + >>> M = np.array([[np.cos(theta),-np.sin(theta),0], + ... [np.sin(theta), np.cos(theta),2], + ... [0, 0, 1]]) + >>> x90 = homography(x, M, order=1) + >>> x90 + array([[3, 6, 9], + [2, 5, 8], + [1, 4, 7]], dtype=uint8) + >>> # translate right by 2 and down by 1 + >>> y = np.zeros((5,5), dtype=np.uint8) + >>> y[1, 1] = 255 + >>> y + array([[ 0, 0, 0, 0, 0], + [ 0, 255, 0, 0, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0]], dtype=uint8) + >>> M = np.array([[ 1., 0., 2.], + ... [ 0., 1., 1.], + ... [ 0., 0., 1.]]) + >>> y21 = homography(y, M, order=1) + >>> y21 + array([[ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 255, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0]], dtype=uint8) + + """ + import warnings + warnings.warn('the homography function is deprecated; ' + 'use the `warp` and `ProjectiveTransform` class instead', + category=DeprecationWarning) + + tform = ProjectiveTransform(H) + return warp(image, inverse_map=tform.inverse, output_shape=output_shape, + order=order, mode=mode, cval=cval) diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx new file mode 100644 index 00000000..ce400ed6 --- /dev/null +++ b/skimage/transform/_warps_cy.pyx @@ -0,0 +1,129 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as np +import numpy as np +from skimage._shared.interpolation cimport (nearest_neighbour_interpolation, + bilinear_interpolation, + biquadratic_interpolation, + bicubic_interpolation) + + +cdef inline void _matrix_transform(double x, double y, double* H, double *x_, + double *y_): + """Apply a homography to a coordinate. + + Parameters + ---------- + x, y : double + Input coordinate. + H : (3,3) *double + Transformation matrix. + x_, y_ : *double + Output coordinate. + + """ + cdef double xx, yy, zz + + xx = H[0] * x + H[1] * y + H[2] + yy = H[3] * x + H[4] * y + H[5] + zz = H[6] * x + H[7] * y + H[8] + + x_[0] = xx / zz + y_[0] = yy / zz + + +def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, + mode='constant', double cval=0): + """Projective transformation (homography). + + Perform a projective transformation (homography) of a + floating point image, using bi-linear interpolation. + + For each pixel, given its homogeneous coordinate :math:`\mathbf{x} + = [x, y, 1]^T`, its target position is calculated by multiplying + with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. + E.g., to rotate by theta degrees clockwise, the matrix should be + + :: + + [[cos(theta) -sin(theta) 0] + [sin(theta) cos(theta) 0] + [0 0 1]] + + or, to translate x by 10 and y by 20, + + :: + + [[1 0 10] + [0 1 20] + [0 0 1 ]]. + + Parameters + ---------- + image : 2-D array + Input image. + H : array of shape ``(3, 3)`` + Transformation matrix H that defines the homography. + output_shape : tuple (rows, cols) + Shape of the output image generated. + order : {0, 1} + Order of interpolation:: + * 0: Nearest-neighbour interpolation. + * 1: Bilinear interpolation (default). + * 2: Biquadratic interpolation (default). + * 3: Bicubic interpolation. + mode : {'constant', 'reflect', 'wrap'} + How to handle values outside the image borders. + cval : string + Used in conjunction with mode 'C' (constant), the value + outside the image boundaries. + + """ + + cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] img = \ + np.ascontiguousarray(image, dtype=np.double) + cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] M = \ + np.ascontiguousarray(H) + + if mode not in ('constant', 'wrap', 'reflect', 'nearest'): + raise ValueError("Invalid mode specified. Please use " + "`constant`, `nearest`, `wrap` or `reflect`.") + cdef char mode_c = ord(mode[0].upper()) + + cdef int out_r, out_c + if output_shape is None: + out_r = img.shape[0] + out_c = img.shape[1] + else: + out_r = output_shape[0] + out_c = output_shape[1] + + cdef np.ndarray[dtype=np.double_t, ndim=2] out = \ + np.zeros((out_r, out_c), dtype=np.double) + + cdef int tfr, tfc + cdef double r, c + cdef int rows = img.shape[0] + cdef int cols = img.shape[1] + + cdef double (*interp_func)(double*, int, int, double, double, + char, double) + if order == 0: + interp_func = nearest_neighbour_interpolation + elif order == 1: + interp_func = bilinear_interpolation + elif order == 2: + interp_func = biquadratic_interpolation + elif order == 3: + interp_func = bicubic_interpolation + + for tfr in range(out_r): + for tfc in range(out_c): + _matrix_transform(tfc, tfr, M.data, &c, &r) + out[tfr, tfc] = interp_func(img.data, rows, cols, r, c, + mode_c, cval) + + return out diff --git a/skimage/transform/finite_radon_transform.py b/skimage/transform/finite_radon_transform.py index 03dd0152..c107546f 100644 --- a/skimage/transform/finite_radon_transform.py +++ b/skimage/transform/finite_radon_transform.py @@ -9,6 +9,7 @@ __docformat__ = "restructuredtext en" import numpy as np from numpy import roll, newaxis + def frt2(a): """Compute the 2-dimensional finite radon transform (FRT) for an n x n integer array. @@ -45,14 +46,6 @@ def frt2(a): >>> f = frt2(img) - Plot the results: - - >>> import matplotlib.pyplot as plt - >>> plt.imshow(f, interpolation='nearest', cmap=plt.cm.gray) - >>> plt.xlabel('Angle') - >>> plt.ylabel('Translation') - >>> plt.show() - References ---------- .. [FRT] A. Kingston and I. Svalbe, "Projective transforms on periodic @@ -65,7 +58,7 @@ def frt2(a): ai = a.copy() n = ai.shape[0] - f = np.empty((n+1, n), np.uint32) + f = np.empty((n + 1, n), np.uint32) f[0] = ai.sum(axis=0) for m in range(1, n): # Roll the pth row of ai left by p places @@ -125,7 +118,7 @@ def ifrt2(a): and Electron Physics, 139 (2006) """ - if a.ndim != 2 or a.shape[0] != a.shape[1]+1: + if a.ndim != 2 or a.shape[0] != a.shape[1] + 1: raise ValueError("Input must be an (n+1) row x n column, 2-D array") ai = a.copy()[:-1] @@ -138,5 +131,5 @@ def ifrt2(a): ai[row] = roll(ai[row], row) f[m] = ai.sum(axis=0) f += a[-1][newaxis].T - f = (f - ai[0].sum())/n + f = (f - ai[0].sum()) / n return f diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 5bbe3b6c..4e3acd6e 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -3,7 +3,8 @@ __all__ = ['hough', 'probabilistic_hough'] from itertools import izip as zip import numpy as np -from ._hough_transform import _probabilistic_hough +from ._hough_transform import _probabilistic_hough + def _hough(img, theta=None): if img.ndim != 2: @@ -60,36 +61,37 @@ except ImportError: pass -def probabilistic_hough(img, threshold=10, line_length=50, line_gap=10, theta=None): - """Performs a progressive probabilistic line Hough transform and returns the detected lines. +def probabilistic_hough(img, threshold=10, line_length=50, line_gap=10, + theta=None): + """Return lines from a progressive probabilistic line Hough transform. Parameters ---------- img : (M, N) ndarray Input image with nonzero values representing edges. threshold : int - Threshold + Threshold line_length : int, optional (default 50) Minimum accepted length of detected lines. Increase the parameter to extract longer lines. line_gap : int, optional, (default 10) - Maximum gap between pixels to still form a line. + Maximum gap between pixels to still form a line. Increase the parameter to merge broken lines more aggresively. theta : 1D ndarray, dtype=double, optional, default (-pi/2 .. pi/2) Angles at which to compute the transform, in radians. - + Returns ------- lines : list - List of lines identified, lines in format ((x0, y0), (x1, y0)), indicating + List of lines identified, lines in format ((x0, y0), (x1, y0)), indicating line start and end. References ---------- - .. [1] C. Galamhos, J. Matas and J. Kittler,"Progressive probabilistic Hough - transform for line detection", in IEEE Computer Society Conference on - Computer Vision and Pattern Recognition, 1999. - """ + .. [1] C. Galamhos, J. Matas and J. Kittler, "Progressive probabilistic + Hough transform for line detection", in IEEE Computer Society + Conference on Computer Vision and Pattern Recognition, 1999. + """ return _probabilistic_hough(img, threshold, line_length, line_gap, theta) @@ -122,24 +124,14 @@ def hough(img, theta=None): >>> img[:, 65] = 1 >>> img[35:45, 35:50] = 1 >>> for i in range(90): - >>> img[i, i] = 1 + ... img[i, i] = 1 >>> img += np.random.random(img.shape) > 0.95 Apply the Hough transform: >>> out, angles, d = hough(img) - Plot the results: - - >>> import matplotlib.pyplot as plt - >>> plt.imshow(out, cmap=plt.cm.bone) - >>> plt.xlabel('Angle (degree)') - >>> plt.ylabel('Distance %d (pixel)' % d[0]) - >>> plt.show() - .. plot:: hough_tf.py """ return _hough(img, theta) - - diff --git a/skimage/transform/integral.py b/skimage/transform/integral.py index 440d10e6..c34b6a19 100644 --- a/skimage/transform/integral.py +++ b/skimage/transform/integral.py @@ -26,6 +26,7 @@ def integral_image(x): """ return x.cumsum(1).cumsum(0) + def integrate(ii, r0, c0, r1, c1): """Use an integral image to integrate over a given window. diff --git a/skimage/transform/project.py b/skimage/transform/project.py deleted file mode 100644 index 030e86a9..00000000 --- a/skimage/transform/project.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Image projection. - -""" - -import numpy as np -from scipy.ndimage import interpolation as ndii -from ._warp import _stackcopy - -__all__ = ['homography'] - -eps = np.finfo(float).eps - -def homography(image, H, output_shape=None, order=1, - mode='constant', cval=0.): - """Perform a projective transformation (homography) on an image. - - For each pixel, given its homogeneous coordinate :math:`\mathbf{x} - = [x, y, 1]^T`, its target position is calculated by multiplying - with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. - E.g., to rotate by theta degrees clockwise, the matrix should be - - :: - - [[cos(theta) -sin(theta) 0] - [sin(theta) cos(theta) 0] - [0 0 1]] - - or, to translate x by 10 and y by 20, - - :: - - [[1 0 10] - [0 1 20] - [0 0 1 ]]. - - Parameters - ---------- - image : 2-D array - Input image. - H : array of shape ``(3, 3)`` - Transformation matrix H that defines the homography. - output_shape : tuple (rows, cols) - Shape of the output image generated. - order : int - Order of splines used in interpolation. - mode : string - How to handle values outside the image borders. Passed as-is - to ndimage. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Examples - -------- - >>> # rotate by 90 degrees around origin and shift down by 2 - >>> x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 - >>> x - array([[1, 2, 3], - [4, 5, 6], - [7, 8, 9]], dtype=uint8) - >>> theta = -np.pi/2 - >>> M = np.array([[np.cos(theta),-np.sin(theta),0], - ... [np.sin(theta), np.cos(theta),2], - ... [0, 0, 1]]) - >>> x90 = homography(x, M, order=1) - >>> x90 - array([[3, 6, 9], - [2, 5, 8], - [1, 4, 7]], dtype=uint8) - >>> # translate right by 2 and down by 1 - >>> y = np.zeros((5,5), dtype=np.uint8) - >>> y[1, 1] = 255 - >>> y - array([[ 0, 0, 0, 0, 0], - [ 0, 255, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0]], dtype=uint8) - >>> M = np.array([[ 1., 0., 2.], - ... [ 0., 1., 1.], - ... [ 0., 0., 1.]]) - >>> y21 = homography(y, M, order=1) - >>> y21 - array([[ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 255, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0]], dtype=uint8) - - """ - if image.ndim < 2: - raise ValueError("Input must have more than 1 dimension.") - - image = np.atleast_3d(image) - ishape = np.array(image.shape) - bands = ishape[2] - - if output_shape is None: - output_shape = ishape - - coords = np.empty(np.r_[3, output_shape], dtype=float) - - # TODO: Refactor this method to use transform.warp instead. - - # Construct transformed coordinates - rows, cols = output_shape[:2] - rows, cols = np.mgrid[:rows, :cols] - tf_coords = np.empty(shape=cols.shape, - dtype=[('cols', float), - ('rows', float), - ('z', float)]) - tf_coords['cols'], tf_coords['rows'] = cols, rows - tf_coords['z'] = 1 - tf_coords = tf_coords.view((float, 3)) - - tf_coords = np.dot(tf_coords, np.linalg.inv(H).transpose()) - tf_coords[np.absolute(tf_coords) < eps] = 0. - - # normalize coordinates - tf_coords[..., :2] /= tf_coords[..., 2, np.newaxis] - - # y-coordinate mapping - _stackcopy(coords[0,...], tf_coords[...,1]) - - # x-coordinate mapping - _stackcopy(coords[1,...], tf_coords[...,0]) - - # colour-coordinate mapping - coords[2,...] = range(bands) - - # Prefilter not necessary for order 1 interpolation - prefilter = order > 1 - mapped = ndii.map_coordinates(image, coords, prefilter=prefilter, - mode=mode, order=order, cval=cval) - - return mapped.squeeze() diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py new file mode 100644 index 00000000..19001de8 --- /dev/null +++ b/skimage/transform/pyramids.py @@ -0,0 +1,302 @@ +import math +import numpy as np +from scipy import ndimage +from skimage.transform import resize +from skimage.util import img_as_float + + +def _smooth(image, sigma, mode, cval): + """Return image with each channel smoothed by the gaussian filter.""" + + smoothed = np.empty(image.shape, dtype=np.double) + + if image.ndim == 3: # apply gaussian filter to all dimensions independently + for dim in range(image.shape[2]): + ndimage.gaussian_filter(image[..., dim], sigma, + output=smoothed[..., dim], + mode=mode, cval=cval) + else: + ndimage.gaussian_filter(image, sigma, output=smoothed, + mode=mode, cval=cval) + + return smoothed + + +def _check_factor(factor): + if factor <= 1: + raise ValueError('scale factor must be greater than 1') + + +def pyramid_reduce(image, downscale=2, sigma=None, order=1, + mode='reflect', cval=0): + """Smooth and then downsample image. + + Parameters + ---------- + image : array + Input image. + downscale : float, optional + Downscale factor. + sigma : float, optional + Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the gaussian distribution. + order : int, optional + Order of splines used in interpolation of downsampling. See + `scipy.ndimage.map_coordinates` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. + + Returns + ------- + out : array + Smoothed and downsampled float image. + + References + ---------- + .. [1] http://web.mit.edu/persci/people/adelson/pub_pdfs/pyramid83.pdf + + """ + + _check_factor(downscale) + + image = img_as_float(image) + + rows = image.shape[0] + cols = image.shape[1] + out_rows = math.ceil(rows / float(downscale)) + out_cols = math.ceil(cols / float(downscale)) + + if sigma is None: + # automatically determine sigma which covers > 99% of distribution + sigma = 2 * downscale / 6.0 + + smoothed = _smooth(image, sigma, mode, cval) + out = resize(smoothed, (out_rows, out_cols), order=order, + mode=mode, cval=cval) + + return out + + +def pyramid_expand(image, upscale=2, sigma=None, order=1, + mode='reflect', cval=0): + """Upsample and then smooth image. + + Parameters + ---------- + image : array + Input image. + upscale : float, optional + Upscale factor. + sigma : float, optional + Sigma for gaussian filter. Default is `2 * upscale / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the gaussian distribution. + order : int, optional + Order of splines used in interpolation of upsampling. See + `scipy.ndimage.map_coordinates` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. + + Returns + ------- + out : array + Upsampled and smoothed float image. + + References + ---------- + .. [1] http://web.mit.edu/persci/people/adelson/pub_pdfs/pyramid83.pdf + + """ + + _check_factor(upscale) + + image = img_as_float(image) + + rows = image.shape[0] + cols = image.shape[1] + out_rows = math.ceil(upscale * rows) + out_cols = math.ceil(upscale * cols) + + if sigma is None: + # automatically determine sigma which covers > 99% of distribution + sigma = 2 * upscale / 6.0 + + resized = resize(image, (out_rows, out_cols), order=order, + mode=mode, cval=cval) + out = _smooth(resized, sigma, mode, cval) + + return out + + +def pyramid_gaussian(image, max_layer=-1, downscale=2, sigma=None, order=1, + mode='reflect', cval=0): + """Yield images of the gaussian pyramid formed by the input image. + + Recursively applies the `pyramid_reduce` function to the image, and yields + the downscaled images. + + Note that the first image of the pyramid will be the original, unscaled + image. The total number of images is `max_layer + 1`. In case all layers + are computed, the last image is either a one-pixel image or the image where + the reduction does not change its shape. + + Parameters + ---------- + image : array + Input image. + max_layer : int + Number of layers for the pyramid. 0th layer is the original image. + Default is -1 which builds all possible layers. + downscale : float, optional + Downscale factor. + sigma : float, optional + Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the gaussian distribution. + order : int, optional + Order of splines used in interpolation of downsampling. See + `scipy.ndimage.map_coordinates` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. + + Returns + ------- + pyramid : generator + Generator yielding pyramid layers as float images. + + References + ---------- + .. [1] http://web.mit.edu/persci/people/adelson/pub_pdfs/pyramid83.pdf + + """ + + _check_factor(downscale) + + # cast to float for consistent data type in pyramid + image = img_as_float(image) + + layer = 0 + rows = image.shape[0] + cols = image.shape[1] + + prev_layer_image = image + yield image + + # build downsampled images until max_layer is reached or downscale process + # does not change image size + while layer != max_layer: + layer += 1 + + layer_image = pyramid_reduce(prev_layer_image, downscale, sigma, order, + mode, cval) + + prev_rows = rows + prev_cols = cols + prev_layer_image = layer_image + rows = layer_image.shape[0] + cols = layer_image.shape[1] + + # no change to previous pyramid layer + if prev_rows == rows and prev_cols == cols: + break + + yield layer_image + + +def pyramid_laplacian(image, max_layer=-1, downscale=2, sigma=None, order=1, + mode='reflect', cval=0): + """Yield images of the laplacian pyramid formed by the input image. + + Each layer contains the difference between the downsampled and the + downsampled, smoothed image:: + + layer = resize(prev_layer) - smooth(resize(prev_layer)) + + Note that the first image of the pyramid will be the difference between the + original, unscaled image and its smoothed version. The total number of + images is `max_layer + 1`. In case all layers are computed, the last image + is either a one-pixel image or the image where the reduction does not + change its shape. + + Parameters + ---------- + image : array + Input image. + max_layer : int + Number of layers for the pyramid. 0th layer is the original image. + Default is -1 which builds all possible layers. + downscale : float, optional + Downscale factor. + sigma : float, optional + Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the gaussian distribution. + order : int, optional + Order of splines used in interpolation of downsampling. See + `scipy.ndimage.map_coordinates` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. + + Returns + ------- + pyramid : generator + Generator yielding pyramid layers as float images. + + References + ---------- + .. [1] http://web.mit.edu/persci/people/adelson/pub_pdfs/pyramid83.pdf + .. [2] http://sepwww.stanford.edu/~morgan/texturematch/paper_html/node3.html + + """ + + _check_factor(downscale) + + # cast to float for consistent data type in pyramid + image = img_as_float(image) + + if sigma is None: + # automatically determine sigma which covers > 99% of distribution + sigma = 2 * downscale / 6.0 + + layer = 0 + rows = image.shape[0] + cols = image.shape[1] + + smoothed_image = _smooth(image, sigma, mode, cval) + yield image - smoothed_image + + # build downsampled images until max_layer is reached or downscale process + # does not change image size + while layer != max_layer: + layer += 1 + + out_rows = math.ceil(rows / float(downscale)) + out_cols = math.ceil(cols / float(downscale)) + + resized_image = resize(smoothed_image, (out_rows, out_cols), + order=order, mode=mode, cval=cval) + smoothed_image = _smooth(resized_image, sigma, mode, cval) + + prev_rows = rows + prev_cols = cols + rows = resized_image.shape[0] + cols = resized_image.shape[1] + + # no change to previous pyramid layer + if prev_rows == rows and prev_cols == cols: + break + + yield resized_image - smoothed_image diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 5da31a72..0213b2bc 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -15,7 +15,7 @@ References: from __future__ import division import numpy as np from scipy.fftpack import fftshift, fft, ifft -from ._project import homography +from ._warps_cy import _warp_fast __all__ = ["radon", "iradon"] @@ -40,10 +40,11 @@ def radon(image, theta=None): """ if image.ndim != 2: raise ValueError('The input image must be 2-D') - if theta == None: + if theta is None: theta = np.arange(180) + height, width = image.shape - diagonal = np.sqrt(height ** 2 + width ** 2) + diagonal = np.sqrt(height**2 + width**2) heightpad = np.ceil(diagonal - height) widthpad = np.ceil(diagonal - width) padded_image = np.zeros((int(height + heightpad), @@ -66,7 +67,6 @@ def radon(image, theta=None): [0, 1, dh], [0, 0, 1]]) - def build_rotation(theta): T = -np.deg2rad(theta) @@ -77,10 +77,10 @@ def radon(image, theta=None): return shift1.dot(R).dot(shift0) for i in range(len(theta)): - rotated = homography(padded_image, - build_rotation(-theta[i])) + rotated = _warp_fast(padded_image, + np.linalg.inv(build_rotation(-theta[i]))) - out[:,i] = rotated.sum(0)[::-1] + out[:, i] = rotated.sum(0)[::-1] return out @@ -100,7 +100,7 @@ def iradon(radon_image, theta=None, output_size=None, the image corresponds to a projection along a different angle. theta : array_like, dtype=float, optional Reconstruction angles (in degrees). Default: m angles evenly spaced - between 0 and 180 (if the shape of `radon_image` is nxm) + between 0 and 180 (if the shape of `radon_image` is (N, M)). output_size : int Number of rows and columns in the reconstruction. filter : str, optional (default ramp) @@ -125,23 +125,30 @@ def iradon(radon_image, theta=None, output_size=None, """ if radon_image.ndim != 2: raise ValueError('The input image must be 2-D') - if theta == None: + + if theta is None: m, n = radon_image.shape theta = np.linspace(0, 180, n, endpoint=False) + else: + theta = np.asarray(theta) + + if len(theta) != radon_image.shape[1]: + raise ValueError("The given ``theta`` does not match the number of " + "projections in ``radon_image``.") + th = (np.pi / 180.0) * theta # if output size not specified, estimate from input radon image if not output_size: - output_size = int(np.floor(np.sqrt((radon_image.shape[0]) ** 2 / 2.0))) + output_size = int(np.floor(np.sqrt((radon_image.shape[0])**2 / 2.0))) n = radon_image.shape[0] img = radon_image.copy() # resize image to next power of two for fourier analysis # speeds up fourier and lessens artifacts - order = max(64., 2 ** np.ceil(np.log(2 * n) / np.log(2))) + order = max(64., 2**np.ceil(np.log(2 * n) / np.log(2))) # zero pad input image img.resize((order, img.shape[1])) # construct the fourier filter - freqs = np.zeros((order, 1)) f = fftshift(abs(np.mgrid[-1:1:2 / order])).reshape(-1, 1) w = 2 * np.pi * f @@ -151,20 +158,22 @@ def iradon(radon_image, theta=None, output_size=None, elif filter == "shepp-logan": f[1:] = f[1:] * np.sin(w[1:] / 2) / (w[1:] / 2) elif filter == "cosine": - f[1:] = f[1:] * np.cos(w[1:] / 2) + f[1:] = f[1:] * np.cos(w[1:] / 2) elif filter == "hamming": - f[1:] = f[1:] * (0.54 + 0.46 * np.cos(w[1:])) + f[1:] = f[1:] * (0.54 + 0.46 * np.cos(w[1:])) elif filter == "hann": - f[1:] = f[1:] * (1 + np.cos(w[1:])) / 2 + f[1:] = f[1:] * (1 + np.cos(w[1:])) / 2 elif filter == None: f[1:] = 1 else: raise ValueError("Unknown filter: %s" % filter) filter_ft = np.tile(f, (1, len(theta))) + # apply filter in fourier domain projection = fft(img, axis=0) * filter_ft radon_filtered = np.real(ifft(projection, axis=0)) + # resize filtered image back to original size radon_filtered = radon_filtered[:radon_image.shape[0], :] reconstructed = np.zeros((output_size, output_size)) @@ -182,15 +191,17 @@ def iradon(radon_image, theta=None, output_size=None, k = np.round(mid_index + xpr * np.sin(th[i]) - ypr * np.cos(th[i])) reconstructed += radon_filtered[ ((((k > 0) & (k < n)) * k) - 1).astype(np.int), i] + elif interpolation == "linear": for i in range(len(theta)): - t = xpr*np.sin(th[i]) - ypr*np.cos(th[i]) - a = np.floor(t) - b = mid_index + a - b0 = ((((b + 1 > 0) & (b + 1 < n)) * (b + 1)) - 1).astype(np.int) - b1 = ((((b > 0) & (b < n)) * b) - 1).astype(np.int) - reconstructed += (t - a) * radon_filtered[b0, i] + \ - (a - t + 1) * radon_filtered[b1, i] + t = xpr * np.sin(th[i]) - ypr * np.cos(th[i]) + a = np.floor(t) + b = mid_index + a + b0 = ((((b + 1 > 0) & (b + 1 < n)) * (b + 1)) - 1).astype(np.int) + b1 = ((((b > 0) & (b < n)) * b) - 1).astype(np.int) + reconstructed += (t - a) * radon_filtered[b0, i] + \ + (a - t + 1) * radon_filtered[b1, i] + else: raise ValueError("Unknown interpolation: %s" % interpolation) diff --git a/skimage/transform/setup.py b/skimage/transform/setup.py index 274dad94..b0093d87 100644 --- a/skimage/transform/setup.py +++ b/skimage/transform/setup.py @@ -6,6 +6,7 @@ from skimage._build import cython base_path = os.path.abspath(os.path.dirname(__file__)) + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs @@ -13,24 +14,23 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['_hough_transform.pyx'], working_path=base_path) - cython(['_project.pyx'], working_path=base_path) + cython(['_warps_cy.pyx'], working_path=base_path) config.add_extension('_hough_transform', sources=['_hough_transform.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_project', sources=['_project.c'], - include_dirs=[get_numpy_include_dirs()]) - + config.add_extension('_warps_cy', sources=['_warps_cy.c'], + include_dirs=[get_numpy_include_dirs(), '../_shared']) return config if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'Scikits-image Developers', - author = 'Scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Transforms', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='scikit-image Developers', + author='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Transforms', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) diff --git a/skimage/transform/tests/test_finite_radon_transform.py b/skimage/transform/tests/test_finite_radon_transform.py index d8504477..b5fcf43a 100644 --- a/skimage/transform/tests/test_finite_radon_transform.py +++ b/skimage/transform/tests/test_finite_radon_transform.py @@ -3,6 +3,7 @@ from numpy.testing import * from skimage.transform import * + def test_frt(): SIZE = 59 try: @@ -15,4 +16,4 @@ def test_frt(): L = np.tri(SIZE, dtype=np.int32) + np.tri(SIZE, dtype=np.int32)[::-1] f = frt2(L) fi = ifrt2(f) - assert len(np.nonzero(L-fi)[0]) == 0 + assert len(np.nonzero(L - fi)[0]) == 0 diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py new file mode 100644 index 00000000..d3a75f7b --- /dev/null +++ b/skimage/transform/tests/test_geometric.py @@ -0,0 +1,174 @@ +import numpy as np +from numpy.testing import assert_equal, assert_array_almost_equal +from skimage.transform._geometric import _stackcopy +from skimage.transform import (estimate_transform, + SimilarityTransform, AffineTransform, + ProjectiveTransform, PolynomialTransform, + PiecewiseAffineTransform) + + +SRC = np.array([ + [-12.3705, -10.5075], + [-10.7865, 15.4305], + [8.6985, 10.8675], + [11.4975, -9.5715], + [7.8435, 7.4835], + [-5.3325, 6.5025], + [6.7905, -6.3765], + [-6.1695, -0.8235], +]) +DST = np.array([ + [0, 0], + [0, 5800], + [4900, 5800], + [4900, 0], + [4479, 4580], + [1176, 3660], + [3754, 790], + [1024, 1931], +]) + + +def test_stackcopy(): + layers = 4 + x = np.empty((3, 3, layers)) + y = np.eye(3, 3) + _stackcopy(x, y) + for i in range(layers): + assert_array_almost_equal(x[..., i], y) + + +def test_similarity_estimation(): + # exact solution + tform = estimate_transform('similarity', SRC[:2, :], DST[:2, :]) + assert_array_almost_equal(tform(SRC[:2, :]), DST[:2, :]) + assert_equal(tform._matrix[0, 0], tform._matrix[1, 1]) + assert_equal(tform._matrix[0, 1], - tform._matrix[1, 0]) + + # over-determined + tform2 = estimate_transform('similarity', SRC, DST) + assert_array_almost_equal(tform2.inverse(tform2(SRC)), SRC) + assert_equal(tform2._matrix[0, 0], tform2._matrix[1, 1]) + assert_equal(tform2._matrix[0, 1], - tform2._matrix[1, 0]) + + # via estimate method + tform3 = SimilarityTransform() + tform3.estimate(SRC, DST) + assert_array_almost_equal(tform3._matrix, tform2._matrix) + + +def test_similarity_init(): + # init with implicit parameters + scale = 0.1 + rotation = 1 + translation = (1, 1) + tform = SimilarityTransform(scale=scale, rotation=rotation, + translation=translation) + assert_array_almost_equal(tform.scale, scale) + assert_array_almost_equal(tform.rotation, rotation) + assert_array_almost_equal(tform.translation, translation) + + # init with transformation matrix + tform2 = SimilarityTransform(tform._matrix) + assert_array_almost_equal(tform2.scale, scale) + assert_array_almost_equal(tform2.rotation, rotation) + assert_array_almost_equal(tform2.translation, translation) + + +def test_affine_estimation(): + # exact solution + tform = estimate_transform('affine', SRC[:3, :], DST[:3, :]) + assert_array_almost_equal(tform(SRC[:3, :]), DST[:3, :]) + + # over-determined + tform2 = estimate_transform('affine', SRC, DST) + assert_array_almost_equal(tform2.inverse(tform2(SRC)), SRC) + + # via estimate method + tform3 = AffineTransform() + tform3.estimate(SRC, DST) + assert_array_almost_equal(tform3._matrix, tform2._matrix) + + +def test_affine_init(): + # init with implicit parameters + scale = (0.1, 0.13) + rotation = 1 + shear = 0.1 + translation = (1, 1) + tform = AffineTransform(scale=scale, rotation=rotation, shear=shear, + translation=translation) + assert_array_almost_equal(tform.scale, scale) + assert_array_almost_equal(tform.rotation, rotation) + assert_array_almost_equal(tform.shear, shear) + assert_array_almost_equal(tform.translation, translation) + + # init with transformation matrix + tform2 = AffineTransform(tform._matrix) + assert_array_almost_equal(tform2.scale, scale) + assert_array_almost_equal(tform2.rotation, rotation) + assert_array_almost_equal(tform2.shear, shear) + assert_array_almost_equal(tform2.translation, translation) + + +def test_piecewise_affine(): + tform = PiecewiseAffineTransform() + tform.estimate(SRC, DST) + # make sure each single affine transform is exactly estimated + assert_array_almost_equal(tform(SRC), DST) + assert_array_almost_equal(tform.inverse(DST), SRC) + + +def test_projective_estimation(): + # exact solution + tform = estimate_transform('projective', SRC[:4, :], DST[:4, :]) + assert_array_almost_equal(tform(SRC[:4, :]), DST[:4, :]) + + # over-determined + tform2 = estimate_transform('projective', SRC, DST) + assert_array_almost_equal(tform2.inverse(tform2(SRC)), SRC) + + # via estimate method + tform3 = ProjectiveTransform() + tform3.estimate(SRC, DST) + assert_array_almost_equal(tform3._matrix, tform2._matrix) + + +def test_projective_init(): + tform = estimate_transform('projective', SRC, DST) + # init with transformation matrix + tform2 = ProjectiveTransform(tform._matrix) + assert_array_almost_equal(tform2._matrix, tform._matrix) + + +def test_polynomial_estimation(): + # over-determined + tform = estimate_transform('polynomial', SRC, DST, order=10) + assert_array_almost_equal(tform(SRC), DST, 6) + + # via estimate method + tform2 = PolynomialTransform() + tform2.estimate(SRC, DST, order=10) + assert_array_almost_equal(tform2._params, tform._params) + + +def test_polynomial_init(): + tform = estimate_transform('polynomial', SRC, DST, order=10) + # init with transformation parameters + tform2 = PolynomialTransform(tform._params) + assert_array_almost_equal(tform2._params, tform._params) + + +def test_union(): + tform1 = SimilarityTransform(scale=0.1, rotation=0.3) + tform2 = SimilarityTransform(scale=0.1, rotation=0.9) + tform3 = SimilarityTransform(scale=0.1 ** 2, rotation=0.3 + 0.9) + + tform = tform1 + tform2 + + assert_array_almost_equal(tform._matrix, tform3._matrix) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index b75291e8..03b63d07 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -5,6 +5,7 @@ import skimage.transform as tf import skimage.transform.hough_transform as ht from skimage.transform import probabilistic_hough + def append_desc(func, description): """Append the test function ``func`` and append ``description`` to its name. @@ -15,6 +16,7 @@ def append_desc(func, description): from skimage.transform import * + def test_hough(): # Generate a test image img = np.zeros((100, 100), dtype=int) @@ -39,6 +41,7 @@ def test_hough_angles(): assert_equal(len(angles), 10) + def test_py_hough(): ht._hough, fast_hough = ht._py_hough, ht._hough @@ -47,6 +50,7 @@ def test_py_hough(): tf._hough = fast_hough + def test_probabilistic_hough(): # Generate a test image img = np.zeros((100, 100), dtype=int) @@ -55,8 +59,9 @@ def test_probabilistic_hough(): img[i, i] = 100 # decrease default theta sampling because similar orientations may confuse # as mentioned in article of Galambos et al - theta=np.linspace(0, np.pi, 45) - lines = probabilistic_hough(img, theta=theta, threshold=10, line_length=10, line_gap=1) + theta = np.linspace(0, np.pi, 45) + lines = probabilistic_hough(img, theta=theta, threshold=10, line_length=10, + line_gap=1) # sort the lines according to the x-axis sorted_lines = [] for line in lines: @@ -69,4 +74,3 @@ def test_probabilistic_hough(): if __name__ == "__main__": run_module_suite() - diff --git a/skimage/transform/tests/test_integral.py b/skimage/transform/tests/test_integral.py index b8905d92..d443189c 100644 --- a/skimage/transform/tests/test_integral.py +++ b/skimage/transform/tests/test_integral.py @@ -6,19 +6,22 @@ from skimage.transform import integral_image, integrate x = (np.random.random((50, 50)) * 255).astype(np.uint8) s = integral_image(x) + def test_validity(): - y = np.arange(12).reshape((4,3)) + y = np.arange(12).reshape((4, 3)) y = (np.random.random((50, 50)) * 255).astype(np.uint8) assert_equal(integral_image(y)[-1, -1], y.sum()) + def test_basic(): assert_equal(x[12:24, 10:20].sum(), integrate(s, 12, 10, 23, 19)) assert_equal(x[:20, :20].sum(), integrate(s, 0, 0, 19, 19)) assert_equal(x[:20, 10:20].sum(), integrate(s, 0, 10, 19, 19)) assert_equal(x[10:20, :20].sum(), integrate(s, 10, 0, 19, 19)) + def test_single(): assert_equal(x[0, 0], integrate(s, 0, 0, 0, 0)) assert_equal(x[10, 10], integrate(s, 10, 10, 10, 10)) diff --git a/skimage/transform/tests/test_project.py b/skimage/transform/tests/test_project.py deleted file mode 100644 index 3446c9a5..00000000 --- a/skimage/transform/tests/test_project.py +++ /dev/null @@ -1,60 +0,0 @@ -import numpy as np -from numpy.testing import assert_array_almost_equal - -from skimage.transform._warp import _stackcopy -from skimage.transform import homography, fast_homography -from skimage import data -from skimage.color import rgb2gray - -def test_stackcopy(): - layers = 4 - x = np.empty((3, 3, layers)) - y = np.eye(3, 3) - _stackcopy(x, y) - for i in range(layers): - assert_array_almost_equal(x[...,i], y) - -def test_homography(): - x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 - theta = -np.pi/2 - M = np.array([[np.cos(theta),-np.sin(theta),0], - [np.sin(theta), np.cos(theta),2], - [0, 0, 1]]) - x90 = homography(x, M, order=1) - assert_array_almost_equal(x90, np.rot90(x)) - -def test_fast_homography(): - img = rgb2gray(data.lena()).astype(np.uint8) - img = img[:, :100] - - theta = np.deg2rad(30) - scale = 0.5 - tx, ty = 50, 50 - - H = np.eye(3) - S = scale * np.sin(theta) - C = scale * np.cos(theta) - - H[:2, :2] = [[C, -S], [S, C]] - H[:2, 2] = [tx, ty] - - for mode in ('constant', 'mirror', 'wrap'): - p0 = homography(img, H, mode=mode, order=1) - p1 = fast_homography(img, H, mode=mode) - p1 = np.round(p1) - - ## import matplotlib.pyplot as plt - ## f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) - ## ax0.imshow(img) - ## ax1.imshow(p0, cmap=plt.cm.gray) - ## ax2.imshow(p1, cmap=plt.cm.gray) - ## ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) - ## plt.show() - - d = np.mean(np.abs(p0 - p1)) - assert d < 0.2 - - -if __name__ == "__main__": - from numpy.testing import run_module_suite - run_module_suite() diff --git a/skimage/transform/tests/test_pyramids.py b/skimage/transform/tests/test_pyramids.py new file mode 100644 index 00000000..6d0609e0 --- /dev/null +++ b/skimage/transform/tests/test_pyramids.py @@ -0,0 +1,72 @@ +from numpy.testing import assert_array_equal, assert_raises, run_module_suite +from skimage import data +from skimage.transform import pyramids + + +image = data.lena() +image_gray = image[..., 0] + + +def test_pyramid_reduce_rgb(): + rows, cols, dim = image.shape + out = pyramids.pyramid_reduce(image, downscale=2) + assert_array_equal(out.shape, (rows / 2, cols / 2, dim)) + + +def test_pyramid_reduce_gray(): + rows, cols = image_gray.shape + out = pyramids.pyramid_reduce(image_gray, downscale=2) + assert_array_equal(out.shape, (rows / 2, cols / 2)) + + +def test_pyramid_expand_rgb(): + rows, cols, dim = image.shape + out = pyramids.pyramid_expand(image, upscale=2) + assert_array_equal(out.shape, (rows * 2, cols * 2, dim)) + + +def test_pyramid_expand_gray(): + rows, cols = image_gray.shape + out = pyramids.pyramid_expand(image_gray, upscale=2) + assert_array_equal(out.shape, (rows * 2, cols * 2)) + + +def test_build_gaussian_pyramid_rgb(): + rows, cols, dim = image.shape + pyramid = pyramids.pyramid_gaussian(image, downscale=2) + for layer, out in enumerate(pyramid): + layer_shape = (rows / 2 ** layer, cols / 2 ** layer, dim) + assert_array_equal(out.shape, layer_shape) + + +def test_build_gaussian_pyramid_gray(): + rows, cols = image_gray.shape + pyramid = pyramids.pyramid_gaussian(image_gray, downscale=2) + for layer, out in enumerate(pyramid): + layer_shape = (rows / 2 ** layer, cols / 2 ** layer) + assert_array_equal(out.shape, layer_shape) + + +def test_build_laplacian_pyramid_rgb(): + rows, cols, dim = image.shape + pyramid = pyramids.pyramid_laplacian(image, downscale=2) + for layer, out in enumerate(pyramid): + layer_shape = (rows / 2 ** layer, cols / 2 ** layer, dim) + assert_array_equal(out.shape, layer_shape) + + +def test_build_laplacian_pyramid_gray(): + rows, cols = image_gray.shape + pyramid = pyramids.pyramid_laplacian(image_gray, downscale=2) + for layer, out in enumerate(pyramid): + layer_shape = (rows / 2 ** layer, cols / 2 ** layer) + assert_array_equal(out.shape, layer_shape) + + +def test_check_factor(): + assert_raises(ValueError, pyramids._check_factor, 0.99) + assert_raises(ValueError, pyramids._check_factor, - 2) + + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index b52ff69b..3b2f19dc 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -4,12 +4,14 @@ import numpy as np from numpy.testing import * from skimage.transform import * + def rescale(x): x = x.astype(float) x -= x.min() x /= x.max() return x + def test_radon_iradon(): size = 100 debug = False @@ -38,6 +40,7 @@ def test_radon_iradon(): image = np.tri(size) + np.tri(size)[::-1] reconstructed = iradon(radon(image), filter="ramp", interpolation="nearest") + def test_iradon_angles(): """ Test with different number of projections @@ -60,10 +63,12 @@ def test_iradon_angles(): s = radon_image_80.sum(axis=0) assert np.allclose(s, s[0], rtol=0.01) reconstructed = iradon(radon_image_80) - delta_80 = np.mean(abs(image/np.max(image) - reconstructed/np.max(reconstructed))) + delta_80 = np.mean(abs(image / np.max(image) - + reconstructed / np.max(reconstructed))) # Loss of quality when the number of projections is reduced assert delta_80 > delta_200 + def test_radon_minimal(): """ Test for small images for various angles @@ -92,5 +97,12 @@ def test_radon_minimal(): assert np.all(abs(c - reconstructed) < 0.4) +def test_reconstruct_with_wrong_angles(): + a = np.zeros((3, 3)) + p = radon(a, theta=[0, 1, 2]) + iradon(p, theta=[0, 1, 2]) + assert_raises(ValueError, iradon, p, theta=[0, 1, 2, 3]) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/transform/tests/test_swirl.py b/skimage/transform/tests/test_swirl.py deleted file mode 100644 index d71f8231..00000000 --- a/skimage/transform/tests/test_swirl.py +++ /dev/null @@ -1,17 +0,0 @@ -import numpy as np -from numpy.testing import assert_array_almost_equal - -from skimage import transform as tf, data, img_as_float - - -def test_roundtrip(): - image = img_as_float(data.checkerboard()) - - swirl_params = {'radius': 80, 'rotation': 0, 'order': 2, 'mode': 'reflect'} - swirled = tf.swirl(image, strength=10, **swirl_params) - unswirled = tf.swirl(swirled, strength=-10, **swirl_params) - - assert np.mean(np.abs(image - unswirled)) < 0.01 - -if __name__ == "__main__": - np.testing.run_module_suite() diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py new file mode 100644 index 00000000..b705ac47 --- /dev/null +++ b/skimage/transform/tests/test_warps.py @@ -0,0 +1,137 @@ +from numpy.testing import assert_array_almost_equal, run_module_suite +import numpy as np +from scipy.ndimage import map_coordinates + +from skimage.transform import (warp, warp_coords, rotate, resize, + AffineTransform, + ProjectiveTransform, + SimilarityTransform, homography) +from skimage import transform as tf, data, img_as_float +from skimage.color import rgb2gray + + +def test_warp(): + x = np.zeros((5, 5), dtype=np.uint8) + x[2, 2] = 255 + x = img_as_float(x) + theta = - np.pi / 2 + tform = SimilarityTransform(scale=1, rotation=theta, translation=(0, 4)) + + x90 = warp(x, tform, order=1) + assert_array_almost_equal(x90, np.rot90(x)) + + x90 = warp(x, tform.inverse, order=1) + assert_array_almost_equal(x90, np.rot90(x)) + + +def test_homography(): + x = np.zeros((5, 5), dtype=np.uint8) + x[1, 1] = 255 + x = img_as_float(x) + theta = -np.pi / 2 + M = np.array([[np.cos(theta), - np.sin(theta), 0], + [np.sin(theta), np.cos(theta), 4], + [0, 0, 1]]) + + x90 = warp(x, + inverse_map=ProjectiveTransform(M).inverse, + order=1) + assert_array_almost_equal(x90, np.rot90(x)) + + +def test_homography_basic(): + homography(np.random.random((25, 25)), np.eye(3)) + + +def test_fast_homography(): + img = rgb2gray(data.lena()).astype(np.uint8) + img = img[:, :100] + + theta = np.deg2rad(30) + scale = 0.5 + tx, ty = 50, 50 + + H = np.eye(3) + S = scale * np.sin(theta) + C = scale * np.cos(theta) + + H[:2, :2] = [[C, -S], [S, C]] + H[:2, 2] = [tx, ty] + + tform = ProjectiveTransform(H) + coords = warp_coords(tform.inverse, (img.shape[0], img.shape[1])) + + for order in range(4): + for mode in ('constant', 'reflect', 'wrap', 'nearest'): + p0 = map_coordinates(img, coords, mode=mode, order=order) + p1 = warp(img, tform, mode=mode, order=order) + + # import matplotlib.pyplot as plt + # f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) + # ax0.imshow(img) + # ax1.imshow(p0, cmap=plt.cm.gray) + # ax2.imshow(p1, cmap=plt.cm.gray) + # ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) + # plt.show() + + d = np.mean(np.abs(p0 - p1)) + assert d < 0.001 + + +def test_rotate(): + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 + x90 = rotate(x, 90) + assert_array_almost_equal(x90, np.rot90(x)) + + +def test_resize(): + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 + resized = resize(x, (10, 10), order=0) + ref = np.zeros((10, 10)) + ref[2:4, 2:4] = 1 + assert_array_almost_equal(resized, ref) + + +def test_swirl(): + image = img_as_float(data.checkerboard()) + + swirl_params = {'radius': 80, 'rotation': 0, 'order': 2, 'mode': 'reflect'} + swirled = tf.swirl(image, strength=10, **swirl_params) + unswirled = tf.swirl(swirled, strength=-10, **swirl_params) + + assert np.mean(np.abs(image - unswirled)) < 0.01 + + +def test_const_cval_out_of_range(): + img = np.random.randn(100, 100) + cval = - 10 + warped = warp(img, AffineTransform(translation=(10, 10)), cval=cval) + assert np.sum(warped == cval) == (2 * 100 * 10 - 10 * 10) + + +def test_warp_identity(): + lena = img_as_float(rgb2gray(data.lena())) + assert len(lena.shape) == 2 + assert np.allclose(lena, warp(lena, AffineTransform(rotation=0))) + assert not np.allclose(lena, warp(lena, AffineTransform(rotation=0.1))) + rgb_lena = np.transpose(np.asarray([lena, np.zeros_like(lena), lena]), + (1, 2, 0)) + warped_rgb_lena = warp(rgb_lena, AffineTransform(rotation=0.1)) + assert np.allclose(rgb_lena, warp(rgb_lena, AffineTransform(rotation=0))) + assert not np.allclose(rgb_lena, warped_rgb_lena) + # assert no cross-talk between bands + assert np.all(0 == warped_rgb_lena[:, :, 1]) + + +def test_warp_coords_example(): + image = data.lena().astype(np.float32) + assert 3 == image.shape[2] + tform = SimilarityTransform(translation=(0, -10)) + coords = warp_coords(tform, (30, 30, 3)) + map_coordinates(image[:, :, 0], coords[:2]) + + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index b039123c..9f804406 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -1,12 +1,15 @@ from __future__ import division import numpy as np -__all__ = ['img_as_float', 'img_as_int', 'img_as_uint', 'img_as_ubyte'] +__all__ = ['img_as_float', 'img_as_int', 'img_as_uint', 'img_as_ubyte', + 'img_as_bool'] from .. import get_log log = get_log('dtype_converter') -dtype_range = {np.uint8: (0, 255), +dtype_range = {np.bool_: (False, True), + np.bool8: (False, True), + np.uint8: (0, 255), np.uint16: (0, 65535), np.int8: (-128, 127), np.int16: (-32768, 32767), @@ -15,7 +18,8 @@ dtype_range = {np.uint8: (0, 255), integer_types = (np.uint8, np.uint16, np.int8, np.int16) -_supported_types = (np.uint8, np.uint16, np.uint32, +_supported_types = (np.bool_, np.bool8, + np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32, np.float32, np.float64) @@ -94,7 +98,7 @@ def convert(image, dtype, force_copy=False, uniform=False): def _dtype2(kind, bits, itemsize=1): # Return dtype of `kind` that can store a `bits` wide unsigned int c = lambda x, y: x <= y if kind == 'u' else x < y - s = next(i for i in (itemsize, ) + (2, 4, 8) if c(bits, i*8)) + s = next(i for i in (itemsize, ) + (2, 4, 8) if c(bits, i * 8)) return np.dtype(kind + str(s)) def _scale(a, n, m, copy=True): @@ -145,6 +149,21 @@ def convert(image, dtype, force_copy=False, uniform=False): kind_in = dtypeobj_in.kind itemsize = dtypeobj.itemsize itemsize_in = dtypeobj_in.itemsize + + if kind == 'b': + # to binary image + if kind_in in "fi": + sign_loss() + prec_loss() + return image > dtype_in(dtype_range[dtype_in][1] / 2) + + if kind_in == 'b': + # from binary image, to float and to integer + result = dtype(image) + if kind != 'f': + result *= dtype(dtype_range[dtype][1]) + return result + if kind in 'ui': imin = np.iinfo(dtype).min imax = np.iinfo(dtype).max @@ -205,26 +224,26 @@ def convert(image, dtype, force_copy=False, uniform=False): if kind_in == 'u': if kind == 'i': # unsigned integer -> signed integer - image = _scale(image, 8*itemsize_in, 8*itemsize-1) + image = _scale(image, 8 * itemsize_in, 8 * itemsize - 1) return image.view(dtype) else: # unsigned integer -> unsigned integer - return _scale(image, 8*itemsize_in, 8*itemsize) + return _scale(image, 8 * itemsize_in, 8 * itemsize) if kind == 'u': # signed integer -> unsigned integer sign_loss() - image = _scale(image, 8*itemsize_in-1, 8*itemsize) + image = _scale(image, 8 * itemsize_in - 1, 8 * itemsize) result = np.empty(image.shape, dtype) np.maximum(image, 0, out=result, dtype=image.dtype, casting='unsafe') return result # signed integer -> signed integer if itemsize_in > itemsize: - return _scale(image, 8*itemsize_in-1, 8*itemsize-1) - image = image.astype(_dtype2('i', itemsize*8)) + return _scale(image, 8 * itemsize_in - 1, 8 * itemsize - 1) + image = image.astype(_dtype2('i', itemsize * 8)) image -= imin_in - image = _scale(image, 8*itemsize_in, 8*itemsize, copy=False) + image = _scale(image, 8 * itemsize_in, 8 * itemsize, copy=False) image += imin return dtype(image) @@ -246,8 +265,8 @@ def img_as_float(image, force_copy=False): Notes ----- - The range of a floating point image is [0, 1]. - Negative input values will be shifted to the positive domain. + The range of a floating point image is [0.0, 1.0] or [-1.0, 1.0] when + converting from unsigned or signed datatypes, respectively. """ return convert(image, np.float64, force_copy) @@ -322,3 +341,27 @@ def img_as_ubyte(image, force_copy=False): """ return convert(image, np.uint8, force_copy) + + +def img_as_bool(image, force_copy=False): + """Convert an image to boolean format. + + Parameters + ---------- + image : ndarray + Input image. + force_copy : bool + Force a copy of the data, irrespective of its current dtype. + + Returns + ------- + out : ndarray of bool (`bool_`) + Output image. + + Notes + ----- + The upper half of the input dtype's positive range is True, and the lower + half is False. All negative values (if present) are False. + + """ + return convert(image, np.bool_, force_copy) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index 57ec7274..587c0939 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -45,8 +45,8 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): Output array where 'alpha' has been determined automatically to fit (at least) the `n_images` in `arr_in`. - Example - ------- + Examples + -------- >>> import numpy as np >>> from skimage.util.montage import montage2d >>> arr_in = np.arange(3 * 2 * 2).reshape(3, 2, 2) @@ -71,6 +71,8 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): assert arr_in.ndim == 3 n_images, height, width = arr_in.shape + + arr_in = arr_in.copy() # -- rescale intensity if necessary if rescale_intensity: @@ -84,7 +86,7 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): if fill == 'mean': fill = arr_in.mean() - n_missing = int((alpha ** 2.) - n_images) + n_missing = int((alpha**2.) - n_images) missing = np.ones((n_missing, height, width), dtype=arr_in.dtype) * fill arr_out = np.vstack((arr_in, missing)) diff --git a/skimage/util/tests/test_dtype.py b/skimage/util/tests/test_dtype.py index 2ef0044f..ae26cd27 100644 --- a/skimage/util/tests/test_dtype.py +++ b/skimage/util/tests/test_dtype.py @@ -12,11 +12,13 @@ dtype_range = {np.uint8: (0, 255), np.float32: (-1.0, 1.0), np.float64: (-1.0, 1.0)} + def _verify_range(msg, x, vmin, vmax, dtype): assert_equal(x[0], vmin) assert_equal(x[-1], vmax) assert x.dtype == dtype + def test_range(): for dtype in dtype_range: imin, imax = dtype_range[dtype] @@ -84,5 +86,20 @@ def test_copy(): assert y is x assert z is not x + +def test_bool(): + img_ = np.zeros((10, 10), np.bool_) + img8 = np.zeros((10, 10), np.bool8) + img_[1, 1] = True + img8[1, 1] = True + for (func, dt) in [(img_as_int, np.int16), + (img_as_float, np.float64), + (img_as_uint, np.uint16), + (img_as_ubyte, np.ubyte)]: + converted_ = func(img_) + assert np.sum(converted_) == dtype_range[dt][1] + converted8 = func(img8) + assert np.sum(converted8) == dtype_range[dt][1] + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/viewer/__init__.py b/skimage/viewer/__init__.py new file mode 100644 index 00000000..cfbeb0c0 --- /dev/null +++ b/skimage/viewer/__init__.py @@ -0,0 +1,4 @@ +try: + from viewers import ImageViewer, CollectionViewer +except ImportError: + print("Could not import PyQt4 -- ImageViewer not available.") diff --git a/skimage/viewer/plugins/__init__.py b/skimage/viewer/plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py new file mode 100644 index 00000000..198bac6a --- /dev/null +++ b/skimage/viewer/plugins/base.py @@ -0,0 +1,241 @@ +""" +Base class for Plugins that interact with ImageViewer. +""" +try: + from PyQt4 import QtGui + from PyQt4.QtCore import Qt + from PyQt4.QtGui import QDialog +except ImportError: + QDialog = object # hack to prevent nosetest and autodoc errors + print("Could not import PyQt4 -- skimage.viewer not available.") + +try: + import matplotlib as mpl +except ImportError: + print("Could not import matplotlib -- skimage.viewer not available.") + +from ..utils import RequiredAttr, init_qtapp + + +class Plugin(QDialog): + """Base class for plugins that interact with an ImageViewer. + + A plugin connects an image filter (or another function) to an image viewer. + Note that a Plugin is initialized *without* an image viewer and attached in + a later step. See example below for details. + + Parameters + ---------- + image_viewer : ImageViewer + Window containing image used in measurement/manipulation. + image_filter : function + Function that gets called to update image in image viewer. This value + can be `None` if, for example, you have a plugin that extracts + information from an image and doesn't manipulate it. Alternatively, + this function can be defined as a method in a Plugin subclass. + height, width : int + Size of plugin window in pixels. Note that Qt will automatically resize + a window to fit components. So if you're adding rows of components, you + can leave `height = 0` and just let Qt determine the final height. + useblit : bool + If True, use blitting to speed up animation. Only available on some + Matplotlib backends. If None, set to True when using Agg backend. + This only has an effect if you draw on top of an image viewer. + + Attributes + ---------- + image_viewer : ImageViewer + Window containing image used in measurement. + name : str + Name of plugin. This is displayed as the window title. + artist : list + List of Matplotlib artists. Any artists created by the plugin should + be added to this list so that it gets cleaned up on close. + + Examples + -------- + >>> from skimage.viewer import ImageViewer + >>> from skimage.viewer.widgets import Slider + >>> from skimage import data + >>> + >>> plugin = Plugin(image_filter=lambda img, threshold: img > threshold) + >>> plugin += Slider('threshold', 0, 255) + >>> + >>> image = data.coins() + >>> viewer = ImageViewer(image) + >>> viewer += plugin + >>> # viewer.show() + + The plugin will automatically delegate parameters to `image_filter` based + on its parameter type, i.e., `ptype` (widgets for required arguments must + be added in the order they appear in the function). The image attached + to the viewer is **automatically passed as the first argument** to the + filter function. + + #TODO: Add flag so image is not passed to filter function by default. + + `ptype = 'kwarg'` is the default for most widgets so it's unnecessary here. + + """ + name = 'Plugin' + image_viewer = RequiredAttr("%s is not attached to ImageViewer" % name) + draws_on_image = False + + def __init__(self, image_filter=None, height=0, width=400, useblit=None): + init_qtapp() + super(Plugin, self).__init__() + + self.image_viewer = None + # If subclass defines `image_filter` method ignore input. + if not hasattr(self, 'image_filter'): + self.image_filter = image_filter + + self.setWindowTitle(self.name) + self.layout = QtGui.QGridLayout(self) + self.resize(width, height) + self.row = 0 + + self.arguments = [] + self.keyword_arguments= {} + + if useblit is None: + useblit = True if mpl.backends.backend.endswith('Agg') else False + self.useblit = useblit + self.cids = [] + self.artists = [] + + def attach(self, image_viewer): + """Attach the plugin to an ImageViewer. + + Note that the ImageViewer will automatically call this method when the + plugin is added to the ImageViewer. For example:: + + viewer += Plugin(...) + + Also note that `attach` automatically calls the filter function so that + the image matches the filtered value specified by attached widgets. + """ + self.setParent(image_viewer) + self.setWindowFlags(Qt.Dialog) + + self.image_viewer = image_viewer + self.image_viewer.plugins.append(self) + #TODO: Always passing image as first argument may be bad assumption. + self.arguments.append(self.image_viewer.original_image) + + if self.draws_on_image: + self.connect_image_event('draw_event', self.on_draw) + # Call filter so that filtered image matches widget values + self.filter_image() + + def add_widget(self, widget): + """Add widget to plugin. + + Alternatively, Plugin's `__add__` method is overloaded to add widgets:: + + plugin += Widget(...) + + Widgets can adjust required or optional arguments of filter function or + parameters for the plugin. This is specified by the Widget's `ptype'. + """ + if widget.ptype == 'kwarg': + name = widget.name.replace(' ', '_') + self.keyword_arguments[name] = widget + widget.callback = self.filter_image + elif widget.ptype == 'arg': + self.arguments.append(widget) + widget.callback = self.filter_image + elif widget.ptype == 'plugin': + widget.callback = self.update_plugin + widget.plugin = self + self.layout.addWidget(widget, self.row, 0) + self.row += 1 + + def __add__(self, widget): + self.add_widget(widget) + return self + + def on_draw(self, event): + """Save image background when blitting. + + The saved image is used to "clear" the figure before redrawing artists. + """ + if self.useblit: + bbox = self.image_viewer.ax.bbox + self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) + + def filter_image(self, *widget_arg): + """Call `image_filter` with widget args and kwargs + + Note: `display_filtered_image` is automatically called. + """ + # `widget_arg` is passed by the active widget but is unused since all + # filter arguments are pulled directly from attached the widgets. + + if self.image_filter is None: + return + arguments = [self._get_value(a) for a in self.arguments] + kwargs = dict([(name, self._get_value(a)) + for name, a in self.keyword_arguments.iteritems()]) + filtered = self.image_filter(*arguments, **kwargs) + self.display_filtered_image(filtered) + + def _get_value(self, param): + # If param is a widget, return its `val` attribute. + return param if not hasattr(param, 'val') else param.val + + def display_filtered_image(self, image): + """Display the filtered image on image viewer. + + If you don't want to simply replace the displayed image with the + filtered image (e.g., you want to display a transparent overlay), + you can override this method. + """ + self.image_viewer.image = image + + def update_plugin(self, name, value): + """Update keyword parameters of the plugin itself. + + These parameters will typically be implemented as class properties so + that they update the image or some other component. + """ + setattr(self, name, value) + + def closeEvent(self, event): + """On close disconnect all artists and events from ImageViewer. + + Note that events must be connected using `self.connect_image_event` and + artists must be appended to `self.artists`. + """ + self.disconnect_image_events() + self.remove_image_artists() + self.image_viewer.plugins.remove(self) + self.image_viewer.reset_image() + self.image_viewer.redraw() + self.close() + + def connect_image_event(self, event, callback): + """Connect callback with an event in the image viewer. + + This should be used in lieu of `figure.canvas.mpl_connect` since this + function stores call back ids for later clean up. + + Parameters + ---------- + event : str + Matplotlib event. + callback : function + Callback function with a matplotlib Event object as its argument. + """ + cid = self.image_viewer.connect_event(event, callback) + self.cids.append(cid) + + def disconnect_image_events(self): + """Disconnect all events created by this widget.""" + for c in self.cids: + self.image_viewer.disconnect_event(c) + + def remove_image_artists(self): + """Disconnect artists that are connected to the image viewer.""" + for a in self.artists: + self.image_viewer.remove_artist(a) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py new file mode 100644 index 00000000..7c7bfb3b --- /dev/null +++ b/skimage/viewer/plugins/canny.py @@ -0,0 +1,18 @@ +from skimage.filter import canny + +from .overlayplugin import OverlayPlugin +from ..widgets import Slider, ComboBox + + +class CannyPlugin(OverlayPlugin): + """Canny filter plugin to show edges of an image.""" + + name = 'Canny Filter' + + def __init__(self, *args, **kwargs): + super(CannyPlugin, self).__init__(image_filter=canny, **kwargs) + + self.add_widget(Slider('sigma', 0, 5, update_on='release')) + self.add_widget(Slider('low threshold', 0, 255, update_on='release')) + self.add_widget(Slider('high threshold', 0, 255, update_on='release')) + self.add_widget(ComboBox('color', self.color_names, ptype='plugin')) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py new file mode 100644 index 00000000..69c9ebfb --- /dev/null +++ b/skimage/viewer/plugins/lineprofile.py @@ -0,0 +1,250 @@ +import numpy as np +import scipy.ndimage as ndi +from skimage.util.dtype import dtype_range + +from .plotplugin import PlotPlugin + + +__all__ = ['LineProfile'] + +#TODO: Extract line tool and add it to a new `canvastools` subpackage. + + +class LineProfile(PlotPlugin): + """Plugin to compute interpolated intensity under a scan line on an image. + + See PlotPlugin and Plugin classes for additional details. + + Parameters + ---------- + linewidth : float + Line width for interpolation. Wider lines average over more pixels. + epsilon : float + Maximum pixel distance allowed when selecting end point of scan line. + limits : tuple or {None, 'image', 'dtype'} + (minimum, maximum) intensity limits for plotted profile. The following + special values are defined: + + None : rescale based on min/max intensity along selected scan line. + 'image' : fixed scale based on min/max intensity in image. + 'dtype' : fixed scale based on min/max intensity of image dtype. + """ + name = 'Line Profile' + draws_on_image = True + + def __init__(self, linewidth=1, epsilon=5, limits='image', **kwargs): + super(LineProfile, self).__init__(**kwargs) + self.linewidth = linewidth + self.epsilon = epsilon + self._active_pt = None + self._limit_type = limits + self.line_kwargs = dict(color='y', lw=linewidth, alpha=0.5, marker='s', + markersize=5, solid_capstyle='butt') + print self.help() + + def attach(self, image_viewer): + super(LineProfile, self).attach(image_viewer) + + image = image_viewer.original_image + + if self._limit_type == 'image': + self.limits = (np.min(image), np.max(image)) + elif self._limit_type == 'dtype': + self.self._limit_type = dtype_range[image.dtype.type] + elif self._limit_type is None or len(self._limit_type) == 2: + self.limits = self._limit_type + else: + raise ValueError("Unrecognized `limits`: %s" % self._limit_type) + + if not self._limit_type is None: + self.ax.set_ylim(self.limits) + + h, w = image.shape + self._init_end_pts = np.array([[w / 3, h / 2], [2 * w / 3, h / 2]]) + self.end_pts = self._init_end_pts.copy() + + x, y = np.transpose(self.end_pts) + self.scan_line = image_viewer.ax.plot(x, y, **self.line_kwargs)[0] + self.artists.append(self.scan_line) + + scan_data = profile_line(image, self.end_pts) + self.profile = self.ax.plot(scan_data, 'k-')[0] + self._autoscale_view() + + self.connect_image_event('key_press_event', self.on_key_press) + self.connect_image_event('button_press_event', self.on_mouse_press) + self.connect_image_event('button_release_event', self.on_mouse_release) + self.connect_image_event('motion_notify_event', self.on_move) + self.connect_image_event('scroll_event', self.on_scroll) + + self.image_viewer.redraw() + + def help(self): + helpstr = ("Line profile tool", + "+ and - keys or mouse scroll changes width of scan line.", + "Select and drag ends of the scan line to adjust it.") + return '\n'.join(helpstr) + + def get_profile(self): + """Return intensity profile of the selected line. + + Returns + ------- + end_pts: (2, 2) array + The positions ((x1, y1), (x2, y2)) of the line ends. + profile: 1d array + Profile of intensity values. + """ + end_pts = self.scan_line.get_xydata() + profile = self.profile.get_ydata() + return end_pts, profile + + def on_scroll(self, event): + if not event.inaxes: + return + if event.button == 'up': + self._thicken_scan_line() + elif event.button == 'down': + self._shrink_scan_line() + + def on_key_press(self, event): + if not event.inaxes: + return + elif event.key == '+': + self._thicken_scan_line() + elif event.key == '-': + self._shrink_scan_line() + elif event.key == 'r': + self.reset() + + def _thicken_scan_line(self): + self.linewidth += 1 + self.line_changed(None, None) + + def _shrink_scan_line(self): + if self.linewidth > 1: + self.linewidth -= 1 + self.line_changed(None, None) + + def _autoscale_view(self): + if self.limits is None: + self.ax.autoscale_view(tight=True) + else: + self.ax.autoscale_view(scaley=False, tight=True) + + def get_pt_under_cursor(self, event): + """Return index of the end point under cursor, if sufficiently close""" + xy = np.asarray(self.scan_line.get_xydata()) + xyt = self.scan_line.get_transform().transform(xy) + xt, yt = xyt[:, 0], xyt[:, 1] + d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2) + indseq = np.nonzero(np.equal(d, np.amin(d)))[0] + ind = indseq[0] + if d[ind] >= self.epsilon: + ind = None + return ind + + def on_mouse_press(self, event): + if event.button != 1: + return + if event.inaxes == None: + return + self._active_pt = self.get_pt_under_cursor(event) + + def on_mouse_release(self, event): + if event.button != 1: + return + self._active_pt = None + + def on_move(self, event): + if event.button != 1: + return + if self._active_pt is None: + return + if not self.image_viewer.ax.in_axes(event): + return + x, y = event.xdata, event.ydata + self.line_changed(x, y) + + def reset(self): + self.end_pts = self._init_end_pts.copy() + self.scan_line.set_data(np.transpose(self.end_pts)) + self.line_changed(None, None) + + def line_changed(self, x, y): + if x is not None: + self.end_pts[self._active_pt, :] = x, y + self.scan_line.set_data(np.transpose(self.end_pts)) + self.scan_line.set_linewidth(self.linewidth) + + scan = profile_line(self.image_viewer.original_image, self.end_pts, + linewidth=self.linewidth) + self.profile.set_xdata(np.arange(scan.shape[0])) + self.profile.set_ydata(scan) + + self.ax.relim() + + if self.useblit: + self.image_viewer.canvas.restore_region(self.img_background) + self.ax.draw_artist(self.scan_line) + self.ax.draw_artist(self.profile) + self.image_viewer.canvas.blit(self.image_viewer.ax.bbox) + + self._autoscale_view() + + self.image_viewer.redraw() + self.redraw() + + +def profile_line(img, end_pts, linewidth=1): + """Return the intensity profile of an image measured along a scan line. + + Parameters + ---------- + img : 2d array + The image. + end_pts: (2, 2) list + End points ((x1, y1), (x2, y2)) of scan line. + linewidth: int + Width of the scan, perpendicular to the line + + Returns + ------- + return_value : array + The intensity profile along the scan line. The length of the profile + is the ceil of the computed length of the scan line. + """ + point1, point2 = end_pts + x1, y1 = point1 = np.asarray(point1, dtype=float) + x2, y2 = point2 = np.asarray(point2, dtype=float) + dx, dy = point2 - point1 + + # Quick calculation if perfectly horizontal or vertical (remove?) + if x1 == x2: + pixels = img[min(y1, y2): max(y1, y2) + 1, + x1 - linewidth / 2: x1 + linewidth / 2 + 1] + intensities = pixels.mean(axis=1) + return intensities + elif y1 == y2: + pixels = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, + min(x1, x2): max(x1, x2) + 1] + intensities = pixels.mean(axis=0) + return intensities + + theta = np.arctan2(dy, dx) + a = dy / dx + b = y1 - a * x1 + length = np.hypot(dx, dy) + + line_x = np.linspace(min(x1, x2), max(x1, x2), np.ceil(length)) + line_y = line_x * a + b + y_width = abs(linewidth * np.cos(theta) / 2) + perp_ys = np.array([np.linspace(yi - y_width, + yi + y_width, linewidth) for yi in line_y]) + perp_xs = - a * perp_ys + (line_x + a * line_y)[:, np.newaxis] + + perp_lines = np.array([perp_ys, perp_xs]) + pixels = ndi.map_coordinates(img, perp_lines) + intensities = pixels.mean(axis=1) + + return intensities diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py new file mode 100644 index 00000000..f9d37e59 --- /dev/null +++ b/skimage/viewer/plugins/overlayplugin.py @@ -0,0 +1,84 @@ +from skimage.util.dtype import dtype_range +from .base import Plugin +from ..utils import ClearColormap + + +class OverlayPlugin(Plugin): + """Plugin for ImageViewer that displays an overlay on top of main image. + + The base Plugin class displays the filtered image directly on the viewer. + OverlayPlugin will instead overlay an image with a transparent colormap. + + See base Plugin class for additional details. + + Attributes + ---------- + overlay : array + Overlay displayed on top of image. This overlay defaults to a color map + with alpha values varying linearly from 0 to 1. + color : int + Color of overlay. + """ + colors = {'red': (1, 0, 0), + 'yellow': (1, 1, 0), + 'green': (0, 1, 0), + 'cyan': (0, 1, 1)} + + def __init__(self, **kwargs): + super(OverlayPlugin, self).__init__(**kwargs) + self._overlay_plot = None + self._overlay = None + self.cmap = None + self.color_names = self.colors.keys() + + def attach(self, image_viewer): + super(OverlayPlugin, self).attach(image_viewer) + #TODO: `color` doesn't update GUI widget when set manually. + self.color = 0 + + @property + def overlay(self): + return self._overlay + + @overlay.setter + def overlay(self, image): + self._overlay = image + ax = self.image_viewer.ax + if image is None: + ax.images.remove(self._overlay_plot) + self._overlay_plot = None + elif self._overlay_plot is None: + vmin, vmax = dtype_range[image.dtype.type] + self._overlay_plot = ax.imshow(image, cmap=self.cmap, + vmin=vmin, vmax=vmax) + else: + self._overlay_plot.set_array(image) + self.image_viewer.redraw() + + @property + def color(self): + return self._color + + @color.setter + def color(self, index): + # Update colormap whenever color is changed. + if isinstance(index, basestring) and index not in self.color_names: + raise ValueError("%s not defined in OverlayPlugin.colors" % index) + else: + name = self.color_names[index] + self._color = name + rgb = self.colors[name] + self.cmap = ClearColormap(rgb) + + if self._overlay_plot is not None: + self._overlay_plot.set_cmap(self.cmap) + self.image_viewer.redraw() + + def display_filtered_image(self, image): + """Display filtered image as an overlay on top of image in viewer.""" + self.overlay = image + + def closeEvent(self, event): + # clear overlay from ImageViewer on close + self.overlay = None + super(OverlayPlugin, self).closeEvent(event) diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py new file mode 100644 index 00000000..fa06088d --- /dev/null +++ b/skimage/viewer/plugins/plotplugin.py @@ -0,0 +1,50 @@ +import numpy as np +from PyQt4 import QtGui + +import matplotlib.pyplot as plt + +from ..utils import MatplotlibCanvas +from .base import Plugin + + +class PlotCanvas(MatplotlibCanvas): + """Canvas for displaying images. + + This canvas derives from Matplotlib, and has attributes `fig` and `ax`, + which point to Matplotlib figure and axes. + """ + def __init__(self, parent, height, width, **kwargs): + self.fig, self.ax = plt.subplots(figsize=(height, width), **kwargs) + super(PlotCanvas, self).__init__(parent, self.fig, **kwargs) + self.setMinimumHeight(150) + +class PlotPlugin(Plugin): + """Plugin for ImageViewer that contains a plot canvas. + + Base class for plugins that contain a Matplotlib plot canvas, which can, + for example, display an image histogram. + + See base Plugin class for additional details. + """ + + def attach(self, image_viewer): + super(PlotPlugin, self).attach(image_viewer) + # Add plot for displaying intensity profile. + self.add_plot() + + def redraw(self): + """Redraw plot.""" + self.canvas.draw_idle() + + def add_plot(self, height=4, width=4): + self.canvas = PlotCanvas(self, height, width) + self.fig = self.canvas.fig + #TODO: Converted color is slightly different than Qt background. + qpalette = QtGui.QPalette() + qcolor = qpalette.color(QtGui.QPalette.Window) + bgcolor = qcolor.toRgb().value() + if np.isscalar(bgcolor): + bgcolor = str(bgcolor / 255.) + self.fig.patch.set_facecolor(bgcolor) + self.ax = self.canvas.ax + self.layout.addWidget(self.canvas, self.row, 0) diff --git a/skimage/viewer/utils/__init__.py b/skimage/viewer/utils/__init__.py new file mode 100644 index 00000000..5af24064 --- /dev/null +++ b/skimage/viewer/utils/__init__.py @@ -0,0 +1 @@ +from core import * diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py new file mode 100644 index 00000000..cf632d5a --- /dev/null +++ b/skimage/viewer/utils/core.py @@ -0,0 +1,137 @@ +import warnings + +import numpy as np + +try: + import matplotlib.pyplot as plt + from matplotlib.colors import LinearSegmentedColormap + from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg +except ImportError: + FigureCanvasQTAgg = object # hack to prevent nosetest and autodoc errors + LinearSegmentedColormap = object + print("Could not import matplotlib -- skimage.viewer not available.") + +try: + from PyQt4 import QtGui +except ImportError: + print("Could not import PyQt4 -- skimage.viewer not available.") + + +__all__ = ['init_qtapp', 'start_qtapp', 'RequiredAttr', 'figimage', + 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas'] + + +QApp = None + + +def init_qtapp(): + """Initialize QAppliction. + + The QApplication needs to be initialized before creating any QWidgets + """ + global QApp + if QApp is None: + QApp = QtGui.QApplication([]) + + +def start_qtapp(): + """Start Qt mainloop""" + QApp.exec_() + + +class RequiredAttr(object): + """A class attribute that must be set before use.""" + + def __init__(self, msg): + self.msg = msg + self.val = None + + def __get__(self, obj, objtype): + if self.val is None: + warnings.warn(self.msg) + return self.val + + def __set__(self, obj, val): + self.val = val + + +def figimage(image, scale=1, dpi=None, **kwargs): + """Return figure and axes with figure tightly surrounding image. + + Unlike pyplot.figimage, this actually plots onto an axes object, which + fills the figure. Plotting the image onto an axes allows for subsequent + overlays of axes artists. + + Parameters + ---------- + image : array + image to plot + scale : float + If scale is 1, the figure and axes have the same dimension as the + image. Smaller values of `scale` will shrink the figure. + dpi : int + Dots per inch for figure. If None, use the default rcParam. + """ + dpi = dpi if dpi is not None else plt.rcParams['figure.dpi'] + kwargs.setdefault('interpolation', 'nearest') + kwargs.setdefault('cmap', 'gray') + + h, w, d = np.atleast_3d(image).shape + figsize = np.array((w, h), dtype=float) / dpi * scale + + fig, ax = plt.subplots(figsize=figsize, dpi=dpi) + fig.subplots_adjust(left=0, bottom=0, right=1, top=1) + + ax.set_axis_off() + ax.imshow(image, **kwargs) + return fig, ax + + +class LinearColormap(LinearSegmentedColormap): + """LinearSegmentedColormap in which color varies smoothly. + + This class is a simplification of LinearSegmentedColormap, which doesn't + support jumps in color intensities. + + Parameters + ---------- + name : str + Name of colormap. + + segmented_data : dict + Dictionary of 'red', 'green', 'blue', and (optionally) 'alpha' values. + Each color key contains a list of `x`, `y` tuples. `x` must increase + monotonically from 0 to 1 and corresponds to input values for a + mappable object (e.g. an image). `y` corresponds to the color + intensity. + + """ + def __init__(self, name, segmented_data, **kwargs): + segmented_data = dict((key, [(x, y, y) for x, y in value]) + for key, value in segmented_data.iteritems()) + LinearSegmentedColormap.__init__(self, name, segmented_data, **kwargs) + + +class ClearColormap(LinearColormap): + """Color map that varies linearly from alpha = 0 to 1 + """ + def __init__(self, rgb, max_alpha=1, name='clear_color'): + r, g, b = rgb + cg_speq = {'blue': [(0.0, b), (1.0, b)], + 'green': [(0.0, g), (1.0, g)], + 'red': [(0.0, r), (1.0, r)], + 'alpha': [(0.0, 0.0), (1.0, max_alpha)]} + LinearColormap.__init__(self, name, cg_speq) + + +class MatplotlibCanvas(FigureCanvasQTAgg): + """Canvas for displaying images.""" + def __init__(self, parent, figure, **kwargs): + self.fig = figure + FigureCanvasQTAgg.__init__(self, self.fig) + FigureCanvasQTAgg.setSizePolicy(self, + QtGui.QSizePolicy.Expanding, + QtGui.QSizePolicy.Expanding) + FigureCanvasQTAgg.updateGeometry(self) + # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. + self.setParent(parent) diff --git a/skimage/viewer/viewers/__init__.py b/skimage/viewer/viewers/__init__.py new file mode 100644 index 00000000..a8339edc --- /dev/null +++ b/skimage/viewer/viewers/__init__.py @@ -0,0 +1 @@ +from .core import ImageViewer, CollectionViewer diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py new file mode 100644 index 00000000..e11967f7 --- /dev/null +++ b/skimage/viewer/viewers/core.py @@ -0,0 +1,279 @@ +""" +ImageViewer class for viewing and interacting with images. +""" +try: + from PyQt4 import QtGui, QtCore + from PyQt4.QtGui import QMainWindow +except ImportError: + QMainWindow = object # hack to prevent nosetest and autodoc errors + print("Could not import PyQt4 -- skimage.viewer not available.") + +from skimage.util.dtype import dtype_range +from .. import utils +from ..widgets import Slider + + +__all__ = ['ImageViewer', 'CollectionViewer'] + + +class ImageCanvas(utils.MatplotlibCanvas): + """Canvas for displaying images.""" + def __init__(self, parent, image, **kwargs): + self.fig, self.ax = utils.figimage(image, **kwargs) + super(ImageCanvas, self).__init__(parent, self.fig, **kwargs) + + +class ImageViewer(QMainWindow): + """Viewer for displaying images. + + This viewer is a simple container object that holds a Matplotlib axes + for showing images. `ImageViewer` doesn't subclass the Matplotlib axes (or + figure) because of the high probability of name collisions. + + Parameters + ---------- + image : array + Image being viewed. + + Attributes + ---------- + canvas, fig, ax : Matplotlib canvas, figure, and axes + Matplotlib canvas, figure, and axes used to display image. + image : array + Image being viewed. Setting this value will update the displayed frame. + original_image : array + Plugins typically operate on (but don't change) the *original* image. + plugins : list + List of attached plugins. + + Examples + -------- + >>> from skimage import data + >>> image = data.coins() + >>> viewer = ImageViewer(image) + >>> # viewer.show() + + """ + def __init__(self, image): + # Start main loop + utils.init_qtapp() + super(ImageViewer, self).__init__() + + #TODO: Add ImageViewer to skimage.io window manager + + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setWindowTitle("Image Viewer") + + self.file_menu = QtGui.QMenu('&File', self) + self.file_menu.addAction('&Quit', self.close, + QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + self.menuBar().addMenu(self.file_menu) + + self.main_widget = QtGui.QWidget() + self.setCentralWidget(self.main_widget) + + self.canvas = ImageCanvas(self.main_widget, image) + self.fig = self.canvas.fig + self.ax = self.canvas.ax + self.ax.autoscale(enable=False) + + self._image_plot = self.ax.images[0] + + self.original_image = image + self.image = image.copy() + self.plugins = [] + + # List of axes artists to check for removal. + self._axes_artists = [self.ax.artists, + self.ax.collections, + self.ax.images, + self.ax.lines, + self.ax.patches, + self.ax.texts] + + self.layout = QtGui.QVBoxLayout(self.main_widget) + self.layout.addWidget(self.canvas) + + status_bar = self.statusBar() + self.status_message = status_bar.showMessage + sb_size = status_bar.sizeHint() + cs_size = self.canvas.sizeHint() + self.resize(cs_size.width(), cs_size.height() + sb_size.height()) + + self.connect_event('motion_notify_event', self._update_status_bar) + + def __add__(self, plugin): + """Add plugin to ImageViewer""" + plugin.attach(self) + return self + + def closeEvent(self, event): + self.close() + + def auto_layout(self): + """Move viewer to top-left and align plugin on right edge of viewer.""" + size = self.geometry() + self.move(0, 0) + w = size.width() + y = 0 + #TODO: Layout isn't quite correct for multiple plugins (overlaps). + for p in self.plugins: + p.move(w, y) + y += p.geometry().height() + + def show(self): + """Show ImageViewer and attached plugins. + + This behaves much like `matplotlib.pyplot.show` and `QWidget.show`. + """ + self.auto_layout() + for p in self.plugins: + p.show() + super(ImageViewer, self).show() + utils.start_qtapp() + + def redraw(self): + self.canvas.draw_idle() + + @property + def image(self): + return self._img + + @image.setter + def image(self, image): + self._img = image + self._image_plot.set_array(image) + clim = dtype_range[image.dtype.type] + if clim[0] < 0 and image.min() >= 0: + clim = (0, clim[1]) + self._image_plot.set_clim(clim) + self.redraw() + + def reset_image(self): + self.image = self.original_image.copy() + + def connect_event(self, event, callback): + """Connect callback function to matplotlib event and return id.""" + cid = self.canvas.mpl_connect(event, callback) + return cid + + def disconnect_event(self, callback_id): + """Disconnect callback by its id (returned by `connect_event`).""" + self.canvas.mpl_disconnect(callback_id) + + def remove_artist(self, artist): + """Disconnect matplotlib artist from image viewer. + + The `closeEvent` method of a Plugin should remove artists (Matplotlib + lines, markers, etc.) from the viewer so that they aren't stranded. + + Parameters + ---------- + artist : Matplotlib Artist + Artists created by Matplotlib functions (e.g., `plot` returns list + of `Line2D` artists) should be saved by the plugin for removal. + """ + # Note: an `add_artist` method is unnecessary since Matplotlib + + # There's probably a smarter way to find where the artist is stored. + for artist_list in self._axes_artists: + if artist in artist_list: + artist_list.remove(artist) + + def _update_status_bar(self, event): + if event.inaxes and event.inaxes.get_navigate(): + self.status_message(self._format_coord(event.xdata, event.ydata)) + else: + self.status_message('') + + def _format_coord(self, x, y): + # callback function to format coordinate display in status bar + x = int(x + 0.5) + y = int(y + 0.5) + try: + return "%4s @ [%4s, %4s]" % (self.image[y, x], x, y) + except IndexError: + return "" + + +class CollectionViewer(ImageViewer): + """Viewer for displaying image collections. + + Select the displayed frame of the image collection using the slider or + with the following keyboard shortcuts: + + left/right arrows + Previous/next image in collection. + number keys, 0--9 + 0% to 90% of collection. For example, "5" goes to the image in the + middle (i.e. 50%) of the collection. + home/end keys + First/last image in collection. + + Subclasses and plugins will likely extend the `update_image` method to add + custom overlays or filter the displayed image. + + Parameters + ---------- + image_collection : list of images + List of images to be displayed. + update_on : {'on_slide' | 'on_release'} + Control whether image is updated on slide or release of the image + slider. Using 'on_release' will give smoother behavior when displaying + large images or when writing a plugin/subclass that requires heavy + computation. + """ + + def __init__(self, image_collection, update_on='move', **kwargs): + self.image_collection = image_collection + self.index = 0 + self.num_images = len(self.image_collection) + + first_image = image_collection[0] + super(CollectionViewer, self).__init__(first_image) + + slider_kws = dict(value=0, low=0, high=self.num_images - 1) + slider_kws['update_on'] = update_on + slider_kws['callback'] = self.update_index + slider_kws['value_type'] = 'int' + self.slider = Slider('frame', **slider_kws) + self.layout.addWidget(self.slider) + + #TODO: Adjust height to accomodate slider; the following doesn't work + # s_size = self.slider.sizeHint() + # cs_size = self.canvas.sizeHint() + # self.resize(cs_size.width(), cs_size.height() + s_size.height()) + + def update_index(self, name, index): + """Select image on display using index into image collection.""" + index = int(round(index)) + + if index == self.index: + return + + # clip index value to collection limits + index = max(index, 0) + index = min(index, self.num_images - 1) + + self.index = index + self.slider.val = index + self.update_image(self.image_collection[index]) + + def update_image(self, image): + """Update displayed image. + + This method can be overridden or extended in subclasses and plugins to + react to image changes. + """ + self.image = image + + def keyPressEvent(self, event): + if type(event) == QtGui.QKeyEvent: + key = event.key() + # Number keys (code: 0 = key 48, 9 = key 57) move to deciles + if 48 <= key < 58: + index = 0.1 * int(key - 48) * self.num_images + self.update_index('', index) + event.accept() + else: + event.ignore() diff --git a/skimage/viewer/widgets/__init__.py b/skimage/viewer/widgets/__init__.py new file mode 100644 index 00000000..6552a313 --- /dev/null +++ b/skimage/viewer/widgets/__init__.py @@ -0,0 +1,2 @@ +from core import * +from history import * diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py new file mode 100644 index 00000000..9382652b --- /dev/null +++ b/skimage/viewer/widgets/core.py @@ -0,0 +1,229 @@ +""" +Widgets for interacting with ImageViewer. + +These widgets should be added to a Plugin subclass using its `add_widget` +method or calling:: + + plugin += Widget(...) + +on a Plugin instance. The Plugin will delegate action based on the widget's +parameter type specified by its `ptype` attribute, which can be: + + 'arg' : positional argument passed to Plugin's `filter_image` method. + 'kwarg' : keyword argument passed to Plugin's `filter_image` method. + 'plugin' : attribute of Plugin. You'll probably need to add a class + property of the same name that updates the display. + +""" +try: + from PyQt4.QtCore import Qt + from PyQt4 import QtGui + from PyQt4 import QtCore + from PyQt4.QtGui import QWidget +except ImportError: + QWidget = object # hack to prevent nosetest and autodoc errors + print("Could not import PyQt4 -- skimage.viewer not available.") + +from ..utils import RequiredAttr + + +__all__ = ['BaseWidget', 'Slider', 'ComboBox'] + + +class BaseWidget(QWidget): + + plugin = RequiredAttr("Widget is not attached to a Plugin.") + + def __init__(self, name, ptype=None, callback=None): + super(BaseWidget, self).__init__() + self.name = name + self.ptype = ptype + self.callback = callback + self.plugin = None + + @property + def val(self): + msg = "Subclass of BaseWidget requires `val` property" + raise NotImplementedError(msg) + + def _value_changed(self, value): + self.callback(self.name, value) + + +class Slider(BaseWidget): + """Slider widget for adjusting numeric parameters. + + Parameters + ---------- + name : str + Name of slider parameter. If this parameter is passed as a keyword + argument, it must match the name of that keyword argument (spaces are + replaced with underscores). In addition, this name is displayed as the + name of the slider. + low, high : float + Range of slider values. + value : float + Default slider value. If None, use midpoint between `low` and `high`. + value : {'float' | 'int'} + Numeric type of slider value. + ptype : {'arg' | 'kwarg' | 'plugin'} + Parameter type. + callback : function + Callback function called in response to slider changes. This function + is typically set when the widget is added to a plugin. + orientation : {'horizontal' | 'vertical'} + Slider orientation. + update_on : {'move' | 'release'} + Control when callback function is called: on slider move or release. + """ + def __init__(self, name, low=0.0, high=1.0, value=None, value_type='float', + ptype='kwarg', callback=None, max_edit_width=60, + orientation='horizontal', update_on='move'): + super(Slider, self).__init__(name, ptype, callback) + + if value is None: + value = (high - low) / 2. + + # Set widget orientation + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + if orientation == 'vertical': + self.slider = QtGui.QSlider(Qt.Vertical) + alignment = QtCore.Qt.AlignHCenter + align_text = QtCore.Qt.AlignHCenter + align_value = QtCore.Qt.AlignHCenter + self.layout = QtGui.QVBoxLayout(self) + elif orientation == 'horizontal': + self.slider = QtGui.QSlider(Qt.Horizontal) + alignment = QtCore.Qt.AlignVCenter + align_text = QtCore.Qt.AlignLeft + align_value = QtCore.Qt.AlignRight + self.layout = QtGui.QHBoxLayout(self) + else: + msg = "Unexpected value %s for 'orientation'" + raise ValueError(msg % orientation) + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # Set slider behavior for float and int values. + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + if value_type == 'float': + # divide slider into 1000 discrete values + slider_max = 1000 + self._scale = float(high - low) / slider_max + self.slider.setRange(0, slider_max) + self.value_fmt = '%2.2f' + elif value_type == 'int': + self.slider.setRange(low, high) + self.value_fmt = '%d' + else: + msg = "Expected `value_type` to be 'float' or 'int'; received: %s" + raise ValueError(msg % value_type) + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + self.value_type = value_type + self._low = low + self._high = high + # Update slider position to default value + self.val = value + + if update_on == 'move': + self.slider.valueChanged.connect(self._on_slider_changed) + elif update_on == 'release': + self.slider.sliderReleased.connect(self._on_slider_changed) + else: + raise ValueError("Unexpected value %s for 'update_on'" % update_on) + + self.name_label = QtGui.QLabel() + self.name_label.setText(self.name) + self.name_label.setAlignment(align_text) + + self.editbox = QtGui.QLineEdit() + self.editbox.setMaximumWidth(max_edit_width) + self.editbox.setText(self.value_fmt % self.val) + self.editbox.setAlignment(align_value) + self.editbox.editingFinished.connect(self._on_editbox_changed) + + self.layout.addWidget(self.name_label, alignment=align_text) + self.layout.addWidget(self.slider, alignment=alignment) + self.layout.addWidget(self.editbox, alignment=align_value) + + def _on_slider_changed(self): + """Call callback function with slider's name and value as parameters""" + value = self.val + self.editbox.setText(str(value)[:4]) + self.callback(self.name, value) + + def _on_editbox_changed(self): + """Validate input and set slider value""" + try: + value = float(self.editbox.text()) + except ValueError: + self._bad_editbox_input() + return + if not self._low <= value <= self._high: + self._bad_editbox_input() + return + + self.val = value + self._good_editbox_input() + self.callback(self.name, value) + + def _good_editbox_input(self): + self.editbox.setStyleSheet("background-color: rgb(255, 255, 255)") + + def _bad_editbox_input(self): + self.editbox.setStyleSheet("background-color: rgb(255, 200, 200)") + + @property + def val(self): + value = self.slider.value() + if self.value_type == 'float': + value = value * self._scale + self._low + return value + + @val.setter + def val(self, value): + if self.value_type == 'float': + value = (value - self._low) / self._scale + self.slider.setValue(value) + + +class ComboBox(BaseWidget): + """ComboBox widget for selecting among a list of choices. + + Parameters + ---------- + name : str + Name of slider parameter. If this parameter is passed as a keyword + argument, it must match the name of that keyword argument (spaces are + replaced with underscores). In addition, this name is displayed as the + name of the slider. + items: list + Allowed parameter values. + ptype : {'arg' | 'kwarg' | 'plugin'} + Parameter type. + callback : function + Callback function called in response to slider changes. This function + is typically set when the widget is added to a plugin. + """ + + def __init__(self, name, items, ptype='kwarg', callback=None): + super(ComboBox, self).__init__(name, ptype, callback) + + self.name_label = QtGui.QLabel() + self.name_label.setText(self.name) + self.name_label.setAlignment(QtCore.Qt.AlignLeft) + + self._combo_box = QtGui.QComboBox() + self._combo_box.addItems(items) + + self.layout = QtGui.QHBoxLayout(self) + self.layout.addWidget(self.name_label) + self.layout.addWidget(self._combo_box, alignment=QtCore.Qt.AlignLeft) + + self._combo_box.currentIndexChanged.connect(self._value_changed) + # self.connect(self._combo_box, + # SIGNAL("currentIndexChanged(int)"), self.updateUi) + + @property + def val(self): + return self._combo_box.value() diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py new file mode 100644 index 00000000..efc8a21c --- /dev/null +++ b/skimage/viewer/widgets/history.py @@ -0,0 +1,99 @@ +import os +from textwrap import dedent + +try: + from PyQt4 import QtGui +except ImportError: + print("Could not import PyQt4 -- skimage.viewer not available.") + +from skimage import io +from .core import BaseWidget + + +__all__ = ['OKCancelButtons', 'SaveButtons'] + + +class OKCancelButtons(BaseWidget): + """Buttons that close the parent plugin. + + OK will replace the original image with the current (filtered) image. + Cancel will just close the plugin. + """ + def __init__(self, button_width=80): + name = 'OK/Cancel' + super(OKCancelButtons, self).__init__(name) + + self.ok = QtGui.QPushButton('OK') + self.ok.clicked.connect(self.update_original_image) + self.ok.setMaximumWidth(button_width) + self.cancel = QtGui.QPushButton('Cancel') + self.cancel.clicked.connect(self.close_plugin) + self.cancel.setMaximumWidth(button_width) + + self.layout = QtGui.QHBoxLayout(self) + self.layout.addStretch() + self.layout.addWidget(self.cancel) + self.layout.addWidget(self.ok) + + def update_original_image(self): + image = self.plugin.image_viewer.image + self.plugin.image_viewer.original_image = image + self.plugin.close() + + def close_plugin(self): + # Image viewer will restore original image on close. + self.plugin.close() + + +class SaveButtons(BaseWidget): + """Buttons to save image to io.stack or to a file.""" + + def __init__(self, name='Save to:', default_format='png'): + super(SaveButtons, self).__init__(name) + + self.default_format = default_format + + self.name_label = QtGui.QLabel() + self.name_label.setText(name) + + self.save_file = QtGui.QPushButton('File') + self.save_file.clicked.connect(self.save_to_file) + self.save_stack = QtGui.QPushButton('Stack') + self.save_stack.clicked.connect(self.save_to_stack) + + self.layout = QtGui.QHBoxLayout(self) + self.layout.addWidget(self.name_label) + self.layout.addWidget(self.save_stack) + self.layout.addWidget(self.save_file) + + def save_to_stack(self): + image = self.plugin.image_viewer.image.copy() + io.push(image) + + msg = dedent('''\ + The image has been pushed to the io stack. + Use io.pop() to retrieve the most recently pushed image. + NOTE: The io stack only works in interactive sessions.''') + notify(msg) + + def save_to_file(self): + filename = str(QtGui.QFileDialog.getSaveFileName()) + if len(filename) == 0: + return + #TODO: io plugins should assign default image formats + basename, ext = os.path.splitext(filename) + if not ext: + filename = '%s.%s' % (filename, self.default_format) + io.imsave(filename, self.plugin.image_viewer.image) + + +def notify(msg): + msglabel = QtGui.QLabel(msg) + dialog = QtGui.QDialog() + ok = QtGui.QPushButton('OK', dialog) + ok.clicked.connect(dialog.accept) + ok.setDefault(True) + dialog.layout = QtGui.QGridLayout(dialog) + dialog.layout.addWidget(msglabel, 0, 0, 1, 3) + dialog.layout.addWidget(ok, 1, 1) + dialog.exec_() diff --git a/viewer_examples/plugins/canny.py b/viewer_examples/plugins/canny.py new file mode 100644 index 00000000..eaa33330 --- /dev/null +++ b/viewer_examples/plugins/canny.py @@ -0,0 +1,9 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.canny import CannyPlugin + + +image = data.camera() +viewer = ImageViewer(image) +viewer += CannyPlugin() +viewer.show() diff --git a/viewer_examples/plugins/canny_simple.py b/viewer_examples/plugins/canny_simple.py new file mode 100644 index 00000000..49bc15eb --- /dev/null +++ b/viewer_examples/plugins/canny_simple.py @@ -0,0 +1,20 @@ +from skimage import data +from skimage.filter import canny + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.plugins.overlayplugin import OverlayPlugin + + +image = data.camera() +# Note: ImageViewer must be called before Plugin b/c it starts the event loop. +viewer = ImageViewer(image) +# You can create a UI for a filter just by passing a filter function... +plugin = OverlayPlugin(image_filter=canny) +# ... and adding widgets to adjust parameter values. +plugin += Slider('sigma', 0, 5, update_on='release') +plugin += Slider('low threshold', 0, 255, update_on='release') +plugin += Slider('high threshold', 0, 255, update_on='release') +# Finally, attach the plugin to the image viewer. +viewer += plugin +viewer.show() diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py new file mode 100644 index 00000000..2f1b2cdc --- /dev/null +++ b/viewer_examples/plugins/lineprofile.py @@ -0,0 +1,9 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.lineprofile import LineProfile + + +image = data.camera() +viewer = ImageViewer(image) +viewer += LineProfile() +viewer.show() diff --git a/viewer_examples/plugins/median_filter.py b/viewer_examples/plugins/median_filter.py new file mode 100644 index 00000000..3a050382 --- /dev/null +++ b/viewer_examples/plugins/median_filter.py @@ -0,0 +1,19 @@ +from skimage import data +from skimage.filter import median_filter + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.widgets.history import OKCancelButtons, SaveButtons +from skimage.viewer.plugins.base import Plugin + + +image = data.coins() +viewer = ImageViewer(image) + +plugin = Plugin(image_filter=median_filter) +plugin += Slider('radius', 2, 10, value_type='int', update_on='release') +plugin += SaveButtons() +plugin += OKCancelButtons() + +viewer += plugin +viewer.show() diff --git a/viewer_examples/viewers/collection_viewer.py b/viewer_examples/viewers/collection_viewer.py new file mode 100644 index 00000000..62cdbb26 --- /dev/null +++ b/viewer_examples/viewers/collection_viewer.py @@ -0,0 +1,31 @@ +""" +===================== +CollectionViewer demo +===================== + +Demo of CollectionViewer for viewing collections of images. This demo uses +the different layers of the gaussian pyramid as image collection. + +You can scroll through images with the slider, or you can interact with the +viewer using your keyboard: + +left/right arrows + Previous/next image in collection. +number keys, 0--9 + 0% to 90% of collection. For example, "5" goes to the image in the + middle (i.e. 50%) of the collection. +home/end keys + First/last image in collection. + +""" +import numpy as np +from skimage import data +from skimage.viewer import CollectionViewer +from skimage.transform import pyramid_gaussian + + +img = data.lena() +img_collection = tuple(pyramid_gaussian(img)) + +view = CollectionViewer(img_collection) +view.show() diff --git a/viewer_examples/viewers/image_viewer.py b/viewer_examples/viewers/image_viewer.py new file mode 100644 index 00000000..e0932787 --- /dev/null +++ b/viewer_examples/viewers/image_viewer.py @@ -0,0 +1,7 @@ +from skimage import data +from skimage.viewer import ImageViewer + + +image = data.camera() +viewer = ImageViewer(image) +viewer.show()