diff --git a/.gitignore b/.gitignore index 1bd4352c..2623ea74 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ doc/source/auto_examples/images/plot_*.png doc/source/auto_examples/images/thumb doc/source/auto_examples/applications/ doc/source/_static/random.js - +.idea/ +*.log diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..f50ba1ac --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +# 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='' PYVER=2.7 + - PYTHON=python3 PYSUF='3' PYVER=3.2 +install: + - sudo apt-get update # needed for python3-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 easy_install$PYSUF pip + - sudo pip-$PYVER 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-$PYVER --exe -v --cover-package=skimage skimage + diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index baaf064b..fd0656db 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,36 @@ - 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. + +- Olivier Debeir + Rank filters (8- and 16-bits) using sliding window. + +- Luis Pedro Coelho + imread plugin + +- Steven Silvester, Karel Zuiderveld + Adaptive Histogram Equalization + +- Anders Boesen Lindbo Larsen + Dense DAISY feature description, circle perimeter drawing. + +- François Boulogne + Andres Method for circle perimeter, ellipse perimeter drawing. + Circular Hough Transform + +- Thouis Jones + Vectorized operators for arrays of 16-bit ints. 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..daffe705 100644 --- a/DEVELOPMENT.txt +++ b/DEVELOPMENT.txt @@ -1,51 +1,94 @@ Development process ------------------- -:doc:`Read this overview ` of how to use Git with -``skimage``. Here's the long and short of it: +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. + * Push your changes back to GitHub and create a Pull Request by + clicking 'Pull Request' in GitHub. + * Optionally, post on the `mailing list `_ to explain your changes. + +Read these :doc:`detailed documents ` on how to use Git with +``scikit-image`` (``_). + +.. 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 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)) + + * Use `Py_ssize_t` as data type for all indexing, shape and size variables in + C/C++ and Cython code. + Test coverage -````````````` +------------- + Tests for a module should ideally cover all code in that module, -i.e. statement coverage should be at 100%. +i.e., statement coverage should be at 100%. To measure the test coverage, install `coverage.py `__ @@ -63,5 +106,6 @@ 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.md similarity index 78% rename from README.rst rename to README.md index a419b83a..64a7dfe9 100644 --- a/README.rst +++ b/README.md @@ -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 ------------------------ @@ -29,8 +29,3 @@ this path to your PYTHONPATH variable and compiling the extensions: License ------- Please read LICENSE.txt in this directory. - -Contact -------- -Stefan van der Walt - diff --git a/RELEASE.txt b/RELEASE.txt index cea5adb7..e42d1c85 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 - ``rm build -rf; make html`` in the docs. - - Push upstream using "make gh-pages" + - Build a clean version of the docs. Run ``make`` in the root dir, then + ``rm -rf build; make html`` in the docs. + - 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..0046b6a3 100644 --- a/TASKS.txt +++ b/TASKS.txt @@ -1,5 +1,6 @@ .. role:: strike + .. _howto_contribute: How to contribute to ``skimage`` @@ -14,112 +15,15 @@ How to contribute to ``skimage`` cell_profiler -Developing Open Source is great fun! Join us on the `skimage mailing -list `_ and tell us which of the +Developing Open Source is great fun! Join us on the `scikit-image mailing +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 for something to implement, you can find a list of `requested features on GitHub `__. In addition, you can browse the `open issues on GitHub `__. +* The technical detail of the `development process`_ is summed up below. + Refer to the :doc:`gitwash ` for a step-by-step tutorial. .. 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..10ee84f2 100644 --- a/bento.info +++ b/bento.info @@ -1,15 +1,15 @@ -Name: scikits-image -Version: 0.6 +Name: scikit-image +Version: 0.8.0 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 @@ -67,6 +64,9 @@ Library: Extension: skimage.filter._ctmf Sources: skimage/filter/_ctmf.pyx + Extension: skimage.filter._denoise_cy + Sources: + skimage/filter/_denoise_cy.pyx Extension: skimage.morphology.ccomp Sources: skimage/morphology/ccomp.pyx @@ -76,15 +76,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 +88,69 @@ Library: Extension: skimage.graph.heap Sources: skimage/graph/heap.pyx + Extension: skimage.morphology._greyreconstruct + Sources: + skimage/morphology/_greyreconstruct.pyx + Extension: skimage.feature.corner_cy + Sources: + skimage/feature/corner_cy.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 + Extension: skimage.filter.rank._core16 + Sources: + skimage/filter/rank/_core16.pyx + Extension: skimage.filter.rank._crank8 + Sources: + skimage/filter/rank/_crank8.pyx + Extension: skimage.filter.rank._crank16 + Sources: + skimage/filter/rank/_crank16.pyx + Extension: skimage.filter.rank._core8 + Sources: + skimage/filter/rank/_core8.pyx + Extension: skimage.filter.rank.rank + Sources: + skimage/filter/rank/rank.pyx + Extension: skimage.filter.rank.bilateral_rank + Sources: + skimage/filter/rank/bilateral_rank.pyx + Extension: skimage.filter.rank._crank16_percentiles + Sources: + skimage/filter/rank/_crank16_percentiles.pyx + Extension: skimage.filter.rank.percentile_rank + Sources: + skimage/filter/rank/percentile_rank.pyx + Extension: skimage.filter.rank._crank8_percentiles + Sources: + skimage/filter/rank/_crank8_percentiles.pyx + Extension: skimage.filter.rank._crank16_bilateral + Sources: + skimage/filter/rank/_crank16_bilateral.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..34b2272a --- /dev/null +++ b/check_bento_build.py @@ -0,0 +1,95 @@ +""" +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*['\"]([\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 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 + + +def each_cy_in_bento(bento_file='bento.info'): + """Yield path 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:'): + path = line.lstrip('Extension:').strip() + yield path + + +def remove_common_extensions(cy_bento, cy_setup): + # normalize so that cy_setup and cy_bento have the same separator + cy_setup = set(ext.replace('/', '.') for ext in cy_setup) + cy_setup_diff = cy_setup.difference(cy_bento) + cy_setup_diff = set(ext.replace('.', '/') for ext in cy_setup_diff) + cy_bento_diff = cy_bento.difference(cy_setup) + return cy_bento_diff, cy_setup_diff + + +def print_results(cy_bento, cy_setup): + def info(text): + print + print(text) + print('-' * len(text)) + + if not (cy_bento or cy_setup): + print "bento.info and setup.py files match." + + if cy_bento: + info("Extensions found in 'bento.info' but not in any 'setup.py:") + print('\n'.join(cy_bento)) + + + if cy_setup: + info("Extensions found in a 'setup.py' but not in any 'bento.info:") + print('\n'.join(cy_setup)) + info("Consider adding the following to the 'bento.info' Library:") + for dir_path in cy_setup: + module_path = dir_path.replace('/', '.') + print BENTO_TEMPLATE.format(module_path=module_path, + dir_path=dir_path) + + +if __name__ == '__main__': + # All cython extensions defined in 'setup.py' files. + cy_setup = set(each_cy_in_setup('skimage')) + + # All cython extensions defined 'bento.info' file. + cy_bento = set(each_cy_in_bento()) + + cy_bento, cy_setup = 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/__init__.py b/doc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/doc/examples/applications/plot_coins_segmentation.py b/doc/examples/applications/plot_coins_segmentation.py index f46bbd33..0d1d8a34 100644 --- a/doc/examples/applications/plot_coins_segmentation.py +++ b/doc/examples/applications/plot_coins_segmentation.py @@ -90,12 +90,8 @@ plt.title('Filling the holes') Small spurious objects are easily removed by setting a minimum size for valid objects. """ - -label_objects, nb_labels = ndimage.label(fill_coins) -sizes = np.bincount(label_objects.ravel()) -mask_sizes = sizes > 20 -mask_sizes[0] = 0 -coins_cleaned = mask_sizes[label_objects] +from skimage import morphology +coins_cleaned = morphology.remove_small_objects(fill_coins, 21) plt.figure(figsize=(4, 3)) plt.imshow(coins_cleaned, cmap=plt.cm.gray, interpolation='nearest') @@ -149,8 +145,7 @@ plt.title('markers') Finally, we use the watershed transform to fill regions of the elevation map starting from the markers determined above: """ -from skimage.morphology import watershed -segmentation = watershed(elevation_map, markers) +segmentation = morphology.watershed(elevation_map, markers) plt.figure(figsize=(4, 3)) plt.imshow(segmentation, cmap=plt.cm.gray, interpolation='nearest') 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/applications/plot_rank_filters.py b/doc/examples/applications/plot_rank_filters.py new file mode 100644 index 00000000..c182671d --- /dev/null +++ b/doc/examples/applications/plot_rank_filters.py @@ -0,0 +1,719 @@ +""" +============ +Rank filters +============ + +Rank filters are non-linear filters using the local greylevels ordering to +compute the filtered value. This ensemble of filters share a common base: the +local grey-level histogram extraction computed on the neighborhood of a pixel +(defined by a 2D structuring element). If the filtered value is taken as the +middle value of the histogram, we get the classical median filter. + +Rank filters can be used for several purposes such as: + +* image quality enhancement + e.g. image smoothing, sharpening + +* image pre-processing + e.g. noise reduction, contrast enhancement + +* feature extraction + e.g. border detection, isolated point detection + +* post-processing + e.g. small object removal, object grouping, contour smoothing + +Some well known filters are specific cases of rank filters [1]_ e.g. +morphological dilation, morphological erosion, median filters. + +The different implementation availables in `skimage` are compared. + +In this example, we will see how to filter a greylevel image using some of the +linear and non-linear filters availables in skimage. We use the `camera` +image from `skimage.data`. + +.. [1] Pierre Soille, On morphological operators based on rank filters, Pattern + Recognition 35 (2002) 527-535. + +""" + +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data + +ima = data.camera() +hist = np.histogram(ima, bins=np.arange(0, 256)) + +plt.figure(figsize=(8, 3)) +plt.subplot(1, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray, interpolation='nearest') +plt.axis('off') +plt.subplot(1, 2, 2) +plt.plot(hist[1][:-1], hist[0], lw=2) +plt.title('histogram of grey values') + +""" + +.. image:: PLOT2RST.current_figure + +Noise removal +============= + +Some noise is added to the image, 1% of pixels are randomly set to 255, 1% are +randomly set to 0. The **median** filter is applied to remove the noise. + +.. note:: + + there are different implementations of median filter : + `skimage.filter.median_filter` and `skimage.filter.rank.median` + +""" + +noise = np.random.random(ima.shape) +nima = data.camera() +nima[noise > 0.99] = 255 +nima[noise < 0.01] = 0 + +from skimage.filter.rank import median +from skimage.morphology import disk + +fig = plt.figure(figsize=[10, 7]) + +lo = median(nima, disk(1)) +hi = median(nima, disk(5)) +ext = median(nima, disk(20)) +plt.subplot(2, 2, 1) +plt.imshow(nima, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('noised image') +plt.subplot(2, 2, 2) +plt.imshow(lo, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('median $r=1$') +plt.subplot(2, 2, 3) +plt.imshow(hi, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('median $r=5$') +plt.subplot(2, 2, 4) +plt.imshow(ext, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('median $r=20$') + +""" + +.. image:: PLOT2RST.current_figure + +The added noise is efficiently removed, as the image defaults are small (1 pixel +wide), a small filter radius is sufficient. As the radius is increasing, objects +with a bigger size are filtered as well, such as the camera tripod. The median +filter is commonly used for noise removal because borders are preserved. + +Image smoothing +================ + +The example hereunder shows how a local **mean** smoothes the camera man image. + +""" + +from skimage.filter.rank import mean + +fig = plt.figure(figsize=[10, 7]) + +loc_mean = mean(nima, disk(10)) +plt.subplot(1, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('original') +plt.subplot(1, 2, 2) +plt.imshow(loc_mean, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('local mean $r=10$') + +""" + +.. image:: PLOT2RST.current_figure + +One may be interested in smoothing an image while preserving important borders +(median filters already achieved this), here we use the **bilateral** filter +that restricts the local neighborhood to pixel having a greylevel similar to +the central one. + +.. note:: + + a different implementation is available for color images in + `skimage.filter.denoise_bilateral`. + +""" + +from skimage.filter.rank import bilateral_mean + +ima = data.camera() +selem = disk(10) + +bilat = bilateral_mean(ima.astype(np.uint16), disk(20), s0=10, s1=10) + +# display results +fig = plt.figure(figsize=[10, 7]) +plt.subplot(2, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray) +plt.xlabel('original') +plt.subplot(2, 2, 3) +plt.imshow(bilat, cmap=plt.cm.gray) +plt.xlabel('bilateral mean') +plt.subplot(2, 2, 2) +plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.subplot(2, 2, 4) +plt.imshow(bilat[200:350, 350:450], cmap=plt.cm.gray) + +""" + +.. image:: PLOT2RST.current_figure + +One can see that the large continuous part of the image (e.g. sky) is smoothed +whereas other details are preserved. + + +Contrast enhancement +==================== + +We compare here how the global histogram equalization is applied locally. + +The equalized image [2]_ has a roughly linear cumulative distribution function +for each pixel neighborhood. The local version [3]_ of the histogram +equalization emphasizes every local greylevel variations. + +.. [2] http://en.wikipedia.org/wiki/Histogram_equalization +.. [3] http://en.wikipedia.org/wiki/Adaptive_histogram_equalization + +""" + +from skimage import exposure +from skimage.filter import rank + +ima = data.camera() +# equalize globally and locally +glob = exposure.equalize(ima) * 255 +loc = rank.equalize(ima, disk(20)) + +# extract histogram for each image +hist = np.histogram(ima, bins=np.arange(0, 256)) +glob_hist = np.histogram(glob, bins=np.arange(0, 256)) +loc_hist = np.histogram(loc, bins=np.arange(0, 256)) + +plt.figure(figsize=(10, 10)) +plt.subplot(321) +plt.imshow(ima, cmap=plt.cm.gray, interpolation='nearest') +plt.axis('off') +plt.subplot(322) +plt.plot(hist[1][:-1], hist[0], lw=2) +plt.title('histogram of grey values') +plt.subplot(323) +plt.imshow(glob, cmap=plt.cm.gray, interpolation='nearest') +plt.axis('off') +plt.subplot(324) +plt.plot(glob_hist[1][:-1], glob_hist[0], lw=2) +plt.title('histogram of grey values') +plt.subplot(325) +plt.imshow(loc, cmap=plt.cm.gray, interpolation='nearest') +plt.axis('off') +plt.subplot(326) +plt.plot(loc_hist[1][:-1], loc_hist[0], lw=2) +plt.title('histogram of grey values') + +""" + +.. image:: PLOT2RST.current_figure + +another way to maximize the number of greylevels used for an image is to apply +a local autoleveling, i.e. here a pixel greylevel is proportionally remapped +between local minimum and local maximum. + +The following example shows how local autolevel enhances the camara man picture. + +""" + +from skimage.filter.rank import autolevel + +ima = data.camera() +selem = disk(10) + +auto = autolevel(ima.astype(np.uint16), disk(20)) + +# display results +fig = plt.figure(figsize=[10, 7]) +plt.subplot(1, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray) +plt.xlabel('original') +plt.subplot(1, 2, 2) +plt.imshow(auto, cmap=plt.cm.gray) +plt.xlabel('local autolevel') + +""" + +.. image:: PLOT2RST.current_figure + +This filter is very sensitive to local outlayers, see the little white spot in +the sky left part. This is due to a local maximum which is very high comparing +to the rest of the neighborhood. One can moderate this using the percentile +version of the autolevel filter which uses given percentiles (one inferior, +one superior) in place of local minimum and maximum. The example below +illustrates how the percentile parameters influence the local autolevel result. + +""" + +from skimage.filter.rank import percentile_autolevel + +image = data.camera() + +selem = disk(20) +loc_autolevel = autolevel(image, selem=selem) +loc_perc_autolevel0 = percentile_autolevel(image, selem=selem, p0=.00, p1=1.0) +loc_perc_autolevel1 = percentile_autolevel(image, selem=selem, p0=.01, p1=.99) +loc_perc_autolevel2 = percentile_autolevel(image, selem=selem, p0=.05, p1=.95) +loc_perc_autolevel3 = percentile_autolevel(image, selem=selem, p0=.1, p1=.9) + +fig, axes = plt.subplots(nrows=3, figsize=(7, 8)) +ax0, ax1, ax2 = axes +plt.gray() + +ax0.imshow(np.hstack((image, loc_autolevel))) +ax0.set_title('original / autolevel') + +ax1.imshow( + np.hstack((loc_perc_autolevel0, loc_perc_autolevel1)), vmin=0, vmax=255) +ax1.set_title('percentile autolevel 0%,1%') +ax2.imshow( + np.hstack((loc_perc_autolevel2, loc_perc_autolevel3)), vmin=0, vmax=255) +ax2.set_title('percentile autolevel 5% and 10%') + +for ax in axes: + ax.axis('off') + +""" + +.. image:: PLOT2RST.current_figure + +The morphological contrast enhancement filter replaces the central pixel by the +local maximum if the original pixel value is closest to local maximum, otherwise +by the minimum local. + +""" + +from skimage.filter.rank import morph_contr_enh + +ima = data.camera() + +enh = morph_contr_enh(ima, disk(5)) + +# display results +fig = plt.figure(figsize=[10, 7]) +plt.subplot(2, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray) +plt.xlabel('original') +plt.subplot(2, 2, 3) +plt.imshow(enh, cmap=plt.cm.gray) +plt.xlabel('local morphlogical contrast enhancement') +plt.subplot(2, 2, 2) +plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.subplot(2, 2, 4) +plt.imshow(enh[200:350, 350:450], cmap=plt.cm.gray) + +""" + +.. image:: PLOT2RST.current_figure + +The percentile version of the local morphological contrast enhancement uses +percentile *p0* and *p1* instead of the local minimum and maximum. + +""" + +from skimage.filter.rank import percentile_morph_contr_enh + +ima = data.camera() + +penh = percentile_morph_contr_enh(ima, disk(5), p0=.1, p1=.9) + +# display results +fig = plt.figure(figsize=[10, 7]) +plt.subplot(2, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray) +plt.xlabel('original') +plt.subplot(2, 2, 3) +plt.imshow(penh, cmap=plt.cm.gray) +plt.xlabel('local percentile morphlogical\n contrast enhancement') +plt.subplot(2, 2, 2) +plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.subplot(2, 2, 4) +plt.imshow(penh[200:350, 350:450], cmap=plt.cm.gray) + +""" + +.. image:: PLOT2RST.current_figure + +Image threshold +=============== + +The Otsu's threshold [1]_ method can be applied locally using the local +greylevel distribution. In the example below, for each pixel, an "optimal" +threshold is determined by maximizing the variance between two classes of pixels +of the local neighborhood defined by a structuring element. + +The example compares the local threshold with the global threshold +`skimage.filter.threshold_otsu`. + +.. note:: + + Local thresholding is much slower than global one. There exists a function + for global Otsu thresholding: `skimage.filter.threshold_otsu`. + +.. [1] http://en.wikipedia.org/wiki/Otsu's_method + +""" + +from skimage.filter.rank import otsu +from skimage.filter import threshold_otsu + +p8 = data.page() + +radius = 10 +selem = disk(radius) + +# t_loc_otsu is an image +t_loc_otsu = otsu(p8, selem) +loc_otsu = p8 >= t_loc_otsu + +# t_glob_otsu is a scalar +t_glob_otsu = threshold_otsu(p8) +glob_otsu = p8 >= t_glob_otsu + +plt.figure() +plt.subplot(2, 2, 1) +plt.imshow(p8, cmap=plt.cm.gray) +plt.xlabel('original') +plt.colorbar() +plt.subplot(2, 2, 2) +plt.imshow(t_loc_otsu, cmap=plt.cm.gray) +plt.xlabel('local Otsu ($radius=%d$)' % radius) +plt.colorbar() +plt.subplot(2, 2, 3) +plt.imshow(p8 >= t_loc_otsu, cmap=plt.cm.gray) +plt.xlabel('original>=local Otsu' % t_glob_otsu) +plt.subplot(2, 2, 4) +plt.imshow(glob_otsu, cmap=plt.cm.gray) +plt.xlabel('global Otsu ($t=%d$)' % t_glob_otsu) + +""" + +.. image:: PLOT2RST.current_figure + +The following example shows how local Otsu's threshold handles a global level +shift applied to a synthetic image . + +""" + +n = 100 +theta = np.linspace(0, 10 * np.pi, n) +x = np.sin(theta) +m = (np.tile(x, (n, 1)) * np.linspace(0.1, 1, n) * 128 + 128).astype(np.uint8) + +radius = 10 +t = rank.otsu(m, disk(radius)) +plt.figure() +plt.subplot(1, 2, 1) +plt.imshow(m) +plt.xlabel('original') +plt.subplot(1, 2, 2) +plt.imshow(m >= t, interpolation='nearest') +plt.xlabel('local Otsu ($radius=%d$)' % radius) + +""" + +.. image:: PLOT2RST.current_figure + +Image morphology +================ + +Local maximum and local minimum are the base operators for greylevel +morphology. + +.. note:: + + `skimage.dilate` and `skimage.erode` are equivalent filters (see below for + comparison). + +Here is an example of the classical morphological greylevel filters: opening, +closing and morphological gradient. + +""" + +from skimage.filter.rank import maximum, minimum, gradient + +ima = data.camera() + +closing = maximum(minimum(ima, disk(5)), disk(5)) +opening = minimum(maximum(ima, disk(5)), disk(5)) +grad = gradient(ima, disk(5)) + +# display results +fig = plt.figure(figsize=[10, 7]) +plt.subplot(2, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray) +plt.xlabel('original') +plt.subplot(2, 2, 2) +plt.imshow(closing, cmap=plt.cm.gray) +plt.xlabel('greylevel closing') +plt.subplot(2, 2, 3) +plt.imshow(opening, cmap=plt.cm.gray) +plt.xlabel('greylevel opening') +plt.subplot(2, 2, 4) +plt.imshow(grad, cmap=plt.cm.gray) +plt.xlabel('morphological gradient') + +""" + +.. image:: PLOT2RST.current_figure + +Feature extraction +=================== + +Local histogram can be exploited to compute local entropy, which is related to +the local image complexity. Entropy is computed using base 2 logarithm i.e. the +filter returns the minimum number of bits needed to encode local greylevel +distribution. + +`skimage.rank.entropy` returns local entropy on a given structuring element. +The following example shows this filter applied on 8- and 16- bit images. + +.. note:: + + to better use the available image bit, the function returns 10x entropy for + 8-bit images and 1000x entropy for 16-bit images. + +""" + +from skimage import data +from skimage.filter.rank import entropy +from skimage.morphology import disk +import numpy as np +import matplotlib.pyplot as plt + +# defining a 8- and a 16-bit test images +a8 = data.camera() +a16 = data.camera().astype(np.uint16) * 4 + +ent8 = entropy(a8, disk(5)) # pixel value contain 10x the local entropy +ent16 = entropy(a16, disk(5)) # pixel value contain 1000x the local entropy + +# display results +plt.figure(figsize=(10, 10)) + +plt.subplot(2, 2, 1) +plt.imshow(a8, cmap=plt.cm.gray) +plt.xlabel('8-bit image') +plt.colorbar() + +plt.subplot(2, 2, 2) +plt.imshow(ent8, cmap=plt.cm.jet) +plt.xlabel('entropy*10') +plt.colorbar() + +plt.subplot(2, 2, 3) +plt.imshow(a16, cmap=plt.cm.gray) +plt.xlabel('16-bit image') +plt.colorbar() + +plt.subplot(2, 2, 4) +plt.imshow(ent16, cmap=plt.cm.jet) +plt.xlabel('entropy*1000') +plt.colorbar() + +""" + +.. image:: PLOT2RST.current_figure + +Implementation +================ + +The central part of the `skimage.rank` filters is build on a sliding window that +update local greylevel histogram. This approach limits the algorithm complexity +to O(n) where n is the number of image pixels. The complexity is also limited +with respect to the structuring element size. + +""" + +from time import time + +from scipy.ndimage.filters import percentile_filter +from skimage.morphology import dilation +from skimage.filter import median_filter +from skimage.filter.rank import median, maximum + + +def exec_and_timeit(func): + """Decorator that returns both function results and execution time.""" + def wrapper(*arg): + t1 = time() + res = func(*arg) + t2 = time() + ms = (t2 - t1) * 1000.0 + return (res, ms) + return wrapper + + +@exec_and_timeit +def cr_med(image, selem): + return median(image=image, selem=selem) + + +@exec_and_timeit +def cr_max(image, selem): + return maximum(image=image, selem=selem) + + +@exec_and_timeit +def cm_dil(image, selem): + return dilation(image=image, selem=selem) + + +@exec_and_timeit +def ctmf_med(image, radius): + return median_filter(image=image, radius=radius) + + +@exec_and_timeit +def ndi_med(image, n): + return percentile_filter(image, 50, size=n * 2 - 1) + +""" + +Comparison between + +* `rank.maximum` +* `cmorph.dilate` + +on increasing structuring element size + +""" + +a = data.camera() + +rec = [] +e_range = range(1, 20, 2) +for r in e_range: + elem = disk(r + 1) + rc, ms_rc = cr_max(a, elem) + rcm, ms_rcm = cm_dil(a, elem) + rec.append((ms_rc, ms_rcm)) + +rec = np.asarray(rec) + +plt.figure() +plt.title('increasing element size') +plt.ylabel('time (ms)') +plt.xlabel('element radius') +plt.plot(e_range, rec) +plt.legend(['crank.maximum', 'cmorph.dilate']) + +""" + +and increasing image size + +.. image:: PLOT2RST.current_figure + +""" + +r = 9 +elem = disk(r + 1) + +rec = [] +s_range = range(100, 1000, 100) +for s in s_range: + a = (np.random.random((s, s)) * 256).astype('uint8') + (rc, ms_rc) = cr_max(a, elem) + (rcm, ms_rcm) = cm_dil(a, elem) + rec.append((ms_rc, ms_rcm)) + +rec = np.asarray(rec) + +plt.figure() +plt.title('increasing image size') +plt.ylabel('time (ms)') +plt.xlabel('image size') +plt.plot(s_range, rec) +plt.legend(['crank.maximum', 'cmorph.dilate']) + + +""" + +.. image:: PLOT2RST.current_figure + +Comparison between: + +* `rank.median` +* `ctmf.median_filter` +* `ndimage.percentile` + +on increasing structuring element size + +""" + +a = data.camera() + +rec = [] +e_range = range(2, 30, 4) +for r in e_range: + elem = disk(r + 1) + rc, ms_rc = cr_med(a, elem) + rctmf, ms_rctmf = ctmf_med(a, r) + rndi, ms_ndi = ndi_med(a, r) + rec.append((ms_rc, ms_rctmf, ms_ndi)) + +rec = np.asarray(rec) + +plt.figure() +plt.title('increasing element size') +plt.plot(e_range, rec) +plt.legend(['rank.median', 'ctmf.median_filter', 'ndimage.percentile']) +plt.ylabel('time (ms)') +plt.xlabel('element radius') + +""" +.. image:: PLOT2RST.current_figure + +comparison of outcome of the three methods + +""" + +plt.figure() +plt.imshow(np.hstack((rc, rctmf, rndi))) +plt.xlabel('rank.median vs ctmf.median_filter vs ndimage.percentile') + +""" +.. image:: PLOT2RST.current_figure + +and increasing image size + +""" + +r = 9 +elem = disk(r + 1) + +rec = [] +s_range = [100, 200, 500, 1000] +for s in s_range: + a = (np.random.random((s, s)) * 256).astype('uint8') + (rc, ms_rc) = cr_med(a, elem) + rctmf, ms_rctmf = ctmf_med(a, r) + rndi, ms_ndi = ndi_med(a, r) + rec.append((ms_rc, ms_rctmf, ms_ndi)) + +rec = np.asarray(rec) + +plt.figure() +plt.title('increasing image size') +plt.plot(s_range, rec) +plt.legend(['rank.median', 'ctmf.median_filter', 'ndimage.percentile']) +plt.ylabel('time (ms)') +plt.xlabel('image size') + +""" +.. image:: PLOT2RST.current_figure + +""" + +plt.show() diff --git a/doc/examples/plot_16bitbilateral.py b/doc/examples/plot_16bitbilateral.py new file mode 100644 index 00000000..473fcadd --- /dev/null +++ b/doc/examples/plot_16bitbilateral.py @@ -0,0 +1,47 @@ +""" +============================== +Bilateral mean +============================== +This example compares + +* local mean +* percentile mean +* bilateral mean + +build on the local histogram distribution +local mean uses all pixels belonging to the structuring element to compute average gray level, +percentile mean uses only values between percentiles p0 and p1 (here 10% and 90%), +whereas bilateral mean uses only pixels of the structuring element having a gray level situated inside +g-s0 and g+s1 (here g-500 and g+500). +The filters are applied on a 16 bit image (actual bitdepth is 12bit). + +Percentile and usual mean give here similar results, these filters smooth the complete image (background and details). +Bilateral mean exhibits a high filtering rate for continuous area (i.e. background) while image higher frequencies +remains untouched. + +""" +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data +from skimage.morphology import disk +import skimage.filter.rank as rank + +a16 = (data.coins()).astype('uint16') * 16 +selem = disk(20) + +f1 = rank.percentile_mean(a16, selem=selem, p0=.1, p1=.9) +f2 = rank.bilateral_mean(a16, selem=selem, s0=500, s1=500) +f3 = rank.mean(a16, selem=selem) + +# display results +fig, axes = plt.subplots(nrows=3, figsize=(15, 10)) +ax0, ax1, ax2 = axes + +ax0.imshow(np.hstack((a16, f1))) +ax0.set_title('percentile mean') +ax1.imshow(np.hstack((a16, f2))) +ax1.set_title('bilateral mean') +ax2.imshow(np.hstack((a16, f3))) +ax2.set_title('local mean') +plt.show() diff --git a/doc/examples/plot_circular_hough_transform.py b/doc/examples/plot_circular_hough_transform.py new file mode 100755 index 00000000..d2f8f2ae --- /dev/null +++ b/doc/examples/plot_circular_hough_transform.py @@ -0,0 +1,72 @@ +""" +======================== +Circular Hough Transform +======================== + +The Hough transform in its simplest form is a `method to detect +straight lines `__ +but it can also be used to detect circles. + +In the following example, the Hough transform is used to detect +coin positions and match their edges. We provide a range of +plausible radii. For each radius, two circles are extracted and +we finally keep the five most prominent candidates. +The result shows that coin positions are well-detected. + + +Algorithm overview +------------------ + +Given a black circle on a white background, we first guess its +radius (or a range of radii) to construct a new circle. +This circle is applied on each black pixel of the original picture +and the coordinates of this circle are voting in an accumulator. +From this geometrical construction, the original circle center +position receives the highest score. + +Note that the accumulator size is built to be larger than the +original picture in order to detect centers outside the frame. +Its size is extended by two times the larger radius. + +""" + + +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data, filter, color +from skimage.transform import hough_circle +from skimage.feature import peak_local_max +from skimage.draw import circle_perimeter + +# Load picture and detect edges +image = data.coins()[0:95, 70:370] +edges = filter.canny(image, sigma=3, low_threshold=10, high_threshold=50) + +fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) + +# Detect two radii +hough_radii = np.arange(15, 30, 2) +hough_res = hough_circle(edges, hough_radii) + +centers = [] +accums = [] +radii = [] + +for radius, h in zip(hough_radii, hough_res): + # For each radius, extract two circles + peaks = peak_local_max(h, num_peaks=2) + centers.extend(peaks - hough_radii.max()) + accums.extend(h[peaks[:, 0], peaks[:, 1]]) + radii.extend([radius, radius]) + +# Draw the most prominent 5 circles +image = color.gray2rgb(image) +for idx in np.argsort(accums)[::-1][:5]: + center_x, center_y = centers[idx] + radius = radii[idx] + cx, cy = circle_perimeter(center_y, center_x, radius) + image[cy, cx] = (220, 20, 20) + +ax.imshow(image, cmap=plt.cm.gray) +plt.show() diff --git a/doc/examples/plot_corner.py b/doc/examples/plot_corner.py new file mode 100644 index 00000000..30f1f0cf --- /dev/null +++ b/doc/examples/plot_corner.py @@ -0,0 +1,37 @@ +""" +================ +Corner detection +================ + +Detect corner points using the Harris corner detector and determine subpixel +position of corners. + +.. [1] http://en.wikipedia.org/wiki/Corner_detection +.. [2] http://en.wikipedia.org/wiki/Interest_point_detection + +""" + +from matplotlib import pyplot as plt + +from skimage import data +from skimage.feature import corner_harris, corner_subpix, corner_peaks +from skimage.transform import warp, AffineTransform +from skimage.draw import ellipse + +tform = AffineTransform(scale=(1.3, 1.1), rotation=1, shear=0.7, + translation=(210, 50)) +image = warp(data.checkerboard(), tform.inverse, output_shape=(350, 350)) +rr, cc = ellipse(310, 175, 10, 100) +image[rr, cc] = 1 +image[180:230, 10:60] = 1 +image[230:280, 60:110] = 1 + +coords = corner_peaks(corner_harris(image), min_distance=5) +coords_subpix = corner_subpix(image, coords, window_size=13) + +plt.gray() +plt.imshow(image, interpolation='nearest') +plt.plot(coords[:, 1], coords[:, 0], '.b', markersize=3) +plt.plot(coords_subpix[:, 1], coords_subpix[:, 0], '+r', markersize=15) +plt.axis((0, 350, 350, 0)) +plt.show() diff --git a/doc/examples/plot_daisy.py b/doc/examples/plot_daisy.py new file mode 100644 index 00000000..af78103d --- /dev/null +++ b/doc/examples/plot_daisy.py @@ -0,0 +1,28 @@ +""" +=============================== +Dense DAISY feature description +=============================== + +The DAISY local image descriptor is based on gradient orientation histograms +similar to the SIFT descriptor. It is formulated in a way that allows for fast +dense extraction which is useful for e.g. bag-of-features image +representations. + +In this example a limited number of DAISY descriptors are extracted at a large +scale for illustrative purposes. +""" + +from skimage.feature import daisy +from skimage import data +import matplotlib.pyplot as plt + + +img = data.camera() +descs, descs_img = daisy(img, step=180, radius=58, rings=2, histograms=6, + orientations=8, visualize=True) + +plt.axis('off') +plt.imshow(descs_img) +descs_num = descs.shape[0] * descs.shape[1] +plt.title('%i DAISY descriptors extracted:' % descs_num) +plt.show() diff --git a/doc/examples/plot_denoise.py b/doc/examples/plot_denoise.py new file mode 100644 index 00000000..debb2ea6 --- /dev/null +++ b/doc/examples/plot_denoise.py @@ -0,0 +1,68 @@ +""" +============================= +Denoising the picture of Lena +============================= + +In this example, we denoise a noisy version of the picture of Lena using the +total variation and bilateral denoising filter. + +These algorithms typically produce "posterized" images with flat domains +separated by sharp edges. It is possible to change the degree of posterization +by controlling the tradeoff between denoising and faithfulness to the original +image. + +Total variation filter +---------------------- + +The result of this filter is an image that has a minimal total variation norm, +while being as close to the initial image as possible. The total variation is +the L1 norm of the gradient of the image. + +Bilateral filter +---------------- + +A bilateral filter is an edge-preserving and noise reducing filter. It averages +pixels based on their spatial closeness and radiometric similarity. + +""" + +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data, color, img_as_float +from skimage.filter import denoise_tv_chambolle, denoise_bilateral + +lena = img_as_float(data.lena()) +lena = lena[220:300, 220:320] + +noisy = lena + 0.6 * lena.std() * np.random.random(lena.shape) +noisy = np.clip(noisy, 0, 1) + +fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(8, 5)) + +plt.gray() + +ax[0, 0].imshow(noisy) +ax[0, 0].axis('off') +ax[0, 0].set_title('noisy') +ax[0, 1].imshow(denoise_tv_chambolle(noisy, weight=0.1, multichannel=True)) +ax[0, 1].axis('off') +ax[0, 1].set_title('TV') +ax[0, 2].imshow(denoise_bilateral(noisy, sigma_range=0.05, sigma_spatial=15)) +ax[0, 2].axis('off') +ax[0, 2].set_title('Bilateral') + +ax[1, 0].imshow(denoise_tv_chambolle(noisy, weight=0.2, multichannel=True)) +ax[1, 0].axis('off') +ax[1, 0].set_title('(more) TV') +ax[1, 1].imshow(denoise_bilateral(noisy, sigma_range=0.1, sigma_spatial=15)) +ax[1, 1].axis('off') +ax[1, 1].set_title('(more) Bilateral') +ax[1, 2].imshow(lena) +ax[1, 2].axis('off') +ax[1, 2].set_title('original') + +fig.subplots_adjust(wspace=0.02, hspace=0.2, + top=0.9, bottom=0.05, left=0, right=1) + +plt.show() diff --git a/doc/examples/plot_entropy.py b/doc/examples/plot_entropy.py new file mode 100644 index 00000000..f019d79c --- /dev/null +++ b/doc/examples/plot_entropy.py @@ -0,0 +1,44 @@ +""" +=================== +Entropy +=================== + + +""" +from skimage import data +from skimage.filter.rank import entropy +from skimage.morphology import disk +import numpy as np +import matplotlib.pyplot as plt + +# defining a 8- and a 16-bit test images +a8 = data.camera() +a16 = data.camera().astype(np.uint16)*4 + +ent8 = entropy(a8,disk(5)) # pixel value contain 10x the local entropy +ent16 = entropy(a16,disk(5)) # pixel value contain 1000x the local entropy + +# display results +plt.figure(figsize=(10, 10)) + +plt.subplot(2,2,1) +plt.imshow(a8, cmap=plt.cm.gray) +plt.xlabel('8-bit image') +plt.colorbar() + +plt.subplot(2,2,2) +plt.imshow(ent8, cmap=plt.cm.jet) +plt.xlabel('entropy*10') +plt.colorbar() + +plt.subplot(2,2,3) +plt.imshow(a16, cmap=plt.cm.gray) +plt.xlabel('16-bit image') +plt.colorbar() + +plt.subplot(2,2,4) +plt.imshow(ent16, cmap=plt.cm.jet) +plt.xlabel('entropy*1000') +plt.colorbar() +plt.show() + diff --git a/doc/examples/plot_equalize.py b/doc/examples/plot_equalize.py index 1e1291d8..3569c4ee 100644 --- a/doc/examples/plot_equalize.py +++ b/doc/examples/plot_equalize.py @@ -18,18 +18,18 @@ that fall within the 2nd and 98th percentiles [2]_. """ -from skimage import data -from skimage.util.dtype import dtype_range +from skimage import data, img_as_float from skimage import exposure import matplotlib.pyplot as plt - import numpy as np + def plot_img_and_hist(img, axes, bins=256): """Plot an image along with its histogram and cumulative histogram. """ + img = img_as_float(img) ax_img, ax_hist = axes ax_cdf = ax_hist.twinx() @@ -38,16 +38,16 @@ def plot_img_and_hist(img, axes, bins=256): ax_img.set_axis_off() # Display histogram - ax_hist.hist(img.ravel(), bins=bins) + ax_hist.hist(img.ravel(), bins=bins, histtype='step', color='black') ax_hist.ticklabel_format(axis='y', style='scientific', scilimits=(0, 0)) ax_hist.set_xlabel('Pixel intensity') - - xmin, xmax = dtype_range[img.dtype.type] - ax_hist.set_xlim(xmin, xmax) + ax_hist.set_xlim(0, 1) + ax_hist.set_yticks([]) # Display cumulative distribution img_cdf, bins = exposure.cumulative_distribution(img, bins) ax_cdf.plot(bins, img_cdf, 'r') + ax_cdf.set_yticks([]) return ax_img, ax_hist, ax_cdf @@ -61,25 +61,33 @@ p98 = np.percentile(img, 98) img_rescale = exposure.rescale_intensity(img, in_range=(p2, p98)) # Equalization -img_eq = exposure.equalize(img) +img_eq = exposure.equalize_hist(img) +# Adaptive Equalization +img_adapteq = exposure.equalize_adapthist(img, clip_limit=0.03) # Display results -f, axes = plt.subplots(2, 3, figsize=(8, 4)) +f, axes = plt.subplots(2, 4, figsize=(8, 4)) ax_img, ax_hist, ax_cdf = plot_img_and_hist(img, axes[:, 0]) ax_img.set_title('Low contrast image') + +y_min, y_max = ax_hist.get_ylim() ax_hist.set_ylabel('Number of pixels') +ax_hist.set_yticks(np.linspace(0, y_max, 5)) ax_img, ax_hist, ax_cdf = plot_img_and_hist(img_rescale, axes[:, 1]) ax_img.set_title('Contrast stretching') ax_img, ax_hist, ax_cdf = plot_img_and_hist(img_eq, axes[:, 2]) ax_img.set_title('Histogram equalization') -ax_cdf.set_ylabel('Fraction of total intensity') +ax_img, ax_hist, ax_cdf = plot_img_and_hist(img_adapteq, axes[:, 3]) +ax_img.set_title('Adaptive equalization') + +ax_cdf.set_ylabel('Fraction of total intensity') +ax_cdf.set_yticks(np.linspace(0, 1, 5)) # prevent overlap of y-axis labels plt.subplots_adjust(wspace=0.4) plt.show() - diff --git a/doc/examples/plot_harris.py b/doc/examples/plot_harris.py deleted file mode 100644 index 6212ac4a..00000000 --- a/doc/examples/plot_harris.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -=============================================================================== -Harris Corner detector -=============================================================================== - -The Harris corner filter [1]_ detects "interest points" [2]_ using edge -detection in multiple directions. - -.. [1] http://en.wikipedia.org/wiki/Corner_detection -.. [2] http://en.wikipedia.org/wiki/Interest_point_detection -""" -import numpy as np -from matplotlib import pyplot as plt - -from skimage import data, img_as_float -from skimage.feature import harris - - -def plot_harris_points(image, filtered_coords): - """ plots corners found in image""" - - plt.imshow(image) - y, x = np.transpose(filtered_coords) - plt.plot(x, y, 'b.') - plt.axis('off') - -# display results -plt.figure(figsize=(8, 3)) -im_lena = img_as_float(data.lena()) -im_text = img_as_float(data.text()) - -filtered_coords = harris(im_lena, min_distance=4) - -plt.axes([0, 0, 0.3, 0.95]) -plot_harris_points(im_lena, filtered_coords) - -filtered_coords = harris(im_text, min_distance=4) - -plt.axes([0.2, 0, 0.77, 1]) -plot_harris_points(im_text, filtered_coords) - -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..b74132da 100644 --- a/doc/examples/plot_hough_transform.py +++ b/doc/examples/plot_hough_transform.py @@ -59,7 +59,7 @@ References ''' -from skimage.transform import hough, probabilistic_hough +from skimage.transform import hough, hough_peaks, probabilistic_hough from skimage.filter import canny from skimage import data @@ -81,11 +81,11 @@ h, theta, d = hough(image) plt.figure(figsize=(8, 4)) -plt.subplot(121) +plt.subplot(131) plt.imshow(image, cmap=plt.cm.gray) plt.title('Input image') -plt.subplot(122) +plt.subplot(132) plt.imshow(np.log(1 + h), extent=[np.rad2deg(theta[-1]), np.rad2deg(theta[0]), d[-1], d[0]], @@ -94,6 +94,15 @@ plt.title('Hough transform') plt.xlabel('Angles (degrees)') plt.ylabel('Distance (pixels)') +plt.subplot(133) +plt.imshow(image, cmap=plt.cm.gray) +rows, cols = image.shape +for _, angle, dist in zip(*hough_peaks(h, theta, d)): + y0 = (dist - 0 * np.cos(angle)) / np.sin(angle) + y1 = (dist - cols * np.cos(angle)) / np.sin(angle) + plt.plot((0, cols), (y0, y1), '-r') +plt.axis((0, cols, rows, 0)) +plt.title('Detected lines') # Line finding, using the Probabilistic Hough Transform @@ -109,7 +118,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) @@ -118,7 +127,6 @@ for line in lines: p0, p1 = line plt.plot((p0[0], p1[0]), (p0[1], p1[1])) -plt.title('Lines found with PHT') +plt.title('Probabilistic Hough') plt.axis('image') plt.show() - diff --git a/doc/examples/plot_join_segmentations.py b/doc/examples/plot_join_segmentations.py new file mode 100644 index 00000000..02600ba6 --- /dev/null +++ b/doc/examples/plot_join_segmentations.py @@ -0,0 +1,69 @@ +""" +========================================== +Find the intersection of two segmentations +========================================== + +When segmenting an image, you may want to combine multiple alternative +segmentations. The `skimage.segmentation.join_segmentations` function +computes the join of two segmentations, in which a pixel is placed in +the same segment if and only if it is in the same segment in _both_ +segmentations. +""" + +import numpy as np +from scipy import ndimage as nd +import matplotlib.pyplot as plt +import matplotlib as mpl + +from skimage.filter import sobel +from skimage.segmentation import slic, join_segmentations +from skimage.morphology import watershed + +from skimage import data + +coins = data.coins() + +# make segmentation using edge-detection and watershed +edges = sobel(coins) +markers = np.zeros_like(coins) +foreground, background = 1, 2 +markers[coins < 30] = background +markers[coins > 150] = foreground + +ws = watershed(edges, markers) +seg1 = nd.label(ws == foreground)[0] + +# make segmentation using SLIC superpixels + +# make the RGB equivalent of `coins` +coins_colour = np.tile(coins[..., np.newaxis], (1, 1, 3)) +seg2 = slic(coins_colour, n_segments=30, max_iter=160, sigma=1, ratio=9, + convert2lab=False) + +# combine the two +segj = join_segmentations(seg1, seg2) + +### Display the result ### + +# make a random colormap for a set number of values +def random_cmap(im): + np.random.seed(9) + cmap_array = np.concatenate( + (np.zeros((1, 3)), np.random.rand(np.ceil(im.max()), 3))) + return mpl.colors.ListedColormap(cmap_array) + +# show the segmentations +fig, axes = plt.subplots(ncols=4, figsize=(9, 2.5)) +axes[0].imshow(coins, cmap=plt.cm.gray, interpolation='nearest') +axes[0].set_title('Image') +axes[1].imshow(seg1, cmap=random_cmap(seg1), interpolation='nearest') +axes[1].set_title('Sobel+Watershed') +axes[2].imshow(seg2, cmap=random_cmap(seg2), interpolation='nearest') +axes[2].set_title('SLIC superpixels') +axes[3].imshow(segj, cmap=random_cmap(segj), interpolation='nearest') +axes[3].set_title('Join') + +for ax in axes: + ax.axis('off') +plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) +plt.show() 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_lena_tv_denoise.py b/doc/examples/plot_lena_tv_denoise.py deleted file mode 100644 index bce07845..00000000 --- a/doc/examples/plot_lena_tv_denoise.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -==================================================== -Denoising the picture of Lena using total variation -==================================================== - -In this example, we denoise a noisy version of the picture of Lena -using the total variation denoising filter. The result of this filter -is an image that has a minimal total variation norm, while being as -close to the initial image as possible. The total variation is the L1 -norm of the gradient of the image, and minimizing the total variation -typically produces "posterized" images with flat domains separated by -sharp edges. - -It is possible to change the degree of posterization by controlling -the tradeoff between denoising and faithfulness to the original image. - -""" - -import numpy as np -import matplotlib.pyplot as plt - -from skimage import data, color, img_as_ubyte -from skimage.filter import tv_denoise - -l = img_as_ubyte(color.rgb2gray(data.lena())) -l = l[230:290, 220:320] - -noisy = l + 0.4 * l.std() * np.random.random(l.shape) - -tv_denoised = tv_denoise(noisy, weight=10) - -plt.figure(figsize=(8, 2)) - -plt.subplot(131) -plt.imshow(noisy, cmap=plt.cm.gray, vmin=40, vmax=220) -plt.axis('off') -plt.title('noisy', fontsize=20) -plt.subplot(132) -plt.imshow(tv_denoised, cmap=plt.cm.gray, vmin=40, vmax=220) -plt.axis('off') -plt.title('TV denoising', fontsize=20) - -tv_denoised = tv_denoise(noisy, weight=50) -plt.subplot(133) -plt.imshow(tv_denoised, cmap=plt.cm.gray, vmin=40, vmax=220) -plt.axis('off') -plt.title('(more) TV denoising', fontsize=20) - -plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, bottom=0, left=0, - right=1) -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_local_equalize.py b/doc/examples/plot_local_equalize.py new file mode 100644 index 00000000..33b2c2e4 --- /dev/null +++ b/doc/examples/plot_local_equalize.py @@ -0,0 +1,84 @@ +""" +=============================== +Local Histogram Equalization +=============================== + +This examples enhances an image with low contrast, using a method called +*local histogram equalization*, which "spreads out the most frequent intensity +values" in an image . +The equalized image [1]_ has a roughly linear cumulative distribution function for each pixel neighborhood. +The local version [2]_ of the histogram equalization emphasized every local graylevel variations. + +.. [1] http://en.wikipedia.org/wiki/Histogram_equalization +.. [2] http://en.wikipedia.org/wiki/Adaptive_histogram_equalization + +""" + +from skimage import data +from skimage.util.dtype import dtype_range +from skimage import exposure +from skimage.morphology import disk + +import matplotlib.pyplot as plt + +import numpy as np +from skimage.filter import rank + + +def plot_img_and_hist(img, axes, bins=256): + """Plot an image along with its histogram and cumulative histogram. + + """ + ax_img, ax_hist = axes + ax_cdf = ax_hist.twinx() + + # Display image + ax_img.imshow(img, cmap=plt.cm.gray) + ax_img.set_axis_off() + + # Display histogram + ax_hist.hist(img.ravel(), bins=bins) + ax_hist.ticklabel_format(axis='y', style='scientific', scilimits=(0, 0)) + ax_hist.set_xlabel('Pixel intensity') + + xmin, xmax = dtype_range[img.dtype.type] + ax_hist.set_xlim(xmin, xmax) + + # Display cumulative distribution + img_cdf, bins = exposure.cumulative_distribution(img, bins) + ax_cdf.plot(bins, img_cdf, 'r') + + return ax_img, ax_hist, ax_cdf + + +# Load an example image +img = data.moon() + +# Contrast stretching +p2 = np.percentile(img, 2) +p98 = np.percentile(img, 98) +img_rescale = exposure.equalize_hist(img) + +# Equalization +selem = disk(30) +img_eq = rank.equalize(img, selem=selem) + + +# Display results +f, axes = plt.subplots(2, 3, figsize=(8, 4)) + +ax_img, ax_hist, ax_cdf = plot_img_and_hist(img, axes[:, 0]) +ax_img.set_title('Low contrast image') +ax_hist.set_ylabel('Number of pixels') + +ax_img, ax_hist, ax_cdf = plot_img_and_hist(img_rescale, axes[:, 1]) +ax_img.set_title('Global equalise') + +ax_img, ax_hist, ax_cdf = plot_img_and_hist(img_eq, axes[:, 2]) +ax_img.set_title('Local equalize') +ax_cdf.set_ylabel('Fraction of total intensity') + + +# prevent overlap of y-axis labels +plt.subplots_adjust(wspace=0.4) +plt.show() diff --git a/doc/examples/plot_local_otsu.py b/doc/examples/plot_local_otsu.py new file mode 100644 index 00000000..968ce6e1 --- /dev/null +++ b/doc/examples/plot_local_otsu.py @@ -0,0 +1,49 @@ +""" +===================== +Local Otsu Threshold +===================== +This example shows how Otsu's threshold [1]_ method can be applied locally. +For each pixel, an "optimal" threshold is determined by maximizing the variance between two classes of pixels +of the local neighborhood defined by a structuring element. + +The example compares the local threshold with the global threshold. + +.. note: local threshold is much slower than global one. + +.. [1] http://en.wikipedia.org/wiki/Otsu's_method + +""" +import matplotlib.pyplot as plt + +from skimage import data +from skimage.morphology.selem import disk +import skimage.filter.rank as rank +from skimage.filter import threshold_otsu + + +p8 = data.page() + +radius = 10 +selem = disk(radius) + +loc_otsu = rank.otsu(p8, selem) +t_glob_otsu = threshold_otsu(p8) +glob_otsu = p8 >= t_glob_otsu + + +plt.figure() +plt.subplot(2, 2, 1) +plt.imshow(p8, cmap=plt.cm.gray) +plt.xlabel('original') +plt.colorbar() +plt.subplot(2, 2, 2) +plt.imshow(loc_otsu, cmap=plt.cm.gray) +plt.xlabel('local Otsu ($radius=%d$)' % radius) +plt.colorbar() +plt.subplot(2, 2, 3) +plt.imshow(p8 >= loc_otsu, cmap=plt.cm.gray) +plt.xlabel('original>=local Otsu' % t_glob_otsu) +plt.subplot(2, 2, 4) +plt.imshow(glob_otsu, cmap=plt.cm.gray) +plt.xlabel('global Otsu ($t=%d$)' % t_glob_otsu) +plt.show() diff --git a/doc/examples/plot_marked_watershed.py b/doc/examples/plot_marked_watershed.py new file mode 100644 index 00000000..e97280c7 --- /dev/null +++ b/doc/examples/plot_marked_watershed.py @@ -0,0 +1,54 @@ +""" +================================ +Markers for watershed transform +================================ + +The watershed is a classical algorithm used for **segmentation**, that +is, for separating different objects in an image. + +Here a marker image is build from the region of low gradient inside the image. + +See Wikipedia_ for more details on the algorithm. + +.. _Wikipedia: http://en.wikipedia.org/wiki/Watershed_(image_processing) + +""" + +from scipy import ndimage +import matplotlib.pyplot as plt +from skimage.morphology import watershed, disk +from skimage import data + +# original data +from skimage.filter import rank + +image = data.camera() + +# denoise image +denoised = rank.median(image, disk(2)) + +# find continuous region (low gradient) --> markers +markers = rank.gradient(denoised, disk(5)) < 10 +markers = ndimage.label(markers)[0] + +#local gradient +gradient = rank.gradient(denoised, disk(2)) + +# process the watershed +labels = watershed(gradient, markers) + +# display results +fig, axes = plt.subplots(ncols=4, figsize=(8, 2.7)) +ax0, ax1, ax2, ax3 = axes + +ax0.imshow(image, cmap=plt.cm.gray, interpolation='nearest') +ax1.imshow(gradient, cmap=plt.cm.spectral, interpolation='nearest') +ax2.imshow(markers, cmap=plt.cm.spectral, interpolation='nearest') +ax3.imshow(image, cmap=plt.cm.gray, interpolation='nearest') +ax3.imshow(labels, cmap=plt.cm.spectral, interpolation='nearest', alpha=.7) + +for ax in axes: + ax.axis('off') + +plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) +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_random_walker_segmentation.py b/doc/examples/plot_random_walker_segmentation.py index 6d194293..7d9f4fa8 100644 --- a/doc/examples/plot_random_walker_segmentation.py +++ b/doc/examples/plot_random_walker_segmentation.py @@ -19,7 +19,6 @@ values, and use the random walker for the segmentation. .. [1] *Random walks for image segmentation*, Leo Grady, IEEE Trans. Pattern Anal. Mach. Intell. 2006 Nov; 28(11):1768-83 """ -print __doc__ import numpy as np from scipy import ndimage 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..418a0903 --- /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, slic, quickshift +from skimage.segmentation import mark_boundaries +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(mark_boundaries(img, segments_fz)) +ax[0].set_title("Felzenszwalbs's method") +ax[1].imshow(mark_boundaries(img, segments_slic)) +ax[1].set_title("SLIC") +ax[2].imshow(mark_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..25a48ebe 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -13,17 +13,17 @@ This example shows how to fill several different shapes: import matplotlib.pyplot as plt -from skimage.draw import line, polygon, circle, ellipse +from skimage.draw import line, polygon, circle, circle_perimeter, ellipse 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,13 +34,17 @@ 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 +# circle +rr, cc = circle_perimeter(120, 400, 50) +img[rr, cc, :] = (255, 0, 255) + plt.imshow(img) -plt.show() \ No newline at end of file +plt.show() 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/examples/plot_watershed.py b/doc/examples/plot_watershed.py index a1cd18cf..50857b2c 100644 --- a/doc/examples/plot_watershed.py +++ b/doc/examples/plot_watershed.py @@ -26,7 +26,7 @@ See Wikipedia_ for more details on the algorithm. """ import numpy as np -from scipy import ndimage + import matplotlib.pyplot as plt from skimage.morphology import watershed, is_local_maximum diff --git a/doc/ext/plot2rst.py b/doc/ext/plot2rst.py index 67355289..00560be4 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 @@ -68,12 +69,16 @@ import os import shutil import token import tokenize +import traceback 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 +165,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) @@ -243,7 +248,16 @@ def write_gallery(gallery_index, src_dir, rst_dir, cfg, depth=0): gallery_index.write(TOCTREE_TEMPLATE % (sub_dir + '\n '.join(ex_names))) for src_name in examples: - write_example(src_name, src_dir, rst_dir, cfg) + + try: + write_example(src_name, src_dir, rst_dir, cfg) + except Exception: + print "Exception raised while running:" + print "%s in %s" % (src_name, src_dir) + print '~' * 60 + traceback.print_exc() + print '~' * 60 + continue link_name = sub_dir.pjoin(src_name) link_name = link_name.replace(os.path.sep, '_') @@ -335,7 +349,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 +360,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/release/release_0.8.txt b/doc/release/release_0.8.txt new file mode 100644 index 00000000..d146659c --- /dev/null +++ b/doc/release/release_0.8.txt @@ -0,0 +1,71 @@ +Announcement: scikits-image 0.8.0 +================================= + +We're happy to announce the 8th version of scikit-image! + +scikit-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://scikit-image.org + + +New Features +------------ + +- New rank filter package with many new functions and a very fast underlying + local histogram algorithm, especially for large structuring elements + `skimage.filter.rank.*` +- New function for small object removal + `skimage.morphology.remove_small_objects` +- New circular hough transformation `skimage.transform.hough_circle` +- New function to draw circle perimeter `skimage.draw.circle_perimeter` and + ellipse perimeter `skimage.draw.ellipse_perimeter` +- New dense DAISY feature descriptor `skimage.feature.daisy` +- New bilateral filter `skimage.filter.denoise_bilateral` +- New faster TV denoising filter based on split-Bregman algorithm + `skimage.filter.denoise_tv_bregman` +- New linear hough peak detection `skimage.transform.hough_peaks` +- New Scharr edge detection `skimage.filter.scharr` +- New geometric image scaling as convenience function + `skimage.transform.rescale` +- New theme for documentation and website +- Faster median filter through vectorization `skimage.filter.median_filter` +- Grayscale images supported for SLIC segmentation +- Unified peak detection with more options `skimage.feature.peak_local_max` +- `imread` can read images via URL and knows more formats `skimage.io.imread` + +Additionally, this release adds lots 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. + +- Adam Ginsburg +- Anders Boesen Lindbo Larsen +- Andreas Mueller +- Christoph Gohlke +- Christos Psaltis +- Colin Lea +- François Boulogne +- Jan Margeta +- Johannes Schönberger +- Josh Warner (Mac) +- Juan Nunez-Iglesias +- Luis Pedro Coelho +- Marianne Corvellec +- Matt McCormick +- Nicolas Pinto +- Olivier Debeir +- Paul Ivanov +- Sergey Karayev +- Stefan van der Walt +- Steven Silvester +- Thouis (Ray) Jones +- Tony S Yu 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..fde9437b 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.8.0', '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..c3b87b2e 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*. @@ -71,11 +71,6 @@ issued:: float64 to uint8 array([ 0, 128, 255], dtype=uint8) -Wherever possible, functions should try to handle input without explicit -conversion. For example, there is no need to force values to a specific type -for doing a convolution; a plotting function, on the other hand, needs to know -the range of the input. - Output types ============ @@ -142,6 +137,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 870430ff..e3574206 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.1' +DOWNLOAD_URL = 'http://github.com/scikit-image/scikit-image' +VERSION = '0.8.0' +PYTHON_VERSION = (2, 5) +DEPENDENCIES = { + 'numpy': (1, 6), + 'Cython': (0, 15), + } import os +import sys +import re 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 re.split('\D+', version_info): + 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(), + + packages=setuptools.find_packages(exclude=['doc']), 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..daac238c 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 @@ -52,6 +52,8 @@ img_as_ubyte """ import os.path as _osp +import imp as _imp +import functools as _functools pkg_dir = _osp.abspath(_osp.dirname(__file__)) data_dir = _osp.join(pkg_dir, 'data') @@ -61,37 +63,30 @@ try: except ImportError: __version__ = "unbuilt-dev" -def _setup_test(verbose=False): - import gzip - import functools - args = ['', '--exe', '-w', pkg_dir] - if verbose: - args.extend(['-v', '-s']) +try: + _imp.find_module('nose') +except ImportError: + def _test(verbose=False): + """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.") +else: + def _test(verbose=False): + """Invoke the skimage test suite.""" + import nose + args = ['', pkg_dir, '--exe'] + if verbose: + args.extend(['-v', '-s']) + nose.run('skimage', argv=args) - try: - import nose as _nose - except ImportError: - print("Could not load nose. Unit tests not available.") - return None - else: - f = functools.partial(_nose.run, 'skimage', argv=args) - f.__doc__ = 'Invoke the skimage test suite.' - return f +# do not use `test` as function name as this leads to a recursion problem with +# the nose test suite +test = _test +test_verbose = _functools.partial(test, verbose=True) +test_verbose.__doc__ = test.__doc__ -test = _setup_test() -if test is None: - try: - del test - except NameError: - pass - -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..771b04b9 100644 --- a/skimage/_build.py +++ b/skimage/_build.py @@ -1,9 +1,15 @@ import sys import os -import shutil import hashlib import subprocess -import platform + + +# WindowsError is not defined on unix systems +try: + WindowsError +except NameError: + WindowsError = None + def cython(pyx_files, working_path=''): """Use Cython to convert the given files to C. @@ -39,17 +45,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 +67,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..3379318c --- /dev/null +++ b/skimage/_shared/geometry.pxd @@ -0,0 +1,6 @@ +cdef unsigned char point_in_polygon(Py_ssize_t nr_verts, double *xp, double *yp, + double x, double y) + +cdef void points_in_polygon(Py_ssize_t nr_verts, double *xp, double *yp, + Py_ssize_t 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..beb07e14 --- /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(Py_ssize_t 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 Py_ssize_t i + cdef unsigned char c = 0 + cdef Py_ssize_t 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(Py_ssize_t nr_verts, double *xp, double *yp, + Py_ssize_t 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 Py_ssize_t 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..c5f32b6a --- /dev/null +++ b/skimage/_shared/interpolation.pxd @@ -0,0 +1,27 @@ + +cdef double nearest_neighbour_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, + double c, char mode, + double cval) + +cdef double bilinear_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, + double r, double c, char mode, + double cval) + +cdef double quadratic_interpolation(double x, double[3] f) +cdef double biquadratic_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, + double r, double c, char mode, + double cval) + +cdef double cubic_interpolation(double x, double[4] f) +cdef double bicubic_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, + double r, double c, char mode, + double cval) + +cdef double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, Py_ssize_t r, + Py_ssize_t c, char mode, double cval) + +cdef double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, Py_ssize_t dims, + Py_ssize_t r, Py_ssize_t c, Py_ssize_t d, char mode, double cval) + +cdef Py_ssize_t coord_map(Py_ssize_t dim, Py_ssize_t coord, char mode) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx new file mode 100644 index 00000000..a8b96014 --- /dev/null +++ b/skimage/_shared/interpolation.pyx @@ -0,0 +1,331 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +from libc.math cimport ceil, floor + + +cdef inline Py_ssize_t round(double r): + return ((r + 0.5) if (r > 0.0) else (r - 0.5)) + + +cdef inline double nearest_neighbour_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t 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_pixel2d(image, rows, cols, round(r), round(c), mode, cval) + + +cdef inline double bilinear_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t 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 Py_ssize_t 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_pixel2d(image, rows, cols, minr, minc, mode, cval) \ + + dc * get_pixel2d(image, rows, cols, minr, maxc, mode, cval) + bottom = (1 - dc) * get_pixel2d(image, rows, cols, maxr, minc, mode, cval) \ + + dc * get_pixel2d(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, Py_ssize_t rows, + Py_ssize_t 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 Py_ssize_t r0 = round(r) + cdef Py_ssize_t 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 Py_ssize_t pr, pc + + # row-wise cubic interpolation + for pr in range(r0, r0 + 3): + for pc in range(c0, c0 + 3): + fc[pc - c0] = get_pixel2d(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, Py_ssize_t rows, + Py_ssize_t 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 Py_ssize_t r0 = r - 1 + cdef Py_ssize_t 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 Py_ssize_t pr, pc + + # row-wise cubic interpolation + for pr in range(r0, r0 + 4): + for pc in range(c0, c0 + 4): + fc[pc - c0] = get_pixel2d(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_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t 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 double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t dims, Py_ssize_t r, Py_ssize_t c, Py_ssize_t d, + char mode, double cval): + """Get a pixel from the image, taking wrapping mode into consideration. + + Parameters + ---------- + image : double array + Input image. + rows, cols, dims : int + Shape of image. + r, c, d : 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 * dims + c * dims + d] + else: + return image[coord_map(rows, r, mode) * cols * dims + + coord_map(cols, c, mode) * dims + + d] + + +cdef inline Py_ssize_t coord_map(Py_ssize_t dim, Py_ssize_t 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..ccb16ff3 --- /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, + Py_ssize_t r0, Py_ssize_t c0, Py_ssize_t r1, Py_ssize_t c1) diff --git a/skimage/_shared/transform.pyx b/skimage/_shared/transform.pyx new file mode 100644 index 00000000..8ce2ab67 --- /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, + Py_ssize_t r0, Py_ssize_t c0, Py_ssize_t r1, Py_ssize_t 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/_shared/utils.py b/skimage/_shared/utils.py new file mode 100644 index 00000000..4075ddb4 --- /dev/null +++ b/skimage/_shared/utils.py @@ -0,0 +1,43 @@ +import warnings +import functools + + +__all__ = ['deprecated'] + + +class deprecated(object): + """Decorator to mark deprecated functions with warning. + + Adapted from . + + Parameters + ---------- + alt_func : str + If given, tell user what function to use instead. + behavior : {'warn', 'raise'} + Behavior during call to deprecated function: 'warn' = warn user that + function is deprecated; 'raise' = raise error. + """ + + def __init__(self, alt_func=None, behavior='warn'): + self.alt_func = alt_func + self.behavior = behavior + + def __call__(self, func): + + msg = "Call to deprecated function `%s`." % func.__name__ + if self.alt_func is not None: + msg = msg + " Use `%s` instead." % self.alt_func + + @functools.wraps(func) + def wrapped(*args, **kwargs): + if self.behavior == 'warn': + warnings.warn_explicit(msg, + category=DeprecationWarning, + filename=func.func_code.co_filename, + lineno=func.func_code.co_firstlineno + 1) + elif self.behavior == 'raise': + raise DeprecationWarning(msg) + return func(*args, **kwargs) + + return wrapped diff --git a/skimage/_shared/vectorized_ops.h b/skimage/_shared/vectorized_ops.h new file mode 100644 index 00000000..ab9647ea --- /dev/null +++ b/skimage/_shared/vectorized_ops.h @@ -0,0 +1,110 @@ +/* Intrinsic declarations */ +#if defined(__SSE2__) +#include +#elif defined(__MMX__) +#include +#elif defined(__ALTIVEC__) +#include +#endif + +/* Compiler peculiarities */ +#if defined(__GNUC__) +#include +#elif defined(_MSC_VER) +#define inline __inline +typedef unsigned __int16 uint16_t; +#endif + +/** + * Add 16 unsigned 16-bit integers using SSE2, MMX or Altivec, if + * available. + */ +#if defined(__SSE2__) +static inline void add16(uint16_t *dest, uint16_t *src) +{ + __m128i *d, *s; + d = (__m128i *) dest; + s = (__m128i *) src; + *d = _mm_add_epi16(*d, *s); + d++; s++; + *d = _mm_add_epi16(*d, *s); +} +#elif defined(__MMX__) +static inline void add16(uint16_t *dest, uint16_t *src) +{ + __m64 *d, *s; + d = (__m64 *) dest; + s = (__m64 *) src; + *d = _mm_add_pi16(*d, *s); + d++; s++; + *d = _mm_add_pi16(*d, *s); + d++; s++; + *d = _mm_add_pi16(*d, *s); + d++; s++; + *d = _mm_add_pi16(*d, *s); +} +#elif defined(__ALTIVEC__) +static inline void add16(uint16_t *dest, uint16_t *src) +{ + vector unsigned short *d, *s; + d = (vector unsigned short *) dest; + s = (vector unsigned short *) src; + *d = vec_add(*d, *s); + d++; s++; + *d = vec_add(*d, *s); +} +#else +static inline void add16(uint16_t *dest, uint16_t *src) +{ + int i; + + for (i = 0; i < 16; i++) dest[i] += src[i]; +} +#endif + +/** + * Subtract 16 unsigned 16-bit integers using SSE2, MMX or Altivec, if + * available. + */ +#if defined(__SSE2__) +static inline void sub16(uint16_t *dest, uint16_t *src) +{ + __m128i *d, *s; + d = (__m128i *) dest; + s = (__m128i *) src; + *d = _mm_sub_epi16(*d, *s); + d++; s++; + *d = _mm_sub_epi16(*d, *s); +} +#elif defined(__MMX__) +static inline void sub16(uint16_t *dest, uint16_t *src) +{ + __m64 *d, *s; + d = (__m64 *) dest; + s = (__m64 *) src; + *d = _mm_sub_pi16(*d, *s); + d++; s++; + *d = _mm_sub_pi16(*d, *s); + d++; s++; + *d = _mm_sub_pi16(*d, *s); + d++; s++; + *d = _mm_sub_pi16(*d, *s); +} +#elif defined(__ALTIVEC__) +static inline void sub16(uint16_t *dest, uint16_t *src) +{ + vector unsigned short *d, *s; + d = (vector unsigned short *) dest; + s = (vector unsigned short *) src; + *d = vec_sub(*d, *s); + d++; s++; + *d = vec_sub(*d, *s); +} +#else +static inline void sub16(uint16_t *dest, uint16_t *src) +{ + int i; + + for (i = 0; i < 16; i++) dest[i] -= src[i]; +} +#endif diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 36165d68..4186682d 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', 'is_rgb', 'is_gray' + ] __docformat__ = "restructuredtext en" @@ -53,6 +55,30 @@ from scipy import linalg from ..util import dtype +def is_rgb(image): + """Test whether the image is RGB or RGBA. + + Parameters + ---------- + image : ndarray + Input image. + + """ + return (image.ndim == 3 and image.shape[2] in (3, 4)) + + +def is_gray(image): + """Test whether the image is gray (i.e. has only one color band). + + Parameters + ---------- + image : ndarray + Input image. + + """ + return image.ndim == 2 + + def convert_colorspace(arr, fromspace, tospace): """Convert an image array to a new color space. @@ -81,11 +107,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 +172,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 +185,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 +203,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 +250,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) """ @@ -264,8 +287,8 @@ sb_primaries = np.array([1. / 155, 1. / 190, 1. / 225]) * 1e5 # From sRGB specification xyz_from_rgb = np.array([[0.412453, 0.357580, 0.180423], - [0.212671, 0.715160, 0.072169], - [0.019334, 0.119193, 0.950227]]) + [0.212671, 0.715160, 0.072169], + [0.019334, 0.119193, 0.950227]]) rgb_from_xyz = linalg.inv(xyz_from_rgb) @@ -282,10 +305,13 @@ rgbcie_from_rgb = np.dot(rgbcie_from_xyz, xyz_from_rgb) rgb_from_rgbcie = np.dot(rgb_from_xyz, xyz_from_rgbcie) -grey_from_rgb = np.array([[0.2125, 0.7154, 0.0721], +gray_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,16 +372,19 @@ 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) + # Follow the algorithm from http://www.easyrgb.com/index.php + # except we don't multiply/divide by 100 in the conversion + arr = _convert(rgb_from_xyz, xyz) + mask = arr > 0.0031308 + arr[mask] = 1.055 * np.power(arr[mask], 1 / 2.4) - 0.055 + arr[~mask] *= 12.92 + return arr def rgb2xyz(rgb): @@ -387,14 +416,17 @@ 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) + # Follow the algorithm from http://www.easyrgb.com/index.php + # except we don't multiply/divide by 100 in the conversion + arr = _prepare_colorarray(rgb).copy() + mask = arr > 0.04045 + arr[mask] = np.power((arr[mask] + 0.055) / 1.055, 2.4) + arr[~mask] /= 12.92 + return _convert(xyz_from_rgb, arr) def rgb2rgbcie(rgb): @@ -421,12 +453,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,19 +485,16 @@ 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) -def rgb2grey(rgb): +def rgb2gray(rgb): """Compute luminance of an RGB image. Parameters @@ -485,7 +511,7 @@ def rgb2grey(rgb): Raises ------ ValueError - If `rgb2grey` is not a 3-D array of shape (.., .., 3) or + If `rgb2gray` is not a 3-D array of shape (.., .., 3) or (.., .., 4). References @@ -503,21 +529,21 @@ 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')) - >>> lena_grey = rgb2grey(lena) + >>> from skimage.color import rgb2gray + >>> from skimage import data + >>> lena = data.lena() + >>> lena_gray = rgb2gray(lena) """ - return _convert(grey_from_rgb, rgb[:, :, :3])[..., 0] + if rgb.ndim == 2: + return rgb -rgb2gray = rgb2grey + return _convert(gray_from_rgb, rgb[:, :, :3])[..., 0] + +rgb2grey = rgb2gray def gray2rgb(image): - """Create an RGB representation of a grey-level image. + """Create an RGB representation of a gray-level image. Parameters ---------- @@ -535,8 +561,163 @@ def gray2rgb(image): If the input is not 2-dimensional. """ - if image.ndim != 2: - raise ValueError('Gray-level image should be two-dimensional.') + if is_rgb(image): + return image + elif is_gray(image): + return np.dstack((image, image, image)) + else: + raise ValueError("Input image expected to be RGB, RGBA or gray.") - 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..962fa9fc 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -23,10 +23,13 @@ from skimage.color import ( rgb2xyz, xyz2rgb, rgb2rgbcie, rgbcie2rgb, convert_colorspace, - rgb2grey, gray2rgb + rgb2grey, gray2rgb, + xyz2lab, lab2xyz, + lab2rgb, rgb2lab, + is_rgb, is_gray ) -from skimage import data_dir +from skimage import data_dir, data import colorsys @@ -43,6 +46,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 +73,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 +89,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,15 +109,17 @@ 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): - # only roundtrip test, we checked rgb2xyz above already assert_almost_equal(xyz2rgb(rgb2xyz(self.colbars_array)), self.colbars_array) + # RGB<->XYZ roundtrip on another image + def test_xyz_rgb_roundtrip(self): + img_rgb = img_as_float(self.img_rgb) + assert_array_almost_equal(xyz2rgb(rgb2xyz(img_rgb)), img_rgb) # RGB to RGB CIE def test_rgb2rgbcie_conversion(self): @@ -117,7 +133,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 +166,42 @@ 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_rgb2lab_brucelindbloom(self): + """ + Test the RGB->Lab conversion by comparing to the calculator on the + authoritative Bruce Lindbloom + [website](http://brucelindbloom.com/index.html?ColorCalculator.html). + """ + # Obtained with D65 white point, sRGB model and gamma + gt_for_colbars = np.array([ + [100,0,0], + [97.1393, -21.5537, 94.4780], + [91.1132, -48.0875, -14.1312], + [87.7347, -86.1827, 83.1793], + [60.3242, 98.2343, -60.8249], + [53.2408, 80.0925, 67.2032], + [32.2970, 79.1875, -107.8602], + [0,0,0]]).T + gt_array = np.swapaxes(gt_for_colbars.reshape(3, 4, 2), 0, 2) + assert_array_almost_equal(rgb2lab(self.colbars_array), gt_array, decimal=2) + + 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) @@ -168,6 +219,23 @@ def test_gray2rgb(): assert_equal(z[..., 0], x) assert_equal(z[0, 1, :], [128, 128, 128]) + +def test_gray2rgb_rgb(): + x = np.random.random((5, 5, 4)) + y = gray2rgb(x) + assert_equal(x, y) + + +def test_is_rgb(): + color = data.lena() + gray = data.camera() + + assert is_rgb(color) + assert not is_gray(color) + + assert is_gray(gray) + assert not is_gray(color) + + if __name__ == "__main__": run_module_suite() - diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index c4467678..d2fba7db 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. @@ -105,3 +114,16 @@ def page(): """ return load("page.png") + + +def clock(): + """Motion blurred clock. + + This photograph of a wall clock was taken while moving the camera in an + aproximately horizontal direction. It may be used to illustrate + inverse filters and deconvolution. + + Released into the public domain by the photographer (Stefan van der Walt). + + """ + return load("clock_motion.png") 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/clock_motion.png b/skimage/data/clock_motion.png new file mode 100644 index 00000000..3a81bd74 Binary files /dev/null and b/skimage/data/clock_motion.png 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..f660c69c 100644 --- a/skimage/data/tests/test_data.py +++ b/skimage/data/tests/test_data.py @@ -1,22 +1,44 @@ 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() + + +def test_page(): + """ Test that "clock" image can be loaded. """ + data.clock() + 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..4aa6bf35 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -1 +1,5 @@ -from .draw import * +from ._draw import line, polygon, ellipse, ellipse_perimeter, \ + circle, circle_perimeter, set_color + + +bresenham = line diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 9dc1d307..8a2572c4 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -1,18 +1,16 @@ -import numpy as np +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import math +import numpy as np + +cimport numpy as cnp from libc.math cimport sqrt -cimport numpy as np -cimport cython +from skimage._shared.geometry cimport point_in_polygon -cdef extern from "../morphology/_pnpoly.h": - int pnpoly(int nr_verts, double *xp, double *yp, - double x, double y) - - -@cython.boundscheck(False) -@cython.wraparound(False) -def line(int y, int x, int y2, int x2): +def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): """Generate line pixel coordinates. Parameters @@ -30,26 +28,31 @@ def line(int y, int x, int y2, int x2): ``img[rr, cc] = 1``. """ - cdef np.ndarray[np.int32_t, ndim=1, mode="c"] rr, cc - cdef int steep = 0 - cdef int dx = abs(x2 - x) - cdef int dy = abs(y2 - y) - cdef int sx, sy, d, i + cdef cnp.ndarray[cnp.intp_t, ndim=1, mode="c"] rr, cc - if (x2 - x) > 0: sx = 1 - else: sx = -1 - if (y2 - y) > 0: sy = 1 - else: sy = -1 + cdef char steep = 0 + cdef Py_ssize_t dx = abs(x2 - x) + cdef Py_ssize_t dy = abs(y2 - y) + cdef Py_ssize_t sx, sy, d, i + + if (x2 - x) > 0: + sx = 1 + else: + sx = -1 + if (y2 - y) > 0: + sy = 1 + else: + sy = -1 if dy > dx: steep = 1 - x,y = y,x - dx,dy = dy,dx - sx,sy = sy,sx + x, y = y, x + dx, dy = dy, dx + sx, sy = sy, sx d = (2 * dy) - dx - rr = np.zeros(int(dx) + 1, dtype=np.int32) - cc = np.zeros(int(dx) + 1, dtype=np.int32) + rr = np.zeros(int(dx) + 1, dtype=np.intp) + cc = np.zeros(int(dx) + 1, dtype=np.intp) for i in range(dx): if steep: @@ -69,18 +72,16 @@ def line(int y, int x, int y2, int x2): return rr, cc -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.nonecheck(False) + def polygon(y, x, shape=None): """Generate coordinates of pixels within polygon. 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. @@ -92,26 +93,28 @@ def polygon(y, x, shape=None): Pixel coordinates of polygon. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. + """ - cdef int nr_verts = x.shape[0] - cdef int minr = max(0, y.min()) - cdef int maxr = math.ceil(y.max()) - cdef int minc = max(0, x.min()) - cdef int maxc = math.ceil(x.max()) + + cdef Py_ssize_t nr_verts = x.shape[0] + cdef Py_ssize_t minr = int(max(0, y.min())) + cdef Py_ssize_t maxr = int(math.ceil(y.max())) + cdef Py_ssize_t minc = int(max(0, x.min())) + cdef Py_ssize_t maxc = int(math.ceil(x.max())) # make sure output coordinates do not exceed image size if shape is not None: - maxr = min(shape[0]-1, maxr) - maxc = min(shape[1]-1, maxc) + maxr = min(shape[0] - 1, maxr) + maxc = min(shape[1] - 1, maxc) - cdef int r, c + cdef Py_ssize_t r, c #: make contigous arrays for r, c coordinates - cdef np.ndarray contiguous_rdata, contiguous_cdata + cdef cnp.ndarray contiguous_rdata, contiguous_cdata contiguous_rdata = np.ascontiguousarray(y, 'double') contiguous_cdata = np.ascontiguousarray(x, 'double') - cdef np.double_t* rptr = contiguous_rdata.data - cdef np.double_t* cptr = contiguous_cdata.data + cdef cnp.double_t* rptr = contiguous_rdata.data + cdef cnp.double_t* cptr = contiguous_cdata.data #: output coordinate arrays cdef list rr = list() @@ -119,25 +122,26 @@ 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) -@cython.cdivision(True) -def ellipse(double cy, double cx, double b, double a, shape=None): + +def ellipse(double cy, double cx, double yradius, double xradius, shape=None): """Generate coordinates of pixels within ellipse. Parameters ---------- cy, cx : double - centre coordinate of ellipse - b, a: double - minor and major semi-axes. (x/a)**2 + (y/b)**2 = 1 + Centre coordinate of ellipse. + yradius, xradius : double + Minor and major semi-axes. ``(x/xradius)**2 + (y/yradius)**2 = 1``. + shape : tuple, optional + image shape which is used to determine maximum extents of output pixel + coordinates. This is useful for ellipses which exceed the image size. + By default the full extents of the ellipse are used. Returns ------- @@ -145,18 +149,20 @@ def ellipse(double cy, double cx, double b, double a, shape=None): Pixel coordinates of ellipse. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. + """ - cdef int minr = max(0, cy-b) - cdef int maxr = math.ceil(cy+b) - cdef int minc = max(0, cx-a) - cdef int maxc = math.ceil(cx+a) + + cdef Py_ssize_t minr = int(max(0, cy - yradius)) + cdef Py_ssize_t maxr = int(math.ceil(cy + yradius)) + cdef Py_ssize_t minc = int(max(0, cx - xradius)) + cdef Py_ssize_t maxc = int(math.ceil(cx + xradius)) # make sure output coordinates do not exceed image size if shape is not None: - maxr = min(shape[0]-1, maxr) - maxc = min(shape[1]-1, maxc) + maxr = min(shape[0] - 1, maxr) + maxc = min(shape[1] - 1, maxc) - cdef int r, c + cdef Py_ssize_t r, c #: output coordinate arrays cdef list rr = list() @@ -164,27 +170,230 @@ def ellipse(double cy, double cx, double b, double a, shape=None): for r in range(minr, maxr+1): for c in range(minc, maxc+1): - if sqrt(((r - cy)/b)**2 + ((c - cx)/a)**2) < 1: + if sqrt(((r - cy) / yradius)**2 + ((c - cx) / xradius)**2) < 1: rr.append(r) cc.append(c) 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. + shape : tuple, optional + image shape which is used to determine maximum extents of output pixel + coordinates. This is useful for circles which exceed the image size. + By default the full extents of the circle are used. 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``. + Notes + ----- + This function is a wrapper for skimage.draw.ellipse() """ + return ellipse(cy, cx, radius, radius, shape) + + +def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, + method='bresenham'): + """Generate circle perimeter coordinates. + + Parameters + ---------- + cy, cx : int + Centre coordinate of circle. + radius: int + Radius of circle. + method : {'bresenham', 'andres'}, optional + bresenham : Bresenham method + andres : Andres method + + + Returns + ------- + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the circle perimeter. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Notes + ----- + Andres method presents the advantage that concentric + circles create a disc whereas Bresenham can make holes. There + is also less distortions when Andres circles are rotated. + Bresenham method is also known as midpoint circle algorithm. + + References + ---------- + .. [1] J.E. Bresenham, "Algorithm for computer control of a digital + plotter", 4 (1965) 25-30. + .. [2] E. Andres, "Discrete circles, rings and spheres", 18 (1994) 695-706. + + """ + + cdef list rr = list() + cdef list cc = list() + + cdef Py_ssize_t x = 0 + cdef Py_ssize_t y = radius + cdef Py_ssize_t d = 0 + cdef char cmethod + if method == 'bresenham': + d = 3 - 2 * radius + cmethod = 'b' + elif method == 'andres': + d = radius - 1 + cmethod = 'a' + else: + raise ValueError('Wrong method') + + while y >= x: + rr.extend([y, -y, y, -y, x, -x, x, -x]) + cc.extend([x, x, -x, -x, y, y, -y, -y]) + + if cmethod == 'b': + if d < 0: + d += 4 * x + 6 + else: + d += 4 * (x - y) + 10 + y -= 1 + x += 1 + elif cmethod == 'a': + if d >= 2 * (x - 1): + d = d - 2 * x + x = x + 1 + elif d <= 2 * (radius - y): + d = d + 2 * y - 1 + y = y - 1 + else: + d = d + 2 * (y - x - 1) + y = y - 1 + x = x + 1 + + return np.array(rr) + cy, np.array(cc) + cx + + +def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, + Py_ssize_t xradius): + """Generate ellipse perimeter coordinates. + + Parameters + ---------- + cy, cx : int + Centre coordinate of ellipse. + yradius, xradius: int + Main radial values. + + Returns + ------- + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the circle perimeter. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + References + ---------- + .. [1] J. Kennedy "A fast Bresenham type algorithm for + drawing ellipses". + + """ + + # If both radii == 0, return the center to avoid infinite loop in 2nd set + if xradius == 0 and yradius == 0: + return np.array(cy), np.array(cx) + + # a and b are xradius an yradius compute 2a^2 and 2b^2 + cdef Py_ssize_t twoasquared = 2 * xradius**2 + cdef Py_ssize_t twobsquared = 2 * yradius**2 + + # Pixels + cdef list px = list() + cdef list py = list() + + # First set of points: + # start at the top + cdef Py_ssize_t x = xradius + cdef Py_ssize_t y = 0 + + cdef Py_ssize_t err = 0 + cdef Py_ssize_t xstop = twobsquared * xradius + cdef Py_ssize_t ystop = 0 + cdef Py_ssize_t xchange = yradius * yradius * (1 - 2 * xradius) + cdef Py_ssize_t ychange = xradius * xradius + + while xstop > ystop: + px.extend([x, -x, -x, x]) + py.extend([y, y, -y, -y]) + y += 1 + ystop += twoasquared + err += ychange + ychange += twoasquared + if (2 * err + xchange) > 0: + x -= 1 + xstop -= twobsquared + err += xchange + xchange += twobsquared + + # Second set of points: + x = 0 + y = yradius + + err = 0 + xstop = 0 + ystop = twoasquared * yradius + xchange = yradius * yradius + ychange = xradius * xradius * (1 - 2 * yradius) + + while xstop <= ystop: + px.extend([x, -x, -x, x]) + py.extend([y, y, -y, -y]) + x += 1 + xstop += twobsquared + err += xchange + xchange += twobsquared + if (2 * err + ychange) > 0: + y -= 1 + ystop -= twoasquared + err += ychange + ychange += twobsquared + + return np.array(py) + cy, np.array(px) + cx + + +def set_color(img, coords, color): + """Set pixel color in the image at the given coordinates. + + Coordinates that exceed the shape of the image will be ignored. + + Parameters + ---------- + img : (M, N, D) ndarray + Image + coords : ((P,) ndarray, (P,) ndarray) + Coordinates of pixels to be colored. + color : (D,) ndarray + Color to be assigned to coordinates in the image. + + Returns + ------- + img : (M, N, D) ndarray + The updated image. + + """ + + rr, cc = coords + rr_inside = np.logical_and(rr >= 0, rr < img.shape[0]) + cc_inside = np.logical_and(cc >= 0, cc < img.shape[1]) + inside = np.logical_and(rr_inside, cc_inside) + img[rr[inside], cc[inside]] = color 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..1414fca8 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..ef09747c 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -1,7 +1,7 @@ from numpy.testing import assert_array_equal import numpy as np -from skimage.draw import line, polygon, circle, ellipse +from skimage.draw import line, polygon, circle, circle_perimeter, ellipse, ellipse_perimeter def test_line_horizontal(): @@ -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,74 @@ def test_circle(): assert_array_equal(img, img_) + +def test_circle_perimeter_bresenham(): + img = np.zeros((15, 15), 'uint8') + rr, cc = circle_perimeter(7, 7, 0, method='bresenham') + img[rr, cc] = 1 + assert(np.sum(img) == 1) + assert(img[7][7] == 1) + + img = np.zeros((17, 15), 'uint8') + rr, cc = circle_perimeter(7, 7, 7, method='bresenham') + img[rr, cc] = 1 + img_ = np.array( + [[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 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, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 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, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 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]] + ) + assert_array_equal(img, img_) + +def test_circle_perimeter_andres(): + img = np.zeros((15, 15), 'uint8') + rr, cc = circle_perimeter(7, 7, 0, method='andres') + img[rr, cc] = 1 + assert(np.sum(img) == 1) + assert(img[7][7] == 1) + + img = np.zeros((17, 15), 'uint8') + rr, cc = circle_perimeter(7, 7, 7, method='andres') + img[rr, cc] = 1 + img_ = np.array( + [[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 1, 1, 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]] + ) + 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], @@ -168,6 +238,50 @@ def test_ellipse(): assert_array_equal(img, img_) +def test_ellipse_perimeter(): + img = np.zeros((30, 15), 'uint8') + rr, cc = ellipse_perimeter(15, 7, 0, 0) + img[rr, cc] = 1 + assert(np.sum(img) == 1) + assert(img[15][7] == 1) + + img = np.zeros((30, 15), 'uint8') + rr, cc = ellipse_perimeter(15, 7, 14, 6) + img[rr, cc] = 1 + img_ = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 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, 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, 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, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 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, 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, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 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, 1, 1, 0, 0, 0, 0, 0, 0]] + ) + + assert_array_equal(img, img_) if __name__ == "__main__": from numpy.testing import run_module_suite diff --git a/skimage/exposure/__init__.py b/skimage/exposure/__init__.py index 7e19d317..ae75c982 100644 --- a/skimage/exposure/__init__.py +++ b/skimage/exposure/__init__.py @@ -1,2 +1,3 @@ -from .exposure import histogram, equalize, cumulative_distribution -from .exposure import rescale_intensity +from .exposure import histogram, equalize, equalize_hist +from .exposure import rescale_intensity, cumulative_distribution +from ._adapthist import equalize_adapthist diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py new file mode 100644 index 00000000..8a825435 --- /dev/null +++ b/skimage/exposure/_adapthist.py @@ -0,0 +1,326 @@ +""" +Adapted code from "Contrast Limited Adaptive Histogram Equalization" by Karel +Zuiderveld , Graphics Gems IV, Academic Press, 1994. + +http://tog.acm.org/resources/GraphicsGems/gems.html#gemsvi + +The Graphics Gems code is copyright-protected. In other words, you cannot +claim the text of the code as your own and resell it. Using the code is +permitted in any program, product, or library, non-commercial or commercial. +Giving credit is not required, though is a nice gesture. The code comes as-is, +and if there are any flaws or problems with any Gems code, nobody involved with +Gems - authors, editors, publishers, or webmasters - are to be held +responsible. Basically, don't be a jerk, and remember that anything free +comes with no guarantee. +""" +import numpy as np +import skimage +from skimage import color +from skimage.exposure import rescale_intensity +from skimage.util import view_as_blocks + + +MAX_REG_X = 16 # max. # contextual regions in x-direction */ +MAX_REG_Y = 16 # max. # contextual regions in y-direction */ +NR_OF_GREY = 16384 # number of grayscale levels to use in CLAHE algorithm + + +def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, + nbins=256): + """Contrast Limited Adaptive Histogram Equalization. + + Parameters + ---------- + image : array-like + Input image. + ntiles_x : int, optional + Number of tile regions in the X direction. Ranges between 2 and 16. + ntiles_y : int, optional + Number of tile regions in the Y direction. Ranges between 2 and 16. + clip_limit : float: optional + Clipping limit, normalized between 0 and 1 (higher values give more + contrast). + nbins : int, optional + Number of gray bins for histogram ("dynamic range"). + + Returns + ------- + out : ndarray + Equalized image. + + Notes + ----- + * The algorithm relies on an image whose rows and columns are even + multiples of the number of tiles, so the extra rows and columns are left + at their original values, thus preserving the input image shape. + * For color images, the following steps are performed: + - The image is converted to LAB color space + - The CLAHE algorithm is run on the L channel + - The image is converted back to RGB space and returned + * For RGBA images, the original alpha channel is removed. + + References + ---------- + .. [1] http://tog.acm.org/resources/GraphicsGems/gems.html#gemsvi + .. [2] https://en.wikipedia.org/wiki/CLAHE#CLAHE + """ + args = [None, ntiles_x, ntiles_y, clip_limit * nbins, nbins] + if image.ndim > 2: + lab_img = color.rgb2lab(skimage.img_as_float(image)) + l_chan = lab_img[:, :, 0] + l_chan /= np.max(np.abs(l_chan)) + l_chan = skimage.img_as_uint(l_chan) + args[0] = rescale_intensity(l_chan, out_range=(0, NR_OF_GREY - 1)) + new_l = _clahe(*args).astype(float) + new_l = rescale_intensity(new_l, out_range=(0, 100)) + lab_img[:new_l.shape[0], :new_l.shape[1], 0] = new_l + image = color.lab2rgb(lab_img) + image = rescale_intensity(image, out_range=(0, 1)) + else: + image = skimage.img_as_uint(image) + args[0] = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) + out = _clahe(*args) + image[:out.shape[0], :out.shape[1]] = out + image = rescale_intensity(image) + return image + + +def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): + """Contrast Limited Adaptive Histogram Equalization. + + Parameters + ---------- + image : array-like + Input image. + ntiles_x : int, optional + Number of tile regions in the X direction. Ranges between 2 and 16. + ntiles_y : int, optional + Number of tile regions in the Y direction. Ranges between 2 and 16. + clip_limit : float, optional + Normalized clipping limit (higher values give more contrast). + nbins : int, optional + Number of gray bins for histogram ("dynamic range"). + + Returns + ------- + out : ndarray + Equalized image. + + The number of "effective" greylevels in the output image is set by `nbins`; + selecting a small value (eg. 128) speeds up processing and still produce + an output image of good quality. The output image will have the same + minimum and maximum value as the input image. A clip limit smaller than 1 + results in standard (non-contrast limited) AHE. + """ + ntiles_x = min(ntiles_x, MAX_REG_X) + ntiles_y = min(ntiles_y, MAX_REG_Y) + ntiles_y = max(ntiles_x, 2) + ntiles_x = max(ntiles_y, 2) + + if clip_limit == 1.0: + return image # is OK, immediately returns original image. + + map_array = np.zeros((ntiles_y, ntiles_x, nbins), dtype=int) + + y_res = image.shape[0] - image.shape[0] % ntiles_y + x_res = image.shape[1] - image.shape[1] % ntiles_x + image = image[: y_res, : x_res] + + x_size = image.shape[1] / ntiles_x # Actual size of contextual regions + y_size = image.shape[0] / ntiles_y + n_pixels = x_size * y_size + + if clip_limit > 0.0: # Calculate actual cliplimit + clip_limit = int(clip_limit * (x_size * y_size) / nbins) + if clip_limit < 1: + clip_limit = 1 + else: + clip_limit = NR_OF_GREY # Large value, do not clip (AHE) + + bin_size = 1 + NR_OF_GREY / nbins + aLUT = np.arange(NR_OF_GREY) + aLUT /= bin_size + img_blocks = view_as_blocks(image, (y_size, x_size)) + + # Calculate greylevel mappings for each contextual region + for y in range(ntiles_y): + for x in range(ntiles_x): + sub_img = img_blocks[y, x] + hist = aLUT[sub_img.ravel()] + hist = np.bincount(hist) + hist = np.append(hist, np.zeros(nbins - hist.size, dtype=int)) + hist = clip_histogram(hist, clip_limit) + hist = map_histogram(hist, 0, NR_OF_GREY - 1, n_pixels) + map_array[y, x] = hist + + # Interpolate greylevel mappings to get CLAHE image + ystart = 0 + for y in range(ntiles_y + 1): + xstart = 0 + if y == 0: # special case: top row + ystep = y_size / 2.0 + yU = 0 + yB = 0 + elif y == ntiles_y: # special case: bottom row + ystep = y_size / 2.0 + yU = ntiles_y - 1 + yB = yU + else: # default values + ystep = y_size + yU = y - 1 + yB = yB + 1 + + for x in range(ntiles_x + 1): + if x == 0: # special case: left column + xstep = x_size / 2.0 + xL = 0 + xR = 0 + elif x == ntiles_x: # special case: right column + xstep = x_size / 2.0 + xL = ntiles_x - 1 + xR = xL + else: # default values + xstep = x_size + xL = x - 1 + xR = xL + 1 + + mapLU = map_array[yU, xL] + mapRU = map_array[yU, xR] + mapLB = map_array[yB, xL] + mapRB = map_array[yB, xR] + + xslice = np.arange(xstart, xstart + xstep) + yslice = np.arange(ystart, ystart + ystep) + interpolate(image, xslice, yslice, + mapLU, mapRU, mapLB, mapRB, aLUT) + + xstart += xstep # set pointer on next matrix */ + + ystart += ystep + + return image + + +def clip_histogram(hist, clip_limit): + """Perform clipping of the histogram and redistribution of bins. + + The histogram is clipped and the number of excess pixels is counted. + Afterwards the excess pixels are equally redistributed across the + whole histogram (providing the bin count is smaller than the cliplimit). + + Parameters + ---------- + hist : ndarray + Histogram array. + clip_limit : int + Maximum allowed bin count. + + Returns + ------- + hist : ndarray + Clipped histogram. + """ + # calculate total number of excess pixels + excess_mask = hist > clip_limit + excess = hist[excess_mask] + n_excess = excess.sum() - excess.size * clip_limit + + # Second part: clip histogram and redistribute excess pixels in each bin + bin_incr = int(n_excess / hist.size) # average binincrement + upper = clip_limit - bin_incr # Bins larger than upper set to cliplimit + + hist[excess_mask] = clip_limit + + low_mask = hist < upper + n_excess -= hist[low_mask].size * bin_incr + hist[low_mask] += bin_incr + + mid_mask = (hist >= upper) & (hist < clip_limit) + mid = hist[mid_mask] + n_excess -= mid.size * clip_limit - mid.sum() + hist[mid_mask] = clip_limit + + while n_excess > 0: # Redistribute remaining excess + index = 0 + while n_excess > 0 and index < hist.size: + step_size = int(hist[hist < clip_limit].size / n_excess) + step_size = max(step_size, 1) + indices = np.arange(index, hist.size, step_size) + under = hist[indices] < clip_limit + hist[under] += 1 + n_excess -= hist[under].size + index += 1 + + return hist + + +def map_histogram(hist, min_val, max_val, n_pixels): + """Calculate the equalized lookup table (mapping). + + It does so by cumulating the input histogram. + + Parameters + ---------- + hist : ndarray + Clipped histogram. + min_val : int + Minimum value for mapping. + max_val : int + Maximum value for mapping. + n_pixels : int + Number of pixels in the region. + + Returns + ------- + out : ndarray + Mapped intensity LUT. + """ + out = np.cumsum(hist).astype(float) + scale = ((float)(max_val - min_val)) / n_pixels + out *= scale + out += min_val + out[out > max_val] = max_val + return out.astype(int) + + +def interpolate(image, xslice, yslice, + mapLU, mapRU, mapLB, mapRB, aLUT): + """Find the new grayscale level for a region using bilinear interpolation. + + Parameters + ---------- + image : ndarray + Full image. + xslice, yslice : array-like + Indices of the region. + map* : ndarray + Mappings of greylevels from histograms. + aLUT : ndarray + Maps grayscale levels in image to histogram levels. + + Returns + ------- + out : ndarray + Original image with the subregion replaced. + + Notes + ----- + This function calculates the new greylevel assignments of pixels within + a submatrix of the image. This is done by a bilinear interpolation between + four different mappings in order to eliminate boundary artifacts. + """ + norm = xslice.size * yslice.size # Normalization factor + # interpolation weight matrices + x_coef, y_coef = np.meshgrid(np.arange(xslice.size), + np.arange(yslice.size)) + x_inv_coef, y_inv_coef = x_coef[:, ::-1] + 1, y_coef[::-1] + 1 + + view = image[yslice[0]: yslice[-1] + 1, xslice[0]: xslice[-1] + 1] + im_slice = aLUT[view] + new = ((y_inv_coef * (x_inv_coef * mapLU[im_slice] + + x_coef * mapRU[im_slice]) + + y_coef * (x_inv_coef * mapLB[im_slice] + + x_coef * mapRB[im_slice])) + / norm) + view[:, :] = new + return image diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index a0a576d6..c7f1e712 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -1,7 +1,10 @@ import numpy as np -import skimage +from skimage import img_as_float from skimage.util.dtype import dtype_range +import skimage.color as color +from skimage.util.dtype import convert +from skimage._shared.utils import deprecated __all__ = ['histogram', 'cumulative_distribution', 'equalize', @@ -76,7 +79,12 @@ def cumulative_distribution(image, nbins=256): return img_cdf, bin_centers +@deprecated('equalize_hist') def equalize(image, nbins=256): + return equalize_hist(image, nbins) + + +def equalize_hist(image, nbins=256): """Return image after histogram equalization. Parameters @@ -101,7 +109,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 +143,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 +197,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..086739de 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -1,21 +1,23 @@ import numpy as np from numpy.testing import assert_array_almost_equal as assert_close - import skimage from skimage import data from skimage import exposure +from skimage.color import rgb2gray +from skimage.util.dtype import dtype_range # Test histogram equalization # =========================== # squeeze image intensities to lower image contrast -test_img = exposure.rescale_intensity(data.camera() / 5. + 100) +test_img = skimage.img_as_float(data.camera()) +test_img = exposure.rescale_intensity(test_img / 5. + 100) def test_equalize_ubyte(): img = skimage.img_as_ubyte(test_img) - img_eq = exposure.equalize(img) + img_eq = exposure.equalize_hist(img) cdf, bin_edges = exposure.cumulative_distribution(img_eq) check_cdf_slope(cdf) @@ -23,7 +25,7 @@ def test_equalize_ubyte(): def test_equalize_float(): img = skimage.img_as_float(test_img) - img_eq = exposure.equalize(img) + img_eq = exposure.equalize_hist(img) cdf, bin_edges = exposure.cumulative_distribution(img_eq) check_cdf_slope(cdf) @@ -71,7 +73,99 @@ def test_rescale_out_range(): assert_close(out, [0, 63, 127]) +# Test adaptive histogram equalization +# ==================================== + +def test_adapthist_scalar(): + '''Test a scalar uint8 image + ''' + img = skimage.img_as_ubyte(data.moon()) + adapted = exposure.equalize_adapthist(img, clip_limit=0.02) + assert adapted.min() == 0 + assert adapted.max() == (1 << 16) - 1 + assert img.shape == adapted.shape + full_scale = skimage.exposure.rescale_intensity(skimage.img_as_uint(img)) + + assert_almost_equal = np.testing.assert_almost_equal + assert_almost_equal(peak_snr(full_scale, adapted), 101.231, 3) + assert_almost_equal(norm_brightness_err(full_scale, adapted), + 0.041, 3) + return img, adapted + + +def test_adapthist_grayscale(): + '''Test a grayscale float image + ''' + img = skimage.img_as_float(data.lena()) + img = rgb2gray(img) + img = np.dstack((img, img, img)) + adapted = exposure.equalize_adapthist(img, 10, 9, clip_limit=0.01, + nbins=128) + assert_almost_equal = np.testing.assert_almost_equal + assert img.shape == adapted.shape + assert_almost_equal(peak_snr(img, adapted), 97.531, 3) + assert_almost_equal(norm_brightness_err(img, adapted), 0.0313, 3) + return data, adapted + + +def test_adapthist_color(): + '''Test an RGB color uint16 image + ''' + img = skimage.img_as_uint(data.lena()) + adapted = exposure.equalize_adapthist(img, clip_limit=0.01) + assert_almost_equal = np.testing.assert_almost_equal + assert adapted.min() == 0 + assert adapted.max() == 1.0 + assert img.shape == adapted.shape + full_scale = skimage.exposure.rescale_intensity(img) + assert_almost_equal(peak_snr(full_scale, adapted), 102.940, 3) + assert_almost_equal(norm_brightness_err(full_scale, adapted), + 0.0110, 3) + return data, adapted + + +def peak_snr(img1, img2): + '''Peak signal to noise ratio of two images + + Parameters + ---------- + img1 : array-like + img2 : array-like + + Returns + ------- + peak_snr : float + Peak signal to noise ratio + ''' + if img1.ndim == 3: + img1, img2 = rgb2gray(img1.copy()), rgb2gray(img2.copy()) + img1 = skimage.img_as_float(img1) + img2 = skimage.img_as_float(img2) + mse = 1. / img1.size * np.square(img1 - img2).sum() + _, max_ = dtype_range[img1.dtype.type] + return 20 * np.log(max_ / mse) + + +def norm_brightness_err(img1, img2): + '''Normalized Absolute Mean Brightness Error between two images + + Parameters + ---------- + img1 : array-like + img2 : array-like + + Returns + ------- + norm_brightness_error : float + Normalized absolute mean brightness error + ''' + if img1.ndim == 3: + img1, img2 = rgb2gray(img1), rgb2gray(img2) + ambe = np.abs(img1.mean() - img2.mean()) + nbe = ambe / dtype_range[img1.dtype.type][1] + return nbe + + 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..9f1e5f92 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -1,5 +1,8 @@ -from .hog import hog -from .greycomatrix import greycomatrix, greycoprops +from ._daisy import daisy +from ._hog import hog +from .texture import greycomatrix, greycoprops, local_binary_pattern from .peak import peak_local_max -from .harris import harris +from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, + corner_foerstner, corner_subpix, corner_peaks) +from .corner_cy import corner_moravec from .template import match_template diff --git a/skimage/feature/_daisy.py b/skimage/feature/_daisy.py new file mode 100644 index 00000000..1a97de8f --- /dev/null +++ b/skimage/feature/_daisy.py @@ -0,0 +1,223 @@ +import numpy as np +from scipy import sqrt, pi, arctan2, cos, sin, exp +from scipy.ndimage import gaussian_filter +import skimage.color +from skimage import img_as_float, draw + + +def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8, + normalization='l1', sigmas=None, ring_radii=None, visualize=False): + '''Extract DAISY feature descriptors densely for the given image. + + DAISY is a feature descriptor similar to SIFT formulated in a way that + allows for fast dense extraction. Typically, this is practical for + bag-of-features image representations. + + The implementation follows Tola et al. [1]_ but deviate on the following + points: + + * Histogram bin contribution are smoothed with a circular Gaussian + window over the tonal range (the angular range). + * The sigma values of the spatial Gaussian smoothing in this code do not + match the sigma values in the original code by Tola et al. [2]_. In + their code, spatial smoothing is applied to both the input image and + the center histogram. However, this smoothing is not documented in [1]_ + and, therefore, it is omitted. + + Parameters + ---------- + img : (M, N) array + Input image (greyscale). + step : int, optional + Distance between descriptor sampling points. + radius : int, optional + Radius (in pixels) of the outermost ring. + rings : int, optional + Number of rings. + histograms : int, optional + Number of histograms sampled per ring. + orientations : int, optional + Number of orientations (bins) per histogram. + normalization : [ 'l1' | 'l2' | 'daisy' | 'off' ], optional + How to normalize the descriptors + + * 'l1': L1-normalization of each descriptor. + * 'l2': L2-normalization of each descriptor. + * 'daisy': L2-normalization of individual histograms. + * 'off': Disable normalization. + + sigmas : 1D array of float, optional + Standard deviation of spatial Gaussian smoothing for the center + histogram and for each ring of histograms. The array of sigmas should + be sorted from the center and out. I.e. the first sigma value defines + the spatial smoothing of the center histogram and the last sigma value + defines the spatial smoothing of the outermost ring. Specifying sigmas + overrides the following parameter. + + ``rings = len(sigmas) - 1`` + + ring_radii : 1D array of int, optional + Radius (in pixels) for each ring. Specifying ring_radii overrides the + following two parameters. + + ``rings = len(ring_radii)`` + ``radius = ring_radii[-1]`` + + If both sigmas and ring_radii are given, they must satisfy the + following predicate since no radius is needed for the center + histogram. + + ``len(ring_radii) == len(sigmas) + 1`` + + visualize : bool, optional + Generate a visualization of the DAISY descriptors + + Returns + ------- + descs : array + Grid of DAISY descriptors for the given image as an array + dimensionality (P, Q, R) where + + ``P = ceil((M - radius*2) / step)`` + ``Q = ceil((N - radius*2) / step)`` + ``R = (rings * histograms + 1) * orientations`` + + descs_img : (M, N, 3) array (only if visualize==True) + Visualization of the DAISY descriptors. + + References + ---------- + .. [1] Tola et al. "Daisy: An efficient dense descriptor applied to wide- + baseline stereo." Pattern Analysis and Machine Intelligence, IEEE + Transactions on 32.5 (2010): 815-830. + .. [2] http://cvlab.epfl.ch/alumni/tola/daisy.html + ''' + + # Validate image format. + if img.ndim > 2: + raise ValueError('Only grey-level images are supported.') + if img.dtype.kind != 'f': + img = img_as_float(img) + + # Validate parameters. + if sigmas is not None and ring_radii is not None \ + and len(sigmas) - 1 != len(ring_radii): + raise ValueError('len(sigmas)-1 != len(ring_radii)') + if ring_radii is not None: + rings = len(ring_radii) + radius = ring_radii[-1] + if sigmas is not None: + rings = len(sigmas) - 1 + if sigmas is None: + sigmas = [radius * (i + 1) / float(2 * rings) for i in range(rings)] + if ring_radii is None: + ring_radii = [radius * (i + 1) / float(rings) for i in range(rings)] + if normalization not in ['l1', 'l2', 'daisy', 'off']: + raise ValueError('Invalid normalization method.') + + # Compute image derivatives. + dx = np.zeros(img.shape) + dy = np.zeros(img.shape) + dx[:, :-1] = np.diff(img, n=1, axis=1) + dy[:-1, :] = np.diff(img, n=1, axis=0) + + # Compute gradient orientation and magnitude and their contribution + # to the histograms. + grad_mag = sqrt(dx ** 2 + dy ** 2) + grad_ori = arctan2(dy, dx) + orientation_kappa = orientations / pi + orientation_angles = [2 * o * pi / orientations - pi + for o in range(orientations)] + hist = np.empty((orientations,) + img.shape, dtype=float) + for i, o in enumerate(orientation_angles): + # Weigh bin contribution by the circular normal distribution + hist[i, :, :] = exp(orientation_kappa * cos(grad_ori - o)) + # Weigh bin contribution by the gradient magnitude + hist[i, :, :] = np.multiply(hist[i, :, :], grad_mag) + + # Smooth orientation histograms for the center and all rings. + sigmas = [sigmas[0]] + sigmas + hist_smooth = np.empty((rings + 1,) + hist.shape, dtype=float) + for i in range(rings + 1): + for j in range(orientations): + hist_smooth[i, j, :, :] = gaussian_filter(hist[j, :, :], + sigma=sigmas[i]) + + # Assemble descriptor grid. + theta = [2 * pi * j / histograms for j in range(histograms)] + desc_dims = (rings * histograms + 1) * orientations + descs = np.empty((desc_dims, img.shape[0] - 2 * radius, + img.shape[1] - 2 * radius)) + descs[:orientations, :, :] = hist_smooth[0, :, radius:-radius, + radius:-radius] + idx = orientations + for i in range(rings): + for j in range(histograms): + y_min = radius + int(round(ring_radii[i] * sin(theta[j]))) + y_max = descs.shape[1] + y_min + x_min = radius + int(round(ring_radii[i] * cos(theta[j]))) + x_max = descs.shape[2] + x_min + descs[idx:idx + orientations, :, :] = hist_smooth[i + 1, :, + y_min:y_max, + x_min:x_max] + idx += orientations + descs = descs[:, ::step, ::step] + descs = descs.swapaxes(0, 1).swapaxes(1, 2) + + # Normalize descriptors. + if normalization != 'off': + descs += 1e-10 + if normalization == 'l1': + descs /= np.sum(descs, axis=2)[:, :, np.newaxis] + elif normalization == 'l2': + descs /= sqrt(np.sum(descs ** 2, axis=2))[:, :, np.newaxis] + elif normalization == 'daisy': + for i in range(0, desc_dims, orientations): + norms = sqrt(np.sum(descs[:, :, i:i + orientations] ** 2, + axis=2)) + descs[:, :, i:i + orientations] /= norms[:, :, np.newaxis] + + if visualize: + descs_img = skimage.color.gray2rgb(img) + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + # Draw center histogram sigma + color = (1, 0, 0) + desc_y = i * step + radius + desc_x = j * step + radius + coords = draw.circle_perimeter(desc_y, desc_x, int(sigmas[0])) + draw.set_color(descs_img, coords, color) + max_bin = np.max(descs[i, j, :]) + for o_num, o in enumerate(orientation_angles): + # Draw center histogram bins + bin_size = descs[i, j, o_num] / max_bin + dy = sigmas[0] * bin_size * sin(o) + dx = sigmas[0] * bin_size * cos(o) + coords = draw.line(desc_y, desc_x, int(desc_y + dy), + int(desc_x + dx)) + draw.set_color(descs_img, coords, color) + for r_num, r in enumerate(ring_radii): + color_offset = float(1 + r_num) / rings + color = (1 - color_offset, 1, color_offset) + for t_num, t in enumerate(theta): + # Draw ring histogram sigmas + hist_y = desc_y + int(round(r * sin(t))) + hist_x = desc_x + int(round(r * cos(t))) + coords = draw.circle_perimeter(hist_y, hist_x, + int(sigmas[r_num + 1])) + draw.set_color(descs_img, coords, color) + for o_num, o in enumerate(orientation_angles): + # Draw histogram bins + bin_size = descs[i, j, orientations + r_num * + histograms * orientations + + t_num * orientations + o_num] + bin_size /= max_bin + dy = sigmas[r_num + 1] * bin_size * sin(o) + dx = sigmas[r_num + 1] * bin_size * cos(o) + coords = draw.line(hist_y, hist_x, + int(hist_y + dy), + int(hist_x + dx)) + draw.set_color(descs_img, coords, color) + return descs, descs_img + else: + return descs 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/hog.py b/skimage/feature/_hog.py similarity index 87% rename from skimage/feature/hog.py rename to skimage/feature/_hog.py index fc5c19da..9fa018b7 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,40 @@ 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(int(centre[0] - dx), + int(centre[1] - dy), + int(centre[0] + dx), + int(centre[1] + dy)) hog_image[rr, cc] += orientation_histogram[y, x, o] """ @@ -166,7 +172,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..03695959 100644 --- a/skimage/feature/_template.pyx +++ b/skimage/feature/_template.pyx @@ -1,3 +1,8 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + """ Template matching using normalized cross-correlation. @@ -30,67 +35,31 @@ the image window *before* squaring.) .. [2] J. P. Lewis, "Fast Normalized Cross-Correlation", Industrial Light and Magic. """ -import cython -cimport numpy as np + import numpy as np from scipy.signal import fftconvolve + +cimport numpy as cnp +from libc.math cimport sqrt, fabs +from skimage._shared.transform cimport integrate + + from skimage.transform import integral -cdef extern from "math.h": - float sqrt(float x) - float fabs(float x) +def match_template(cnp.ndarray[float, ndim=2, mode="c"] image, + cnp.ndarray[float, ndim=2, mode="c"] template): - -@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 - - -@cython.boundscheck(False) -def match_template(np.ndarray[float, ndim=2, mode="c"] image, - np.ndarray[float, ndim=2, mode="c"] template): - cdef np.ndarray[float, ndim=2, mode="c"] corr - cdef np.ndarray[float, ndim=2, mode="c"] image_sat - cdef np.ndarray[float, ndim=2, mode="c"] image_sqr_sat + cdef cnp.ndarray[float, ndim=2, mode="c"] corr + cdef cnp.ndarray[float, ndim=2, mode="c"] image_sat + cdef cnp.ndarray[float, ndim=2, mode="c"] image_sqr_sat cdef float template_mean = np.mean(template) cdef float template_ssd cdef float inv_area + cdef Py_ssize_t r, c, r_end, c_end + cdef Py_ssize_t template_rows = template.shape[0] + cdef Py_ssize_t template_cols = template.shape[1] + cdef float den, window_sqr_sum, window_mean_sqr, window_sum image_sat = integral.integral_image(image) image_sqr_sat = integral.integral_image(image**2) @@ -106,24 +75,23 @@ def match_template(np.ndarray[float, ndim=2, mode="c"] image, mode="valid"), dtype=np.float32) - cdef int i, j - cdef float den, window_sqr_sum, window_mean_sqr, window_sum, - # move window through convolution results, normalizing in the process - for i in range(corr.shape[0]): - for j in range(corr.shape[1]): - # subtract 1 because `i_end` and `j_end` are used for indexing into - # summed-area table, instead of slicing windows of the image. - i_end = i + template.shape[0] - 1 - j_end = j + template.shape[1] - 1 - window_sum = integrate(image_sat, i, j, i_end, j_end) + # move window through convolution results, normalizing in the process + for r in range(corr.shape[0]): + for c in range(corr.shape[1]): + # subtract 1 because `i_end` and `c_end` are used for indexing into + # summed-area table, instead of slicing windows of the image. + r_end = r + template_rows - 1 + c_end = c + template_cols - 1 + + window_sum = integrate(image_sat, r, c, r_end, c_end) window_mean_sqr = window_sum * window_sum * inv_area - window_sqr_sum = integrate(image_sqr_sat, i, j, i_end, j_end) + window_sqr_sum = integrate(image_sqr_sat, r, c, r_end, c_end) if window_sqr_sum <= window_mean_sqr: - corr[i, j] = 0 + corr[r, c] = 0 continue den = sqrt((window_sqr_sum - window_mean_sqr) * template_ssd) - corr[i, j] /= den - return corr + corr[r, c] /= den + return corr diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx new file mode 100644 index 00000000..f98ed4ca --- /dev/null +++ b/skimage/feature/_texture.pyx @@ -0,0 +1,184 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +cimport numpy as cnp +from libc.math cimport sin, cos, abs +from skimage._shared.interpolation cimport bilinear_interpolation + + +def _glcm_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] image, + cnp.ndarray[dtype=cnp.float64_t, ndim=1, + negative_indices=False, mode='c'] distances, + cnp.ndarray[dtype=cnp.float64_t, ndim=1, + negative_indices=False, mode='c'] angles, + int levels, + cnp.ndarray[dtype=cnp.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: + Py_ssize_t a_idx, d_idx, r, c, rows, cols, row, col + cnp.uint8_t i, j + cnp.float64_t angle, distance + + rows = image.shape[0] + cols = image.shape[1] + + for a_idx in range(len(angles)): + angle = angles[a_idx] + for d_idx in range(len(distances)): + distance = distances[d_idx] + 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(cnp.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 cnp.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 cnp.ndarray[double, ndim=2] coords = np.round(np.vstack([rp, cp]).T, 5) + + # pre allocate arrays for computation + cdef cnp.ndarray[double, ndim=1] texture = np.zeros(P, np.double) + cdef cnp.ndarray[char, ndim=1] signed_texture = np.zeros(P, np.int8) + cdef cnp.ndarray[int, ndim=1] rotation_chain = np.zeros(P, np.int32) + + output_shape = (image.shape[0], image.shape[1]) + cdef cnp.ndarray[double, ndim=2] output = np.zeros(output_shape, np.double) + + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + + cdef double lbp + cdef Py_ssize_t 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/corner.py b/skimage/feature/corner.py new file mode 100644 index 00000000..12d3fb70 --- /dev/null +++ b/skimage/feature/corner.py @@ -0,0 +1,505 @@ +import numpy as np +from scipy import ndimage +from scipy import stats +from skimage.color import rgb2grey +from skimage.util import img_as_float +from skimage.feature import peak_local_max + + +def _compute_derivatives(image): + """Compute derivatives in x and y direction using the Sobel operator. + + Parameters + ---------- + image : ndarray + Input image. + + Returns + ------- + imx : ndarray + Derivative in x-direction. + imy : ndarray + Derivative in y-direction. + + """ + + imy = ndimage.sobel(image, axis=0, mode='constant', cval=0) + imx = ndimage.sobel(image, axis=1, mode='constant', cval=0) + + return imx, imy + + +def _compute_auto_correlation(image, sigma): + """Compute auto-correlation matrix using sum of squared differences. + + Parameters + ---------- + image : ndarray + Input image. + sigma : float + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + Axx : ndarray + Element of the auto-correlation matrix for each pixel in input image. + Axy : ndarray + Element of the auto-correlation matrix for each pixel in input image. + Ayy : ndarray + Element of the auto-correlation matrix for each pixel in input image. + + """ + + if image.ndim == 3: + image = img_as_float(rgb2grey(image)) + + imx, imy = _compute_derivatives(image) + + # structure tensore + Axx = ndimage.gaussian_filter(imx * imx, sigma, mode='constant', cval=0) + Axy = ndimage.gaussian_filter(imx * imy, sigma, mode='constant', cval=0) + Ayy = ndimage.gaussian_filter(imy * imy, sigma, mode='constant', cval=0) + + return Axx, Axy, Ayy + + +def corner_kitchen_rosenfeld(image): + """Compute Kitchen and Rosenfeld corner measure response image. + + The corner measure is calculated as follows:: + + (imxx * imy**2 + imyy * imx**2 - 2 * imxy * imx * imy) + ------------------------------------------------------ + (imx**2 + imy**2) + + Where imx and imy are the first and imxx, imxy, imyy the second derivatives. + + Parameters + ---------- + image : ndarray + Input image. + + Returns + ------- + response : ndarray + Kitchen and Rosenfeld response image. + + """ + + imx, imy = _compute_derivatives(image) + imxx, imxy = _compute_derivatives(imx) + imyx, imyy = _compute_derivatives(imy) + + response = (imxx * imy**2 + imyy * imx**2 - 2 * imxy * imx * imy) \ + / (imx**2 + imy**2) + + return response + + +def corner_harris(image, method='k', k=0.05, eps=1e-6, sigma=1): + """Compute Harris corner measure response image. + + This corner detector uses information from the auto-correlation matrix A:: + + A = [(imx**2) (imx*imy)] = [Axx Axy] + [(imx*imy) (imy**2)] [Axy Ayy] + + Where imx and imy are the first derivatives averaged with a gaussian filter. + The corner measure is then defined as:: + + det(A) - k * trace(A)**2 + + or:: + + 2 * det(A) / (trace(A) + eps) + + Parameters + ---------- + image : ndarray + Input image. + method : {'k', 'eps'}, optional + Method to compute the response image from the auto-correlation matrix. + k : float, optional + Sensitivity factor to separate corners from edges, typically in range + `[0, 0.2]`. Small values of k result in detection of sharp corners. + eps : float, optional + Normalisation factor (Noble's corner measure). + sigma : float, optional + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + response : ndarray + Harris response image. + + References + ---------- + ..[1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/harris.htm + ..[2] http://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_harris, corner_peaks + >>> square = np.zeros([10, 10]) + >>> square[2:8, 2:8] = 1 + >>> square + array([[ 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, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 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]]) + >>> corner_peaks(corner_harris(square), min_distance=1) + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) + + """ + + Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + + # determinant + detA = Axx * Ayy - Axy**2 + # trace + traceA = Axx + Ayy + + if method == 'k': + response = detA - k * traceA**2 + else: + response = 2 * detA / (traceA + eps) + + return response + + +def corner_shi_tomasi(image, sigma=1): + """Compute Shi-Tomasi (Kanade-Tomasi) corner measure response image. + + This corner detector uses information from the auto-correlation matrix A:: + + A = [(imx**2) (imx*imy)] = [Axx Axy] + [(imx*imy) (imy**2)] [Axy Ayy] + + Where imx and imy are the first derivatives averaged with a gaussian filter. + The corner measure is then defined as the smaller eigenvalue of A:: + + ((Axx + Ayy) - sqrt((Axx - Ayy)**2 + 4 * Axy**2)) / 2 + + Parameters + ---------- + image : ndarray + Input image. + sigma : float, optional + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + response : ndarray + Shi-Tomasi response image. + + References + ---------- + ..[1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/harris.htm + ..[2] http://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_shi_tomasi, corner_peaks + >>> square = np.zeros([10, 10]) + >>> square[2:8, 2:8] = 1 + >>> square + array([[ 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, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 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]]) + >>> corner_peaks(corner_shi_tomasi(square), min_distance=1) + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) + + """ + + Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + + # minimum eigenvalue of A + response = ((Axx + Ayy) - np.sqrt((Axx - Ayy)**2 + 4 * Axy**2)) / 2 + + return response + + +def corner_foerstner(image, sigma=1): + """Compute Foerstner corner measure response image. + + This corner detector uses information from the auto-correlation matrix A:: + + A = [(imx**2) (imx*imy)] = [Axx Axy] + [(imx*imy) (imy**2)] [Axy Ayy] + + Where imx and imy are the first derivatives averaged with a gaussian filter. + The corner measure is then defined as:: + + w = det(A) / trace(A) (size of error ellipse) + q = 4 * det(A) / trace(A)**2 (roundness of error ellipse) + + Parameters + ---------- + image : ndarray + Input image. + sigma : float, optional + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + w : ndarray + Error ellipse sizes. + q : ndarray + Roundness of error ellipse. + + References + ---------- + ..[1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/\ + foerstner87.fast.pdf + ..[2] http://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_foerstner, corner_peaks + >>> square = np.zeros([10, 10]) + >>> square[2:8, 2:8] = 1 + >>> square + array([[ 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, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 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]]) + >>> w, q = corner_foerstner(square) + >>> accuracy_thresh = 0.5 + >>> roundness_thresh = 0.3 + >>> foerstner = (q > roundness_thresh) * (w > accuracy_thresh) * w + >>> corner_peaks(foerstner, min_distance=1) + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) + + """ + + Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + + # determinant + detA = Axx * Ayy - Axy**2 + # trace + traceA = Axx + Ayy + + w = detA / traceA + q = 4 * detA / traceA**2 + + return w, q + + +def corner_subpix(image, corners, window_size=11, alpha=0.99): + """Determine subpixel position of corners. + + Parameters + ---------- + image : ndarray + Input image. + corners : (N, 2) ndarray + Corner coordinates `(row, col)`. + window_size : int, optional + Search window size for subpixel estimation. + alpha : float, optional + Significance level for point classification. + + Returns + ------- + positions : (N, 2) ndarray + Subpixel corner positions. NaN for "not classified" corners. + + References + ---------- + ..[1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/\ + foerstner87.fast.pdf + ..[2] http://en.wikipedia.org/wiki/Corner_detection + + """ + + # window extent in one direction + wext = (window_size - 1) / 2 + + # normal equation arrays + N_dot = np.zeros((2, 2), dtype=np.double) + N_edge = np.zeros((2, 2), dtype=np.double) + b_dot = np.zeros((2, ), dtype=np.double) + b_edge = np.zeros((2, ), dtype=np.double) + + # critical statistical test values + redundancy = window_size**2 - 2 + t_crit_dot = stats.f.isf(1 - alpha, redundancy, redundancy) + t_crit_edge = stats.f.isf(alpha, redundancy, redundancy) + + # coordinates of pixels within window + y, x = np.mgrid[- wext:wext + 1, - wext:wext + 1] + + corners_subpix = np.zeros_like(corners, dtype=np.double) + + for i, (y0, x0) in enumerate(corners): + + # crop window around corner + border for sobel operator + miny = y0 - wext - 1 + maxy = y0 + wext + 2 + minx = x0 - wext - 1 + maxx = x0 + wext + 2 + window = image[miny:maxy, minx:maxx] + + winx, winy = _compute_derivatives(window) + + # compute gradient suares and remove border + winx_winx = (winx * winx)[1:-1, 1:-1] + winx_winy = (winx * winy)[1:-1, 1:-1] + winy_winy = (winy * winy)[1:-1, 1:-1] + + # sum of squared differences (mean instead of gaussian filter) + Axx = np.sum(winx_winx) + Axy = np.sum(winx_winy) + Ayy = np.sum(winy_winy) + + # sum of squared differences weighted with coordinates + # (mean instead of gaussian filter) + bxx_x = np.sum(winx_winx * x) + bxx_y = np.sum(winx_winx * y) + bxy_x = np.sum(winx_winy * x) + bxy_y = np.sum(winx_winy * y) + byy_x = np.sum(winy_winy * x) + byy_y = np.sum(winy_winy * y) + + # normal equations for subpixel position + N_dot[0, 0] = Axx + N_dot[0, 1] = N_dot[1, 0] = - Axy + N_dot[1, 1] = Ayy + + N_edge[0, 0] = Ayy + N_edge[0, 1] = N_edge[1, 0] = Axy + N_edge[1, 1] = Axx + + b_dot[:] = bxx_y - bxy_x, byy_x - bxy_y + b_edge[:] = byy_y + bxy_x, bxx_x + bxy_y + + # estimated positions + est_dot = np.linalg.solve(N_dot, b_dot) + est_edge = np.linalg.solve(N_edge, b_edge) + + # residuals + ry_dot = y - est_dot[0] + rx_dot = x - est_dot[1] + ry_edge = y - est_edge[0] + rx_edge = x - est_edge[1] + # squared residuals + rxx_dot = rx_dot * rx_dot + rxy_dot = rx_dot * ry_dot + ryy_dot = ry_dot * ry_dot + rxx_edge = rx_edge * rx_edge + rxy_edge = rx_edge * ry_edge + ryy_edge = ry_edge * ry_edge + + # determine corner class (dot or edge) + # variance for different models + var_dot = np.sum(winx_winx * ryy_dot - 2 * winx_winy * rxy_dot \ + + winy_winy * rxx_dot) + var_edge = np.sum(winy_winy * ryy_edge + 2 * winx_winy * rxy_edge \ + + winx_winx * rxx_edge) + # test value (F-distributed) + t = var_edge / var_dot + # 1 for edge, -1 for dot, 0 for "not classified" + corner_class = (t < t_crit_edge) - (t > t_crit_dot) + + if corner_class == - 1: + corners_subpix[i, :] = y0 + est_dot[0], x0 + est_dot[1] + elif corner_class == 0: + corners_subpix[i, :] = np.nan, np.nan + elif corner_class == 1: + corners_subpix[i, :] = y0 + est_edge[0], x0 + est_edge[1] + + return corners_subpix + + +def corner_peaks(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, + exclude_border=True, indices=True, num_peaks=np.inf, + footprint=None, labels=None): + """Find corners in corner measure response image. + + This differs from `skimage.feature.peak_local_max` in that it suppresses + multiple connected peaks with the same accumulator value. + + Parameters + ---------- + See `skimage.feature.peak_local_max`. + + Returns + ------- + See `skimage.feature.peak_local_max`. + + Examples + -------- + >>> from skimage.feature import peak_local_max, corner_peaks + >>> response = np.zeros((5, 5)) + >>> response[2:4, 2:4] = 1 + >>> response + array([[ 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0.], + [ 0., 0., 1., 1., 0.], + [ 0., 0., 1., 1., 0.], + [ 0., 0., 0., 0., 0.]]) + >>> peak_local_max(response, exclude_border=False) + array([[2, 2], + [2, 3], + [3, 2], + [3, 3]]) + >>> corner_peaks(response, exclude_border=False) + array([[2, 2]]) + >>> corner_peaks(response, exclude_border=False, min_distance=0) + array([[2, 2], + [2, 3], + [3, 2], + [3, 3]]) + + """ + + peaks = peak_local_max(image, min_distance=min_distance, + threshold_abs=threshold_abs, + threshold_rel=threshold_rel, + exclude_border=exclude_border, + indices=False, num_peaks=np.inf, + footprint=footprint, labels=labels) + if min_distance > 0: + coords = np.transpose(peaks.nonzero()) + for r, c in coords: + if peaks[r, c]: + peaks[r - min_distance:r + min_distance + 1, + c - min_distance:c + min_distance + 1] = False + peaks[r, c] = True + + if indices is True: + return np.transpose(peaks.nonzero()) + else: + return peaks diff --git a/skimage/feature/corner_cy.pyx b/skimage/feature/corner_cy.pyx new file mode 100644 index 00000000..71c748f8 --- /dev/null +++ b/skimage/feature/corner_cy.pyx @@ -0,0 +1,91 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +cimport numpy as cnp +from libc.float cimport DBL_MAX + +from skimage.color import rgb2grey +from skimage.util import img_as_float + + +def corner_moravec(image, Py_ssize_t window_size=1): + """Compute Moravec corner measure response image. + + This is one of the simplest corner detectors and is comparatively fast but + has several limitations (e.g. not rotation invariant). + + Parameters + ---------- + image : ndarray + Input image. + window_size : int, optional + Window size. + + Returns + ------- + response : ndarray + Moravec response image. + + References + ---------- + ..[1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/moravec.htm + ..[2] http://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import moravec, peak_local_max + >>> square = np.zeros([7, 7]) + >>> square[3, 3] = 1 + >>> square + 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., 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.]]) + >>> moravec(square) + array([[ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 1., 1., 1., 0., 0.], + [ 0., 0., 1., 2., 1., 0., 0.], + [ 0., 0., 1., 1., 1., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.]]) + """ + + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + + cdef cnp.ndarray[dtype=cnp.double_t, ndim=2, mode='c'] cimage, out + + if image.ndim == 3: + cimage = rgb2grey(image) + cimage = np.ascontiguousarray(img_as_float(image)) + + out = np.zeros(image.shape, dtype=np.double) + + cdef double* image_data = cimage.data + cdef double* out_data = out.data + + cdef double msum, min_msum + cdef Py_ssize_t r, c, br, bc, mr, mc, a, b + for r in range(2 * window_size, rows - 2 * window_size): + for c in range(2 * window_size, cols - 2 * window_size): + min_msum = DBL_MAX + for br in range(r - window_size, r + window_size + 1): + for bc in range(c - window_size, c + window_size + 1): + if br != r and bc != c: + msum = 0 + for mr in range(- window_size, window_size + 1): + for mc in range(- window_size, window_size + 1): + a = (r + mr) * cols + c + mc + b = (br + mr) * cols + bc + mc + msum += (image_data[a] - image_data[b]) ** 2 + min_msum = min(msum, min_msum) + + out_data[r * cols + c] = min_msum + + return out diff --git a/skimage/feature/harris.py b/skimage/feature/harris.py deleted file mode 100644 index 6d0da9c0..00000000 --- a/skimage/feature/harris.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Harris corner detector - -Inspired from Solem's implementation -http://www.janeriksolem.net/2009/01/harris-corner-detector-in-python.html -""" -from scipy import ndimage - -from . import peak - - -def _compute_harris_response(image, eps=1e-6, gaussian_deviation=1): - """Compute the Harris corner detector response function - for each pixel in the image - - Parameters - ---------- - image : ndarray of floats - Input image. - - eps : float, optional - Normalisation factor. - - gaussian_deviation : integer, optional - Standard deviation used for the Gaussian kernel. - - Returns - -------- - image : (M, N) ndarray - Harris image response - """ - if len(image.shape) == 3: - image = image.mean(axis=2) - - # derivatives - image = ndimage.gaussian_filter(image, gaussian_deviation) - imx = ndimage.sobel(image, axis=0, mode='constant') - imy = ndimage.sobel(image, axis=1, mode='constant') - - Wxx = ndimage.gaussian_filter(imx * imx, 1.5, mode='constant') - Wxy = ndimage.gaussian_filter(imx * imy, 1.5, mode='constant') - Wyy = ndimage.gaussian_filter(imy * imy, 1.5, mode='constant') - - # determinant and trace - Wdet = Wxx * Wyy - Wxy ** 2 - Wtr = Wxx + Wyy - # Alternate formula for Harris response. - # Alison Noble, "Descriptions of Image Surfaces", PhD thesis (1989) - harris = Wdet / (Wtr + eps) - - return harris - - -def harris(image, min_distance=10, threshold=0.1, eps=1e-6, - gaussian_deviation=1): - """Return corners from a Harris response image - - Parameters - ---------- - image : ndarray of floats - Input image. - - min_distance : int, optional - Minimum number of pixels separating interest points and image boundary. - - threshold : float, optional - Relative threshold impacting the number of interest points. - - eps : float, optional - Normalisation factor. - - gaussian_deviation : integer, optional - Standard deviation used for the Gaussian kernel. - - Returns - ------- - coordinates : (N, 2) array - (row, column) coordinates of interest points. - - Examples - ------- - >>> square = np.zeros([10,10]) - >>> square[2:8,2:8] = 1 - >>> square - array([[ 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., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 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.]]) - >>> harris(square, min_distance=1) - - Corners of the square - - array([[3, 3], - [3, 6], - [6, 3], - [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 - diff --git a/skimage/feature/peak.py b/skimage/feature/peak.py index 5d3f375b..9c7a934f 100644 --- a/skimage/feature/peak.py +++ b/skimage/feature/peak.py @@ -1,52 +1,68 @@ -import warnings import numpy as np -from scipy import ndimage +import scipy.ndimage as ndi +from ..filter import rank_order -def peak_local_max(image, min_distance=10, threshold='deprecated', - threshold_abs=0, threshold_rel=0.1, num_peaks=np.inf): - """Return coordinates of peaks in an image. +def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, + exclude_border=True, indices=True, num_peaks=np.inf, + footprint=None, labels=None): + """ + Find peaks in an image, and return them as coordinates or a boolean array. Peaks are the local maxima in a region of `2 * min_distance + 1` (i.e. peaks are separated by at least `min_distance`). - NOTE: If peaks are flat (i.e. multiple pixels have exact same intensity), - the coordinates of all pixels are returned. + NOTE: If peaks are flat (i.e. multiple adjacent pixels have identical + intensities), the coordinates of all such pixels are returned. Parameters ---------- - image: ndarray of floats + image : ndarray of floats Input image. - - min_distance: int - Minimum number of pixels separating peaks and image boundary. - - threshold : float - Deprecated. See `threshold_rel`. - - threshold_abs: float + min_distance : int + Minimum number of pixels separating peaks in a region of `2 * + min_distance + 1` (i.e. peaks are separated by at least + `min_distance`). If `exclude_border` is True, this value also excludes + a border `min_distance` from the image boundary. + To find the maximum number of peaks, use `min_distance=1`. + threshold_abs : float Minimum intensity of peaks. - - threshold_rel: float + threshold_rel : float Minimum intensity of peaks calculated as `max(image) * threshold_rel`. - + exclude_border : bool + If True, `min_distance` excludes peaks from the border of the image as + well as from each other. + indices : bool + If True, the output will be a matrix representing peak coordinates. + If False, the output will be a boolean matrix shaped as `image.shape` + with peaks present at True elements. num_peaks : int Maximum number of peaks. When the number of peaks exceeds `num_peaks`, - return `num_peaks` coordinates based on peak intensity. + return `num_peaks` peaks based on highest peak intensity. + footprint : ndarray of bools, optional + If provided, `footprint == 1` represents the local region within which + to search for peaks at every point in `image`. Overrides + `min_distance`, except for border exclusion if `exclude_border=True`. + labels : ndarray of ints, optional + If provided, each unique region `labels == value` represents a unique + region to search for peaks. Zero is reserved for background. Returns ------- - coordinates : (N, 2) array - (row, column) coordinates of peaks. - + output : (N, 2) array or ndarray of bools + + * If `indices = True` : (row, column) coordinates of peaks. + * If `indices = False` : Boolean array shaped like `image`, with peaks + represented by True values. + 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, - peak_local_max function returns the coordinates of peaks where - dilated image = original. - + 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, peak_local_max function returns the + coordinates of peaks where dilated image = original. + Examples -------- >>> im = np.zeros((7, 7)) @@ -60,45 +76,79 @@ 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]]) - + """ + out = np.zeros_like(image, dtype=np.bool) + # In the case of labels, recursively build and return an output + # operating on each label separately + if labels is not None: + label_values = np.unique(labels) + # Reorder label values to have consecutive integers (no gaps) + if np.any(np.diff(label_values) != 1): + mask = labels >= 1 + labels[mask] = 1 + rank_order(labels[mask])[0].astype(labels.dtype) + labels = labels.astype(np.int32) + + # New values for new ordering + label_values = np.unique(labels) + for label in label_values[label_values != 0]: + maskim = (labels == label) + out += peak_local_max(image * maskim, min_distance=min_distance, + threshold_abs=threshold_abs, + threshold_rel=threshold_rel, + exclude_border=exclude_border, + indices=False, num_peaks=np.inf, + footprint=footprint, labels=None) + + if indices is True: + return np.transpose(out.nonzero()) + else: + return out.astype(np.bool) + if np.all(image == image.flat[0]): - return [] + if indices is True: + return [] + else: + return out + image = image.copy() # Non maximum filter - size = 2 * min_distance + 1 - image_max = ndimage.maximum_filter(image, size=size, mode='constant') + if footprint is not None: + image_max = ndi.maximum_filter(image, footprint=footprint, + mode='constant') + else: + size = 2 * min_distance + 1 + image_max = ndi.maximum_filter(image, size=size, mode='constant') mask = (image == image_max) image *= mask - # Remove the image borders - image[:min_distance] = 0 - image[-min_distance:] = 0 - image[:, :min_distance] = 0 - image[:, -min_distance:] = 0 + if exclude_border: + # Remove the image borders + image[:min_distance] = 0 + image[-min_distance:] = 0 + image[:, :min_distance] = 0 + image[:, -min_distance:] = 0 - if not threshold == 'deprecated': - msg = "`threshold` parameter deprecated; use `threshold_rel instead." - warnings.warn(msg, DeprecationWarning) - threshold_rel = threshold # find top peak candidates above a threshold peak_threshold = max(np.max(image.ravel()) * threshold_rel, threshold_abs) - image_t = (image > peak_threshold) * 1 # get coordinates of peaks - coordinates = np.transpose(image_t.nonzero()) + coordinates = np.transpose((image > peak_threshold).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] - - return coordinates + coordinates = coordinates[idx_maxsort][:num_peaks] + if indices is True: + return coordinates + else: + out[coordinates[:, 0], coordinates[:, 1]] = True + return out diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index 13d4fae5..e769621d 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -5,29 +5,33 @@ 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(['corner_cy.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'], + config.add_extension('corner_cy', sources=['corner_cy.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_corner.py b/skimage/feature/tests/test_corner.py new file mode 100644 index 00000000..a7ee01ff --- /dev/null +++ b/skimage/feature/tests/test_corner.py @@ -0,0 +1,116 @@ +import numpy as np +from numpy.testing import assert_array_equal + +from skimage import data +from skimage import img_as_float + +from skimage.feature import (corner_moravec, corner_harris, corner_shi_tomasi, + corner_subpix, peak_local_max, corner_peaks) + + +def test_square_image(): + im = np.zeros((50, 50)).astype(float) + im[:25, :25] = 1. + + # Moravec + results = peak_local_max(corner_moravec(im)) + # interest points along edge + assert len(results) == 57 + + # Harris + results = peak_local_max(corner_harris(im)) + # interest at corner + assert len(results) == 1 + + # Shi-Tomasi + results = peak_local_max(corner_shi_tomasi(im)) + # interest at corner + assert len(results) == 1 + + +def test_noisy_square_image(): + im = np.zeros((50, 50)).astype(float) + im[:25, :25] = 1. + np.random.seed(seed=1234) + im = im + np.random.uniform(size=im.shape) * .2 + + # Moravec + results = peak_local_max(corner_moravec(im)) + # undefined number of interest points + assert results.any() + + # Harris + results = peak_local_max(corner_harris(im, sigma=1.5)) + assert len(results) == 1 + + # Shi-Tomasi + results = peak_local_max(corner_shi_tomasi(im, sigma=1.5)) + assert len(results) == 1 + + +def test_squared_dot(): + im = np.zeros((50, 50)) + im[4:8, 4:8] = 1 + im = img_as_float(im) + + # Moravec fails + + # Harris + results = peak_local_max(corner_harris(im)) + assert (results == np.array([[6, 6]])).all() + + # Shi-Tomasi + results = peak_local_max(corner_shi_tomasi(im)) + 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 + rotation. + """ + im = img_as_float(data.lena().mean(axis=2)) + im_rotated = im.T + + # Moravec + results = peak_local_max(corner_moravec(im)) + results_rotated = peak_local_max(corner_moravec(im_rotated)) + assert (np.sort(results[:, 0]) == np.sort(results_rotated[:, 1])).all() + assert (np.sort(results[:, 1]) == np.sort(results_rotated[:, 0])).all() + + # Harris + results = peak_local_max(corner_harris(im)) + results_rotated = peak_local_max(corner_harris(im_rotated)) + assert (np.sort(results[:, 0]) == np.sort(results_rotated[:, 1])).all() + assert (np.sort(results[:, 1]) == np.sort(results_rotated[:, 0])).all() + + # Shi-Tomasi + results = peak_local_max(corner_shi_tomasi(im)) + results_rotated = peak_local_max(corner_shi_tomasi(im_rotated)) + assert (np.sort(results[:, 0]) == np.sort(results_rotated[:, 1])).all() + assert (np.sort(results[:, 1]) == np.sort(results_rotated[:, 0])).all() + + +def test_subpix(): + img = np.zeros((50, 50)) + img[:25,:25] = 255 + img[25:,25:] = 255 + corner = peak_local_max(corner_harris(img), num_peaks=1) + subpix = corner_subpix(img, corner) + assert_array_equal(subpix[0], (24.5, 24.5)) + + +def test_corner_peaks(): + response = np.zeros((5, 5)) + response[2:4, 2:4] = 1 + + corners = corner_peaks(response, exclude_border=False) + assert len(corners) == 1 + + corners = corner_peaks(response, exclude_border=False, min_distance=0) + assert len(corners) == 4 + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_daisy.py b/skimage/feature/tests/test_daisy.py new file mode 100644 index 00000000..40781a64 --- /dev/null +++ b/skimage/feature/tests/test_daisy.py @@ -0,0 +1,95 @@ +import numpy as np +from numpy.testing import assert_raises, assert_almost_equal +from numpy import sqrt, ceil + +from skimage import data +from skimage import img_as_float +from skimage.feature import daisy + + +def test_daisy_color_image_unsupported_error(): + img = np.zeros((20, 20, 3)) + assert_raises(ValueError, daisy, img) + + +def test_daisy_desc_dims(): + img = img_as_float(data.lena()[:128, :128].mean(axis=2)) + rings = 2 + histograms = 4 + orientations = 3 + descs = daisy(img, rings=rings, histograms=histograms, + orientations=orientations) + assert(descs.shape[2] == (rings * histograms + 1) * orientations) + + rings = 4 + histograms = 5 + orientations = 13 + descs = daisy(img, rings=rings, histograms=histograms, + orientations=orientations) + assert(descs.shape[2] == (rings * histograms + 1) * orientations) + + +def test_descs_shape(): + img = img_as_float(data.lena()[:256, :256].mean(axis=2)) + radius = 20 + step = 8 + descs = daisy(img, radius=radius, step=step) + assert(descs.shape[0] == ceil((img.shape[0] - radius * 2) / float(step))) + assert(descs.shape[1] == ceil((img.shape[1] - radius * 2) / float(step))) + + img = img[:-1, :-2] + radius = 5 + step = 3 + descs = daisy(img, radius=radius, step=step) + assert(descs.shape[0] == ceil((img.shape[0] - radius * 2) / float(step))) + assert(descs.shape[1] == ceil((img.shape[1] - radius * 2) / float(step))) + + +def test_daisy_incompatible_sigmas_and_radii(): + img = img_as_float(data.lena()[:128, :128].mean(axis=2)) + sigmas = [1, 2] + radii = [1, 2] + assert_raises(ValueError, daisy, img, sigmas=sigmas, ring_radii=radii) + + +def test_daisy_normalization(): + img = img_as_float(data.lena()[:64, :64].mean(axis=2)) + + descs = daisy(img, normalization='l1') + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + assert_almost_equal(np.sum(descs[i, j, :]), 1) + descs_ = daisy(img) + assert_almost_equal(descs, descs_) + + descs = daisy(img, normalization='l2') + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + assert_almost_equal(sqrt(np.sum(descs[i, j, :] ** 2)), 1) + + orientations = 8 + descs = daisy(img, orientations=orientations, normalization='daisy') + desc_dims = descs.shape[2] + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + for k in range(0, desc_dims, orientations): + assert_almost_equal(sqrt(np.sum( + descs[i, j, k:k + orientations] ** 2)), 1) + + img = np.zeros((50, 50)) + descs = daisy(img, normalization='off') + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + assert_almost_equal(np.sum(descs[i, j, :]), 0) + + assert_raises(ValueError, daisy, img, normalization='does_not_exist') + + +def test_daisy_visualization(): + img = img_as_float(data.lena()[:128, :128].mean(axis=2)) + descs, descs_img = daisy(img, visualize=True) + assert(descs_img.shape == (128, 128, 3)) + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_harris.py b/skimage/feature/tests/test_harris.py deleted file mode 100644 index 758bfa5e..00000000 --- a/skimage/feature/tests/test_harris.py +++ /dev/null @@ -1,47 +0,0 @@ -import numpy as np - -from skimage import data -from skimage import img_as_float - -from skimage.feature import harris - - -def test_square_image(): - im = np.zeros((50, 50)).astype(float) - im[:25, :25] = 1. - results = harris(im) - assert results.any() - assert len(results) == 1 - -def test_noisy_square_image(): - im = np.zeros((50, 50)).astype(float) - im[:25, :25] = 1. - im = im + np.random.uniform(size=im.shape) * .5 - results = harris(im) - assert results.any() - assert len(results) == 1 - -def test_squared_dot(): - im = np.zeros((50, 50)) - im[4:8, 4:8] = 1 - im = img_as_float(im) - 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 - rotation. - """ - im = img_as_float(data.lena().mean(axis=2)) - results = harris(im) - im_rotated = im.T - results_rotated = harris(im_rotated) - assert (np.sort(results[:, 0]) == np.sort(results_rotated[:, 1])).all() - assert (np.sort(results[:, 1]) == np.sort(results_rotated[:, 0])).all() - - -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..cfef2da0 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(int(height / 2), int(width / 2), int(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..3ef1f12d 100644 --- a/skimage/feature/tests/test_peak.py +++ b/skimage/feature/tests/test_peak.py @@ -1,9 +1,17 @@ import numpy as np from numpy.testing import assert_array_almost_equal as assert_close - +import scipy.ndimage from skimage.feature import peak +def test_trivial_case(): + trivial = np.zeros((25, 25)) + peak_indices = peak.peak_local_max(trivial, min_distance=1, indices=True) + assert not peak_indices # inherent boolean-ness of empty list + peaks = peak.peak_local_max(trivial, min_distance=1, indices=False) + assert (peaks.astype(np.bool) == trivial).all() + + def test_noisy_peaks(): peak_locations = [(7, 7), (7, 13), (13, 7), (13, 13)] @@ -51,18 +59,64 @@ 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 + + +def test_reorder_labels(): + np.random.seed(21) + image = np.random.uniform(size=(40, 60)) + i, j = np.mgrid[0:40, 0:60] + labels = 1 + (i >= 20) + (j >= 30) * 2 + labels[labels == 4] = 5 + 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 = (expected == image) + result = peak.peak_local_max(image, labels=labels, min_distance=1, + threshold_rel=0, footprint=footprint, + indices=False, exclude_border=False) + assert (result == expected).all() + + +def test_indices_with_labels(): + np.random.seed(21) + 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) + 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 = (expected == image) + result = peak.peak_local_max(image, labels=labels, min_distance=1, + threshold_rel=0, footprint=footprint, + indices=True, exclude_border=False) + assert (result == np.transpose(expected.nonzero())).all() 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..f32c3b23 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,7 +1,9 @@ from .lpi_filter import * from .ctmf import median_filter -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 ._canny import canny +from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, + hprewitt, vprewitt) +from ._denoise import denoise_tv_chambolle, tv_denoise +from ._denoise_cy import denoise_bilateral, denoise_tv_bregman +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/_ctmf.pyx b/skimage/filter/_ctmf.pyx index c133d8d6..1a03ff5a 100644 --- a/skimage/filter/_ctmf.pyx +++ b/skimage/filter/_ctmf.pyx @@ -10,14 +10,20 @@ Copyright (c) 2009-2011 Broad Institute All rights reserved. Original author: Lee Kamentsky ''' + import numpy as np -cimport numpy as np + +cimport numpy as cnp cimport cython from libc.stdlib cimport malloc, free from libc.string cimport memset -np.import_array() + +cdef extern from "../_shared/vectorized_ops.h": + void add16(cnp.uint16_t *dest, cnp.uint16_t *src) + void sub16(cnp.uint16_t *dest, cnp.uint16_t *src) + ############################################################################## # @@ -39,7 +45,7 @@ np.import_array() DTYPE_UINT32 = np.uint32 DTYPE_BOOL = np.bool -ctypedef np.uint16_t pixel_count_t +ctypedef cnp.uint16_t pixel_count_t ########### # @@ -54,15 +60,15 @@ ctypedef np.uint16_t pixel_count_t ########### cdef struct HistogramPiece: - np.uint16_t coarse[16] - np.uint16_t fine[256] + cnp.uint16_t coarse[16] + cnp.uint16_t fine[256] cdef struct Histogram: - HistogramPiece top_left # top-left corner - HistogramPiece top_right # top-right corner - HistogramPiece edge # leading/trailing edge - HistogramPiece bottom_left # bottom-left corner - HistogramPiece bottom_right # bottom-right corner + HistogramPiece top_left # top-left corner + HistogramPiece top_right # top-right corner + HistogramPiece edge # leading/trailing edge + HistogramPiece bottom_left # bottom-left corner + HistogramPiece bottom_right # bottom-right corner # The pixel count has the number of pixels histogrammed in # each of the five compartments for this position. This changes @@ -80,27 +86,27 @@ cdef struct PixelCount: # relative offsets from the octagon center # cdef struct SCoord: - np.int32_t stride # add the stride to the memory location - np.int32_t x - np.int32_t y + Py_ssize_t stride # add the stride to the memory location + Py_ssize_t x + Py_ssize_t y cdef struct Histograms: void *memory # pointer to the allocated memory Histogram *histogram # pointer to the histogram memory PixelCount *pixel_count # pointer to the pixel count memory - np.uint8_t *data # pointer to the image data - np.uint8_t *mask # pointer to the image mask - np.uint8_t *output # pointer to the output array - np.int32_t column_count # number of columns represented by this + cnp.uint8_t *data # pointer to the image data + cnp.uint8_t *mask # pointer to the image mask + cnp.uint8_t *output # pointer to the output array + Py_ssize_t column_count # number of columns represented by this # structure - np.int32_t stripe_length # number of columns including "radius" before + Py_ssize_t stripe_length # number of columns including "radius" before # and after - np.int32_t row_count # number of rows available in image - np.int32_t current_column # the column being processed - np.int32_t current_row # the row being processed - np.int32_t current_stride # offset in data and mask to current location - np.int32_t radius # the "radius" of the octagon - np.int32_t a_2 # 1/2 of the length of a side of the octagon + Py_ssize_t row_count # number of rows available in image + Py_ssize_t current_column # the column being processed + Py_ssize_t current_row # the row being processed + Py_ssize_t current_stride # offset in data and mask to current location + Py_ssize_t radius # the "radius" of the octagon + Py_ssize_t a_2 # 1/2 of the length of a side of the octagon # # # The strides are the offsets in the array to the points that need to @@ -123,83 +129,83 @@ cdef struct Histograms: # # x --> # - SCoord last_top_left # (-) left side of octagon's top - 1 row - SCoord top_left # (+) -1 row from trailing edge top - SCoord last_top_right # (-) right side of octagon's top - 1 col - 1 row - SCoord top_right # (+) -1 col -1 row from leading edge top - SCoord last_leading_edge # (-) leading edge (right) top stride - 1 row - SCoord leading_edge # (+) leading edge bottom stride - SCoord last_bottom_right # (-) leading edge bottom - 1 col - SCoord bottom_right # (+) right side of octagon's bottom - 1 col - SCoord last_bottom_left # (-) trailing edge bottom - 1 col - SCoord bottom_left # (+) left side of octagon's bottom - 1 col + SCoord last_top_left # (-) left side of octagon's top - 1 row + SCoord top_left # (+) -1 row from trailing edge top + SCoord last_top_right # (-) right side of octagon's top - 1 col - 1 row + SCoord top_right # (+) -1 col -1 row from leading edge top + SCoord last_leading_edge # (-) leading edge (right) top stride - 1 row + SCoord leading_edge # (+) leading edge bottom stride + SCoord last_bottom_right # (-) leading edge bottom - 1 col + SCoord bottom_right # (+) right side of octagon's bottom - 1 col + SCoord last_bottom_left # (-) trailing edge bottom - 1 col + SCoord bottom_left # (+) left side of octagon's bottom - 1 col - np.int32_t row_stride # stride between one row and the next - np.int32_t col_stride # stride between one column and the next + Py_ssize_t row_stride # stride between one row and the next + Py_ssize_t col_stride # stride between one column and the next # The accumulator holds the running histogram # HistogramPiece accumulator # # The running count of pixels in the accumulator # - np.uint32_t accumulator_count + Py_ssize_t accumulator_count # # The percent of pixels within the octagon whose value is # less than or equal to the median-filtered value (e.g. for # median, this is 50, for lower quartile it's 25) # - np.int32_t percent + Py_ssize_t percent # # last_update_column keeps track of the column # of the last update # to the fine histogram accumulator. Short-term, the median # stays in one coarse block so only one fine histogram might # need to be updated # - np.int32_t last_update_column[16] + Py_ssize_t last_update_column[16] ############################################################################ # # allocate_histograms - allocates the Histograms structure for the run # ############################################################################ -cdef Histograms *allocate_histograms(np.int32_t rows, - np.int32_t columns, - np.int32_t row_stride, - np.int32_t col_stride, - np.int32_t radius, - np.int32_t percent, - np.uint8_t *data, - np.uint8_t *mask, - np.uint8_t *output): +cdef Histograms *allocate_histograms(Py_ssize_t rows, + Py_ssize_t columns, + Py_ssize_t row_stride, + Py_ssize_t col_stride, + Py_ssize_t radius, + Py_ssize_t percent, + cnp.uint8_t *data, + cnp.uint8_t *mask, + cnp.uint8_t *output): cdef: - unsigned int adjusted_stripe_length = columns + 2*radius + 1 - unsigned int memory_size + Py_ssize_t adjusted_stripe_length = columns + 2*radius + 1 + Py_ssize_t memory_size void *ptr Histograms *ph - size_t roundoff - int a + Py_ssize_t roundoff + Py_ssize_t a SCoord *psc memory_size = (adjusted_stripe_length * - (sizeof(Histogram) + sizeof(PixelCount))+ - sizeof(Histograms)+32) + (sizeof(Histogram) + sizeof(PixelCount)) + + sizeof(Histograms) + 32) ptr = malloc(memory_size) memset(ptr, 0, memory_size) - ph = ptr + ph = ptr if not ptr: return ph ph.memory = ptr - ptr = (ph+1) - ph.pixel_count = ptr - ptr = (ph.pixel_count + adjusted_stripe_length) + ptr = (ph + 1) + ph.pixel_count = ptr + ptr = (ph.pixel_count + adjusted_stripe_length) # # Align histogram memory to a 32-byte boundary # - roundoff = ptr + roundoff = ptr roundoff += 31 roundoff -= roundoff % 32 - ptr = roundoff - ph.histogram = ptr + ptr = roundoff + ph.histogram = ptr # # Fill in the statistical things we keep around # @@ -228,7 +234,7 @@ cdef Histograms *allocate_histograms(np.int32_t rows, # a_2 is the offset from the center to each of the octagon # corners # - a = (radius * 2.0 / 2.414213) + a = (radius * 2.0 / 2.414213) a_2 = a / 2 if a_2 == 0: a_2 = 1 @@ -322,34 +328,18 @@ cdef void set_stride(Histograms *ph, SCoord *psc): # a column that is "radius" to the left. # ############################################################################ -cdef inline np.int32_t tl_br_colidx(Histograms *ph, np.int32_t colidx): +cdef inline Py_ssize_t tl_br_colidx(Histograms *ph, Py_ssize_t colidx): return (colidx + 3*ph.radius + ph.current_row) % ph.stripe_length -cdef inline np.int32_t tr_bl_colidx(Histograms *ph, np.int32_t colidx): +cdef inline Py_ssize_t tr_bl_colidx(Histograms *ph, Py_ssize_t colidx): return (colidx + 3*ph.radius + ph.row_count-ph.current_row) % \ ph.stripe_length -cdef inline np.int32_t leading_edge_colidx(Histograms *ph, np.int32_t colidx): +cdef inline Py_ssize_t leading_edge_colidx(Histograms *ph, Py_ssize_t colidx): return (colidx + 5*ph.radius) % ph.stripe_length -cdef inline np.int32_t trailing_edge_colidx(Histograms *ph, np.int32_t colidx): +cdef inline Py_ssize_t trailing_edge_colidx(Histograms *ph, Py_ssize_t colidx): return (colidx + 3*ph.radius - 1) % ph.stripe_length -# -# add16 - add 16 consecutive integers -# -# Add an array of 16 16-bit integers to an accumulator of 16 16-bit integers -# -# TO_DO - optimize using SIMD instructions -# -cdef inline void add16(np.uint16_t *dest, np.uint16_t *src): - cdef int i - for i in range(16): - dest[i] += src[i] - -cdef inline void sub16(np.uint16_t *dest, np.uint16_t *src): - cdef int i - for i in range(16): - dest[i] -= src[i] ############################################################################ # @@ -360,9 +350,8 @@ cdef inline void sub16(np.uint16_t *dest, np.uint16_t *src): # colidx - the index of the column to add # ############################################################################ -cdef inline void accumulate_coarse_histogram(Histograms *ph, np.int32_t colidx): - cdef: - int offset +cdef inline void accumulate_coarse_histogram(Histograms *ph, Py_ssize_t colidx): + cdef Py_ssize_t offset offset = tr_bl_colidx(ph, colidx) if ph.pixel_count[offset].top_right > 0: @@ -383,9 +372,8 @@ cdef inline void accumulate_coarse_histogram(Histograms *ph, np.int32_t colidx): # for a given column # ############################################################################ -cdef inline void deaccumulate_coarse_histogram(Histograms *ph, np.int32_t colidx): - cdef: - int offset +cdef inline void deaccumulate_coarse_histogram(Histograms *ph, Py_ssize_t colidx): + cdef Py_ssize_t offset # # The trailing diagonals don't appear until here # @@ -414,11 +402,11 @@ cdef inline void deaccumulate_coarse_histogram(Histograms *ph, np.int32_t colidx # ############################################################################ cdef inline void accumulate_fine_histogram(Histograms *ph, - np.int32_t colidx, - np.uint32_t fineidx): + Py_ssize_t colidx, + Py_ssize_t fineidx): cdef: - int fineoffset = fineidx * 16 - int offset + Py_ssize_t fineoffset = fineidx * 16 + Py_ssize_t offset offset = tr_bl_colidx(ph, colidx) add16(ph.accumulator.fine + fineoffset, @@ -438,11 +426,11 @@ cdef inline void accumulate_fine_histogram(Histograms *ph, # ############################################################################ cdef inline void deaccumulate_fine_histogram(Histograms *ph, - np.int32_t colidx, - np.uint32_t fineidx): + Py_ssize_t colidx, + Py_ssize_t fineidx): cdef: - int fineoffset = fineidx * 16 - int offset + Py_ssize_t fineoffset = fineidx * 16 + Py_ssize_t offset # # The trailing diagonals don't appear until here @@ -470,10 +458,7 @@ cdef inline void deaccumulate_fine_histogram(Histograms *ph, ############################################################################ cdef inline void accumulate(Histograms *ph): - cdef: - int i - int j - np.int32_t accumulator + cdef cnp.int32_t accumulator accumulate_coarse_histogram(ph, ph.current_column) deaccumulate_coarse_histogram(ph, ph.current_column) @@ -497,11 +482,11 @@ cdef inline void accumulate(Histograms *ph): # to choose remains to be done. ############################################################################ -cdef inline void update_fine(Histograms *ph, int fineidx): +cdef inline void update_fine(Histograms *ph, Py_ssize_t fineidx): cdef: - int first_update_column = ph.last_update_column[fineidx]+1 - int update_limit = ph.current_column+1 - int i + Py_ssize_t first_update_column = ph.last_update_column[fineidx]+1 + Py_ssize_t update_limit = ph.current_column+1 + Py_ssize_t i for i in range(first_update_column, update_limit): accumulate_fine_histogram(ph, i, fineidx) @@ -526,23 +511,23 @@ cdef inline void update_histogram(Histograms *ph, SCoord *last_coord, SCoord *coord): cdef: - np.int32_t current_column = ph.current_column - np.int32_t current_row = ph.current_row - np.int32_t current_stride = ph.current_stride - np.int32_t column_count = ph.column_count - np.int32_t row_count = ph.row_count - np.uint8_t value - np.int32_t stride - np.int32_t x - np.int32_t y + Py_ssize_t current_column = ph.current_column + Py_ssize_t current_row = ph.current_row + Py_ssize_t current_stride = ph.current_stride + Py_ssize_t column_count = ph.column_count + Py_ssize_t row_count = ph.row_count + cnp.uint8_t value + Py_ssize_t stride + Py_ssize_t x + Py_ssize_t y x = last_coord.x + current_column y = last_coord.y + current_row stride = current_stride+last_coord.stride - if (x >= 0 and x < column_count and - y >= 0 and y < row_count and - ph.mask[stride]): + if (x >= 0 and x < column_count and \ + y >= 0 and y < row_count and \ + ph.mask[stride]): value = ph.data[stride] pixel_count[0] -= 1 hist_piece.fine[value] -= 1 @@ -552,9 +537,9 @@ cdef inline void update_histogram(Histograms *ph, y = coord.y + current_row stride = current_stride + coord.stride - if (x >= 0 and x < column_count and - y >= 0 and y < row_count and - ph.mask[stride]): + if (x >= 0 and x < column_count and \ + y >= 0 and y < row_count and \ + ph.mask[stride]): value = ph.data[stride] pixel_count[0] += 1 hist_piece.fine[value] += 1 @@ -567,21 +552,21 @@ cdef inline void update_histogram(Histograms *ph, ############################################################################ cdef inline void update_current_location(Histograms *ph): cdef: - np.int32_t current_column = ph.current_column - np.int32_t radius = ph.radius - np.int32_t top_left_off = tl_br_colidx(ph, current_column) - np.int32_t top_right_off = tr_bl_colidx(ph, current_column) - np.int32_t bottom_left_off = tr_bl_colidx(ph, current_column) - np.int32_t bottom_right_off = tl_br_colidx(ph, current_column) - np.int32_t leading_edge_off = leading_edge_colidx(ph, current_column) - np.int32_t *coarse_histogram - np.int32_t *fine_histogram - np.int32_t last_xoff - np.int32_t last_yoff - np.int32_t last_stride - np.int32_t xoff - np.int32_t yoff - np.int32_t stride + Py_ssize_t current_column = ph.current_column + Py_ssize_t radius = ph.radius + Py_ssize_t top_left_off = tl_br_colidx(ph, current_column) + Py_ssize_t top_right_off = tr_bl_colidx(ph, current_column) + Py_ssize_t bottom_left_off = tr_bl_colidx(ph, current_column) + Py_ssize_t bottom_right_off = tl_br_colidx(ph, current_column) + Py_ssize_t leading_edge_off = leading_edge_colidx(ph, current_column) + cnp.int32_t *coarse_histogram + cnp.int32_t *fine_histogram + Py_ssize_t last_xoff + Py_ssize_t last_yoff + Py_ssize_t last_stride + Py_ssize_t xoff + Py_ssize_t yoff + Py_ssize_t stride update_histogram(ph, &ph.histogram[top_left_off].top_left, &ph.pixel_count[top_left_off].top_left, @@ -614,18 +599,20 @@ cdef inline void update_current_location(Histograms *ph): # ############################################################################ -cdef inline np.uint8_t find_median(Histograms *ph): +cdef inline cnp.uint8_t find_median(Histograms *ph): cdef: - np.uint32_t pixels_below # of pixels below the median - int i - int j - int k - np.uint32_t accumulator + Py_ssize_t pixels_below # of pixels below the median + Py_ssize_t i + Py_ssize_t j + Py_ssize_t k + cnp.uint32_t accumulator if ph.accumulator_count == 0: return 0 - pixels_below = ((ph.accumulator_count * ph.percent + 50) - / 100) # +50 for roundoff + + # +50 for roundoff + pixels_below = (ph.accumulator_count * ph.percent + 50) / 100 + if pixels_below > 0: pixels_below -= 1 @@ -637,10 +624,10 @@ cdef inline np.uint8_t find_median(Histograms *ph): accumulator -= ph.accumulator.coarse[i] update_fine(ph, i) - for j in range(i*16,(i+1)*16): + for j in range(i*16, (i + 1)*16): accumulator += ph.accumulator.fine[j] if accumulator > pixels_below: - return j + return j return 0 @@ -659,30 +646,30 @@ cdef inline np.uint8_t find_median(Histograms *ph): # output - array to be filled with filtered pixels # ############################################################################ -cdef int c_median_filter(np.int32_t rows, - np.int32_t columns, - np.int32_t row_stride, - np.int32_t col_stride, - np.int32_t radius, - np.int32_t percent, - np.uint8_t *data, - np.uint8_t *mask, - np.uint8_t *output): +cdef int c_median_filter(Py_ssize_t rows, + Py_ssize_t columns, + Py_ssize_t row_stride, + Py_ssize_t col_stride, + Py_ssize_t radius, + Py_ssize_t percent, + cnp.uint8_t *data, + cnp.uint8_t *mask, + cnp.uint8_t *output): cdef: Histograms *ph Histogram *phistogram - int row - int col - int i - np.int32_t top_left_off - np.int32_t top_right_off - np.int32_t bottom_left_off - np.int32_t bottom_right_off + Py_ssize_t row + Py_ssize_t col + Py_ssize_t i + Py_ssize_t top_left_off + Py_ssize_t top_right_off + Py_ssize_t bottom_left_off + Py_ssize_t bottom_right_off ph = allocate_histograms(rows, columns, row_stride, col_stride, radius, percent, data, mask, output) if not ph: - return 1 + return 1 for row in range(-radius, rows): # @@ -721,7 +708,7 @@ cdef int c_median_filter(np.int32_t rows, # Update locations and coarse accumulator for the octagon # for points before 0 # - for col in range(-radius, 0 if row >=0 else columns+radius): + for col in range(-radius, 0 if row >= 0 else columns+radius): ph.current_column = col ph.current_stride = row * row_stride + col * col_stride update_current_location(ph) @@ -742,16 +729,18 @@ cdef int c_median_filter(np.int32_t rows, ph.current_stride = row * row_stride + col * col_stride update_current_location(ph) - free_histograms(ph) return 0 -def median_filter( - np.ndarray[dtype=np.uint8_t, ndim=2, negative_indices=False, mode='c'] data, - np.ndarray[dtype=np.uint8_t, ndim=2, negative_indices=False, mode='c'] mask, - np.ndarray[dtype=np.uint8_t, ndim=2, negative_indices=False, mode='c'] output, - int radius, - np.int32_t percent): + +def median_filter(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] data, + cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] mask, + cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] output, + int radius, + cnp.int32_t percent): """Median filter with octagon shape and masking. Parameters @@ -773,10 +762,10 @@ def median_filter( """ if percent < 0: - raise ValueError('Median filter percent = %d is less than zero' % \ + raise ValueError('Median filter percent = %d is less than zero' % percent) if percent > 100: - raise ValueError('Median filter percent = %d is greater than 100' % \ + raise ValueError('Median filter percent = %d is greater than 100' % percent) if data.shape[0] != mask.shape[0] or data.shape[1] != mask.shape[1]: raise ValueError('Data shape (%d, %d) is not mask shape (%d, %d)' % @@ -786,10 +775,12 @@ def median_filter( raise ValueError('Data shape (%d, %d) is not output shape (%d, %d)' % (data.shape[0], data.shape[1], output.shape[0], output.shape[1])) - if c_median_filter(data.shape[0], data.shape[1], - data.strides[0], data.strides[1], + if c_median_filter(data.shape[0], + data.shape[1], + data.strides[0], + data.strides[1], radius, percent, - data.data, - mask.data, - output.data): + data.data, + mask.data, + output.data): raise MemoryError('Failed to allocate scratchpad memory') diff --git a/skimage/filter/_denoise.py b/skimage/filter/_denoise.py new file mode 100644 index 00000000..87850759 --- /dev/null +++ b/skimage/filter/_denoise.py @@ -0,0 +1,263 @@ +import numpy as np +from skimage import img_as_float +from skimage._shared.utils import deprecated + + +def _denoise_tv_chambolle_3d(im, weight=100, eps=2.e-4, n_iter_max=200): + """Perform total-variation denoising on 3D images. + + Parameters + ---------- + im : ndarray + 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`). + eps : float, optional + 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 + + n_iter_max : int, optional + Maximal number of iterations used for the optimization. + + Returns + ------- + out : ndarray + Denoised array of floats. + + Notes + ----- + Rudin, Osher and Fatemi algorithm. + + Examples + -------- + >>> x, y, z = np.ogrid[0:40, 0:40, 0:40] + >>> mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 + >>> mask = mask.astype(np.float) + >>> mask += 0.2 * np.random.randn(*mask.shape) + >>> res = denoise_tv(mask, weight=100) + + """ + + px = np.zeros_like(im) + py = np.zeros_like(im) + pz = np.zeros_like(im) + gx = np.zeros_like(im) + gy = np.zeros_like(im) + gz = np.zeros_like(im) + d = np.zeros_like(im) + i = 0 + while i < n_iter_max: + d = - px - py - pz + 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) + norm = np.sqrt(gx**2 + gy**2 + gz**2) + E += weight * norm.sum() + norm *= 0.5 / weight + norm += 1. + px -= 1. / 6. * gx + px /= norm + py -= 1. / 6. * gy + py /= norm + pz -= 1 / 6. * gz + pz /= norm + E /= float(im.size) + if i == 0: + E_init = E + E_previous = E + else: + if np.abs(E_previous - E) < eps * E_init: + break + else: + E_previous = E + i += 1 + return out + + +def _denoise_tv_chambolle_2d(im, weight=50, eps=2.e-4, n_iter_max=200): + """Perform total-variation denoising on 2D images. + + Parameters + ---------- + im : ndarray + Input data to be denoised. + weight : float, optional + 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 + the stop criterion. The algorithm stops when: + + (E_(n-1) - E_n) < eps * E_0 + + n_iter_max : int, optional + Maximal number of iterations used for the optimization. + + Returns + ------- + out : ndarray + 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 + 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, + Springer, 2004, 20, 89-97. + + Examples + -------- + >>> from skimage import color, data + >>> lena = color.rgb2gray(data.lena()) + >>> lena += 0.5 * lena.std() * np.random.randn(*lena.shape) + >>> denoised_lena = denoise_tv(lena, weight=60) + + """ + + px = np.zeros_like(im) + py = np.zeros_like(im) + gx = np.zeros_like(im) + gy = np.zeros_like(im) + d = np.zeros_like(im) + i = 0 + while i < n_iter_max: + 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) + norm = np.sqrt(gx**2 + gy**2) + E += weight * norm.sum() + norm *= 0.5 / weight + norm += 1 + px -= 0.25 * gx + px /= norm + py -= 0.25 * gy + py /= norm + E /= float(im.size) + if i == 0: + E_init = E + E_previous = E + else: + if np.abs(E_previous - E) < eps * E_init: + break + else: + E_previous = E + i += 1 + return out + + +def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, + multichannel=False): + """Perform total-variation denoising on 2D and 3D images. + + Parameters + ---------- + 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 + of the denoised image. + weight : float, optional + 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 the stop criterion. The algorithm stops when: + + (E_(n-1) - E_n) < eps * E_0 + + n_iter_max : int, optional + Maximal number of iterations used for the optimization. + multichannel : bool, optional + Apply total-variation denoising separately for each channel. This + option should be true for color images, otherwise the denoising is + also applied in the 3rd dimension. + + Returns + ------- + out : ndarray + Denoised image. + + Notes + ----- + Make sure to set the multichannel parameter appropriately for color images. + + The principle of total variation denoising is explained in + 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, + piecewise-constant images. + + 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, + Springer, 2004, 20, 89-97. + + Examples + -------- + 2D example on Lena image: + + >>> from skimage import color, data + >>> lena = color.rgb2gray(data.lena()) + >>> lena += 0.5 * lena.std() * np.random.randn(*lena.shape) + >>> denoised_lena = denoise_tv(lena, weight=60) + + 3D example on synthetic data: + + >>> x, y, z = np.ogrid[0:40, 0:40, 0:40] + >>> mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 + >>> mask = mask.astype(np.float) + >>> mask += 0.2*np.random.randn(*mask.shape) + >>> res = denoise_tv(mask, weight=100) + + """ + + im_type = im.dtype + if not im_type.kind == 'f': + im = img_as_float(im) + + if im.ndim == 2: + out = _denoise_tv_chambolle_2d(im, weight, eps, n_iter_max) + elif im.ndim == 3: + if multichannel: + out = np.zeros_like(im) + for c in range(im.shape[2]): + out[..., c] = _denoise_tv_chambolle_2d(im[..., c], weight, eps, + n_iter_max) + else: + out = _denoise_tv_chambolle_3d(im, weight, eps, n_iter_max) + else: + raise ValueError('only 2-d and 3-d images may be denoised with this ' + 'function') + return out + + +tv_denoise = deprecated('skimage.filter.denoise_tv_chambolle')\ + (denoise_tv_chambolle) diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/filter/_denoise_cy.pyx new file mode 100644 index 00000000..5a85e84a --- /dev/null +++ b/skimage/filter/_denoise_cy.pyx @@ -0,0 +1,320 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +import numpy as np +from libc.math cimport exp, fabs, sqrt +from libc.stdlib cimport malloc, free +from libc.float cimport DBL_MAX +from skimage._shared.interpolation cimport get_pixel3d +from skimage.util import img_as_float +from skimage._shared.utils import deprecated + + +cdef inline double _gaussian_weight(double sigma, double value): + return exp(-0.5 * (value / sigma)**2) + + +cdef double* _compute_color_lut(Py_ssize_t bins, double sigma, double max_value): + + cdef: + double* color_lut = malloc(bins * sizeof(double)) + Py_ssize_t b + + for b in range(bins): + color_lut[b] = _gaussian_weight(sigma, b * max_value / bins) + + return color_lut + + +cdef double* _compute_range_lut(Py_ssize_t win_size, double sigma): + + cdef: + double* range_lut = malloc(win_size**2 * sizeof(double)) + Py_ssize_t kr, kc + Py_ssize_t window_ext = (win_size - 1) / 2 + double dist + + for kr in range(win_size): + for kc in range(win_size): + dist = sqrt((kr - window_ext)**2 + (kc - window_ext)**2) + range_lut[kr * win_size + kc] = _gaussian_weight(sigma, dist) + + return range_lut + + +def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, + double sigma_spatial=1, Py_ssize_t bins=10000, + mode='constant', double cval=0): + """Denoise image using bilateral filter. + + This is an edge-preserving and noise reducing denoising filter. It averages + pixels based on their spatial closeness and radiometric similarity. + + Spatial closeness is measured by the gaussian function of the euclidian + distance between two pixels and a certain standard deviation + (`sigma_spatial`). + + Radiometric similarity is measured by the gaussian function of the euclidian + distance between two color values and a certain standard deviation + (`sigma_range`). + + Parameters + ---------- + image : ndarray + Input image. + win_size : int + Window size for filtering. + sigma_range : float + Standard deviation for grayvalue/color distance (radiometric + similarity). A larger value results in averaging of pixels with larger + radiometric differences. Note, that the image will be converted using + the `img_as_float` function and thus the standard deviation is in + respect to the range `[0, 1]`. + sigma_spatial : float + Standard deviation for range distance. A larger value results in + averaging of pixels with larger spatial differences. + bins : int + Number of discrete values for gaussian weights of color filtering. + A larger value results in improved accuracy. + 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. + + Returns + ------- + denoised : ndarray + Denoised image. + + References + ---------- + .. [1] http://users.soe.ucsc.edu/~manduchi/Papers/ICCV98.pdf + + """ + + image = np.atleast_3d(img_as_float(image)) + + cdef: + Py_ssize_t rows = image.shape[0] + Py_ssize_t cols = image.shape[1] + Py_ssize_t dims = image.shape[2] + Py_ssize_t window_ext = (win_size - 1) / 2 + + double max_value = image.max() + + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] cimage = \ + np.ascontiguousarray(image) + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] out = \ + np.zeros((rows, cols, dims), dtype=np.double) + + double* image_data = cimage.data + double* out_data = out.data + + double* color_lut = _compute_color_lut(bins, sigma_range, max_value) + double* range_lut = _compute_range_lut(win_size, sigma_spatial) + + Py_ssize_t r, c, d, wr, wc, kr, kc, rr, cc, pixel_addr + double value, weight, dist, total_weight, csigma_range, color_weight, \ + range_weight + double dist_scale = bins / dims / max_value + double* values = malloc(dims * sizeof(double)) + double* centres = malloc(dims * sizeof(double)) + double* total_values = malloc(dims * sizeof(double)) + + if sigma_range is None: + csigma_range = image.std() + else: + csigma_range = sigma_range + + if mode not in ('constant', 'wrap', 'reflect', 'nearest'): + raise ValueError("Invalid mode specified. Please use " + "`constant`, `nearest`, `wrap` or `reflect`.") + cdef char cmode = ord(mode[0].upper()) + + for r in range(rows): + for c in range(cols): + pixel_addr = r * cols * dims + c * dims + total_weight = 0 + for d in range(dims): + total_values[d] = 0 + centres[d] = image_data[pixel_addr + d] + for wr in range(-window_ext, window_ext + 1): + rr = wr + r + kr = wr + window_ext + for wc in range(-window_ext, window_ext + 1): + cc = wc + c + kc = wc + window_ext + + # save pixel values for all dims and compute euclidian + # distance between centre stack and current position + dist = 0 + for d in range(dims): + value = get_pixel3d(image_data, rows, cols, dims, + rr, cc, d, cmode, cval) + values[d] = value + dist += (centres[d] - value)**2 + dist = sqrt(dist) + + range_weight = range_lut[kr * win_size + kc] + color_weight = color_lut[(dist * dist_scale)] + + weight = range_weight * color_weight + for d in range(dims): + total_values[d] += values[d] * weight + total_weight += weight + for d in range(dims): + out_data[pixel_addr + d] = total_values[d] / total_weight + + free(color_lut) + free(range_lut) + free(values) + free(centres) + free(total_values) + + return np.squeeze(out) + + +def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): + """Perform total-variation denoising using split-Bregman optimization. + + Total-variation denoising (also know as total-variation regularization) + tries to find an image with less total-variation under the constraint + of being similar to the input image, which is controlled by the + regularization parameter. + + Parameters + ---------- + image : ndarray + Input data to be denoised (converted using img_as_float`). + weight : float, optional + Denoising weight. The smaller the `weight`, the more denoising (at + the expense of less similarity to the `input`). The regularization + parameter `lambda` is chosen as `2 * weight`. + eps : float, optional + Relative difference of the value of the cost function that determines + the stop criterion. The algorithm stops when:: + + SUM((u(n) - u(n-1))**2) < eps + + max_iter: int, optional + Maximal number of iterations used for the optimization. + + Returns + ------- + u : ndarray + Denoised image. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Total_variation_denoising + .. [2] Tom Goldstein and Stanley Osher, "The Split Bregman Method For L1 + Regularized Problems", + ftp://ftp.math.ucla.edu/pub/camreport/cam08-29.pdf + .. [3] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising + using Split Bregman" in Image Processing On Line on 2012–05–19, + http://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf + + """ + + image = np.atleast_3d(img_as_float(image)) + + cdef: + Py_ssize_t rows = image.shape[0] + Py_ssize_t cols = image.shape[1] + Py_ssize_t dims = image.shape[2] + Py_ssize_t rows2 = rows + 2 + Py_ssize_t cols2 = cols + 2 + Py_ssize_t r, c, k + + Py_ssize_t total = rows * cols * dims + + shape_ext = (rows2, cols2, dims) + + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] cimage = \ + np.ascontiguousarray(image) + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] u = \ + np.zeros(shape_ext, dtype=np.double) + + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] dx = \ + np.zeros(shape_ext, dtype=np.double) + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] dy = \ + np.zeros(shape_ext, dtype=np.double) + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] bx = \ + np.zeros(shape_ext, dtype=np.double) + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] by = \ + np.zeros(shape_ext, dtype=np.double) + + double ux, uy, uprev, unew, bxx, byy, dxx, dyy, s + int i = 0 + double lam = 2 * weight + double rmse = DBL_MAX + double norm = (weight + 4 * lam) + + u[1:-1, 1:-1] = image + + # reflect image + u[0, 1:-1] = image[1, :] + u[1:-1, 0] = image[:, 1] + u[-1, 1:-1] = image[-2, :] + u[1:-1, -1] = image[:, -2] + + while i < max_iter and rmse > eps: + + rmse = 0 + + for k in range(dims): + for r in range(1, rows + 1): + for c in range(1, cols + 1): + + uprev = u[r, c, k] + + # forward derivatives + ux = u[r, c + 1, k] - uprev + uy = u[r + 1, c, k] - uprev + + # Gauss-Seidel method + unew = ( + lam * ( + + u[r + 1, c, k] + + u[r - 1, c, k] + + u[r, c + 1, k] + + u[r, c - 1, k] + + + dx[r, c - 1, k] + - dx[r, c, k] + + dy[r - 1, c, k] + - dy[r, c, k] + + - bx[r, c - 1, k] + + bx[r, c, k] + - by[r - 1, c, k] + + by[r, c, k] + ) + weight * cimage[r - 1, c - 1, k] + ) / norm + u[r, c, k] = unew + + # update root mean square error + rmse += (unew - uprev)**2 + + bxx = bx[r, c, k] + byy = by[r, c, k] + + s = sqrt((ux + bxx)**2 + (uy + byy)**2) + dxx = s * lam * (ux + bxx) / (s * lam + 1) + dyy = s * lam * (uy + byy) / (s * lam + 1) + + dx[r, c, k] = dxx + dy[r, c, k] = dyy + + bx[r, c, k] += ux - dxx + by[r, c, k] += uy - dyy + + rmse = sqrt(rmse / total) + i += 1 + + return np.squeeze(u[1:-1, 1:-1]) 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/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..fcd9f548 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -1,6 +1,7 @@ -"""edges.py - Sobel edge filter +"""edges.py - Edge filters -Originally part of CellProfiler, code licensed under both GPL and BSD licenses. +Sobel and Prewitt filters 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 @@ -12,20 +13,43 @@ 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. + """Find the edge magnitude using the Sobel transform. Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process. - mask : array_like, dtype=bool, optional + mask : 2-D array, 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 ------- output : ndarray - The Sobel edge map. + The Sobel edge map. Notes ----- @@ -38,20 +62,23 @@ 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. Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process. - mask : array_like, dtype=bool, optional + mask : 2-D array, 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 ------- output : ndarray - The Sobel edge map. + The Sobel edge map. Notes ----- @@ -64,32 +91,29 @@ 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. Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process - mask : array_like, dtype=bool, optional + mask : 2-D array, 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 ------- output : ndarray - The Sobel edge map. + The Sobel edge map. Notes ----- @@ -102,54 +126,166 @@ 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 scharr(image, mask=None): + """Find the edge magnitude using the Scharr transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, 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 + ------- + output : ndarray + The Scharr edge map. + + Notes + ----- + Take the square root of the sum of the squares of the horizontal and + vertical Scharrs to get a magnitude that's somewhat insensitive to + direction. + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical Optimization + of Kernel Based Image Derivatives. + + """ + return np.sqrt(hscharr(image, mask)**2 + vscharr(image, mask)**2) + + +def hscharr(image, mask=None): + """Find the horizontal edges of an image using the Scharr transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, 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 + ------- + output : ndarray + The Scharr edge map. + + Notes + ----- + We use the following kernel and return the absolute value of the + result at each point:: + + 3 10 3 + 0 0 0 + -3 -10 -3 + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical Optimization + of Kernel Based Image Derivatives. + + """ + image = img_as_float(image) + result = np.abs(convolve(image, + np.array([[ 3, 10, 3], + [ 0, 0, 0], + [-3, -10, -3]]).astype(float) / 16.0)) + return _mask_filter_result(result, mask) + + +def vscharr(image, mask=None): + """Find the vertical edges of an image using the Scharr transform. + + Parameters + ---------- + image : 2-D array + Image to process + mask : 2-D array, 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 + ------- + output : ndarray + The Scharr edge map. + + Notes + ----- + We use the following kernel and return the absolute value of the + result at each point:: + + 3 0 -3 + 10 0 -10 + 3 0 -3 + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical Optimization + of Kernel Based Image Derivatives. + + """ + image = img_as_float(image) + result = np.abs(convolve(image, + np.array([[ 3, 0, -3], + [10, 0, -10], + [ 3, 0, -3]]).astype(float) / 16.0)) + return _mask_filter_result(result, mask) + def prewitt(image, mask=None): """Find the edge magnitude using the Prewitt transform. Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process. - mask : array_like, dtype=bool, optional + mask : 2-D array, 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 ------- output : ndarray - The Prewitt edge map. + The Prewitt edge map. Notes ----- 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. Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process. - mask : array_like, dtype=bool, optional + mask : 2-D array, 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 ------- output : ndarray - The Prewitt edge map. + The Prewitt edge map. Notes ----- @@ -162,32 +298,29 @@ 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. Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process. - mask : array_like, dtype=bool, optional + mask : 2-D array, 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 ------- output : ndarray - The Prewitt edge map. + The Prewitt edge map. Notes ----- @@ -200,14 +333,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/rank/.gitignore b/skimage/filter/rank/.gitignore new file mode 100644 index 00000000..08a24d5c --- /dev/null +++ b/skimage/filter/rank/.gitignore @@ -0,0 +1 @@ +demo/ \ No newline at end of file diff --git a/skimage/filter/rank/README.rst b/skimage/filter/rank/README.rst new file mode 100644 index 00000000..cdf8205c --- /dev/null +++ b/skimage/filter/rank/README.rst @@ -0,0 +1,32 @@ +To do +----- + +* add simple examples, adapt documentation on existing examples +* add/check existing doc +* adapting tests for each type of filter + +General remarks +--------------- + +Basically these filters compute local histogram for each pixel. A histogram is +built using a moving window in order to limit redundant computation. The path +followed by the moving window is given hereunder + + ...-----------------------\ +/--------------------------/ +\-------------------------- ... + +We compare cmorph.dilate to this histogram based method to show how +computational costs increase with respect to image size or structuring element +size. This implementation gives better results for large structuring elements. + +The local histogram is updated at each pixel as the structuring element window +moves by, i.e. only those pixels entering and leaving the structuring element +update the local histogram. The histogram size is 8-bit (256 bins) for 8-bit +images and 2 to 12-bit (up to 4096 bins) for 16-bit images depending on the +maximum value of the image. Pixel values higher than 4095 raise a ValueError. + +The filter is applied up to the image border, the neighboorhood used is adjusted +accordingly. The user may provide a mask image (same size as input image) where +non zero values are the part of the image participating in the histogram +computation. By default the entire image is filtered. diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py new file mode 100644 index 00000000..30d936db --- /dev/null +++ b/skimage/filter/rank/__init__.py @@ -0,0 +1,3 @@ +from .rank import * +from .percentile_rank import * +from .bilateral_rank import * diff --git a/skimage/filter/rank/_core16.pxd b/skimage/filter/rank/_core16.pxd new file mode 100644 index 00000000..5586aea1 --- /dev/null +++ b/skimage/filter/rank/_core16.pxd @@ -0,0 +1,20 @@ +cimport numpy as cnp + + +ctypedef cnp.uint16_t dtype_t + + +cdef int int_max(int a, int b) +cdef int int_min(int a, int b) + + +# 16-bit core kernel receives extra information about data bitdepth +cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, + Py_ssize_t, Py_ssize_t, Py_ssize_t, float, + float, Py_ssize_t, Py_ssize_t), + cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask, + cnp.ndarray[dtype_t, ndim=2] out, + char shift_x, char shift_y, Py_ssize_t bitdepth, + float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except * diff --git a/skimage/filter/rank/_core16.pyx b/skimage/filter/rank/_core16.pyx new file mode 100644 index 00000000..36bc500d --- /dev/null +++ b/skimage/filter/rank/_core16.pyx @@ -0,0 +1,255 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +import numpy as np + +cimport numpy as cnp +from libc.stdlib cimport malloc, free +from _core8 cimport is_in_mask + + +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 inline void histogram_increment(Py_ssize_t * histo, float * pop, + dtype_t value): + histo[value] += 1 + pop[0] += 1 + + +cdef inline void histogram_decrement(Py_ssize_t * histo, float * pop, + dtype_t value): + histo[value] -= 1 + pop[0] -= 1 + + +cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, + Py_ssize_t, Py_ssize_t, Py_ssize_t, float, + float, Py_ssize_t, Py_ssize_t), + cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask, + cnp.ndarray[dtype_t, ndim=2] out, + char shift_x, char shift_y, Py_ssize_t bitdepth, + float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except *: + """Compute histogram for each pixel neighborhood, apply kernel function and + use kernel function return value for output image. + """ + + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + cdef Py_ssize_t srows = selem.shape[0] + cdef Py_ssize_t scols = selem.shape[1] + + cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) + shift_y + cdef Py_ssize_t centre_c = int(selem.shape[1] / 2) + shift_x + + # check that structuring element center is inside the element bounding box + assert centre_r >= 0 + assert centre_c >= 0 + assert centre_r < srows + assert centre_c < scols + assert bitdepth in range(2, 13) + + maxbin_list = [0, 0, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096] + midbin_list = [0, 0, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048] + + # set maxbin and midbin + cdef Py_ssize_t maxbin = maxbin_list[bitdepth] + cdef Py_ssize_t midbin = midbin_list[bitdepth] + + assert (image < maxbin).all() + + # define pointers to the data + cdef dtype_t * out_data = out.data + cdef dtype_t * image_data = image.data + cdef cnp.uint8_t * mask_data = mask.data + + # define local variable types + cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row + # number of pixels actually inside the neighborhood (float) + cdef float pop + + # allocate memory with malloc + cdef Py_ssize_t max_se = srows * scols + + # number of element in each attack border + cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w + + # the current local histogram distribution + cdef Py_ssize_t * histo = malloc(maxbin * sizeof(Py_ssize_t)) + + # these lists contain the relative pixel row and column for each of the 4 + # attack borders east, west, north and south e.g. se_e_r lists the rows of + # the east structuring element border + cdef Py_ssize_t * se_e_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_e_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_w_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_w_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_n_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_n_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_s_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_s_c = malloc(max_se * sizeof(Py_ssize_t)) + + # build attack and release borders + # by using difference along axis + t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) + t_e = np.diff(t, axis=1) == -1 + + t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) + t_w = np.diff(t, axis=1) == 1 + + t = np.vstack((selem, np.zeros((1, selem.shape[1])))) + t_s = np.diff(t, axis=0) == -1 + + t = np.vstack((np.zeros((1, selem.shape[1])), selem)) + t_n = np.diff(t, axis=0) == 1 + + num_se_n = num_se_s = num_se_e = num_se_w = 0 + + for r in range(srows): + for c in range(scols): + if t_e[r, c]: + se_e_r[num_se_e] = r - centre_r + se_e_c[num_se_e] = c - centre_c + num_se_e += 1 + if t_w[r, c]: + se_w_r[num_se_w] = r - centre_r + se_w_c[num_se_w] = c - centre_c + num_se_w += 1 + if t_n[r, c]: + se_n_r[num_se_n] = r - centre_r + se_n_c[num_se_n] = c - centre_c + num_se_n += 1 + if t_s[r, c]: + se_s_r[num_se_s] = r - centre_r + se_s_c[num_se_s] = c - centre_c + num_se_s += 1 + + # initial population and histogram + for i in range(maxbin): + histo[i] = 0 + + pop = 0 + + for r in range(srows): + for c in range(scols): + rr = r - centre_r + cc = c - centre_c + if selem[r, c]: + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + r = 0 + c = 0 + # kernel ------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + bitdepth, maxbin, midbin, p0, p1, s0, s1) + # kernel ------------------------------------------- + + # main loop + r = 0 + for even_row in range(0, rows, 2): + # ---> west to east + for c in range(1, cols): + for s in range(num_se_e): + rr = r + se_e_r[s] + cc = c + se_e_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_w): + rr = r + se_w_r[s] + cc = c + se_w_c[s] - 1 + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ------------------------------------------- + out_data[r * cols + c] = kernel( + histo, pop, image_data[r * cols + c], + bitdepth, maxbin, midbin, p0, p1, s0, s1) + # kernel ------------------------------------------- + + r += 1 # pass to the next row + if r >= rows: + break + + # ---> north to south + for s in range(num_se_s): + rr = r + se_s_r[s] + cc = c + se_s_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_n): + rr = r + se_n_r[s] - 1 + cc = c + se_n_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + bitdepth, maxbin, midbin, p0, p1, s0, s1) + # kernel ------------------------------------------- + + # ---> east to west + for c in range(cols - 2, -1, -1): + for s in range(num_se_w): + rr = r + se_w_r[s] + cc = c + se_w_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_e): + rr = r + se_e_r[s] + cc = c + se_e_c[s] + 1 + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ------------------------------------------- + out_data[r * cols + c] = kernel( + histo, pop, image_data[r * cols + c], + bitdepth, maxbin, midbin, p0, p1, s0, s1) + # kernel ------------------------------------------- + + r += 1 # pass to the next row + if r >= rows: + break + + # ---> north to south + for s in range(num_se_s): + rr = r + se_s_r[s] + cc = c + se_s_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_n): + rr = r + se_n_r[s] - 1 + cc = c + se_n_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + bitdepth, maxbin, midbin, p0, p1, s0, s1) + # kernel ------------------------------------------- + + # release memory allocated by malloc + + free(se_e_r) + free(se_e_c) + free(se_w_r) + free(se_w_c) + free(se_n_r) + free(se_n_c) + free(se_s_r) + free(se_s_c) + + free(histo) diff --git a/skimage/filter/rank/_core8.pxd b/skimage/filter/rank/_core8.pxd new file mode 100644 index 00000000..d3b6d8c2 --- /dev/null +++ b/skimage/filter/rank/_core8.pxd @@ -0,0 +1,25 @@ +cimport numpy as cnp + + +ctypedef cnp.uint8_t dtype_t + + +cdef dtype_t uint8_max(dtype_t a, dtype_t b) +cdef dtype_t uint8_min(dtype_t a, dtype_t b) + + +cdef dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t c, + dtype_t * mask) + + +# 8-bit core kernel receives extra information about data inferior and superior +# percentiles +cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, + float, Py_ssize_t, Py_ssize_t), + cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask, + cnp.ndarray[dtype_t, ndim=2] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1) except * diff --git a/skimage/filter/rank/_core8.pyx b/skimage/filter/rank/_core8.pyx new file mode 100644 index 00000000..eca47891 --- /dev/null +++ b/skimage/filter/rank/_core8.pyx @@ -0,0 +1,257 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +import numpy as np + +cimport numpy as cnp +from libc.stdlib cimport malloc, free + + +cdef inline dtype_t uint8_max(dtype_t a, dtype_t b): + return a if a >= b else b + + +cdef inline dtype_t uint8_min(dtype_t a, dtype_t b): + return a if a <= b else b + + +cdef inline void histogram_increment(Py_ssize_t * histo, float * pop, + dtype_t value): + histo[value] += 1 + pop[0] += 1 + + +cdef inline void histogram_decrement(Py_ssize_t * histo, float * pop, + dtype_t value): + histo[value] -= 1 + pop[0] -= 1 + + +cdef inline dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t c, + dtype_t * mask): + """Check whether given coordinate is within image and mask is true.""" + if r < 0 or r > rows - 1 or c < 0 or c > cols - 1: + return 0 + else: + if mask[r * cols + c]: + return 1 + else: + return 0 + + +cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, + float, Py_ssize_t, Py_ssize_t), + cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask, + cnp.ndarray[dtype_t, ndim=2] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1) except *: + """Compute histogram for each pixel neighborhood, apply kernel function and + use kernel function return value for output image. + """ + + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + cdef Py_ssize_t srows = selem.shape[0] + cdef Py_ssize_t scols = selem.shape[1] + + cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) + shift_y + cdef Py_ssize_t centre_c = int(selem.shape[1] / 2) + shift_x + + # check that structuring element center is inside the element bounding box + assert centre_r >= 0 + assert centre_c >= 0 + assert centre_r < srows + assert centre_c < scols + + # define pointers to the data + + cdef dtype_t * out_data = out.data + cdef dtype_t * image_data = image.data + cdef dtype_t * mask_data = mask.data + + # define local variable types + cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row + + # number of pixels actually inside the neighborhood (float) + cdef float pop + + # allocate memory with malloc + cdef Py_ssize_t max_se = srows * scols + + # number of element in each attack border + cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w + + # the current local histogram distribution + cdef Py_ssize_t * histo = malloc(256 * sizeof(Py_ssize_t)) + + # these lists contain the relative pixel row and column for each of the 4 + # attack borders east, west, north and south e.g. se_e_r lists the rows of + # the east structuring element border + cdef Py_ssize_t * se_e_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_e_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_w_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_w_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_n_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_n_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_s_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_s_c = malloc(max_se * sizeof(Py_ssize_t)) + + # build attack and release borders + # by using difference along axis + t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) + t_e = np.diff(t, axis=1) == -1 + + t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) + t_w = np.diff(t, axis=1) == 1 + + t = np.vstack((selem, np.zeros((1, selem.shape[1])))) + t_s = np.diff(t, axis=0) == -1 + + t = np.vstack((np.zeros((1, selem.shape[1])), selem)) + t_n = np.diff(t, axis=0) == 1 + + num_se_n = num_se_s = num_se_e = num_se_w = 0 + + for r in range(srows): + for c in range(scols): + if t_e[r, c]: + se_e_r[num_se_e] = r - centre_r + se_e_c[num_se_e] = c - centre_c + num_se_e += 1 + if t_w[r, c]: + se_w_r[num_se_w] = r - centre_r + se_w_c[num_se_w] = c - centre_c + num_se_w += 1 + if t_n[r, c]: + se_n_r[num_se_n] = r - centre_r + se_n_c[num_se_n] = c - centre_c + num_se_n += 1 + if t_s[r, c]: + se_s_r[num_se_s] = r - centre_r + se_s_c[num_se_s] = c - centre_c + num_se_s += 1 + + # initial population and histogram (kernel is centered on the first row and + # column) + for i in range(256): + histo[i] = 0 + + pop = 0 + + for r in range(srows): + for c in range(scols): + rr = r - centre_r + cc = c - centre_c + if selem[r, c]: + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + r = 0 + c = 0 + # kernel ------------------------------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + p0, p1, s0, s1) + # kernel ------------------------------------------------------------------- + + # main loop + r = 0 + for even_row in range(0, rows, 2): + # ---> west to east + for c in range(1, cols): + for s in range(num_se_e): + rr = r + se_e_r[s] + cc = c + se_e_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_w): + rr = r + se_w_r[s] + cc = c + se_w_c[s] - 1 + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ----------------------------------------------------------- + out_data[r * cols + c] = \ + kernel(histo, pop, image_data[r * cols + c], p0, p1, s0, s1) + # kernel ----------------------------------------------------------- + + r += 1 # pass to the next row + if r >= rows: + break + + # ---> north to south + for s in range(num_se_s): + rr = r + se_s_r[s] + cc = c + se_s_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_n): + rr = r + se_n_r[s] - 1 + cc = c + se_n_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel --------------------------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + p0, p1, s0, s1) + # kernel --------------------------------------------------------------- + + # ---> east to west + for c in range(cols - 2, -1, -1): + for s in range(num_se_w): + rr = r + se_w_r[s] + cc = c + se_w_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_e): + rr = r + se_e_r[s] + cc = c + se_e_c[s] + 1 + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ----------------------------------------------------------- + out_data[r * cols + c] = kernel( + histo, pop, image_data[r * cols + c], p0, p1, s0, s1) + # kernel ----------------------------------------------------------- + + r += 1 # pass to the next row + if r >= rows: + break + + # ---> north to south + for s in range(num_se_s): + rr = r + se_s_r[s] + cc = c + se_s_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_n): + rr = r + se_n_r[s] - 1 + cc = c + se_n_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel --------------------------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + p0, p1, s0, s1) + # kernel --------------------------------------------------------------- + + # release memory allocated by malloc + + free(se_e_r) + free(se_e_c) + free(se_w_r) + free(se_w_c) + free(se_n_r) + free(se_n_c) + free(se_s_r) + free(se_s_c) + + free(histo) diff --git a/skimage/filter/rank/_crank16.pyx b/skimage/filter/rank/_crank16.pyx new file mode 100644 index 00000000..232d6812 --- /dev/null +++ b/skimage/filter/rank/_crank16.pyx @@ -0,0 +1,422 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log +from skimage.filter.rank._core16 cimport _core16 + + +# ----------------------------------------------------------------- +# kernels uint16 take extra parameter for defining the bitdepth +# ----------------------------------------------------------------- + + +ctypedef cnp.uint16_t dtype_t + + +cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i, imin, imax, delta + + if pop: + for i in range(maxbin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(maxbin): + if histo[i]: + imin = i + break + delta = imax - imin + if delta > 0: + return (1. * (maxbin - 1) * (g - imin) / delta) + else: + return (imax - imin) + + +cdef inline dtype_t kernel_bottomhat(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + + if pop: + for i in range(maxbin): + if histo[i]: + break + + return (g - i) + else: + return (0) + +cdef inline dtype_t kernel_equalize(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float sum = 0. + + if pop: + for i in range(maxbin): + sum += histo[i] + if i >= g: + break + + return (((maxbin - 1) * sum) / pop) + else: + return (0) + + +cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(maxbin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(maxbin): + if histo[i]: + imin = i + break + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_maximum(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + + if pop: + for i in range(maxbin - 1, -1, -1): + if histo[i]: + return (i) + + return (0) + + +cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(maxbin): + mean += histo[i] * i + return (mean / pop) + else: + return (0) + + +cdef inline dtype_t kernel_meansubstraction(Py_ssize_t * histo, + float pop, + dtype_t g, + Py_ssize_t bitdepth, + Py_ssize_t maxbin, + Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(maxbin): + mean += histo[i] * i + return ((g - mean / pop) / 2. + (midbin - 1)) + else: + return (0) + + +cdef inline dtype_t kernel_median(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float sum = pop / 2.0 + + if pop: + for i in range(maxbin): + if histo[i]: + sum -= histo[i] + if sum < 0: + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_minimum(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + + if pop: + for i in range(maxbin): + if histo[i]: + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_modal(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t hmax = 0, imax = 0 + + if pop: + for i in range(maxbin): + if histo[i] > hmax: + hmax = histo[i] + imax = i + return (imax) + else: + return (0) + + +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, + float pop, + dtype_t g, + Py_ssize_t bitdepth, + Py_ssize_t maxbin, + Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(maxbin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(maxbin): + if histo[i]: + imin = i + break + if imax - g < g - imin: + return (imax) + else: + return (imin) + else: + return (0) + + +cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + return (pop) + + +cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(maxbin): + mean += histo[i] * i + return (g > (mean / pop)) + else: + return (0) + + +cdef inline dtype_t kernel_tophat(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + + if pop: + for i in range(maxbin - 1, -1, -1): + if histo[i]: + break + + return (i - g) + else: + return (0) + +cdef inline dtype_t kernel_entropy(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float e, p + + if pop: + e = 0. + + for i in range(maxbin): + p = histo[i] / pop + if p > 0: + e -= p * log(p) / 0.6931471805599453 + + return e * 1000 + else: + return (0) + +# ----------------------------------------------------------------- +# python wrappers +# ----------------------------------------------------------------- + + +def autolevel(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def bottomhat(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_bottomhat, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def equalize(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_equalize, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def gradient(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_gradient, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def maximum(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_maximum, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def mean(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def meansubstraction(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_meansubstraction, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def median(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_median, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def minimum(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_minimum, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def modal(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_modal, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def pop(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def threshold(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_threshold, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def tophat(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_tophat, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def entropy(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_entropy, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) diff --git a/skimage/filter/rank/_crank16_bilateral.pyx b/skimage/filter/rank/_crank16_bilateral.pyx new file mode 100644 index 00000000..e431e42b --- /dev/null +++ b/skimage/filter/rank/_crank16_bilateral.pyx @@ -0,0 +1,82 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from skimage.filter.rank._core16 cimport _core16 + + +# ----------------------------------------------------------------- +# kernels uint16 take extra parameter for defining the bitdepth +# ----------------------------------------------------------------- + + +ctypedef cnp.uint16_t dtype_t + + +cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, bilat_pop = 0 + cdef float mean = 0. + + if pop: + for i in range(maxbin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + mean += histo[i] * i + if bilat_pop: + return (mean / bilat_pop) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, bilat_pop = 0 + + if pop: + for i in range(maxbin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + return (bilat_pop) + else: + return (0) + + +# ----------------------------------------------------------------- +# python wrappers +# ----------------------------------------------------------------- + + +def mean(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, int s0=1, int s1=1): + """average greylevel (clipped on uint8) + """ + _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0., 0., s0, s1) + + +def pop(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, int s0=1, int s1=1): + """returns the number of actual pixels of the structuring element inside + the mask + """ + _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, + bitdepth, .0, .0, s0, s1) diff --git a/skimage/filter/rank/_crank16_percentiles.pyx b/skimage/filter/rank/_crank16_percentiles.pyx new file mode 100644 index 00000000..0ab71353 --- /dev/null +++ b/skimage/filter/rank/_crank16_percentiles.pyx @@ -0,0 +1,330 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from skimage.filter.rank._core16 cimport _core16, int_min, int_max + + +# ----------------------------------------------------------------- +# kernels uint16 (SOFT version using percentiles) +# ----------------------------------------------------------------- + + +ctypedef cnp.uint16_t dtype_t + + +cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(maxbin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range(maxbin - 1, -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + + delta = imax - imin + if delta > 0: + return (1.0 * (maxbin - 1) + * (int_min(int_max(imin, g), imax) + - imin) / delta) + else: + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(maxbin): + sum += histo[i] + if sum >= p0 * pop: + imin = i + break + sum = 0 + for i in range((maxbin - 1), -1, -1): + sum += histo[i] + if sum >= p1 * pop: + imax = i + break + + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(maxbin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + + if n > 0: + return (1.0 * mean / n) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t kernel_mean_substraction(Py_ssize_t * histo, + float pop, + dtype_t g, + Py_ssize_t bitdepth, + Py_ssize_t maxbin, + Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(maxbin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + if n > 0: + return ((g - (mean / n)) * .5 + midbin) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, + float pop, + dtype_t g, + Py_ssize_t bitdepth, + Py_ssize_t maxbin, + Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(maxbin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range((maxbin - 1), -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + if g > imax: + return imax + if g < imin: + return imin + if imax - g < g - imin: + return imax + else: + return imin + else: + return (0) + + +cdef inline dtype_t kernel_percentile(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i + cdef float sum = 0. + + if pop: + for i in range(maxbin): + sum += histo[i] + if sum >= p0 * pop: + break + + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, sum, n + + if pop: + sum = 0 + n = 0 + for i in range(maxbin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + return (n) + else: + return (0) + + +cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i + cdef float sum = 0. + + if pop: + for i in range(maxbin): + sum += histo[i] + if sum >= p0 * pop: + break + + return ((maxbin - 1) * (g >= i)) + else: + return (0) + + +# ----------------------------------------------------------------- +# python wrappers +# ----------------------------------------------------------------- + + +def autolevel(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """bottom hat + """ + _core16(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def gradient(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return p0,p1 percentile gradient + """ + _core16(kernel_gradient, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def mean(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return mean between [p0 and p1] percentiles + """ + _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def mean_substraction(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return original - mean between [p0 and p1] percentiles *.5 +127 + """ + _core16( + kernel_mean_substraction, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """reforce contrast using percentiles + """ + _core16(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def percentile(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return p0 percentile + """ + _core16(kernel_percentile, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def pop(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return nb of pixels between [p0 and p1] + """ + _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def threshold(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return (maxbin-1) if g > percentile p0 + """ + _core16(kernel_threshold, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) diff --git a/skimage/filter/rank/_crank8.pyx b/skimage/filter/rank/_crank8.pyx new file mode 100644 index 00000000..cb7febac --- /dev/null +++ b/skimage/filter/rank/_crank8.pyx @@ -0,0 +1,483 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log +from skimage.filter.rank._core8 cimport _core8 + + +# ----------------------------------------------------------------- +# kernels uint8 +# ----------------------------------------------------------------- + + +ctypedef cnp.uint8_t dtype_t + + +cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, delta + + if pop: + for i in range(255, -1, -1): + if histo[i]: + imax = i + break + for i in range(256): + if histo[i]: + imin = i + break + delta = imax - imin + if delta > 0: + return (255. * (g - imin) / delta) + else: + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_bottomhat(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(256): + if histo[i]: + break + + return (g - i) + else: + return (0) + + +cdef inline dtype_t kernel_equalize(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float sum = 0. + + if pop: + for i in range(256): + sum += histo[i] + if i >= g: + break + + return ((255 * sum) / pop) + else: + return (0) + + +cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(255, -1, -1): + if histo[i]: + imax = i + break + for i in range(256): + if histo[i]: + imin = i + break + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_maximum(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(255, -1, -1): + if histo[i]: + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(256): + mean += histo[i] * i + return (mean / pop) + else: + return (0) + + +cdef inline dtype_t kernel_meansubstraction(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(256): + mean += histo[i] * i + return ((g - mean / pop) / 2. + 127) + else: + return (0) + + +cdef inline dtype_t kernel_median(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float sum = pop / 2.0 + + if pop: + for i in range(256): + if histo[i]: + sum -= histo[i] + if sum < 0: + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_minimum(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(256): + if histo[i]: + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_modal(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t hmax = 0, imax = 0 + + if pop: + for i in range(256): + if histo[i] > hmax: + hmax = histo[i] + imax = i + return (imax) + else: + return (0) + + +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(255, -1, -1): + if histo[i]: + imax = i + break + for i in range(256): + if histo[i]: + imin = i + break + if imax - g < g - imin: + return (imax) + else: + return (imin) + else: + return (0) + + +cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + return (pop) + + +cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(256): + mean += histo[i] * i + return (g > (mean / pop)) + else: + return (0) + + +cdef inline dtype_t kernel_tophat(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(255, -1, -1): + if histo[i]: + break + + return (i - g) + else: + return (0) + +cdef inline dtype_t kernel_noise_filter(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t min_i + + # early stop if at least one pixel of the neighborhood has the same g + if histo[g] > 0: + return 0 + + for i in range(g, -1, -1): + if histo[i]: + break + min_i = g - i + for i in range(g, 256): + if histo[i]: + break + if i - g < min_i: + return (i - g) + else: + return min_i + + +cdef inline dtype_t kernel_entropy(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float e, p + + if pop: + e = 0. + + for i in range(256): + p = histo[i] / pop + if p > 0: + e -= p * log(p) / 0.6931471805599453 + + return e * 10 + else: + return (0) + +cdef inline dtype_t kernel_otsu(Py_ssize_t * histo, float pop, dtype_t g, + float p0, float p1, Py_ssize_t s0, + Py_ssize_t s1): + cdef Py_ssize_t i + cdef Py_ssize_t max_i + cdef float P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b + cdef float mu = 0. + + # compute local mean + if pop: + for i in range(256): + mu += histo[i] * i + mu = (mu / pop) + else: + return (0) + + # maximizing the between class variance + max_i = 0 + q1 = histo[0] / pop + m1 = 0. + max_sigma_b = 0. + + for i in range(1, 256): + P = histo[i] / pop + new_q1 = q1 + P + if new_q1 > 0: + mu1 = (q1 * mu1 + i * P) / new_q1 + mu2 = (mu - new_q1 * mu1) / (1. - new_q1) + sigma_b = new_q1 * (1. - new_q1) * (mu1 - mu2) ** 2 + if sigma_b > max_sigma_b: + max_sigma_b = sigma_b + max_i = i + q1 = new_q1 + + return max_i + + +# ----------------------------------------------------------------- +# python wrappers +# used only internally +# ----------------------------------------------------------------- + + +def autolevel(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def bottomhat(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_bottomhat, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def equalize(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_equalize, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def gradient(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_gradient, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def maximum(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_maximum, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def mean(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_mean, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def meansubstraction(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_meansubstraction, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def median(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_median, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def minimum(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_minimum, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def modal(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_modal, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def pop(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_pop, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def threshold(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_threshold, image, selem, mask, out, shift_x, shift_y, 0, 0, + 0, 0) + + +def tophat(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_tophat, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def noise_filter(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_noise_filter, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def entropy(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_entropy, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def otsu(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_otsu, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) diff --git a/skimage/filter/rank/_crank8_percentiles.pyx b/skimage/filter/rank/_crank8_percentiles.pyx new file mode 100644 index 00000000..d085957e --- /dev/null +++ b/skimage/filter/rank/_crank8_percentiles.pyx @@ -0,0 +1,294 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from skimage.filter.rank._core8 cimport _core8, uint8_max, uint8_min + + +# ----------------------------------------------------------------- +# kernels uint8 (SOFT version using percentiles) +# ----------------------------------------------------------------- + + +ctypedef cnp.uint8_t dtype_t + + +cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + imin = 0 + imax = 255 + + for i in range(256): + sum += histo[i] + if sum > (p0 * pop): + imin = i + break + sum = 0 + for i in range(255, -1, -1): + sum += histo[i] + if sum > (p1 * pop): + imax = i + break + delta = imax - imin + if delta > 0: + return (255 * (uint8_min(uint8_max(imin, g), imax) + - imin) / delta) + else: + return (imax - imin) + else: + return (128) + + +cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(256): + sum += histo[i] + if sum >= p0 * pop: + imin = i + break + sum = 0 + for i in range(255, -1, -1): + sum += histo[i] + if sum >= p1 * pop: + imax = i + break + + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(256): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + if n > 0: + return (1.0 * mean / n) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t kernel_mean_substraction(Py_ssize_t * histo, + float pop, + dtype_t g, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(256): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + if n > 0: + return ((g - (mean / n)) * .5 + 127) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, + float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(256): + sum += histo[i] + if sum >= p0 * pop: + imin = i + break + sum = 0 + for i in range(255, -1, -1): + sum += histo[i] + if sum >= p1 * pop: + imax = i + break + if g > imax: + return imax + if g < imin: + return imin + if imax - g < g - imin: + return imax + else: + return imin + else: + return (0) + + +cdef inline dtype_t kernel_percentile(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i + cdef float sum = 0. + + if pop: + for i in range(256): + sum += histo[i] + if sum >= p0 * pop: + break + + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, sum, n + + if pop: + sum = 0 + n = 0 + for i in range(256): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + return (n) + else: + return (0) + + +cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i + cdef float sum = 0. + + if pop: + for i in range(256): + sum += histo[i] + if sum >= p0 * pop: + break + + return (255 * (g >= i)) + else: + return (0) + + +# ----------------------------------------------------------------- +# python wrappers +# ----------------------------------------------------------------- + + +def autolevel(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """autolevel + """ + _core8(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, p0, p1, + 0, 0) + + +def gradient(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return p0,p1 percentile gradient + """ + _core8(kernel_gradient, image, selem, mask, out, shift_x, shift_y, p0, p1, + 0, 0) + + +def mean(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return mean between [p0 and p1] percentiles + """ + _core8(kernel_mean, image, selem, mask, out, shift_x, shift_y, p0, p1, + 0, 0) + + +def mean_substraction(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return original - mean between [p0 and p1] percentiles *.5 +127 + """ + _core8(kernel_mean_substraction, image, selem, mask, out, shift_x, shift_y, + p0, p1, 0, 0) + + +def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """reforce contrast using percentiles + """ + _core8(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, + p0, p1, 0, 0) + + +def percentile(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return p0 percentile + """ + _core8(kernel_percentile, image, selem, mask, out, shift_x, shift_y, + p0, p1, 0, 0) + + +def pop(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return nb of pixels between [p0 and p1] + """ + _core8(kernel_pop, image, selem, mask, out, shift_x, shift_y, p0, p1, + 0, 0) + + +def threshold(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return 255 if g > percentile p0 + """ + _core8(kernel_threshold, image, selem, mask, out, shift_x, shift_y, p0, p1, + 0, 0) diff --git a/skimage/filter/rank/bilateral_rank.pyx b/skimage/filter/rank/bilateral_rank.pyx new file mode 100644 index 00000000..04f6cb35 --- /dev/null +++ b/skimage/filter/rank/bilateral_rank.pyx @@ -0,0 +1,192 @@ +"""Approximate bilateral rank filter for local (custom kernel) mean. + +The local histogram is computed using a sliding window similar to the method +described in [1]_. + +Input image must be 16-bit with a value < 4096 (i.e. 12 bit), +the number of histogram bins is determined from the +maximum value present in the image. + +The pixel neighborhood is defined by: + +* the given structuring element +* an interval [g-s0,g+s1] in greylevel around g the processed pixel greylevel + +The kernel is flat (i.e. each pixel belonging to the neighborhood contributes +equally). + +Result image is 16-bit with respect to the input image. + +References +---------- + +.. [1] Huang, T. ,Yang, G. ; Tang, G.. "A fast two-dimensional + median filtering algorithm", IEEE Transactions on Acoustics, Speech and + Signal Processing, Feb 1979. Volume: 27 , Issue: 1, Page(s): 13 - 18. + +""" + +import numpy as np +from skimage import img_as_ubyte +from skimage.filter.rank import _crank16_bilateral +from skimage.filter.rank.generic import find_bitdepth + + +__all__ = ['bilateral_mean', 'bilateral_pop'] + + +def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, s0, s1): + selem = img_as_ubyte(selem) + image = np.ascontiguousarray(image) + + if mask is None: + mask = np.ones(image.shape, dtype=np.uint8) + else: + mask = np.ascontiguousarray(mask) + mask = img_as_ubyte(mask) + + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + + if image.dtype == np.uint8: + if func8 is None: + raise TypeError("Not implemented for uint8 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint8) + func8(image, selem, shift_x=shift_x, shift_y=shift_y, + mask=mask, out=out, s0=s0, s1=s1) + elif image.dtype == np.uint16: + if func16 is None: + raise TypeError("Not implemented for uint16 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint16) + bitdepth = find_bitdepth(image) + if bitdepth > 11: + raise ValueError("Only uint16 <4096 image (12bit) supported.") + func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + bitdepth=bitdepth + 1, out=out, s0=s0, s1=s1) + else: + raise TypeError("Only uint8 and uint16 image supported.") + + return out + + +def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, s0=10, s1=10): + """Apply a flat kernel bilateral filter. + + This is an edge-preserving and noise reducing denoising filter. It averages + pixels based on their spatial closeness and radiometric similarity. + + Spatial closeness is measured by considering only the local pixel + neighborhood given by a structuring element (selem). + + Radiometric similarity is defined by the greylevel interval [g-s0,g+s1] + where g is the current pixel greylevel. Only pixels belonging to the + structuring element AND having a greylevel inside this interval are + averaged. Return greyscale local bilateral_mean of an image. + + Parameters + ---------- + image : ndarray + Image array (uint16). As the algorithm uses max. 12bit histogram, + an exception will be raised if image has a value > 4095 + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : (int) + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + s0, s1 : int + define the [s0, s1] interval to be considered for computing the value. + + Returns + ------- + out : uint16 array + The result of the local bilateral mean. + + See also + -------- + skimage.filter.denoise_bilateral() for a gaussian bilateral filter. + + Notes + ----- + + * input image are 16-bit only + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import bilateral_mean + >>> # Load test image / cast to uint16 + >>> ima = data.camera().astype(np.uint16) + >>> # bilateral filtering of cameraman image using a flat kernel + >>> bilat_ima = bilateral_mean(ima, disk(20), s0=10,s1=10) + """ + + return _apply(None, _crank16_bilateral.mean, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) + + +def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, s0=10, s1=10): + """Return the number (population) of pixels actually inside the bilateral + neighborhood, i.e. being inside the structuring element AND having a gray + level inside the interval [g-s0, g+s1]. + + Parameters + ---------- + image : ndarray + Image array (uint16). As the algorithm uses max. 12bit histogram, + an exception will be raised if image has a value > 4095 + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : (int) + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + s0, s1 : int + define the [s0, s1] interval to be considered for computing the value. + + Returns + ------- + out : uint16 array + the local number of pixels inside the bilateral neighborhood + + Notes + ----- + + * input image are 16-bit only + + Examples + -------- + >>> # Local mean + >>> from skimage.morphology import square + >>> import skimage.filter.rank as rank + >>> ima16 = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint16) + >>> rank.bilateral_pop(ima16, square(3), s0=10,s1=10) + array([[3, 4, 3, 4, 3], + [4, 4, 6, 4, 4], + [3, 6, 9, 6, 3], + [4, 4, 6, 4, 4], + [3, 4, 3, 4, 3]], dtype=uint16) + + """ + + return _apply(None, _crank16_bilateral.pop, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py new file mode 100644 index 00000000..94fc3130 --- /dev/null +++ b/skimage/filter/rank/generic.py @@ -0,0 +1,11 @@ +import numpy as np + + +def find_bitdepth(image): + """returns the max bith depth of a uint16 image + """ + umax = np.max(image) + if umax > 2: + return int(np.log2(umax)) + else: + return 1 diff --git a/skimage/filter/rank/percentile_rank.pyx b/skimage/filter/rank/percentile_rank.pyx new file mode 100644 index 00000000..7deae623 --- /dev/null +++ b/skimage/filter/rank/percentile_rank.pyx @@ -0,0 +1,396 @@ +"""Inferior and superior ranks, provided by the user, are passed to the kernel +function to provide a softer version of the rank filters. E.g. +percentile_autolevel will stretch image levels between percentile [p0, p1] +instead of using [min, max]. It means that isolated bright or dark pixels will +not produce halos. + +The local histogram is computed using a sliding window similar to the method +described in [1]_. + +Input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit), for 16-bit +input images, the number of histogram bins is determined from the maximum value +present in the image. + +Result image is 8 or 16-bit with respect to the input image. + +References +---------- + +.. [1] Huang, T. ,Yang, G. ; Tang, G.. "A fast two-dimensional + median filtering algorithm", IEEE Transactions on Acoustics, Speech and + Signal Processing, Feb 1979. Volume: 27 , Issue: 1, Page(s): 13 - 18. + +""" + +import numpy as np +from skimage import img_as_ubyte +from skimage.filter.rank.generic import find_bitdepth +from skimage.filter.rank import _crank16_percentiles, _crank8_percentiles + + +__all__ = ['percentile_autolevel', 'percentile_gradient', + 'percentile_mean', 'percentile_mean_substraction', + 'percentile_morph_contr_enh', 'percentile', 'percentile_pop', + 'percentile_threshold'] + + +def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, p0, p1): + selem = img_as_ubyte(selem) + image = np.ascontiguousarray(image) + + if mask is None: + mask = np.ones(image.shape, dtype=np.uint8) + else: + mask = np.ascontiguousarray(mask) + mask = img_as_ubyte(mask) + + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + + if image.dtype == np.uint8: + if func8 is None: + raise TypeError("Not implemented for uint8 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint8) + func8(image, selem, shift_x=shift_x, shift_y=shift_y, + mask=mask, out=out, p0=p0, p1=p1) + elif image.dtype == np.uint16: + if func16 is None: + raise TypeError("Not implemented for uint16 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint16) + bitdepth = find_bitdepth(image) + if bitdepth > 11: + raise ValueError("Only uint16 <4096 image (12bit) supported.") + func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + bitdepth=bitdepth + 1, out=out, p0=p0, p1=p1) + else: + raise TypeError("Only uint8 and uint16 image supported.") + + return out + + +def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local autolevel of an image. + + Autolevel is computed on the given structuring element. Only levels between + percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local autolevel : uint8 array or uint16 + The result of the local autolevel. + + """ + + return _apply( + _crank8_percentiles.autolevel, _crank16_percentiles.autolevel, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local percentile_gradient of an image. + + percentile_gradient is computed on the given structuring element. Only + levels between percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local percentile_gradient : uint8 array or uint16 + The result of the local percentile_gradient. + + """ + + return _apply(_crank8_percentiles.gradient, _crank16_percentiles.gradient, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_mean(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local mean of an image. + + Mean is computed on the given structuring element. Only levels between + percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local mean : uint8 array or uint16 + The result of the local mean. + + """ + + return _apply(_crank8_percentiles.mean, _crank16_percentiles.mean, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_mean_substraction(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, p0=.0, p1=1.): + """Return greyscale local mean_substraction of an image. + + mean_substraction is computed on the given structuring element. Only levels + between percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local mean_substraction : uint8 array or uint16 + The result of the local mean_substraction. + + """ + + return _apply(_crank8_percentiles.mean_substraction, + _crank16_percentiles.mean_substraction, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_morph_contr_enh( + image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local morph_contr_enh of an image. + + morph_contr_enh is computed on the given structuring element. Only levels + between percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local morph_contr_enh : uint8 array or uint16 + The result of the local morph_contr_enh. + + """ + + return _apply(_crank8_percentiles.morph_contr_enh, + _crank16_percentiles.morph_contr_enh, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, + p0=.0, p1=1.): + """Return greyscale local percentile of an image. + + percentile is computed on the given structuring element. Only levels between + percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local percentile : uint8 array or uint16 + The result of the local percentile. + + """ + + return _apply(_crank8_percentiles.percentile, + _crank16_percentiles.percentile, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_pop(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local pop of an image. + + pop is computed on the given structuring element. Only levels between + percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local pop : uint8 array or uint16 + The result of the local pop. + + """ + + return _apply(_crank8_percentiles.pop, _crank16_percentiles.pop, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local threshold of an image. + + threshold is computed on the given structuring element. Only levels between + percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local threshold : uint8 array or uint16 + The result of the local threshold. + + """ + + return _apply( + _crank8_percentiles.threshold, _crank16_percentiles.threshold, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) diff --git a/skimage/filter/rank/rank.pyx b/skimage/filter/rank/rank.pyx new file mode 100644 index 00000000..e8a4c8f1 --- /dev/null +++ b/skimage/filter/rank/rank.pyx @@ -0,0 +1,769 @@ +"""The local histogram is computed using a sliding window similar to the method +described in [1]_. + +Input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit), for 16-bit +input images, the number of histogram bins is determined from the maximum value +present in the image. + +Result image is 8 or 16-bit with respect to the input image. + +References +---------- + +.. [1] Huang, T. ,Yang, G. ; Tang, G.. "A fast two-dimensional + median filtering algorithm", IEEE Transactions on Acoustics, Speech and + Signal Processing, Feb 1979. Volume: 27 , Issue: 1, Page(s): 13 - 18. + +""" + +import numpy as np +from skimage import img_as_ubyte +from skimage.filter.rank import _crank8, _crank16 +from skimage.filter.rank.generic import find_bitdepth + + +__all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', + 'meansubstraction', 'median', 'minimum', 'modal', 'morph_contr_enh', + 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] + + +def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): + selem = img_as_ubyte(selem) + image = np.ascontiguousarray(image) + + if mask is None: + mask = np.ones(image.shape, dtype=np.uint8) + else: + mask = np.ascontiguousarray(mask) + mask = img_as_ubyte(mask) + + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + + if image.dtype == np.uint8: + if func8 is None: + raise TypeError("Not implemented for uint8 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint8) + func8(image, selem, shift_x=shift_x, shift_y=shift_y, + mask=mask, out=out) + elif image.dtype == np.uint16: + if func16 is None: + raise TypeError("Not implemented for uint16 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint16) + bitdepth = find_bitdepth(image) + if bitdepth > 11: + raise ValueError("Only uint16 <4096 image (12bit) supported.") + func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + bitdepth=bitdepth + 1, out=out) + else: + raise TypeError("Only uint8 and uint16 image supported.") + + return out + + +def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Autolevel image using local histogram. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local autolevel. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import autolevel + >>> # Load test image + >>> ima = data.camera() + >>> # Stretch image contrast locally + >>> auto = autolevel(ima, disk(20)) + + """ + + return _apply(_crank8.autolevel, _crank16.autolevel, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns greyscale local bottomhat of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + local bottomhat : uint8 array or uint16 array depending on input image + The result of the local bottomhat. + + """ + + return _apply(_crank8.bottomhat, _crank16.bottomhat, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Equalize image using local histogram. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local equalize. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import equalize + >>> # Load test image + >>> ima = data.camera() + >>> # Local equalization + >>> equ = equalize(ima, disk(20)) + + """ + + return _apply(_crank8.equalize, _crank16.equalize, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local gradient of an image (i.e. local maximum - local + minimum). + + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local gradient. + + """ + + return _apply(_crank8.gradient, _crank16.gradient, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local maximum of an image. + + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local maximum. + + See also + -------- + skimage.morphology.dilation + + Note + ---- + * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) + * the lower algorithm complexity makes the rank.maximum() more efficient for + larger images and structuring elements + + """ + + return _apply(_crank8.maximum, _crank16.maximum, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local mean of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local mean. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import mean + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = mean(ima, disk(20)) + + """ + + return _apply(_crank8.mean, _crank16.mean, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def meansubstraction(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Return image substracted from its local mean. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local meansubstraction. + + """ + + return _apply(_crank8.meansubstraction, _crank16.meansubstraction, image, + selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local median of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local median. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import median + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = median(ima, disk(20)) + + """ + + return _apply(_crank8.median, _crank16.median, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local minimum of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local minimum. + + See also + -------- + skimage.morphology.erosion + + Note + ---- + * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) + * the lower algorithm complexity makes the rank.minimum() more efficient + for larger images and structuring elements + + """ + + return _apply(_crank8.minimum, _crank16.minimum, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local mode of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local modal. + + """ + + return _apply(_crank8.modal, _crank16.modal, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Enhance an image replacing each pixel by the local maximum if pixel + greylevel is closest to maximimum than local minimum OR local minimum + otherwise. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local morph_contr_enh. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import morph_contr_enh + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = morph_contr_enh(ima, disk(20)) + + """ + + return _apply(_crank8.morph_contr_enh, _crank16.morph_contr_enh, image, + selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return the number (population) of pixels actually inside the + neighborhood. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The number of pixels belonging to the neighborhood. + + Examples + -------- + >>> # Local mean + >>> from skimage.morphology import square + >>> import skimage.filter.rank as rank + >>> ima = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> rank.pop(ima, square(3)) + array([[4, 6, 6, 6, 4], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [4, 6, 6, 6, 4]], dtype=uint8) + + """ + + return _apply(_crank8.pop, _crank16.pop, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local threshold of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local threshold. + + Examples + -------- + >>> # Local threshold + >>> from skimage.morphology import square + >>> from skimage.filter.rank import threshold + >>> ima = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> threshold(ima, square(3)) + array([[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + """ + + return _apply(_crank8.threshold, _crank16.threshold, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local tophat of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The image tophat. + + """ + + return _apply(_crank8.tophat, _crank16.tophat, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def noise_filter(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Returns the noise feature as described in [Hashimoto12]_ + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + References + ---------- + .. [Hashimoto12] N. Hashimoto et al. Referenceless image quality evaluation + for whole slide imaging. J Pathol Inform 2012;3:9. + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The image noise. + + """ + + # ensure that the central pixel in the structuring element is empty + centre_r = int(selem.shape[0] / 2) + shift_y + centre_c = int(selem.shape[1] / 2) + shift_x + # make a local copy + selem_cpy = selem.copy() + selem_cpy[centre_r, centre_c] = 0 + + return _apply(_crank8.noise_filter, None, image, selem_cpy, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns the entropy [wiki_entropy]_ computed locally. Entropy is computed + using base 2 logarithm i.e. the filter returns the minimum number of + bits needed to encode local greylevel distribution. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + entropy x10 (uint8 images) and entropy x1000 (uint16 images) + + References + ---------- + .. [wiki_entropy] http://en.wikipedia.org/wiki/Entropy_(information_theory) + + Examples + -------- + >>> # Local entropy + >>> from skimage import data + >>> from skimage.filter.rank import entropy + >>> from skimage.morphology import disk + >>> # defining a 8- and a 16-bit test images + >>> a8 = data.camera() + >>> a16 = data.camera().astype(np.uint16) * 4 + >>> # pixel values contain 10x the local entropy + >>> ent8 = entropy(a8, disk(5)) + >>> # pixel values contain 1000x the local entropy + >>> ent16 = entropy(a16, disk(5)) + + """ + + return _apply(_crank8.entropy, _crank16.entropy, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns the Otsu's threshold value for each pixel. + + Parameters + ---------- + image : ndarray + Image array (uint8 array). + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array + Otsu's threshold values + + References + ---------- + .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method + + Notes + ----- + * input image are 8-bit only + + Examples + -------- + >>> # Local entropy + >>> from skimage import data + >>> from skimage.filter.rank import otsu + >>> from skimage.morphology import disk + >>> # defining a 8-bit test images + >>> a8 = data.camera() + >>> loc_otsu = otsu(a8, disk(5)) + >>> thresh_image = a8 >= loc_otsu + + """ + + return _apply(_crank8.otsu, None, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py new file mode 100644 index 00000000..43ff030f --- /dev/null +++ b/skimage/filter/rank/tests/test_rank.py @@ -0,0 +1,380 @@ +import numpy as np +from numpy.testing import run_module_suite, assert_array_equal, assert_raises + +from skimage import data +from skimage.morphology import cmorph, disk +from skimage.filter import rank + + +def test_random_sizes(): + # make sure the size is not a problem + + niter = 10 + elem = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8) + for m, n in np.random.random_integers(1, 100, size=(10, 2)): + mask = np.ones((m, n), dtype=np.uint8) + + image8 = np.ones((m, n), dtype=np.uint8) + out8 = np.empty_like(image8) + rank.mean(image=image8, selem=elem, mask=mask, out=out8, + shift_x=0, shift_y=0) + assert_array_equal(image8.shape, out8.shape) + rank.mean(image=image8, selem=elem, mask=mask, out=out8, + shift_x=+1, shift_y=+1) + assert_array_equal(image8.shape, out8.shape) + + image16 = np.ones((m, n), dtype=np.uint16) + out16 = np.empty_like(image8, dtype=np.uint16) + rank.mean(image=image16, selem=elem, mask=mask, out=out16, + shift_x=0, shift_y=0) + assert_array_equal(image16.shape, out16.shape) + rank.mean(image=image16, selem=elem, mask=mask, out=out16, + shift_x=+1, shift_y=+1) + assert_array_equal(image16.shape, out16.shape) + + rank.percentile_mean(image=image16, mask=mask, out=out16, + selem=elem, shift_x=0, shift_y=0, p0=.1, p1=.9) + assert_array_equal(image16.shape, out16.shape) + rank.percentile_mean(image=image16, mask=mask, out=out16, + selem=elem, shift_x=+1, shift_y=+1, p0=.1, p1=.9) + assert_array_equal(image16.shape, out16.shape) + + +def test_compare_with_cmorph_dilate(): + # compare the result of maximum filter with dilate + + image = (np.random.random((100, 100)) * 256).astype(np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + + for r in range(1, 20, 1): + elem = np.ones((r, r), dtype=np.uint8) + rank.maximum(image=image, selem=elem, out=out, mask=mask) + cm = cmorph.dilate(image=image, selem=elem) + assert_array_equal(out, cm) + + +def test_compare_with_cmorph_erode(): + # compare the result of maximum filter with erode + + image = (np.random.random((100, 100)) * 256).astype(np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + + for r in range(1, 20, 1): + elem = np.ones((r, r), dtype=np.uint8) + rank.minimum(image=image, selem=elem, out=out, mask=mask) + cm = cmorph.erode(image=image, selem=elem) + assert_array_equal(out, cm) + + +def test_bitdepth(): + # test the different bit depth for rank16 + + elem = np.ones((3, 3), dtype=np.uint8) + out = np.empty((100, 100), dtype=np.uint16) + mask = np.ones((100, 100), dtype=np.uint8) + + for i in range(5): + image = np.ones((100, 100), dtype=np.uint16) * 255 * 2 ** i + r = rank.percentile_mean(image=image, selem=elem, mask=mask, + out=out, shift_x=0, shift_y=0, p0=.1, p1=.9) + + +def test_population(): + # check the number of valid pixels in the neighborhood + + image = np.zeros((5, 5), dtype=np.uint8) + elem = np.ones((3, 3), dtype=np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + + rank.pop(image=image, selem=elem, out=out, mask=mask) + r = np.array([[4, 6, 6, 6, 4], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [4, 6, 6, 6, 4]]) + assert_array_equal(r, out) + + +def test_structuring_element8(): + # check the output for a custom structuring element + + r = np.array([[0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 255, 0, 0, 0], + [0, 0, 255, 255, 255, 0], + [0, 0, 0, 255, 255, 0], + [0, 0, 0, 0, 0, 0]]) + + # 8-bit + image = np.zeros((6, 6), dtype=np.uint8) + image[2, 2] = 255 + elem = np.asarray([[1, 1, 0], [1, 1, 1], [0, 0, 1]], dtype=np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=1, shift_y=1) + assert_array_equal(r, out) + + # 16-bit + image = np.zeros((6, 6), dtype=np.uint16) + image[2, 2] = 255 + out = np.empty_like(image) + + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=1, shift_y=1) + assert_array_equal(r, out) + + +def test_fail_on_bitdepth(): + # should fail because data bitdepth is too high for the function + + image = np.ones((100, 100), dtype=np.uint16) * 2 ** 12 + elem = np.ones((3, 3), dtype=np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + assert_raises(ValueError, rank.percentile_mean, image=image, + selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) + + +def test_pass_on_bitdepth(): + # should pass because data bitdepth is not too high for the function + + image = np.ones((100, 100), dtype=np.uint16) * 2 ** 11 + elem = np.ones((3, 3), dtype=np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + + +def test_inplace_output(): + # rank filters are not supposed to filter inplace + + selem = disk(20) + image = (np.random.random((500, 500)) * 256).astype(np.uint8) + out = image + assert_raises(NotImplementedError, rank.mean, image, selem, out=out) + + +def test_compare_autolevels(): + # compare autolevel and percentile autolevel with p0=0.0 and p1=1.0 + # should returns the same arrays + + image = data.camera() + + selem = disk(20) + loc_autolevel = rank.autolevel(image, selem=selem) + loc_perc_autolevel = rank.percentile_autolevel(image, selem=selem, + p0=.0, p1=1.) + + assert_array_equal(loc_autolevel, loc_perc_autolevel) + + +def test_compare_autolevels_16bit(): + # compare autolevel(16-bit) and percentile autolevel(16-bit) with p0=0.0 and + # p1=1.0 should returns the same arrays + + image = data.camera().astype(np.uint16) * 4 + + selem = disk(20) + loc_autolevel = rank.autolevel(image, selem=selem) + loc_perc_autolevel = rank.percentile_autolevel(image, selem=selem, + p0=.0, p1=1.) + + assert_array_equal(loc_autolevel, loc_perc_autolevel) + + +def test_compare_8bit_vs_16bit(): + # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of + # dynamic) should be identical + + image8 = data.camera() + image16 = image8.astype(np.uint16) + assert_array_equal(image8, image16) + + methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', + 'mean', 'meansubstraction', 'median', 'minimum', 'modal', + 'morph_contr_enh', 'pop', 'threshold', 'tophat'] + + for method in methods: + func = getattr(rank, method) + f8 = func(image8, disk(3)) + f16 = func(image16, disk(3)) + assert_array_equal(f8, f16) + + +def test_trivial_selem8(): + # check that min, max and mean returns identity if structuring element + # contains only central pixel + + image = np.zeros((5, 5), dtype=np.uint8) + out = np.zeros_like(image) + mask = np.ones_like(image, dtype=np.uint8) + image[2, 2] = 255 + image[2, 3] = 128 + image[1, 2] = 16 + + elem = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.uint8) + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.minimum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + + +def test_trivial_selem16(): + # check that min, max and mean returns identity if structuring element + # contains only central pixel + + image = np.zeros((5, 5), dtype=np.uint16) + out = np.zeros_like(image) + mask = np.ones_like(image, dtype=np.uint8) + image[2, 2] = 255 + image[2, 3] = 128 + image[1, 2] = 16 + + elem = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.uint8) + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.minimum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + + +def test_smallest_selem8(): + # check that min, max and mean returns identity if structuring element + # contains only central pixel + + image = np.zeros((5, 5), dtype=np.uint8) + out = np.zeros_like(image) + mask = np.ones_like(image, dtype=np.uint8) + image[2, 2] = 255 + image[2, 3] = 128 + image[1, 2] = 16 + + elem = np.array([[1]], dtype=np.uint8) + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.minimum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + + +def test_smallest_selem16(): + # check that min, max and mean returns identity if structuring element + # contains only central pixel + + image = np.zeros((5, 5), dtype=np.uint16) + out = np.zeros_like(image) + mask = np.ones_like(image, dtype=np.uint8) + image[2, 2] = 255 + image[2, 3] = 128 + image[1, 2] = 16 + + elem = np.array([[1]], dtype=np.uint8) + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.minimum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + + +def test_empty_selem(): + # check that min, max and mean returns zeros if structuring element is empty + + image = np.zeros((5, 5), dtype=np.uint16) + out = np.zeros_like(image) + mask = np.ones_like(image, dtype=np.uint8) + res = np.zeros_like(image) + image[2, 2] = 255 + image[2, 3] = 128 + image[1, 2] = 16 + + elem = np.array([[0, 0, 0], [0, 0, 0]], dtype=np.uint8) + + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(res, out) + rank.minimum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(res, out) + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(res, out) + + +def test_otsu(): + # test the local Otsu segmentation on a synthetic image + # (left to right ramp * sinus) + + test = np.tile( + [128, 145, 103, 127, 165, 83, 127, 185, 63, 127, 205, 43, + 127, 225, 23, 127], + (16, 1)) + test = test.astype(np.uint8) + res = np.tile([1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1], + (16, 1)) + selem = np.ones((6, 6), dtype=np.uint8) + th = 1 * (test >= rank.otsu(test, selem)) + assert_array_equal(th, res) + + +def test_entropy(): + # verify that entropy is coherent with bitdepth of the input data + + selem = np.ones((16, 16), dtype=np.uint8) + # 1 bit per pixel + data = np.tile(np.asarray([0, 1]), (100, 100)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 10) + + # 2 bit per pixel + data = np.tile(np.asarray([[0, 1], [2, 3]]), (10, 10)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 20) + + # 3 bit per pixel + data = np.tile( + np.asarray([[0, 1, 2, 3], [4, 5, 6, 7]]), (10, 10)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 30) + + # 4 bit per pixel + data = np.tile( + np.reshape(np.arange(16), (4, 4)), (10, 10)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 40) + + # 6 bit per pixel + data = np.tile( + np.reshape(np.arange(64), (8, 8)), (10, 10)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 60) + + # 8-bit per pixel + data = np.tile( + np.reshape(np.arange(256), (16, 16)), (10, 10)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 80) + + # 12 bit per pixel + selem = np.ones((64, 64), dtype=np.uint8) + data = np.tile( + np.reshape(np.arange(4096), (64, 64)), (2, 2)).astype(np.uint16) + assert(np.max(rank.entropy(data, selem)) == 12000) + + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index 12cb84a7..934d3f2e 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 @@ -12,19 +13,58 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['_ctmf.pyx'], working_path=base_path) + cython(['_denoise_cy.pyx'], working_path=base_path) + cython(['rank/_core8.pyx'], working_path=base_path) + cython(['rank/_core16.pyx'], working_path=base_path) + cython(['rank/_crank8.pyx'], working_path=base_path) + cython(['rank/_crank8_percentiles.pyx'], working_path=base_path) + cython(['rank/_crank16.pyx'], working_path=base_path) + cython(['rank/_crank16_percentiles.pyx'], working_path=base_path) + cython(['rank/_crank16_bilateral.pyx'], working_path=base_path) + cython(['rank/rank.pyx'], working_path=base_path) + cython(['rank/percentile_rank.pyx'], working_path=base_path) + cython(['rank/bilateral_rank.pyx'], working_path=base_path) config.add_extension('_ctmf', sources=['_ctmf.c'], - include_dirs=[get_numpy_include_dirs()]) + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_denoise_cy', sources=['_denoise_cy.c'], + include_dirs=[get_numpy_include_dirs(), '../_shared']) + config.add_extension('rank._core8', sources=['rank/_core8.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('rank._core16', sources=['rank/_core16.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('rank._crank8', sources=['rank/_crank8.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank._crank8_percentiles', sources=['rank/_crank8_percentiles.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('rank._crank16', sources=['rank/_crank16.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank._crank16_percentiles', sources=['rank/_crank16_percentiles.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank._crank16_bilateral', sources=['rank/_crank16_bilateral.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank.rank', sources=['rank/rank.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank.percentile_rank', sources=['rank/percentile_rank.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank.bilateral_rank', sources=['rank/bilateral_rank.c'], + include_dirs=[get_numpy_include_dirs()]) 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 = '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_denoise.py b/skimage/filter/tests/test_denoise.py new file mode 100644 index 00000000..f81ba07b --- /dev/null +++ b/skimage/filter/tests/test_denoise.py @@ -0,0 +1,140 @@ +import numpy as np +from numpy.testing import run_module_suite, assert_raises, assert_equal + +from skimage import filter, data, color, img_as_float + + +np.random.seed(1234) + + +lena = img_as_float(data.lena()[:256, :256]) +lena_gray = color.rgb2gray(lena) + + +def test_denoise_tv_chambolle_2d(): + # lena image + img = lena_gray + # add noise to lena + img += 0.5 * img.std() * np.random.random(img.shape) + # clip noise so that it does not exceed allowed range for float images. + img = np.clip(img, 0, 1) + # denoise + denoised_lena = filter.denoise_tv_chambolle(img, weight=60.0) + # which dtype? + assert denoised_lena.dtype in [np.float, np.float32, np.float64] + from scipy import ndimage + grad = ndimage.morphological_gradient(img, size=((3, 3))) + grad_denoised = ndimage.morphological_gradient( + denoised_lena, size=((3, 3))) + # test if the total variation has decreased + assert grad_denoised.dtype == np.float + assert (np.sqrt((grad_denoised**2).sum()) + < np.sqrt((grad**2).sum()) / 2) + + +def test_denoise_tv_chambolle_multichannel(): + denoised0 = filter.denoise_tv_chambolle(lena[..., 0], weight=60.0) + denoised = filter.denoise_tv_chambolle(lena, weight=60.0, multichannel=True) + assert_equal(denoised[..., 0], denoised0) + + +def test_denoise_tv_chambolle_float_result_range(): + # lena image + img = lena_gray + int_lena = np.multiply(img, 255).astype(np.uint8) + assert np.max(int_lena) > 1 + denoised_int_lena = filter.denoise_tv_chambolle(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_denoise_tv_chambolle_3d(): + """Apply the TV denoising algorithm on a 3D image representing 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 = 100 * mask.astype(np.float) + mask += 60 + mask += 20 * np.random.random(mask.shape) + mask[mask < 0] = 0 + mask[mask > 255] = 255 + res = filter.denoise_tv_chambolle(mask.astype(np.uint8), weight=100) + assert res.dtype == np.float + assert res.std() * 255 < mask.std() + + # test wrong number of dimensions + assert_raises(ValueError, filter.denoise_tv_chambolle, + np.random.random((8, 8, 8, 8))) + + +def test_denoise_tv_bregman_2d(): + img = lena_gray + # add some random noise + img += 0.5 * img.std() * np.random.random(img.shape) + img = np.clip(img, 0, 1) + + out1 = filter.denoise_tv_bregman(img, weight=10) + out2 = filter.denoise_tv_bregman(img, weight=5) + + # make sure noise is reduced + assert img.std() > out1.std() + assert out1.std() > out2.std() + + +def test_denoise_tv_bregman_float_result_range(): + # lena image + img = lena_gray + int_lena = np.multiply(img, 255).astype(np.uint8) + assert np.max(int_lena) > 1 + denoised_int_lena = filter.denoise_tv_bregman(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_denoise_tv_bregman_3d(): + img = lena + # add some random noise + img += 0.5 * img.std() * np.random.random(img.shape) + img = np.clip(img, 0, 1) + + out1 = filter.denoise_tv_bregman(img, weight=10) + out2 = filter.denoise_tv_bregman(img, weight=5) + + # make sure noise is reduced + assert img.std() > out1.std() + assert out1.std() > out2.std() + + +def test_denoise_bilateral_2d(): + img = lena_gray + # add some random noise + img += 0.5 * img.std() * np.random.random(img.shape) + img = np.clip(img, 0, 1) + + out1 = filter.denoise_bilateral(img, sigma_range=0.1, sigma_spatial=20) + out2 = filter.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) + + # make sure noise is reduced + assert img.std() > out1.std() + assert out1.std() > out2.std() + + +def test_denoise_bilateral_3d(): + img = lena + # add some random noise + img += 0.5 * img.std() * np.random.random(img.shape) + img = np.clip(img, 0, 1) + + out1 = filter.denoise_bilateral(img, sigma_range=0.1, sigma_spatial=20) + out2 = filter.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) + + # make sure noise is reduced + assert img.std() > out1.std() + assert out1.std() > out2.std() + + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/filter/tests/test_edges.py b/skimage/filter/tests/test_edges.py index 0c306273..628de16d 100644 --- a/skimage/filter/tests/test_edges.py +++ b/skimage/filter/tests/test_edges.py @@ -1,206 +1,356 @@ -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_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_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_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)) -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_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_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_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_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)) - 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)) -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_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_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_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_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)) -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_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)) - 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_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_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_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)) -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_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_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_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_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)) - 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_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_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_scharr_zeros(): + """Scharr on an array of all zeros""" + result = F.scharr(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_scharr_mask(): + """Scharr on a masked array should be zero""" + np.random.seed(0) + result = F.scharr(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_scharr_horizontal(): + """Scharr on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.scharr(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_scharr_vertical(): + """Scharr on a vertical edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.scharr(image) + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + assert (np.all(result[np.abs(j) > 1] == 0)) + + +def test_hscharr_zeros(): + """Horizontal Scharr on an array of all zeros""" + result = F.hscharr(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_hscharr_mask(): + """Horizontal Scharr on a masked array should be zero""" + np.random.seed(0) + result = F.hscharr(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_hscharr_horizontal(): + """Horizontal Scharr on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.hscharr(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_hscharr_vertical(): + """Horizontal Scharr on a vertical edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.hscharr(image) + assert (np.all(result == 0)) + + +def test_vscharr_zeros(): + """Vertical Scharr on an array of all zeros""" + result = F.vscharr(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_vscharr_mask(): + """Vertical Scharr on a masked array should be zero""" + np.random.seed(0) + result = F.vscharr(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_vscharr_vertical(): + """Vertical Scharr on an edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.vscharr(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_vscharr_horizontal(): + """vertical Scharr on a horizontal edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.vscharr(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)) + + +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_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_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_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, F.hscharr): + 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, F.vscharr): + 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 deleted file mode 100644 index f851f4f1..00000000 --- a/skimage/filter/tests/test_tv_denoise.py +++ /dev/null @@ -1,62 +0,0 @@ -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(): - - def test_tv_denoise_2d(self): - """ - Apply the TV denoising algorithm on the lena image provided - by scipy - """ - # lena image - lena = color.rgb2gray(data.lena())[:256, :256] - # add noise to lena - 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 - denoised_lena = filter.tv_denoise(lena, weight=60.0) - # 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))) - # 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') - - - def test_tv_denoise_3d(self): - """ - Apply the TV denoising algorithm on a 3D image representing - 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 = 100 * mask.astype(np.float) - mask += 60 - 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') - # test wrong number of dimensions - a = np.random.random((8, 8, 8, 8)) - try: - res = filter.tv_denoise(a) - except ValueError: - pass - - -if __name__ == "__main__": - run_module_suite() 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/filter/tv_denoise.py b/skimage/filter/tv_denoise.py deleted file mode 100644 index bb74a4bc..00000000 --- a/skimage/filter/tv_denoise.py +++ /dev/null @@ -1,257 +0,0 @@ -import numpy as np - -def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): - """ - Perform total-variation denoising on 3-D arrays - - Parameters - ---------- - im: ndarray - 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``) - - eps: float, optional - 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 - - n_iter_max: int, optional - maximal number of iterations used for the optimization. - - Returns - ------- - out: ndarray - denoised array - - Notes - ----- - Rudin, Osher and Fatemi algorithm - - Examples - --------- - First build synthetic noisy data - >>> x, y, z = np.ogrid[0:40, 0:40, 0:40] - >>> mask = (x -22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 - >>> mask = mask.astype(np.float) - >>> mask += 0.2*np.random.randn(*mask.shape) - >>> res = tv_denoise_3d(mask, weight=100) - """ - px = np.zeros_like(im) - py = np.zeros_like(im) - pz = np.zeros_like(im) - gx = np.zeros_like(im) - gy = np.zeros_like(im) - gz = np.zeros_like(im) - d = np.zeros_like(im) - i = 0 - while i < n_iter_max: - d = - px - py - pz - 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) - norm = np.sqrt(gx**2 + gy**2 + gz**2) - E += weight * norm.sum() - norm *= 0.5 / weight - norm += 1. - px -= 1./6.*gx - px /= norm - py -= 1./6.*gy - py /= norm - pz -= 1/6.*gz - pz /= norm - E /= float(im.size) - if i == 0: - E_init = E - E_previous = E - else: - if np.abs(E_previous - E) < eps * E_init: - break - else: - 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 - - Parameters - ---------- - im: ndarray - input data to be denoised - - weight: float, optional - 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 - the stop criterion. The algorithm stops when: - - (E_(n-1) - E_n) < eps * E_0 - - n_iter_max: int, optional - maximal number of iterations used for the optimization. - - Returns - ------- - out: ndarray - denoised array - - 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 - 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, - Springer, 2004, 20, 89-97. - - Examples - --------- - >>> import scipy - >>> lena = scipy.lena() - >>> import scipy - >>> lena = scipy.lena().astype(np.float) - >>> lena += 0.5 * lena.std()*np.random.randn(*lena.shape) - >>> denoised_lena = tv_denoise(lena, weight=60.0) - """ - px = np.zeros_like(im) - py = np.zeros_like(im) - gx = np.zeros_like(im) - gy = np.zeros_like(im) - d = np.zeros_like(im) - i = 0 - while i < n_iter_max: - 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) - norm = np.sqrt(gx**2 + gy**2) - E += weight * norm.sum() - norm *= 0.5 / weight - norm += 1 - px -= 0.25*gx - px /= norm - py -= 0.25*gy - py /= norm - E /= float(im.size) - if i == 0: - E_init = E - E_previous = E - else: - if np.abs(E_previous - E) < eps * E_init: - break - else: - E_previous = E - i += 1 - return out - -def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): - """ - Perform total-variation denoising on 2-d and 3-d images - - Parameters - ---------- - 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 - of the denoised image. - - weight: float, optional - 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 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 - - - Notes - ----- - The principle of total variation denoising is explained in - 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, - piecewise-constant images. - - 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, - Springer, 2004, 20, 89-97. - - Examples - --------- - >>> import scipy - >>> # 2D example using lena - >>> lena = scipy.lena() - >>> import scipy - >>> lena = scipy.lena().astype(np.float) - >>> lena += 0.5 * lena.std()*np.random.randn(*lena.shape) - >>> denoised_lena = tv_denoise(lena, weight=60) - >>> # 3D example on synthetic data - >>> x, y, z = np.ogrid[0:40, 0:40, 0:40] - >>> mask = (x -22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 - >>> mask = mask.astype(np.float) - >>> mask += 0.2*np.random.randn(*mask.shape) - >>> res = tv_denoise_3d(mask, weight=100) - """ - im_type = im.dtype - if not im_type.kind == 'f': - im = im.astype(np.float) - - 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 - diff --git a/skimage/graph/_mcp.pxd b/skimage/graph/_mcp.pxd index 4222d877..b2e2a548 100644 --- a/skimage/graph/_mcp.pxd +++ b/skimage/graph/_mcp.pxd @@ -4,11 +4,11 @@ other cython modules can "cimport mcp" and subclass it. """ cimport heap -cimport numpy as np +cimport numpy as cnp ctypedef heap.BOOL_T BOOL_T -ctypedef unsigned char DIM_T -ctypedef np.float64_t FLOAT_T +ctypedef unsigned char DIM_T +ctypedef cnp.float64_t FLOAT_T cdef class MCP: cdef heap.FastUpdateBinaryHeap costs_heap @@ -23,7 +23,7 @@ cdef class MCP: cdef object flat_offsets cdef object offset_lengths cdef BOOL_T dirty - cdef BOOL_T use_start_cost + cdef BOOL_T use_start_cost # if use_start_cost is true, the cost of the starting element is added to # the cost of the path. Set to true by default in the base class... diff --git a/skimage/graph/_mcp.pyx b/skimage/graph/_mcp.pyx index beb814fe..4e5f6d52 100644 --- a/skimage/graph/_mcp.pyx +++ b/skimage/graph/_mcp.pyx @@ -1,5 +1,7 @@ -# -*- python -*- - +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False """Cython implementation of Dijkstra's minimum cost path algorithm, for use with data on a n-dimensional lattice. @@ -32,19 +34,19 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ -import cython -cimport numpy as np import numpy as np -cimport heap import heap -ctypedef np.int8_t OFFSET_T +cimport numpy as cnp +cimport heap + +ctypedef cnp.int8_t OFFSET_T OFFSET_D = np.int8 -ctypedef np.int16_t OFFSETS_INDEX_T +ctypedef cnp.int16_t OFFSETS_INDEX_T OFFSETS_INDEX_D = np.int16 -ctypedef np.int8_t EDGE_T +ctypedef cnp.int8_t EDGE_T EDGE_D = np.int8 -ctypedef np.intp_t INDEX_T +ctypedef cnp.intp_t INDEX_T INDEX_D = np.intp FLOAT_D = np.float64 @@ -102,7 +104,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 +114,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 +132,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 +218,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 +297,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( @@ -317,7 +319,6 @@ cdef class MCP: FLOAT_T new_cost, FLOAT_T offset_length): return new_cost - @cython.boundscheck(False) def find_costs(self, starts, ends=None, find_all_ends=True): """ Find the minimum-cost path from the given starting points. @@ -366,7 +367,7 @@ cdef class MCP: cdef BOOL_T use_ends = 0 cdef INDEX_T num_ends cdef BOOL_T all_ends = find_all_ends - cdef np.ndarray[INDEX_T, ndim=1] flat_ends + cdef cnp.ndarray[INDEX_T, ndim=1] flat_ends starts = _normalize_indices(starts, self.costs_shape) if starts is None: raise ValueError('start points must all be within the costs array') @@ -385,18 +386,18 @@ cdef class MCP: # lookup and array-ify object attributes for fast use cdef heap.FastUpdateBinaryHeap costs_heap = self.costs_heap - cdef np.ndarray[FLOAT_T, ndim=1] flat_costs = self.flat_costs - cdef np.ndarray[FLOAT_T, ndim=1] flat_cumulative_costs = \ + cdef cnp.ndarray[FLOAT_T, ndim=1] flat_costs = self.flat_costs + cdef cnp.ndarray[FLOAT_T, ndim=1] flat_cumulative_costs = \ self.flat_cumulative_costs - cdef np.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ + cdef cnp.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ self.traceback_offsets - cdef np.ndarray[EDGE_T, ndim=2] flat_pos_edge_map = \ + cdef cnp.ndarray[EDGE_T, ndim=2] flat_pos_edge_map = \ self.flat_pos_edge_map - cdef np.ndarray[EDGE_T, ndim=2] flat_neg_edge_map = \ + cdef cnp.ndarray[EDGE_T, ndim=2] flat_neg_edge_map = \ self.flat_neg_edge_map - cdef np.ndarray[OFFSET_T, ndim=2] offsets = self.offsets - cdef np.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets - cdef np.ndarray[FLOAT_T, ndim=1] offset_lengths = self.offset_lengths + cdef cnp.ndarray[OFFSET_T, ndim=2] offsets = self.offsets + cdef cnp.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets + cdef cnp.ndarray[FLOAT_T, ndim=1] offset_lengths = self.offset_lengths cdef DIM_T dim = self.dim cdef int num_offsets = len(flat_offsets) @@ -449,7 +450,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 +491,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. @@ -514,7 +515,6 @@ cdef class MCP: self.dirty = 1 return cumulative_costs, traceback - @cython.boundscheck(False) def traceback(self, end): """traceback(end) @@ -555,12 +555,12 @@ cdef class MCP: raise ValueError('no minimum-cost path was found ' 'to the specified end point') - cdef np.ndarray[INDEX_T, ndim=1] position = \ + cdef cnp.ndarray[INDEX_T, ndim=1] position = \ np.array(ends[0], dtype=INDEX_D) - cdef np.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ + cdef cnp.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ self.traceback_offsets - cdef np.ndarray[OFFSET_T, ndim=2] offsets = self.offsets - cdef np.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets + cdef cnp.ndarray[OFFSET_T, ndim=2] offsets = self.offsets + cdef cnp.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets cdef OFFSETS_INDEX_T offset cdef DIM_T d 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/heap.pxd b/skimage/graph/heap.pxd index 51b4f79c..e31aa150 100644 --- a/skimage/graph/heap.pxd +++ b/skimage/graph/heap.pxd @@ -1,7 +1,7 @@ """ This is the definition file for heap.pyx. It contains the definitions of the heap classes, such that other cython modules can "cimport heap" and thus use the -C versions of pop(), push(), and value_of(): pop_fast(), push_fast() and +C versions of pop(), push(), and value_of(): pop_fast(), push_fast() and value_of_fast() """ @@ -14,16 +14,16 @@ ctypedef unsigned char LEVELS_T cdef class BinaryHeap: cdef readonly INDEX_T count - cdef readonly LEVELS_T levels, min_levels + cdef readonly LEVELS_T levels, min_levels cdef VALUE_T *_values cdef REFERENCE_T *_references cdef REFERENCE_T _popped_ref - + cdef void _add_or_remove_level(self, LEVELS_T add_or_remove) cdef void _update(self) cdef void _update_one(self, INDEX_T i) cdef void _remove(self, INDEX_T i) - + cdef INDEX_T push_fast(self, VALUE_T value, REFERENCE_T reference) cdef VALUE_T pop_fast(self) @@ -32,8 +32,7 @@ cdef class FastUpdateBinaryHeap(BinaryHeap): cdef INDEX_T *_crossref cdef BOOL_T _invalid_ref cdef BOOL_T _pushed - + cdef VALUE_T value_of_fast(self, REFERENCE_T reference) - cdef INDEX_T push_if_lower_fast(self, VALUE_T value, + cdef INDEX_T push_if_lower_fast(self, VALUE_T value, REFERENCE_T reference) - \ No newline at end of file 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..f8f395bd 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,13 +1,71 @@ -__all__ = ['imread', 'imread_collection', 'imsave', 'imshow', 'show', +__all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] +import os +import re +import urllib2 +import tempfile +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 = [] +URL_REGEX = re.compile(r'http://|https://|ftp://|file://|file:\\') + + +def is_url(filename): + """Return True if string is an http or ftp path.""" + return (isinstance(filename, basestring) and + URL_REGEX.match(filename) is not None) + + +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 +80,7 @@ def push(img): _image_stack.append(img) + def pop(): """Pop an image from the shared image stack. @@ -33,6 +92,7 @@ def pop(): """ return _image_stack.pop() + def imread(fname, as_grey=False, plugin=None, flatten=None, **plugin_args): """Load an image from file. @@ -40,7 +100,7 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, Parameters ---------- fname : string - Image file name, e.g. ``test.jpg``. + Image file name, e.g. ``test.jpg`` or URL. as_grey : bool If True, convert color images to grey-scale (32-bit floats). Images that are already in grey-scale format are not converted. @@ -69,12 +129,20 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, if flatten is not None: as_grey = flatten - img = call_plugin('imread', fname, plugin=plugin, **plugin_args) + if is_url(fname): + with tempfile.NamedTemporaryFile(delete=False) as f: + u = urllib2.urlopen(fname) + f.write(u.read()) + img = call_plugin('imread', f.name, plugin=plugin, **plugin_args) + os.remove(f.name) + else: + img = call_plugin('imread', fname, plugin=plugin, **plugin_args) 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 +196,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 +215,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..64ac62ff 100644 --- a/skimage/io/_plugins/_colormixer.pyx +++ b/skimage/io/_plugins/_colormixer.pyx @@ -1,4 +1,7 @@ -# -*- python -*- +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False """Colour Mixer @@ -8,21 +11,15 @@ integers, so currently the only way to clip results efficiently one. """ - -import numpy as np -cimport numpy as np - import cython -cdef extern from "math.h": - float exp(float) nogil - float pow(float, float) nogil +cimport numpy as cnp +from libc.math cimport exp, pow -@cython.boundscheck(False) -def add(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, - int channel, int amount): +def add(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, + Py_ssize_t channel, Py_ssize_t amount): """Add a given amount to a colour channel of `stateimg`, and store the result in `img`. Overflow is clipped. @@ -38,38 +35,37 @@ def add(np.ndarray[np.uint8_t, ndim=3] img, Value to add. """ - cdef int height = img.shape[0] - cdef int width = img.shape[1] - cdef int k = channel - cdef int n = amount + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] + cdef Py_ssize_t k = channel + cdef Py_ssize_t n = amount - cdef np.int16_t op_result + cdef cnp.int16_t op_result - cdef np.uint8_t lut[256] + cdef cnp.uint8_t lut[256] - cdef int i, j, l + cdef Py_ssize_t i, j, l with nogil: for l from 0 <= l < 256: - op_result = (l + n) + op_result = (l + n) if op_result > 255: op_result = 255 elif op_result < 0: op_result = 0 else: pass - lut[l] = op_result + lut[l] = op_result for i from 0 <= i < height: for j from 0 <= j < width: img[i, j, k] = lut[stateimg[i,j,k]] -@cython.boundscheck(False) -def multiply(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, - int channel, float amount): +def multiply(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, + Py_ssize_t channel, float amount): """Multiply a colour channel of `stateimg` by a certain amount, and store the result in `img`. Overflow is clipped. @@ -85,16 +81,16 @@ def multiply(np.ndarray[np.uint8_t, ndim=3] img, Multiplication factor. """ - cdef int height = img.shape[0] - cdef int width = img.shape[1] - cdef int k = channel + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] + cdef Py_ssize_t k = channel cdef float n = amount cdef float op_result - cdef np.uint8_t lut[256] + cdef cnp.uint8_t lut[256] - cdef int i, j, l + cdef Py_ssize_t i, j, l with nogil: @@ -106,17 +102,16 @@ def multiply(np.ndarray[np.uint8_t, ndim=3] img, op_result = 0 else: pass - lut[l] = op_result + lut[l] = op_result for i from 0 <= i < height: for j from 0 <= j < width: img[i,j,k] = lut[stateimg[i,j,k]] -@cython.boundscheck(False) -def brightness(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, - float factor, int offset): +def brightness(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, + float factor, Py_ssize_t offset): """Modify the brightness of an image. 'factor' is multiplied to all channels, which are then added by 'amount'. Overflow is clipped. @@ -134,13 +129,13 @@ def brightness(np.ndarray[np.uint8_t, ndim=3] img, """ - cdef int height = img.shape[0] - cdef int width = img.shape[1] + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] cdef float op_result - cdef np.uint8_t lut[256] + cdef cnp.uint8_t lut[256] - cdef int i, j, k + cdef Py_ssize_t i, j, k with nogil: for k from 0 <= k < 256: @@ -151,7 +146,7 @@ def brightness(np.ndarray[np.uint8_t, ndim=3] img, op_result = 0 else: pass - lut[k] = op_result + lut[k] = op_result for i from 0 <= i < height: for j from 0 <= j < width: @@ -160,27 +155,25 @@ def brightness(np.ndarray[np.uint8_t, ndim=3] img, img[i,j,2] = lut[stateimg[i,j,2]] -@cython.boundscheck(False) -@cython.cdivision(True) -def sigmoid_gamma(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, +def sigmoid_gamma(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, float alpha, float beta): - cdef int height = img.shape[0] - cdef int width = img.shape[1] + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] - cdef int i, j, k + cdef Py_ssize_t i, j, k cdef float c1 = 1 / (1 + exp(beta)) cdef float c2 = 1 / (1 + exp(beta - alpha)) - c1 - cdef np.uint8_t lut[256] + cdef cnp.uint8_t lut[256] with nogil: # compute the lut for k from 0 <= k < 256: - lut[k] = (((1 / (1 + exp(beta - (k / 255.) * alpha))) + lut[k] = (((1 / (1 + exp(beta - (k / 255.) * alpha))) - c1) * 255 / c2) for i from 0 <= i < height: for j from 0 <= j < width: @@ -189,18 +182,16 @@ 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, +def gamma(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, float gamma): - cdef int height = img.shape[0] - cdef int width = img.shape[1] + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] - cdef np.uint8_t lut[256] + cdef cnp.uint8_t lut[256] - cdef int i, j, k + cdef Py_ssize_t i, j, k if gamma == 0: gamma = 0.00000000000000000001 @@ -210,7 +201,7 @@ def gamma(np.ndarray[np.uint8_t, ndim=3] img, # compute the lut for k from 0 <= k < 256: - lut[k] = ((pow((k / 255.), gamma) * 255)) + lut[k] = ((pow((k / 255.), gamma) * 255)) for i from 0 <= i < height: for j from 0 <= j < width: @@ -219,8 +210,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 R = RGB[0] @@ -283,11 +272,11 @@ 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 cdef float f, p, q, t, r, g, b - cdef int hi + cdef Py_ssize_t hi H = HSV[0] S = HSV[1] @@ -388,6 +377,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. @@ -427,9 +417,8 @@ def py_rgb_2_hsv(R, G, B): return (H, S, V) -@cython.boundscheck(False) -def hsv_add(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, +def hsv_add(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, float h_amt, float s_amt, float v_amt): """Modify the image color by specifying additive HSV Values. @@ -460,13 +449,13 @@ def hsv_add(np.ndarray[np.uint8_t, ndim=3] img, """ - cdef int height = img.shape[0] - cdef int width = img.shape[1] + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] cdef float HSV[3] cdef float RGB[3] - cdef int i, j + cdef Py_ssize_t i, j with nogil: for i from 0 <= i < height: @@ -488,14 +477,13 @@ def hsv_add(np.ndarray[np.uint8_t, ndim=3] img, RGB[1] *= 255 RGB[2] *= 255 - img[i, j, 0] = RGB[0] - img[i, j, 1] = RGB[1] - img[i, j, 2] = RGB[2] + img[i, j, 0] = RGB[0] + img[i, j, 1] = RGB[1] + img[i, j, 2] = RGB[2] -@cython.boundscheck(False) -def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, +def hsv_multiply(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, float h_amt, float s_amt, float v_amt): """Modify the image color by specifying multiplicative HSV Values. @@ -530,13 +518,13 @@ def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img, """ - cdef int height = img.shape[0] - cdef int width = img.shape[1] + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] cdef float HSV[3] cdef float RGB[3] - cdef int i, j + cdef Py_ssize_t i, j with nogil: for i from 0 <= i < height: @@ -558,12 +546,6 @@ def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img, RGB[1] *= 255 RGB[2] *= 255 - img[i, j, 0] = RGB[0] - img[i, j, 1] = RGB[1] - img[i, j, 2] = RGB[2] - - - - - - + img[i, j, 0] = RGB[0] + img[i, j, 1] = RGB[1] + img[i, j, 2] = RGB[2] diff --git a/skimage/io/_plugins/_histograms.pyx b/skimage/io/_plugins/_histograms.pyx index 41ec79a1..1f4344d1 100644 --- a/skimage/io/_plugins/_histograms.pyx +++ b/skimage/io/_plugins/_histograms.pyx @@ -1,7 +1,10 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np -import cython +cimport numpy as cnp cdef inline float tri_max(float a, float b, float c): @@ -18,8 +21,7 @@ cdef inline float tri_max(float a, float b, float c): return c -@cython.boundscheck(False) -def histograms(np.ndarray[np.uint8_t, ndim=3] img, int nbins): +def histograms(cnp.ndarray[cnp.uint8_t, ndim=3] img, int nbins): '''Calculate the channel histograms of the current image. Parameters @@ -39,10 +41,7 @@ def histograms(np.ndarray[np.uint8_t, ndim=3] img, int nbins): ''' cdef int width = img.shape[1] cdef int height = img.shape[0] - cdef np.ndarray[np.int32_t, ndim=1] r - cdef np.ndarray[np.int32_t, ndim=1] g - cdef np.ndarray[np.int32_t, ndim=1] b - cdef np.ndarray[np.int32_t, ndim=1] v + cdef cnp.ndarray[cnp.int32_t, ndim=1] r, g, b, v r = np.zeros((nbins,), dtype=np.int32) g = np.zeros((nbins,), dtype=np.int32) 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 9cb9c467..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 @@ -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,19 +607,20 @@ 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: @@ -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/imread_plugin.ini b/skimage/io/_plugins/imread_plugin.ini new file mode 100644 index 00000000..a6a5ddb1 --- /dev/null +++ b/skimage/io/_plugins/imread_plugin.ini @@ -0,0 +1,3 @@ +[imread] +description = Image reading and writing via imread +provides = imread, imsave diff --git a/skimage/io/_plugins/imread_plugin.py b/skimage/io/_plugins/imread_plugin.py new file mode 100644 index 00000000..323fbec8 --- /dev/null +++ b/skimage/io/_plugins/imread_plugin.py @@ -0,0 +1,44 @@ +__all__ = ['imread', 'imsave'] + +import numpy as np +from skimage.utils.dtype import convert + +try: + import imread as _imread +except ImportError: + raise ImportError("Imread could not be found" + "Please refer to http://pypi.python.org/pypi/imread/ " + "for further instructions.") + +def imread(fname, dtype=None): + """Load an image from file. + + Parameters + ---------- + fname : str + Name of input file + + """ + im = _imread.imread(fname) + if dtype is not None: + im = convert(im, dtype) + return im + +def imsave(fname, arr, format_str=None): + """Save an image to disk. + + Parameters + ---------- + fname : str + Name of destination file. + arr : ndarray of uint8 or uint16 + Array (image) to save. + format_str: str,optional + Format to save as. + + Notes + ----- + Currently, only 8-bit precision is supported. + """ + return _imread.imsave(fname, arr, formatstr=format_str) + 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/simpleitk_plugin.ini b/skimage/io/_plugins/simpleitk_plugin.ini new file mode 100644 index 00000000..75a6d995 --- /dev/null +++ b/skimage/io/_plugins/simpleitk_plugin.ini @@ -0,0 +1,3 @@ +[simpleitk] +description = Image reading and writing via SimpleITK +provides = imread, imsave diff --git a/skimage/io/_plugins/simpleitk_plugin.py b/skimage/io/_plugins/simpleitk_plugin.py new file mode 100644 index 00000000..90f7cbc6 --- /dev/null +++ b/skimage/io/_plugins/simpleitk_plugin.py @@ -0,0 +1,21 @@ +__all__ = ['imread', 'imsave'] + +try: + import SimpleITK as sitk +except ImportError: + raise ImportError("SimpleITK could not be found. " + "Please try " + " easy_install SimpleITK " + "or refer to " + " http://simpleitk.org/ " + "for further instructions.") + + +def imread(fname): + sitk_img = sitk.ReadImage(fname) + return sitk.GetArrayFromImage(sitk_img) + + +def imsave(fname, arr): + sitk_img = sitk.GetImageFromArray(arr, isVector=True) + sitk.WriteImage(sitk_img, fname) 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_imread.py b/skimage/io/tests/test_imread.py new file mode 100644 index 00000000..afb9eac9 --- /dev/null +++ b/skimage/io/tests/test_imread.py @@ -0,0 +1,73 @@ +import os.path +import numpy as np +from numpy.testing import * +from numpy.testing.decorators import skipif + +from tempfile import NamedTemporaryFile + +from skimage import data_dir +from skimage.io import imread, imsave, use_plugin, reset_plugins + +try: + import imread as _imread + use_plugin('imread') +except ImportError: + imread_available = False +else: + imread_available = True + + +def teardown(): + reset_plugins() + + +@skipif(not imread_available) +def test_imread_flatten(): + # a color image is flattened + img = imread(os.path.join(data_dir, 'color.png'), flatten=True) + assert img.ndim == 2 + assert img.dtype == np.float64 + img = imread(os.path.join(data_dir, 'camera.png'), flatten=True) + # check that flattening does not occur for an image that is grey already. + assert np.sctype2char(img.dtype) in np.typecodes['AllInteger'] + + +@skipif(not imread_available) +def test_imread_palette(): + img = imread(os.path.join(data_dir, 'palette_color.png')) + assert img.ndim == 3 + + +@skipif(not imread_available) +def test_bilevel(): + expected = np.zeros((10, 10), bool) + expected[::2] = 1 + + img = imread(os.path.join(data_dir, 'checker_bilevel.png')) + assert_array_equal(img, expected) + + +class TestSave: + def roundtrip(self, x, scaling=1): + f = NamedTemporaryFile(suffix='.png') + fname = f.name + f.close() + imsave(fname, x) + y = imread(fname) + + assert_array_almost_equal((x * scaling).astype(np.int32), y) + + @skipif(not imread_available) + def test_imsave_roundtrip(self): + dtype = np.uint8 + for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: + x = np.ones(shape, dtype=dtype) * np.random.random(shape) + + if np.issubdtype(dtype, float): + yield self.roundtrip, x, 255 + else: + x = (x * 255).astype(dtype) + yield self.roundtrip, x + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index ba05d2d3..484784ee 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -1,7 +1,11 @@ +import os + from numpy.testing import * import numpy as np import skimage.io as io +from skimage import data_dir + def test_stack_basic(): x = np.arange(12).reshape(3, 4) @@ -9,9 +13,20 @@ def test_stack_basic(): assert_array_equal(io.pop(), x) + @raises(ValueError) def test_stack_non_array(): io.push([[1, 2, 3]]) + +def test_imread_url(): + # tweak data path so that file URI works on both unix and windows. + data_path = data_dir.lstrip(os.path.sep) + data_path = data_path.replace(os.path.sep, '/') + image_url = 'file:///{0}/camera.png'.format(data_path) + image = io.imread(image_url) + assert image.shape == (512, 512) + + if __name__ == "__main__": run_module_suite() 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/tests/test_simpleitk.py b/skimage/io/tests/test_simpleitk.py new file mode 100644 index 00000000..4bb2cc23 --- /dev/null +++ b/skimage/io/tests/test_simpleitk.py @@ -0,0 +1,93 @@ +import os.path +import numpy as np +from numpy.testing import * +from numpy.testing.decorators import skipif + +from tempfile import NamedTemporaryFile + +from skimage import data_dir +from skimage.io import imread, imsave, use_plugin, reset_plugins + +try: + import SimpleITK as sitk + use_plugin('simpleitk') +except ImportError: + sitk_available = False +else: + sitk_available = True + + +def teardown(): + reset_plugins() + + +def setup_module(self): + """The effect of the `plugin.use` call may be overridden by later imports. + Call `use_plugin` directly before the tests to ensure that sitk is used. + + """ + try: + use_plugin('simpleitk') + except ImportError: + pass + + +@skipif(not sitk_available) +def test_imread_flatten(): + # a color image is flattened + img = imread(os.path.join(data_dir, 'color.png'), flatten=True) + assert img.ndim == 2 + assert img.dtype == np.float64 + img = imread(os.path.join(data_dir, 'camera.png'), flatten=True) + # check that flattening does not occur for an image that is grey already. + assert np.sctype2char(img.dtype) in np.typecodes['AllInteger'] + + +@skipif(not sitk_available) +def test_bilevel(): + expected = np.zeros((10, 10)) + expected[::2] = 255 + + img = imread(os.path.join(data_dir, 'checker_bilevel.png')) + assert_array_equal(img, expected) + + +@skipif(not sitk_available) +def test_imread_uint16(): + expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) + img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16.tif')) + assert np.issubdtype(img.dtype, np.uint16) + assert_array_almost_equal(img, expected) + + +@skipif(not sitk_available) +def test_imread_uint16_big_endian(): + expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) + img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16B.tif')) + assert_array_almost_equal(img, expected) + + +class TestSave: + def roundtrip(self, dtype, x): + f = NamedTemporaryFile(suffix='.mha') + fname = f.name + f.close() + imsave(fname, x) + y = imread(fname) + + assert_array_almost_equal(x, y) + + @skipif(not sitk_available) + def test_imsave_roundtrip(self): + for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: + for dtype in (np.uint8, np.uint16, np.float32, np.float64): + x = np.ones(shape, dtype=dtype) * np.random.random(shape) + + if np.issubdtype(dtype, float): + yield self.roundtrip, dtype, x + else: + x = (x * 255).astype(dtype) + yield self.roundtrip, dtype, x + +if __name__ == "__main__": + run_module_suite() 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..d05d9aa7 100644 --- a/skimage/measure/_find_contours.pyx +++ b/skimage/measure/_find_contours.pyx @@ -1,20 +1,21 @@ -# -*- python -*- -# cython: cdivision=True - +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np -np.import_array() +cimport numpy as cnp -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, - double level, int vertex_connect_high): +def iterate_and_store(cnp.ndarray[double, ndim=2] array, + double level, Py_ssize_t vertex_connect_high): """Iterate across the given array in a marching-squares fashion, looking for segments that cross 'level'. If such a segment is found, its coordinates are added to a growing list of segments, @@ -27,7 +28,7 @@ def iterate_and_store(np.ndarray[double, ndim=2, mode='c'] array, raise ValueError("Input array must be at least 2x2.") cdef list arc_list = [] - cdef int n + cdef Py_ssize_t n # The plan is to iterate a 2x2 square across the input array. This means # that the upper-left corner of the square needs to iterate across a @@ -39,17 +40,18 @@ def iterate_and_store(np.ndarray[double, ndim=2, mode='c'] array, # index varies the fastest). # Current coords start at 0,0. - cdef int[2] coords + cdef Py_ssize_t[2] coords coords[0] = 0 coords[1] = 0 # Calculate the number of iterations we'll need - cdef int num_square_steps = (array.shape[0] - 1) * (array.shape[1] - 1) + cdef Py_ssize_t num_square_steps = (array.shape[0] - 1) \ + * (array.shape[1] - 1) cdef unsigned char square_case = 0 cdef tuple top, bottom, left, right cdef double ul, ur, ll, lr - cdef int r0, r1, c0, c1 + cdef Py_ssize_t r0, r1, c0, c1 for n in range(num_square_steps): # There are sixteen different possible square types, diagramed below. @@ -92,7 +94,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 +105,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/_moments.pyx b/skimage/measure/_moments.pyx index f84e14dd..145f6052 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -1,14 +1,16 @@ -#cython: boundscheck=False -#cython: wraparound=False #cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np + +cimport numpy as cnp -def central_moments(np.ndarray[np.double_t, ndim=2] array, double cr, double cc, - int order): - cdef int p, q, r, c - cdef np.ndarray[np.double_t, ndim=2] mu +def central_moments(cnp.ndarray[cnp.double_t, ndim=2] array, double cr, + double cc, int order): + cdef Py_ssize_t p, q, r, c + cdef cnp.ndarray[cnp.double_t, ndim=2] mu mu = np.zeros((order + 1, order + 1), 'double') for p in range(order + 1): for q in range(order + 1): @@ -17,9 +19,10 @@ def central_moments(np.ndarray[np.double_t, ndim=2] array, double cr, double cc, mu[p,q] += array[r,c] * (r - cr) ** q * (c - cc) ** p return mu -def normalized_moments(np.ndarray[np.double_t, ndim=2] mu, int order): - cdef int p, q - cdef np.ndarray[np.double_t, ndim=2] nu + +def normalized_moments(cnp.ndarray[cnp.double_t, ndim=2] mu, int order): + cdef Py_ssize_t p, q + cdef cnp.ndarray[cnp.double_t, ndim=2] nu nu = np.zeros((order + 1, order + 1), 'double') for p in range(order + 1): for q in range(order + 1): @@ -29,8 +32,9 @@ def normalized_moments(np.ndarray[np.double_t, ndim=2] mu, int order): nu[p,q] = np.nan return nu -def hu_moments(np.ndarray[np.double_t, ndim=2] nu): - cdef np.ndarray[np.double_t, ndim=1] hu = np.zeros((7,), 'double') + +def hu_moments(cnp.ndarray[cnp.double_t, ndim=2] nu): + cdef cnp.ndarray[cnp.double_t, ndim=1] hu = np.zeros((7,), 'double') cdef double t0 = nu[3,0] + nu[1,2] cdef double t1 = nu[2,1] + nu[0,3] cdef double q0 = t0 * t0 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..72d1e2f4 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,51 @@ 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_value=0) + 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]]), + mode='constant', cval=0) + 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..8185bf67 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, 55.2487373415) + + per = perimeter(SAMPLE.astype('double'), neighbourhood=8) + assert_almost_equal(per, 46.8284271247) + 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..ec5ce7ef 100644 --- a/skimage/measure/tests/test_structural_similarity.py +++ b/skimage/measure/tests/test_structural_similarity.py @@ -1,8 +1,11 @@ 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 + + +np.random.seed(1234) + def test_ssim_patch_range(): N = 51 @@ -12,6 +15,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 +27,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 -## -## 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) +# 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 grad(Y): -## return ssim(X, Y, dynamic_range=255, gradient=True)[1] + f = ssim(X, Y, dynamic_range=255) + g = ssim(X, Y, dynamic_range=255, gradient=True) + + 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 +58,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..044fcf89 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -1,6 +1,10 @@ +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 +from .misc import remove_small_objects diff --git a/skimage/morphology/_convex_hull.pyx b/skimage/morphology/_convex_hull.pyx index dc426900..e4b6470c 100644 --- a/skimage/morphology/_convex_hull.pyx +++ b/skimage/morphology/_convex_hull.pyx @@ -1,9 +1,13 @@ -# -*- python -*- - -cimport numpy as np +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -def possible_hull(np.ndarray[dtype=np.uint8_t, ndim=2, mode="c"] img): +cimport numpy as cnp + + +def possible_hull(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] img): """Return positions of pixels that possibly belong to the convex hull. Parameters @@ -13,47 +17,44 @@ def possible_hull(np.ndarray[dtype=np.uint8_t, ndim=2, mode="c"] img): Returns ------- - coords : ndarray (N, 2) + coords : ndarray (cols, 2) The ``(row, column)`` coordinates of all pixels that possibly belong to the convex hull. """ - cdef int i, j, k - cdef unsigned int M, N - - M = img.shape[0] - N = img.shape[1] + cdef Py_ssize_t r, c + cdef Py_ssize_t rows = img.shape[0] + cdef Py_ssize_t cols = img.shape[1] - # Output: M storage slots for left boundary pixels - # N storage slots for top boundary pixels - # M storage slots for right boundary pixels - # N storage slots for bottom boundary pixels - cdef np.ndarray[dtype=np.int_t, ndim=2] nonzero = \ - np.ones((2 * (M + N), 2), dtype=np.int) - nonzero *= -1 + # Output: rows storage slots for left boundary pixels + # cols storage slots for top boundary pixels + # rows storage slots for right boundary pixels + # cols storage slots for bottom boundary pixels + cdef cnp.ndarray[dtype=cnp.intp_t, ndim=2] nonzero = \ + np.ones((2 * (rows + cols), 2), dtype=np.int) + nonzero *= -1 - k = 0 - for i in range(M): - for j in range(N): - if img[i, j] != 0: + for r in range(rows): + for c in range(cols): + if img[r, c] != 0: # Left check - if nonzero[i, 1] == -1: - nonzero[i, 0] = i - nonzero[i, 1] = j + if nonzero[r, 1] == -1: + nonzero[r, 0] = r + nonzero[r, 1] = c # Right check - elif nonzero[M + N + i, 1] < j: - nonzero[M + N + i, 0] = i - nonzero[M + N + i, 1] = j + elif nonzero[rows + cols + r, 1] < c: + nonzero[rows + cols + r, 0] = r + nonzero[rows + cols + r, 1] = c # Top check - if nonzero[M + j, 1] == -1: - nonzero[M + j, 0] = i - nonzero[M + j, 1] = j + if nonzero[rows + c, 1] == -1: + nonzero[rows + c, 0] = r + nonzero[rows + c, 1] = c # Bottom check - elif nonzero[2 * M + N + j, 0] < i: - nonzero[2 * M + N + j, 0] = i - nonzero[2 * M + N + j, 1] = j - + elif nonzero[2 * rows + cols + c, 0] < r: + nonzero[2 * rows + cols + c, 0] = r + nonzero[2 * rows + cols + c, 1] = c + return nonzero[nonzero[:, 0] != -1] 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 cnp.ndarray[cnp.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 cnp.ndarray[cnp.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 89% rename from skimage/morphology/skeletonize.py rename to skimage/morphology/_skeletonize.py index e734d8c6..b48beb86 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,38 +250,37 @@ 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() distance = distance[result] - i = np.ascontiguousarray(i[result], np.int32) - j = np.ascontiguousarray(j[result], np.int32) + i = np.ascontiguousarray(i[result], np.intp) + j = np.ascontiguousarray(j[result], np.intp) 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 80% rename from skimage/morphology/_skeletonize.pyx rename to skimage/morphology/_skeletonize_cy.pyx index ff5fcdf2..13e303d4 100644 --- a/skimage/morphology/_skeletonize.pyx +++ b/skimage/morphology/_skeletonize_cy.pyx @@ -1,3 +1,8 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + ''' Originally part of CellProfiler, code licensed under both GPL and BSD licenses. Website: http://www.cellprofiler.org @@ -10,21 +15,20 @@ Original author: Lee Kamentsky ''' import numpy as np -cimport numpy as np -cimport cython + +cimport numpy as cnp -@cython.boundscheck(False) -def _skeletonize_loop(np.ndarray[dtype=np.uint8_t, ndim=2, - negative_indices=False, mode='c'] result, - np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices=False, mode='c'] i, - np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices=False, mode='c'] j, - np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices=False, mode='c'] order, - np.ndarray[dtype=np.uint8_t, ndim=1, - negative_indices=False, mode='c'] table): +def _skeletonize_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] result, + cnp.ndarray[dtype=cnp.intp_t, ndim=1, + negative_indices=False, mode='c'] i, + cnp.ndarray[dtype=cnp.intp_t, ndim=1, + negative_indices=False, mode='c'] j, + cnp.ndarray[dtype=cnp.int32_t, ndim=1, + negative_indices=False, mode='c'] order, + cnp.ndarray[dtype=cnp.uint8_t, ndim=1, + negative_indices=False, mode='c'] table): """ Inner loop of skeletonize function @@ -37,13 +41,13 @@ def _skeletonize_loop(np.ndarray[dtype=np.uint8_t, ndim=2, i, j : ndarrays The coordinates of each foreground pixel in the image - + order : ndarray The index of each pixel, in the order of processing (order[0] is the first pixel to process, etc.) - + table : ndarray - The 512-element lookup table of values after transformation + The 512-element lookup table of values after transformation (whether to keep or not each configuration in a binary 3x3 array) Notes @@ -55,15 +59,15 @@ def _skeletonize_loop(np.ndarray[dtype=np.uint8_t, ndim=2, the quench-line of the brushfire will be evaluated later than a point closer to the edge. - Note that the neighbourhood of a pixel may evolve before the loop - arrives at this pixel. This is why it is possible to compute the + Note that the neighbourhood of a pixel may evolve before the loop + arrives at this pixel. This is why it is possible to compute the skeleton in only one pass, thanks to an adapted ordering of the pixels. """ cdef: - np.int32_t accumulator - np.int32_t index, order_index - np.int32_t ii, jj + cnp.int32_t accumulator + Py_ssize_t index, order_index + Py_ssize_t ii, jj for index in range(order.shape[0]): accumulator = 16 @@ -92,9 +96,10 @@ def _skeletonize_loop(np.ndarray[dtype=np.uint8_t, ndim=2, # Assign the value of table corresponding to the configuration result[ii, jj] = table[accumulator] -@cython.boundscheck(False) -def _table_lookup_index(np.ndarray[dtype=np.uint8_t, ndim=2, - negative_indices=False, mode='c'] image): + + +def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] image): """ Return an index into a table per pixel of a binary image @@ -110,27 +115,27 @@ def _table_lookup_index(np.ndarray[dtype=np.uint8_t, ndim=2, 256 128 64 32 16 8 4 2 1 - + but this runs about twice as fast because of inlining and the hardwired kernel. """ cdef: - np.ndarray[dtype=np.int32_t, ndim=2, - negative_indices=False, mode='c'] indexer - np.int32_t *p_indexer - np.uint8_t *p_image - np.int32_t i_stride - np.int32_t i_shape - np.int32_t j_shape - np.int32_t i - np.int32_t j - np.int32_t offset + cnp.ndarray[dtype=cnp.int32_t, ndim=2, + negative_indices=False, mode='c'] indexer + cnp.int32_t *p_indexer + cnp.uint8_t *p_image + Py_ssize_t i_stride + Py_ssize_t i_shape + Py_ssize_t j_shape + Py_ssize_t i + Py_ssize_t j + Py_ssize_t offset i_shape = image.shape[0] j_shape = image.shape[1] indexer = np.zeros((i_shape, j_shape), np.int32) - p_indexer = indexer.data - p_image = image.data + p_indexer = indexer.data + p_image = image.data i_stride = image.strides[0] assert i_shape >= 3 and j_shape >= 3, \ "Please use the slow method for arrays < 3x3" diff --git a/skimage/morphology/_watershed.pyx b/skimage/morphology/_watershed.pyx index c86d8744..122f0262 100644 --- a/skimage/morphology/_watershed.pyx +++ b/skimage/morphology/_watershed.pyx @@ -9,39 +9,33 @@ All rights reserved. Original author: Lee Kamentsky """ - -cdef extern from "numpy/arrayobject.h": - cdef void import_array() -import_array() - import numpy as np cimport numpy as np cimport cython -DTYPE_INT32 = np.int32 + ctypedef np.int32_t DTYPE_INT32_t DTYPE_BOOL = np.bool ctypedef np.int8_t DTYPE_BOOL_t + include "heap_watershed.pxi" + @cython.boundscheck(False) -def watershed(np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, - mode='c'] image, - np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, - mode='c'] pq, - DTYPE_INT32_t age, - np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, - mode='c'] structure, - DTYPE_INT32_t ndim, - np.ndarray[DTYPE_BOOL_t, ndim=1, negative_indices=False, - mode='c'] mask, - np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, - mode='c'] image_shape, - np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, - mode='c'] output): +def watershed(np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, + mode='c'] image, + np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, + mode='c'] pq, + Py_ssize_t age, + np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, + mode='c'] structure, + np.ndarray[DTYPE_BOOL_t, ndim=1, negative_indices=False, + mode='c'] mask, + np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, + mode='c'] output): """Do heavy lifting of watershed algorithm - + Parameters ---------- @@ -58,20 +52,17 @@ def watershed(np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, in a flattened array. The remaining elements are the offsets from the point to its neighbor in the various dimensions - ndim - # of dimensions in the image mask - numpy boolean (char) array indicating which pixels to consider and which to ignore. Also flattened. - image_shape - the dimensions of the image, for boundary checking, - a numpy array of np.int32 output - put the image labels in here """ cdef Heapitem elem cdef Heapitem new_elem - cdef DTYPE_INT32_t nneighbors = structure.shape[0] - cdef DTYPE_INT32_t i = 0 - cdef DTYPE_INT32_t index = 0 - cdef DTYPE_INT32_t old_index = 0 - cdef DTYPE_INT32_t max_index = image.shape[0] + cdef Py_ssize_t nneighbors = structure.shape[0] + cdef Py_ssize_t i = 0 + cdef Py_ssize_t index = 0 + cdef Py_ssize_t old_index = 0 + cdef Py_ssize_t max_index = image.shape[0] cdef Heap *hp = heap_from_numpy2() 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..569921fa --- /dev/null +++ b/skimage/morphology/ccomp.pxd @@ -0,0 +1,10 @@ +"""Export fast union find in Cython""" +cimport numpy as cnp + +DTYPE = cnp.intp +ctypedef cnp.intp_t DTYPE_t + +cdef DTYPE_t find_root(DTYPE_t *forest, DTYPE_t n) +cdef set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root) +cdef join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m) +cdef link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node) diff --git a/skimage/morphology/ccomp.pyx b/skimage/morphology/ccomp.pyx index a1d5f303..1db78a6d 100644 --- a/skimage/morphology/ccomp.pyx +++ b/skimage/morphology/ccomp.pyx @@ -1,8 +1,11 @@ -# -*- python -*- #cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np + +cimport numpy as cnp """ See also: @@ -23,24 +26,25 @@ See also: # Tree operations implemented by an array as described in Wu et al. # The term "forest" is used to indicate an array that stores one or more trees -DTYPE = np.int -ctypedef np.int_t DTYPE_t +DTYPE = np.intp -cdef DTYPE_t find_root(np.int_t *forest, np.int_t n): + +cdef DTYPE_t find_root(DTYPE_t *forest, DTYPE_t n): """Find the root of node n. """ - cdef np.int_t root = n + cdef DTYPE_t root = n while (forest[root] < root): root = forest[root] return root -cdef set_root(np.int_t *forest, np.int_t n, np.int_t root): + +cdef set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root): """ Set all nodes on a path to point to new_root. """ - cdef np.int_t j + cdef DTYPE_t j while (forest[n] < n): j = forest[n] forest[n] = root @@ -49,12 +53,12 @@ cdef set_root(np.int_t *forest, np.int_t n, np.int_t root): forest[n] = root -cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m): +cdef join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m): """Join two trees containing nodes n and m. """ - cdef np.int_t root = find_root(forest, n) - cdef np.int_t root_m + cdef DTYPE_t root = find_root(forest, n) + cdef DTYPE_t root_m if (n != m): root_m = find_root(forest, m) @@ -65,7 +69,8 @@ cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m): set_root(forest, n, root) set_root(forest, m, root) -cdef link_bg(np.int_t *forest, np.int_t n, np.int_t *background_node): + +cdef link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node): """ Link a node to the background node. @@ -77,8 +82,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, DTYPE_t neighbors=8, DTYPE_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 +93,7 @@ def label(np.ndarray[DTYPE_t, ndim=2] input, [ ] [ ] [ ] [ ] | \ | / [ ]--[ ]--[ ] [ ]--[ ]--[ ] - | / | \ + | / | \ [ ] [ ] [ ] [ ] Parameters @@ -136,20 +140,21 @@ def label(np.ndarray[DTYPE_t, ndim=2] input, [-1 -1 -1]] """ - cdef np.int_t rows = input.shape[0] - cdef np.int_t cols = input.shape[1] + cdef DTYPE_t rows = input.shape[0] + cdef DTYPE_t cols = input.shape[1] - cdef np.ndarray[DTYPE_t, ndim=2] data = input.copy() - cdef np.ndarray[DTYPE_t, ndim=2] forest + cdef cnp.ndarray[DTYPE_t, ndim=2] data = np.array(input, copy=True, + dtype=DTYPE) + cdef cnp.ndarray[DTYPE_t, ndim=2] forest forest = np.arange(data.size, dtype=DTYPE).reshape((rows, cols)) - cdef np.int_t *forest_p = forest.data - cdef np.int_t *data_p = data.data + cdef DTYPE_t *forest_p = forest.data + cdef DTYPE_t *data_p = data.data - cdef np.int_t i, j + cdef DTYPE_t i, j - cdef np.int_t background_node = -999 + cdef DTYPE_t background_node = -999 if neighbors != 4 and neighbors != 8: raise ValueError('Neighbors must be either 4 or 8.') @@ -198,7 +203,7 @@ def label(np.ndarray[DTYPE_t, ndim=2] input, # Label output - cdef np.int_t ctr = 0 + cdef DTYPE_t ctr = 0 for i in range(rows): for j in range(cols): if (i*cols + j) == background_node: @@ -209,4 +214,8 @@ def label(np.ndarray[DTYPE_t, ndim=2] input, else: data[i, j] = data_p[forest[i, j]] - return data + # Work around a bug in ndimage's type checking on 32-bit platforms + if data.dtype == np.int32: + return data.view(np.int32) + else: + return data diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 6870b500..a09a39a3 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 Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + cdef Py_ssize_t srows = selem.shape[0] + cdef Py_ssize_t 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 Py_ssize_t centre_r = int(selem.shape[0] / 2) - shift_y + cdef Py_ssize_t 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 Py_ssize_t r, c, rr, cc, s, value, local_max - cdef int sw = selem.shape[0], sh = selem.shape[1] + cdef Py_ssize_t selem_num = np.sum(selem != 0) + cdef Py_ssize_t* sr = malloc(selem_num * sizeof(Py_ssize_t)) + cdef Py_ssize_t* sc = malloc(selem_num * sizeof(Py_ssize_t)) - 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 Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + cdef Py_ssize_t srows = selem.shape[0] + cdef Py_ssize_t scols = selem.shape[1] + + cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) - shift_y + cdef Py_ssize_t 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 Py_ssize_t selem_num = np.sum(selem != 0) + cdef Py_ssize_t* sr = malloc(selem_num * sizeof(Py_ssize_t)) + cdef Py_ssize_t* sc = malloc(selem_num * sizeof(Py_ssize_t)) - 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/heap_general.pxi b/skimage/morphology/heap_general.pxi index a113b98e..67b21fa6 100644 --- a/skimage/morphology/heap_general.pxi +++ b/skimage/morphology/heap_general.pxi @@ -10,21 +10,18 @@ All rights reserved. Original author: Lee Kamentsky """ -cdef extern from "stdlib.h": - ctypedef unsigned long size_t - void free(void *ptr) - void *malloc(size_t size) - void *realloc(void *ptr, size_t size) +from libc.stdlib cimport free, malloc, realloc + cdef struct Heap: - unsigned int items - unsigned int space + Py_ssize_t items + Py_ssize_t space Heapitem *data Heapitem **ptrs cdef inline Heap *heap_from_numpy2(): - cdef unsigned int k - cdef Heap *heap + cdef Py_ssize_t k + cdef Heap *heap heap = malloc(sizeof (Heap)) heap.items = 0 heap.space = 1000 @@ -39,7 +36,7 @@ cdef inline void heap_done(Heap *heap): free(heap.ptrs) free(heap) -cdef inline void swap(unsigned int a, unsigned int b, Heap *h): +cdef inline void swap(Py_ssize_t a, Py_ssize_t b, Heap *h): h.ptrs[a], h.ptrs[b] = h.ptrs[b], h.ptrs[a] @@ -47,13 +44,13 @@ cdef inline void swap(unsigned int a, unsigned int b, Heap *h): # heappop - inlined # # pop an element off the heap, maintaining heap invariant -# +# # Note: heap ordering is the same as python heapq, i.e., smallest first. ###################################################### -cdef inline void heappop(Heap *heap, - Heapitem *dest): - cdef unsigned int i, smallest, l, r # heap indices - +cdef inline void heappop(Heap *heap, Heapitem *dest): + + cdef Py_ssize_t i, smallest, l, r # heap indices + # # Start by copying the first element to the destination # @@ -76,10 +73,10 @@ cdef inline void heappop(Heap *heap, smallest = i while True: # loop invariant here: smallest == i - + # find smallest of (i, l, r), and swap it to i's position if necessary - l = i*2+1 #__left(i) - r = i*2+2 #__right(i) + l = i * 2 + 1 #__left(i) + r = i * 2 + 2 #__right(i) if l < heap.items: if smaller(heap.ptrs[l], heap.ptrs[i]): smallest = l @@ -88,13 +85,14 @@ cdef inline void heappop(Heap *heap, else: # this is unnecessary, but trims 0.04 out of 0.85 seconds... break - # the element at i is smaller than either of its children, heap invariant restored. + # the element at i is smaller than either of its children, heap + # invariant restored. if smallest == i: break # swap swap(i, smallest, heap) i = smallest - + ################################################## # heappush - inlined # @@ -102,34 +100,36 @@ cdef inline void heappop(Heap *heap, # # Note: heap ordering is the same as python heapq, i.e., smallest first. ################################################## -cdef inline void heappush(Heap *heap, - Heapitem *new_elem): - cdef unsigned int child = heap.items - cdef unsigned int parent - cdef unsigned int k - cdef Heapitem *new_data +cdef inline void heappush(Heap *heap, Heapitem *new_elem): - # grow if necessary - if heap.items == heap.space: + cdef Py_ssize_t child = heap.items + cdef Py_ssize_t parent + cdef Py_ssize_t k + cdef Heapitem *new_data + + # grow if necessary + if heap.items == heap.space: heap.space = heap.space * 2 - new_data = realloc( heap.data, (heap.space * sizeof(Heapitem))) - heap.ptrs = realloc( heap.ptrs, (heap.space * sizeof(Heapitem *))) + new_data = realloc(heap.data, + (heap.space * sizeof(Heapitem))) + heap.ptrs = realloc(heap.ptrs, + (heap.space * sizeof(Heapitem *))) for k in range(heap.items): heap.ptrs[k] = new_data + (heap.ptrs[k] - heap.data) for k in range(heap.items, heap.space): heap.ptrs[k] = new_data + k heap.data = new_data - # insert new data at child - heap.ptrs[child][0] = new_elem[0] - heap.items += 1 + # insert new data at child + heap.ptrs[child][0] = new_elem[0] + heap.items += 1 - # restore heap invariant, all parents <= children - while child>0: - parent = (child + 1) / 2 - 1 # __parent(i) - - if smaller(heap.ptrs[child], heap.ptrs[parent]): - swap(parent, child, heap) - child = parent - else: - break + # restore heap invariant, all parents <= children + while child > 0: + parent = (child + 1) / 2 - 1 # __parent(i) + + if smaller(heap.ptrs[child], heap.ptrs[parent]): + swap(parent, child, heap) + child = parent + else: + break diff --git a/skimage/morphology/heap_watershed.pxi b/skimage/morphology/heap_watershed.pxi index ea66da26..07b29f5c 100644 --- a/skimage/morphology/heap_watershed.pxi +++ b/skimage/morphology/heap_watershed.pxi @@ -9,18 +9,19 @@ All rights reserved. Original author: Lee Kamentsky """ -import numpy as np -cimport numpy as np -cimport cython +cimport numpy as cnp + cdef struct Heapitem: - np.int32_t value - np.int32_t age - np.int32_t index + cnp.int32_t value + cnp.int32_t age + Py_ssize_t index + cdef inline int smaller(Heapitem *a, Heapitem *b): if a.value <> b.value: - return a.value < b.value + return a.value < b.value return a.age < b.age + include "heap_general.pxi" diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py new file mode 100644 index 00000000..6274df53 --- /dev/null +++ b/skimage/morphology/misc.py @@ -0,0 +1,83 @@ +import numpy as np +import scipy.ndimage as nd + + +def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): + """Remove connected components smaller than the specified size. + + Parameters + ---------- + ar : ndarray (arbitrary shape, int or bool type) + The array containing the connected components of interest. If the array + type is int, it is assumed that it contains already-labeled objects. + The ints must be non-negative. + min_size : int, optional (default: 64) + The smallest allowable connected component size. + connectivity : int, {1, 2, ..., ar.ndim}, optional (default: 1) + The connectivity defining the neighborhood of a pixel. + in_place : bool, optional (default: False) + If `True`, remove the connected components in the input array itself. + Otherwise, make a copy. + + Raises + ------ + ValueError + If the input array is of an invalid type, such as float or string. + + Returns + ------- + out : ndarray, same shape and type as input `ar` + The input array with small connected components removed. + + Examples + -------- + >>> from skimage import morphology + >>> from scipy import ndimage as nd + >>> a = np.array([[0, 0, 0, 1, 0], + ... [1, 1, 1, 0, 0], + ... [1, 1, 1, 0, 1]], bool) + >>> b = morphology.remove_small_connected_components(a, 6) + >>> b + array([[False, False, False, False, False], + [ True, True, True, False, False], + [ True, True, True, False, False]], dtype=bool) + >>> c = morphology.remove_small_connected_components(a, 7, connectivity=2) + >>> c + array([[False, False, False, True, False], + [ True, True, True, False, False], + [ True, True, True, False, False]], dtype=bool) + >>> d = morphology.remove_small_connected_components(a, 6, in_place=True) + >>> d is a + True + """ + # Should use `issubdtype` below, but there's a bug in numpy 1.7 + if not (ar.dtype == bool or np.issubdtype(ar.dtype, int)): + raise ValueError("Only bool or integer image types are supported. " + "Got %s." % ar.dtype) + + if in_place: + out = ar + else: + out = ar.copy() + + if min_size == 0: # shortcut for efficiency + return out + + if out.dtype == bool: + selem = nd.generate_binary_structure(ar.ndim, connectivity) + ccs = nd.label(ar, selem)[0] + else: + ccs = out + + try: + component_sizes = np.bincount(ccs.ravel()) + except ValueError: + raise ValueError("Negative value labels are not supported. Try " + "relabeling the input with `scipy.ndimage.label` or " + "`skimage.morphology.label`.") + + too_small = component_sizes < min_size + too_small_mask = too_small[ccs] + out[too_small_mask] = 0 + + return out 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..1936377b 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_misc.py b/skimage/morphology/tests/test_misc.py new file mode 100644 index 00000000..abc718c2 --- /dev/null +++ b/skimage/morphology/tests/test_misc.py @@ -0,0 +1,56 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_equal, assert_raises +from skimage.morphology import remove_small_objects + +test_image = np.array([[0, 0, 0, 1, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 1]], bool) + + +def test_one_connectivity(): + expected = np.array([[0, 0, 0, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0]], bool) + observed = remove_small_objects(test_image, min_size=6) + assert_array_equal(observed, expected) + + +def test_two_connectivity(): + expected = np.array([[0, 0, 0, 1, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0]], bool) + observed = remove_small_objects(test_image, min_size=7, connectivity=2) + assert_array_equal(observed, expected) + + +def test_in_place(): + observed = remove_small_objects(test_image, min_size=6, in_place=True) + assert_equal(observed is test_image, True, + "remove_small_objects in_place argument failed.") + + +def test_labeled_image(): + labeled_image = np.array([[2, 2, 2, 0, 1], + [2, 2, 2, 0, 1], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=int) + expected = np.array([[2, 2, 2, 0, 0], + [2, 2, 2, 0, 0], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=int) + observed = remove_small_objects(labeled_image, min_size=3) + assert_array_equal(observed, expected) + + +def test_float_input(): + float_test = np.random.rand(5, 5) + assert_raises(ValueError, remove_small_objects, float_test) + + +def test_negative_input(): + negative_int = np.random.randint(-4, -1, size=(5, 5)) + assert_raises(ValueError, remove_small_objects, negative_int) + + +if __name__ == "__main__": + np.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..fbd63281 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -24,13 +24,14 @@ 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 .._shared.utils import deprecated from . import _watershed -import warnings + def watershed(image, markers, connectivity=None, offset=None, mask=None): """ @@ -87,8 +88,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 +123,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 +158,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 +189,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 +213,8 @@ 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_mask, - np.array(c_image.shape,np.int32), c_output) c_output = c_output.reshape(c_image.shape)[[slice(1, -1, None)] * image.ndim] @@ -227,21 +224,20 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): return c_output +@deprecated('feature.peak_local_max') 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 @@ -249,6 +245,16 @@ def is_local_maximum(image, labels=None, footprint=None): result: ndarray of bools mask that is True for pixels that are local maxima of `image` + See also + -------- + skimage.feature.peak_local_max: Unified peak finding backend. + The more capable backend for finding local maxima. + + Notes + ----- + This function is now a wrapper for skimage.feature.peak_local_max() and is + retained only for convenience and backward compatibility. + Examples -------- >>> image = np.zeros((4, 4)) @@ -263,7 +269,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,72 +287,19 @@ 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') - """ - if labels is None: - labels = np.ones(image.shape, dtype=np.uint8) - if footprint is 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 - if np.all(footprint_extent == 0): - return labels > 0 - result = (labels > 0).copy() - # - # 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, - labels.dtype) - 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]] - - fp_image_offsets = np.sum(image_strides[:, np.newaxis] * - footprint_offsets[:, footprint], 0) - fp_big_offsets = np.sum(big_strides[:, np.newaxis] * - footprint_offsets[:, footprint], 0) - # - # 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] - image_indexes = np.sum(image_strides[:, np.newaxis] * indexes, 0) - big_indexes = np.sum(big_strides[:, np.newaxis] * - (indexes + footprint_extent[:, np.newaxis]), 0) - result_indexes = np.sum(result_strides[:, np.newaxis] * indexes, 0) - # - # Now operate on the raveled images - # - big_labels_raveled = big_labels.ravel() - image_raveled = image.ravel() - result_raveled = result.ravel() - # - # A hit is a hit if the label at the offset matches the label at the pixel - # and if the intensity at the pixel is greater or equal to the intensity - # at the offset. - # - for fp_image_offset, fp_big_offset in zip(fp_image_offsets, fp_big_offsets): - 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]) - result_raveled[result_indexes[same_label][less_than]] = False - - return result + [False, True, False, True]], dtype=bool) + """ + # call import here to prevent circular imports + from ..feature import peak_local_max + return peak_local_max(image, labels=labels, min_distance=1, + threshold_rel=0, footprint=footprint, + indices=False, exclude_border=False) # ---------------------- 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 +307,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 +334,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 +363,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..a0fab77a 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1 +1,7 @@ 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, mark_boundaries +from ._clear_border import clear_border +from ._join import join_segmentations, relabel_from_one 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..f285e3db --- /dev/null +++ b/skimage/segmentation/_felzenszwalb_cy.pyx @@ -0,0 +1,116 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +import scipy + +cimport cython +cimport numpy as cnp +from skimage.morphology.ccomp cimport find_root, join_trees + +from ..util import img_as_float + + +def _felzenszwalb_grey(image, double scale=1, sigma=0.8, Py_ssize_t 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 cnp.ndarray[cnp.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 cnp.ndarray[cnp.intp_t, ndim=2] segments \ + = np.arange(width * height, dtype=np.intp).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 cnp.ndarray[cnp.intp_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 cnp.intp_t *segments_p = segments.data + cdef cnp.intp_t *edges_p = edges.data + cdef cnp.float_t *costs_p = costs.data + cdef cnp.ndarray[cnp.intp_t, ndim=1] segment_size \ + = np.ones(width * height, dtype=np.int) + # inner cost of segments + cdef cnp.ndarray[cnp.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/_join.py b/skimage/segmentation/_join.py new file mode 100644 index 00000000..454da71e --- /dev/null +++ b/skimage/segmentation/_join.py @@ -0,0 +1,93 @@ +import numpy as np + +def join_segmentations(s1, s2): + """Return the join of the two input segmentations. + + The join J of S1 and S2 is defined as the segmentation in which two voxels + are in the same segment in J if and only if they are in the same segment + in *both* S1 and S2. + + Parameters + ---------- + s1, s2 : numpy arrays + s1 and s2 are label fields of the same shape. + + Returns + ------- + j : numpy array + The join segmentation of s1 and s2. + + Examples + -------- + >>> import numpy as np + >>> from skimage.segmentation import join_segmentations + >>> s1 = np.array([[0, 0, 1, 1], + ... [0, 2, 1, 1], + ... [2, 2, 2, 1]]) + >>> s2 = np.array([[0, 1, 1, 0], + ... [0, 1, 1, 0], + ... [0, 1, 1, 1]]) + >>> join_segmentations(s1, s2) + array([[0, 1, 3, 2], + [0, 5, 3, 2], + [4, 5, 5, 3]]) + """ + if s1.shape != s2.shape: + raise ValueError("Cannot join segmentations of different shape. " + + "s1.shape: %s, s2.shape: %s" % (s1.shape, s2.shape)) + s1 = relabel_from_one(s1)[0] + s2 = relabel_from_one(s2)[0] + j = (s2.max() + 1) * s1 + s2 + j = relabel_from_one(j)[0] + return j + +def relabel_from_one(label_field): + """Convert labels in an arbitrary label field to {1, ... number_of_labels}. + + This function also returns the forward map (mapping the original labels to + the reduced labels) and the inverse map (mapping the reduced labels back + to the original ones). + + Parameters + ---------- + label_field : numpy ndarray (integer type) + + Returns + ------- + relabeled : numpy array of same shape as ar + forward_map : 1d numpy array of length np.unique(ar) + 1 + inverse_map : 1d numpy array of length len(np.unique(ar)) + The length is len(np.unique(ar)) + 1 if 0 is not in np.unique(ar) + + Examples + -------- + >>> import numpy as np + >>> from skimage.segmentation import relabel_from_one + >>> label_field = array([1, 1, 5, 5, 8, 99, 42]) + >>> relab, fw, inv = relabel_from_one(label_field) + >>> relab + array([1, 1, 2, 2, 3, 5, 4]) + >>> fw + array([0, 1, 0, 0, 0, 2, 0, 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, 4, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 5]) + >>> inv + array([ 0, 1, 5, 8, 42, 99]) + >>> (fw[label_field] == relab).all() + True + >>> (inv[relab] == label_field).all() + True + """ + labels = np.unique(label_field) + labels0 = labels[labels != 0] + m = labels.max() + if m == len(labels0): # nothing to do, already 1...n labels + return label_field, labels, labels + forward_map = np.zeros(m+1, int) + forward_map[labels0] = np.arange(1, len(labels0) + 1) + if not (labels == 0).any(): + labels = np.concatenate(([0], labels)) + inverse_map = labels + return forward_map[label_field], forward_map, inverse_map diff --git a/skimage/segmentation/_quickshift.pyx b/skimage/segmentation/_quickshift.pyx new file mode 100644 index 00000000..cc649e8f --- /dev/null +++ b/skimage/segmentation/_quickshift.pyx @@ -0,0 +1,164 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +from scipy import ndimage +from itertools import product + +cimport numpy as cnp +from libc.math cimport exp, sqrt + +from ..util import img_as_float +from ..color import rgb2lab + + +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 cnp.ndarray[dtype=cnp.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 Py_ssize_t height = image_c.shape[0] + cdef Py_ssize_t width = image_c.shape[1] + cdef Py_ssize_t channels = image_c.shape[2] + cdef double current_density, closest, dist + + cdef Py_ssize_t r, c, r_, c_, channel, r_min, c_min + + cdef cnp.float_t* image_p = image_c.data + cdef cnp.float_t* current_pixel_p = image_p + + cdef cnp.ndarray[dtype=cnp.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 cnp.ndarray[dtype=cnp.int_t, ndim=2] parent \ + = np.arange(width * height).reshape(height, width) + cdef cnp.ndarray[dtype=cnp.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..7db072c0 --- /dev/null +++ b/skimage/segmentation/_slic.pyx @@ -0,0 +1,141 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +from time import time +from scipy import ndimage + +cimport numpy as cnp + +from ..util import img_as_float +from ..color import rgb2lab, gray2rgb + + +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) + """ + if image.ndim == 2: + image = gray2rgb(image) + if image.ndim != 3 or image.shape[2] != 3: + ValueError("Only 1- or 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 Py_ssize_t height, width + height, width = image.shape[:2] + # approximate grid size for desired n_segments + cdef Py_ssize_t step = int(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 cnp.ndarray[dtype=cnp.float_t, ndim=2] means \ + = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) + cdef cnp.float_t* current_mean + cdef cnp.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 cnp.ndarray[dtype=cnp.float_t, ndim=3] image_yx \ + = np.dstack([grid_y, grid_x, image / ratio]).copy("C") + cdef Py_ssize_t i, k, x, y, x_min, x_max, y_min, y_max, changes + cdef double dist_mean + + cdef cnp.ndarray[dtype=cnp.intp_t, ndim=2] nearest_mean \ + = np.zeros((height, width), dtype=np.intp) + cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] distance \ + = np.empty((height, width)) + cdef cnp.float_t* image_p = image_yx.data + cdef cnp.float_t* distance_p = distance.data + cdef cnp.float_t* current_distance + cdef cnp.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..c5bf985b --- /dev/null +++ b/skimage/segmentation/boundaries.py @@ -0,0 +1,45 @@ +import numpy as np +from ..morphology import dilation, square +from ..util import img_as_float +from ..color import gray2rgb +from .._shared.utils import deprecated + + +def find_boundaries(label_img): + """Return bool array where boundaries between labeled regions are True.""" + 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 mark_boundaries(image, label_img, color=(1, 1, 0), outline_color=(0, 0, 0)): + """Return image with boundaries between labeled regions highlighted. + + Parameters + ---------- + image : (M, N[, 3]) array + Grayscale or RGB image. + label_img : (M, N) array + Label array where regions are marked by different integer values. + color : length-3 sequence + RGB color of boundaries in the output image. + outline_color : length-3 sequence + RGB color surrounding boundaries in the output image. If None, no + outline is drawn. + """ + if image.ndim == 2: + image = gray2rgb(image) + image = img_as_float(image, force_copy=True) + + boundaries = find_boundaries(label_img) + if outline_color is not None: + outer_boundaries = dilation(boundaries.astype(np.uint8), square(2)) + image[outer_boundaries != 0, :] = np.array(outline_color) + image[boundaries, :] = np.array(color) + return image + + +@deprecated('mark_boundaries') +def visualize_boundaries(*args, **kwargs): + return mark_boundaries(*args, **kwargs) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 130be926..6df714d2 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -14,26 +14,23 @@ import numpy as np from scipy import sparse, ndimage try: from scipy.sparse.linalg.dsolve import umfpack - u = umfpack.UmfpackContext() + UmfpackContext = umfpack.UmfpackContext() except: - warnings.warn("""Scipy was built without UMFPACK. Consider rebuilding - Scipy with UMFPACK, this will greatly speed up the random walker - functions. You may also install pyamg and run the random walker function - in cg_mg mode (see the docstrings) - """) + UmfpackContext = None try: from pyamg import ruge_stuben_solver amg_loaded = True except ImportError: amg_loaded = False from scipy.sparse.linalg import cg +from ..util import img_as_float +from ..filter import rank_order #-----------Laplacian-------------------- def _make_graph_edges_3d(n_x, n_y, n_z): - """ - Returns a list of edges for a 3D image. + """Returns a list of edges for a 3D image. Parameters ---------- @@ -47,9 +44,12 @@ def _make_graph_edges_3d(n_x, n_y, n_z): Returns ------- edges : (2, N) ndarray - with the total number of edges N = n_x * n_y * (nz - 1) + - n_x * (n_y - 1) * nz + - (n_x - 1) * n_y * nz + with the total number of edges:: + + N = n_x * n_y * (nz - 1) + + n_x * (n_y - 1) * nz + + (n_x - 1) * n_y * nz + Graph edges with each column describing a node-id pair. """ vertices = np.arange(n_x * n_y * n_z).reshape((n_x, n_y, n_z)) @@ -62,17 +62,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 +107,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 +144,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,27 +172,33 @@ 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): - """ - Random walker algorithm for segmentation from markers. +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 is implemented for gray-level or multichannel + images. Parameters ---------- - data : array_like - Image to be segmented in phases. `data` can be two- or - three-dimensional. - - labels : array of ints, of same shape as `data` + 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` 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 (the greater `beta`, the more difficult the diffusion). - mode : {'bf', 'cg_mg', 'cg'} (default: 'bf') Mode for solving the linear system in the random walker algorithm. @@ -186,12 +207,10 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): computed. This is fast for small images (<1024x1024), but very slow (due to the memory cost) and memory-consuming for big images (in 3-D for example). - - 'cg' (conjugate gradient): the linear system is solved iteratively using the Conjugate Gradient method from scipy.sparse.linalg. This is less memory-consuming than the brute force method for large images, but it is quite slow. - - 'cg_mg' (conjugate gradient with multigrid preconditioner): a preconditioner is computed using a multigrid solver, then the solution is computed with the Conjugate Gradient method. This mode @@ -202,28 +221,50 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): tol : float tolerance to achieve when solving the linear system, in cg' and 'cg_mg' modes. - copy : bool If copy is False, the `labels` array will be overwritten with 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 -------- - skimage.morphology.watershed: watershed segmentation A segmentation algorithm based on mathematical morphology and "flooding" of regions from markers. 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. @@ -247,7 +288,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,36 +299,61 @@ 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. Examples -------- - >>> a = np.zeros((10, 10)) + 0.2*np.random.random((10, 10)) >>> a[5:8, 5:8] += 1 >>> b = np.zeros_like(a) >>> 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) + + if UmfpackContext is None: + warnings.warn('SciPy was built without UMFPACK. Consider rebuilding ' + 'SciPy with UMFPACK, this will greatly speed up the ' + 'random walker functions. You may also install pyamg ' + 'and run the random walker function in cg_mg mode ' + '(see the docstrings)') + + # 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 +362,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 +412,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 +429,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 +448,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_join.py b/skimage/segmentation/tests/test_join.py new file mode 100644 index 00000000..f03244e9 --- /dev/null +++ b/skimage/segmentation/tests/test_join.py @@ -0,0 +1,40 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage.segmentation import join_segmentations, relabel_from_one + +def test_join_segmentations(): + s1 = np.array([[0, 0, 1, 1], + [0, 2, 1, 1], + [2, 2, 2, 1]]) + s2 = np.array([[0, 1, 1, 0], + [0, 1, 1, 0], + [0, 1, 1, 1]]) + + # test correct join + # NOTE: technically, equality to j_ref is not required, only that there + # is a one-to-one mapping between j and j_ref. I don't know of an easy way + # to check this (i.e. not as error-prone as the function being tested) + j = join_segmentations(s1, s2) + j_ref = np.array([[0, 1, 3, 2], + [0, 5, 3, 2], + [4, 5, 5, 3]]) + assert_array_equal(j, j_ref) + + # test correct exception when arrays are different shapes + s3 = np.array([[0, 0, 1, 1], [0, 2, 2, 1]]) + assert_raises(ValueError, join_segmentations, s1, s3) + +def test_relabel_from_one(): + ar = np.array([1, 1, 5, 5, 8, 99, 42]) + ar_relab, fw, inv = relabel_from_one(ar) + ar_relab_ref = np.array([1, 1, 2, 2, 3, 5, 4]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 1; fw_ref[5] = 2; fw_ref[8] = 3; fw_ref[42] = 4; fw_ref[99] = 5 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + +if __name__ == "__main__": + np.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..a940f859 --- /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], 1) + assert_array_equal(seg[10:, :10], 2) + assert_array_equal(seg[:10, 10:], 0) + assert_array_equal(seg[10:, 10:], 3) + + 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..89dee59b --- /dev/null +++ b/skimage/segmentation/tests/test_slic.py @@ -0,0 +1,43 @@ +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 + 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) + +def test_gray(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21)) + img[:10, :10] = 0.33 + img[10:, :10] = 0.67 + img[10:, 10:] = 1.00 + img += 0.0033 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, sigma=0, n_segments=4, ratio=50.0) + + 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..089487ee 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, resize, rotate, rescale +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..448829de --- /dev/null +++ b/skimage/transform/_geometric.py @@ -0,0 +1,1024 @@ +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=2): + """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, optional + 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, + default order is 2) + + 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. + Coordinates are in the shape (P, 2), where P is the number + of coordinates and each element is a ``(x, y)`` pair. + 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 up and to the right: + + >>> from skimage import data + >>> from scipy.ndimage import map_coordinates + >>> + >>> def shift_up10_left20(xy): + ... return xy - np.array([-20, 10])[None, :] + >>> + >>> image = data.lena().astype(np.float32) + >>> coords = warp_coords(shift_up10_left20, 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..2b1acc7c 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -1,130 +1,204 @@ -cimport cython +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np -from random import randint -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) +cimport numpy as cnp +cimport cython -cdef double round(double val): - return floor(val + 0.5); +from libc.math cimport abs, fabs, sqrt, ceil +from libc.stdlib cimport rand + +from skimage.draw import circle_perimeter cdef double PI_2 = 1.5707963267948966 cdef double NEG_PI_2 = -PI_2 -@cython.boundscheck(False) -def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): - + +cdef inline Py_ssize_t round(double r): + return ((r + 0.5) if (r > 0.0) else (r - 0.5)) + + +def _hough_circle(cnp.ndarray img, + cnp.ndarray[ndim=1, dtype=cnp.intp_t] radius, + char normalize=True): + """Perform a circular Hough transform. + + Parameters + ---------- + img : (M, N) ndarray + Input image with nonzero values representing edges. + radius : ndarray + Radii at which to compute the Hough transform. + normalize : boolean, optional + Normalize the accumulator with the number + of pixels used to draw the radius + + Returns + ------- + H : 3D ndarray (radius index, (M, N) ndarray) + Hough transform accumulator for each radius + + """ + if img.ndim != 2: + raise ValueError('The input image must be 2D.') + + # compute the nonzero indexes + cdef cnp.ndarray[ndim=1, dtype=cnp.intp_t] x, y + x, y = np.nonzero(img) + + cdef Py_ssize_t num_pixels = x.size + + # Offset the image + cdef Py_ssize_t max_radius = radius.max() + x = x + max_radius + y = y + max_radius + + cdef Py_ssize_t i, p, c, num_circle_pixels, tx, ty + cdef double incr + cdef cnp.ndarray[ndim=1, dtype=cnp.intp_t] circle_x, circle_y + + cdef cnp.ndarray[ndim=3, dtype=cnp.double_t] acc = \ + np.zeros((radius.size, + img.shape[0] + 2 * max_radius, + img.shape[1] + 2 * max_radius), dtype=np.double) + + for i, rad in enumerate(radius): + # Store in memory the circle of given radius + # centered at (0,0) + circle_x, circle_y = circle_perimeter(0, 0, rad) + + num_circle_pixels = circle_x.size + + if normalize: + incr = 1.0 / num_circle_pixels + else: + incr = 1 + + # For each non zero pixel + for p in range(num_pixels): + # Plug the circle at (px, py), + # its coordinates are (tx, ty) + for c in range(num_circle_pixels): + tx = circle_x[c] + x[p] + ty = circle_y[c] + y[p] + acc[i, tx, ty] += incr + + return acc + + +def _hough(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): + if img.ndim != 2: raise ValueError('The input image must be 2D.') # Compute the array of angles and their sine and cosine - cdef np.ndarray[ndim=1, dtype=np.double_t] ctheta - cdef np.ndarray[ndim=1, dtype=np.double_t] stheta + cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] ctheta + cdef cnp.ndarray[ndim=1, dtype=cnp.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) # 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 cnp.ndarray[ndim=2, dtype=cnp.uint64_t] accum + cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] bins + cdef Py_ssize_t max_distance, offset - max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + - img.shape[1] * img.shape[1]))) + 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) + # compute the nonzero indexes + cdef cnp.ndarray[ndim=1, dtype=cnp.npy_intp] x_idxs, y_idxs + y_idxs, x_idxs = np.nonzero(img) # finally, run the transform - cdef int nidxs, nthetas, i, j, x, y, accum_idx + cdef Py_ssize_t nidxs, nthetas, i, j, x, y, accum_idx nidxs = y_idxs.shape[0] # x and y are the same shape 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 return accum, theta, bins -import math -@cython.cdivision(True) -@cython.boundscheck(False) -def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ - int line_gap, np.ndarray[ndim=1, dtype=np.double_t] theta=None): +def _probabilistic_hough(cnp.ndarray img, int value_threshold, + int line_length, int line_gap, + cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): + if img.ndim != 2: raise ValueError('The input image must be 2D.') - # compute the array of angles and their sine and cosine - cdef np.ndarray[ndim=1, dtype=np.double_t] ctheta - cdef np.ndarray[ndim=1, dtype=np.double_t] stheta - # calculate thetas if none specified + if theta is None: - theta = np.linspace(math.pi/2, -math.pi/2, 180) - theta = math.pi/2-np.arange(180)/180.0* math.pi - ctheta = np.cos(theta) - stheta = np.sin(theta) - cdef int height = img.shape[0] - cdef int width = img.shape[1] + theta = PI_2 - np.arange(180) / 180.0 * 2 * PI_2 + + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] + # compute the bins and allocate the accumulator array - cdef np.ndarray[ndim=2, dtype=np.int64_t] accum - cdef np.ndarray[ndim=2, dtype=np.uint8_t] mask = np.zeros((height, width), dtype=np.uint8) - cdef np.ndarray[ndim=2, dtype=np.int32_t] line_end = np.zeros((2, 2), dtype=np.int32) - cdef int max_distance, offset, num_indexes, index + cdef cnp.ndarray[ndim=2, dtype=cnp.int64_t] accum + cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] ctheta, stheta + cdef cnp.ndarray[ndim=2, dtype=cnp.uint8_t] mask = \ + np.zeros((height, width), dtype=np.uint8) + cdef cnp.ndarray[ndim=2, dtype=cnp.int32_t] line_end = \ + np.zeros((2, 2), dtype=np.int32) + cdef Py_ssize_t max_distance, offset, num_indexes, index cdef double a, b - cdef int nidxs, nthetas, i, j, x, y, px, py, accum_idx, value, max_value, max_theta + cdef Py_ssize_t nidxs, i, j, x, y, px, py, accum_idx + cdef int value, max_value, max_theta cdef int shift = 16 # 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] + + cdef Py_ssize_t lines_max = 2 ** 15 + cdef Py_ssize_t xflag, x0, y0, dx0, dy0, dx, dy, gap, x1, y1, \ + good_line, count + cdef list lines = list() + + 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 - # find the nonzero indexes - cdef np.ndarray[ndim=1, dtype=np.npy_intp] x_idxs, y_idxs - y_idxs, x_idxs = np.nonzero(img) - num_indexes = y_idxs.shape[0] # x and y are the same shape nthetas = theta.shape[0] - points = [] - for i in range(num_indexes): - points.append((x_idxs[i], y_idxs[i])) - lines = [] - # create mask of all non-zero indexes - for i in range(num_indexes): - mask[y_idxs[i], x_idxs[i]] = 1 + + # compute sine and cosine of angles + ctheta = np.cos(theta) + stheta = np.sin(theta) + + # find the nonzero indexes + y_idxs, x_idxs = np.nonzero(img) + points = list(zip(x_idxs, y_idxs)) + # mask all non-zero indexes + mask[y_idxs, x_idxs] = 1 + while 1: - # select random non-zero point + + # quit if no remaining points count = len(points) if count == 0: - break - index = rand() % (count) + break + + # select random non-zero point + 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 + value = 0 - max_value = value_threshold-1 + max_value = value_threshold - 1 max_theta = -1 + # apply hough transform on point for j in range(nthetas): accum_idx = round((ctheta[j] * x + stheta[j] * y)) + offset @@ -135,7 +209,9 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ max_theta = j if max_value < value_threshold: continue - # from the random point walk in opposite directions and find line beginning and end + + # from the random point walk in opposite directions and find line + # beginning and end a = -stheta[max_theta] b = ctheta[max_theta] x0 = x @@ -147,7 +223,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 +232,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 @@ -191,6 +267,7 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ # confirm line length is sufficient good_line = abs(line_end[1, 1] - line_end[0, 1]) >= line_length or \ abs(line_end[1, 0] - line_end[0, 0]) >= line_length + # pass 2: walk the line again and reset accumulator and mask for k in range(2): px = x0 @@ -208,9 +285,10 @@ 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 @@ -221,9 +299,9 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ # add line to the result if good_line: - lines.append(((line_end[0, 0], line_end[0, 1]), (line_end[1, 0], line_end[1, 1]))) + lines.append(((line_end[0, 0], line_end[0, 1]), + (line_end[1, 0], line_end[1, 1]))) if len(lines) > lines_max: return lines + return lines - - 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..7ac3e63f --- /dev/null +++ b/skimage/transform/_warps.py @@ -0,0 +1,255 @@ +import numpy as np +from scipy import ndimage +from ._geometric import (warp, SimilarityTransform, AffineTransform, + ProjectiveTransform) + + +def resize(image, output_shape, order=1, mode='constant', cval=0.): + """Resize image to match a certain size. + + Parameters + ---------- + image : ndarray + Input image. + output_shape : tuple or ndarray + Size of the generated output image `(rows, cols[, dim])`. If `dim` is + not provided, the number of channels are preserved. In case the number + of input channels does not equal the number of output channels a + 3-dimensional interpolation is applied. + + 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[0], output_shape[1] + orig_rows, orig_cols = image.shape[0], image.shape[1] + + row_scale = float(orig_rows) / rows + col_scale = float(orig_cols) / cols + + # 3-dimensional interpolation + if len(output_shape) == 3 and (image.ndim == 2 + or output_shape[2] != image.shape[2]): + dim = output_shape[2] + orig_dim = 1 if image.ndim == 2 else image.shape[2] + dim_scale = float(orig_dim) / dim + + map_rows, map_cols, map_dims = np.mgrid[:rows, :cols, :dim] + map_rows = row_scale * (map_rows + 0.5) - 0.5 + map_cols = col_scale * (map_cols + 0.5) - 0.5 + map_dims = dim_scale * (map_dims + 0.5) - 0.5 + + coord_map = np.array([map_rows, map_cols, map_dims]) + + out = ndimage.map_coordinates(image, coord_map, order=order, mode=mode, + cval=cval) + + else: # 2-dimensional interpolation + + # 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] = col_scale * (src_corners[:, 0] + 0.5) - 0.5 + dst_corners[:, 1] = row_scale * (src_corners[:, 1] + 0.5) - 0.5 + + tform = AffineTransform() + tform.estimate(src_corners, dst_corners) + + out = warp(image, tform, output_shape=output_shape, order=order, + mode=mode, cval=cval) + + return out + + +def rescale(image, scale, order=1, mode='constant', cval=0.): + """Scale image by a certain factor. + + Parameters + ---------- + image : ndarray + Input image. + scale : {float, tuple of floats} + Scale factors. Separate scale factors can be defined as + `(row_scale, col_scale)`. + + Returns + ------- + scaled : ndarray + Scaled 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. + + """ + + try: + row_scale, col_scale = scale + except TypeError: + row_scale = col_scale = scale + + orig_rows, orig_cols = image.shape[0], image.shape[1] + rows = np.round(row_scale * orig_rows) + cols = np.round(col_scale * orig_cols) + output_shape = (rows, cols) + + return resize(image, 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 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) diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx new file mode 100644 index 00000000..968f643a --- /dev/null +++ b/skimage/transform/_warps_cy.pyx @@ -0,0 +1,129 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np + +cimport numpy as cnp +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(cnp.ndarray image, cnp.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 cnp.ndarray[dtype=cnp.double_t, ndim=2, mode="c"] img = \ + np.ascontiguousarray(image, dtype=np.double) + cdef cnp.ndarray[dtype=cnp.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 Py_ssize_t 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 cnp.ndarray[dtype=cnp.double_t, ndim=2] out = \ + np.zeros((out_r, out_c), dtype=np.double) + + cdef Py_ssize_t tfr, tfc + cdef double r, c + cdef Py_ssize_t rows = img.shape[0] + cdef Py_ssize_t cols = img.shape[1] + + cdef double (*interp_func)(double*, Py_ssize_t, Py_ssize_t, 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..17b76826 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -1,9 +1,12 @@ -__all__ = ['hough', 'probabilistic_hough'] +__all__ = ['hough', 'hough_line', 'hough_circle', 'hough_peaks', 'probabilistic_hough'] from itertools import izip as zip import numpy as np -from ._hough_transform import _probabilistic_hough +from scipy import ndimage +from ._hough_transform import _probabilistic_hough +from skimage import measure, morphology + def _hough(img, theta=None): if img.ndim != 2: @@ -60,40 +63,48 @@ 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) +from skimage._shared.utils import deprecated +@deprecated('hough_line') def hough(img, theta=None): + return hough_line(img, theta) + +from ._hough_transform import _hough_circle + +def hough_line(img, theta=None): """Perform a straight line Hough transform. Parameters @@ -108,10 +119,10 @@ def hough(img, theta=None): ------- H : 2-D ndarray of uint64 Hough transform accumulator. - distances : ndarray - Distance values. theta : ndarray Angles at which the transform was computed. + distances : ndarray + Distance values. Examples -------- @@ -122,24 +133,158 @@ 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) +def hough_circle(img, radius, normalize=True): + """Perform a circular Hough transform. + Parameters + ---------- + img : (M, N) ndarray + Input image with nonzero values representing edges. + radius : ndarray + Radii at which to compute the Hough transform. + normalize : boolean, optional + Normalize the accumulator with the number + of pixels used to draw the radius + + Returns + ------- + H : 3D ndarray (radius index, (M, N) ndarray) + Hough transform accumulator for each radius + + """ + return _hough_circle(img, radius, normalize) + +def hough_peaks(hspace, angles, dists, min_distance=10, min_angle=10, + threshold=None, num_peaks=np.inf): + """Return peaks in hough transform. + + Identifies most prominent lines separated by a certain angle and distance in + a hough transform. Non-maximum suppression with different sizes is applied + separately in the first (distances) and second (angles) dimension of the + hough space to identify peaks. + + Parameters + ---------- + hspace : (N, M) array + Hough space returned by the `hough` function. + angles : (M,) array + Angles returned by the `hough` function. Assumed to be continuous + (`angles[-1] - angles[0] == PI`). + dists : (N, ) array + Distances returned by the `hough` function. + min_distance : int + Minimum distance separating lines (maximum filter size for first + dimension of hough space). + min_angle : int + Minimum angle separating lines (maximum filter size for second + dimension of hough space). + threshold : float + Minimum intensity of peaks. Default is `0.5 * max(hspace)`. + num_peaks : int + Maximum number of peaks. When the number of peaks exceeds `num_peaks`, + return `num_peaks` coordinates based on peak intensity. + + Returns + ------- + hspace, angles, dists : tuple of array + Peak values in hough space, angles and distances. + + Examples + -------- + >>> import numpy as np + >>> from skimage.transform import hough, hough_peaks + >>> from skimage.draw import line + >>> img = np.zeros((15, 15), dtype=np.bool_) + >>> rr, cc = line(0, 0, 14, 14) + >>> img[rr, cc] = 1 + >>> rr, cc = line(0, 14, 14, 0) + >>> img[cc, rr] = 1 + >>> hspace, angles, dists = hough(img) + >>> hspace, angles, dists = hough_peaks(hspace, angles, dists) + >>> angles + array([ 0.74590887, -0.79856126]) + >>> dists + array([ 10.74418605, 0.51162791]) + + """ + + hspace = hspace.copy() + rows, cols = hspace.shape + + if threshold is None: + threshold = 0.5 * np.max(hspace) + + distance_size = 2 * min_distance + 1 + angle_size = 2 * min_angle + 1 + hspace_max = ndimage.maximum_filter1d(hspace, size=distance_size, axis=0, + mode='constant', cval=0) + hspace_max = ndimage.maximum_filter1d(hspace_max, size=angle_size, axis=1, + mode='constant', cval=0) + mask = (hspace == hspace_max) + hspace *= mask + hspace_t = hspace > threshold + + label_hspace = morphology.label(hspace_t) + props = measure.regionprops(label_hspace, ['Centroid']) + coords = np.array([np.round(p['Centroid']) for p in props], dtype=int) + + hspace_peaks = [] + dist_peaks = [] + angle_peaks = [] + + # relative coordinate grid for local neighbourhood suppression + dist_ext, angle_ext = np.mgrid[-min_distance:min_distance + 1, + -min_angle:min_angle + 1] + + for dist_idx, angle_idx in coords: + accum = hspace[dist_idx, angle_idx] + if accum > threshold: + # absolute coordinate grid for local neighbourhood suppression + dist_nh = dist_idx + dist_ext + angle_nh = angle_idx + angle_ext + + # no reflection for distance neighbourhood + dist_in = np.logical_and(dist_nh > 0, dist_nh < rows) + dist_nh = dist_nh[dist_in] + angle_nh = angle_nh[dist_in] + + # reflect angles and assume angles are continuous, e.g. + # (..., 88, 89, -90, -89, ..., 89, -90, -89, ...) + angle_low = angle_nh < 0 + dist_nh[angle_low] = rows - dist_nh[angle_low] + angle_nh[angle_low] += cols + angle_high = angle_nh >= cols + dist_nh[angle_high] = rows - dist_nh[angle_high] + angle_nh[angle_high] -= cols + + # suppress neighbourhood + hspace[dist_nh, angle_nh] = 0 + + # add current line to peaks + hspace_peaks.append(accum) + dist_peaks.append(dists[dist_idx]) + angle_peaks.append(angles[angle_idx]) + + hspace_peaks = np.array(hspace_peaks) + dist_peaks = np.array(dist_peaks) + angle_peaks = np.array(angle_peaks) + + if num_peaks < len(hspace_peaks): + idx_maxsort = np.argsort(hspace_peaks)[::-1][:num_peaks] + hspace_peaks = hspace_peaks[idx_maxsort] + dist_peaks = dist_peaks[idx_maxsort] + angle_peaks = angle_peaks[idx_maxsort] + + return hspace_peaks, angle_peaks, dist_peaks 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..c7f9f832 --- /dev/null +++ b/skimage/transform/tests/test_geometric.py @@ -0,0 +1,180 @@ +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_polynomial_default_order(): + tform = estimate_transform('polynomial', SRC, DST) + tform2 = estimate_transform('polynomial', SRC, DST, order=2) + 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..00427332 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -4,6 +4,8 @@ from numpy.testing import * import skimage.transform as tf import skimage.transform.hough_transform as ht from skimage.transform import probabilistic_hough +from skimage.draw import circle_perimeter + def append_desc(func, description): """Append the test function ``func`` and append @@ -13,7 +15,6 @@ def append_desc(func, description): return func -from skimage.transform import * def test_hough(): # Generate a test image @@ -21,7 +22,7 @@ def test_hough(): for i in range(25, 75): img[100 - i, i] = 1 - out, angles, d = tf.hough(img) + out, angles, d = tf.hough_line(img) y, x = np.where(out == out.max()) dist = d[y[0]] @@ -35,10 +36,11 @@ def test_hough_angles(): img = np.zeros((10, 10)) img[0, 0] = 1 - out, angles, d = tf.hough(img, np.linspace(0, 360, 10)) + out, angles, d = tf.hough_line(img, np.linspace(0, 360, 10)) assert_equal(len(angles), 10) + def test_py_hough(): ht._hough, fast_hough = ht._py_hough, ht._hough @@ -47,6 +49,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 +58,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: @@ -67,6 +71,58 @@ def test_probabilistic_hough(): assert([(25, 25), (74, 74)] in sorted_lines) +def test_hough_peaks_dist(): + img = np.zeros((100, 100), dtype=np.bool_) + img[:, 30] = True + img[:, 40] = True + hspace, angles, dists = tf.hough_line(img) + assert len(tf.hough_peaks(hspace, angles, dists, min_distance=5)[0]) == 2 + assert len(tf.hough_peaks(hspace, angles, dists, min_distance=15)[0]) == 1 + + +def test_hough_peaks_angle(): + img = np.zeros((100, 100), dtype=np.bool_) + img[:, 0] = True + img[0, :] = True + + hspace, angles, dists = tf.hough_line(img) + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + + theta = np.linspace(0, np.pi, 100) + hspace, angles, dists = tf.hough_line(img, theta) + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + + theta = np.linspace(np.pi / 3, 4. / 3 * np.pi, 100) + hspace, angles, dists = tf.hough_line(img, theta) + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + + +def test_hough_peaks_num(): + img = np.zeros((100, 100), dtype=np.bool_) + img[:, 30] = True + img[:, 40] = True + hspace, angles, dists = tf.hough_line(img) + assert len(tf.hough_peaks(hspace, angles, dists, min_distance=0, + min_angle=0, num_peaks=1)[0]) == 1 + + +def test_houghcircle(): + # Prepare picture + img = np.zeros((120, 100), dtype=int) + radius = 20 + x_0, y_0 = (99, 50) + x, y = circle_perimeter(y_0, x_0, radius) + img[y, x] = 1 + + out = tf.hough_circle(img, np.array([radius])) + + x, y = np.where(out[0] == out[0].max()) + # Offset for x_0, y_0 + assert_equal(x[0], x_0 + radius) + assert_equal(y[0], y_0 + radius) + 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..93f87320 --- /dev/null +++ b/skimage/transform/tests/test_warps.py @@ -0,0 +1,198 @@ +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, rescale, + AffineTransform, + ProjectiveTransform, + SimilarityTransform) +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_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_rotate_resize(): + x = np.zeros((10, 10), dtype=np.double) + + x45 = rotate(x, 45, resize=False) + assert x45.shape == (10, 10) + + x45 = rotate(x, 45, resize=True) + # new dimension should be d = sqrt(2 * (10/2)^2) + assert x45.shape == (14, 14) + + +def test_rescale(): + # same scale factor + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 + scaled = rescale(x, 2, order=0) + ref = np.zeros((10, 10)) + ref[2:4, 2:4] = 1 + assert_array_almost_equal(scaled, ref) + + # different scale factors + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 + scaled = rescale(x, (2, 1), order=0) + ref = np.zeros((10, 5)) + ref[2:4, 1] = 1 + assert_array_almost_equal(scaled, ref) + + +def test_resize2d(): + 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_resize3d_keep(): + # keep 3rd dimension + x = np.zeros((5, 5, 3), dtype=np.double) + x[1, 1, :] = 1 + resized = resize(x, (10, 10), order=0) + ref = np.zeros((10, 10, 3)) + ref[2:4, 2:4, :] = 1 + assert_array_almost_equal(resized, ref) + resized = resize(x, (10, 10, 3), order=0) + assert_array_almost_equal(resized, ref) + + +def test_resize3d_resize(): + # resize 3rd dimension + x = np.zeros((5, 5, 3), dtype=np.double) + x[1, 1, :] = 1 + resized = resize(x, (10, 10, 1), order=0) + ref = np.zeros((10, 10, 1)) + ref[2:4, 2:4] = 1 + assert_array_almost_equal(resized, ref) + + +def test_resize3d_bilinear(): + # bilinear 3rd dimension + x = np.zeros((5, 5, 2), dtype=np.double) + x[1, 1, 0] = 0 + x[1, 1, 1] = 1 + resized = resize(x, (10, 10, 1), order=1) + ref = np.zeros((10, 10, 1)) + ref[1:5, 1:5, :] = 0.03125 + ref[1:5, 2:4, :] = 0.09375 + ref[2:4, 1:5, :] = 0.09375 + ref[2:4, 2:4, :] = 0.28125 + 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()