diff --git a/.travis.yml b/.travis.yml index f50ba1ac..91b9b5fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ # 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 +# We pretend to be erlang because we 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 @@ -19,11 +19,24 @@ install: - sudo easy_install$PYSUF pip - sudo pip-$PYVER install cython - sudo apt-get install libfreeimage3 + - if [[ $PYVER == '2.7' ]]; then sudo apt-get install $PYTHON-matplotlib; fi + - if [[ $PYVER == '3.2' ]]; then sudo pip-$PYVER install git+git://github.com/matplotlib/matplotlib.git@v1.2.x; fi + - sudo pip-$PYVER install flake8 - $PYTHON setup.py build - sudo $PYTHON setup.py install script: + # Check if setup.py's match bento.info + - $PYTHON check_bento_build.py # Change into an innocuous directory and find tests from installation + - mkdir $HOME/.matplotlib + - "echo 'backend : Agg' > $HOME/.matplotlib/matplotlibrc" + - "echo 'backend.qt4 : PyQt4' >> $HOME/.matplotlib/matplotlibrc" - mkdir for_test - cd for_test - nosetests-$PYVER --exe -v --cover-package=skimage skimage - + # Change back to repository root directory and run all doc examples + - cd .. + - for f in doc/examples/*.py; do $PYTHON "$f"; if [ $? -ne 0 ]; then exit 1; fi done + - for f in doc/examples/applications/*.py; do $PYTHON "$f"; if [ $? -ne 0 ]; then exit 1; fi done + # Run pep8 and flake tests + - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt new file mode 100644 index 00000000..331b4ed8 --- /dev/null +++ b/CONTRIBUTING.txt @@ -0,0 +1,180 @@ +Development process +------------------- + +Here's the long and short of it: + +1. If you are a first-time contributor: + + * Go to `https://github.com/scikit-image/scikit-image + `_ and click the + "fork" button to create your own copy of the project. + + * Clone the project to your local computer:: + + git clone git@github.com:your-username/scikit-image.git + + * Add upstream repository:: + + git remote add upstream git@github.com:scikit-image/scikit-image.git + + * Now, you have remote repositories named: + + - ``upstream``, which refers to the ``scikit-image`` repository + - ``origin``, which refers to your personal fork + +2. Develop your contribution: + + * Pull the latest changes from upstream:: + + git checkout master + git pull upstream master + + * Create a 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 'transform-speedups':: + + git checkout -b transform-speedups + + * Commit locally as you progress (``git add`` and ``git commit``) + +3. To submit your contribution: + + * Push your changes back to your fork on GitHub:: + + git push origin transform-speedups + + * Go to GitHub. The new branch will show up with a Pull Request button - + click it. + + * If you want, post on the `mailing list + `_ to explain your changes or + to ask for review. + +For a more detailed discussion, read these :doc:`detailed documents +` on how to use Git with ``scikit-image`` +(``_). + +.. note:: + + To reviewers: add a short explanation of what a branch did to the merge + message and, if closing a bug, also add "Closes gh-123" where 123 is the + bug number. + + +Divergence between ``upstream master`` and your feature branch +.............................................................. + +Do *not* ever merge the main branch into yours. If GitHub indicates that the +branch of your Pull Request can no longer be merged automatically, rebase +onto master:: + + git checkout master + git pull upstream master + git checkout transform-speedups + git rebase master + +If any conflicts occur, fix the according files and continue:: + + git add conflict-file1 conflict-file2 + git rebase --continue + +However, you should only rebase your own branches and must generally not +rebase any branch which you collaborate on with someone else. + +Finally, you must push your rebased branch:: + + git push --force origin transform-speedups + +(If you are curious, here's a further discussion on the +`dangers of rebasing `__. +Also see this `LWN article `__.) + +Guidelines +---------- + +* All code should have tests (see `test coverage`_ below for more details). +* All code should be documented, to the same + `standard `_ + as NumPy and SciPy. +* For new functionality, always add an example to the + gallery. +* No changes should be committed without review. Ask on the + `mailing list `_ if + you get no response to your pull request. + **Never merge your own pull request.** +* Examples in the gallery should have a maximum figure width of 8 inches. + +Stylistic Guidelines +-------------------- + +* Set up your editor to remove trailing whitespace. Follow `PEP08 + `__. Check code with pyflakes / flake8. + +* 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`` + and then refer to ``M`` and ``N`` in the docstring, if necessary. + +* 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%. + +To measure the test coverage, install +`coverage.py `__ +(using ``easy_install coverage``) and then run:: + + $ make coverage + +This will print a report with one line for each file in `skimage`, +detailing the test coverage:: + + Name Stmts Exec Cover Missing + ------------------------------------------------------------------------------ + skimage/color/colorconv 77 77 100% + skimage/filter/__init__ 1 1 100% + ... + +Activate Travis-CI for your fork (optional) +------------------------------------------- + +Travis-CI checks all unittests in the project to prevent breakage. + +Before sending a pull request, you may want to check that Travis-CI +successfully passes all tests. To do so, + + * Go to `Travis-CI `__ and follow the Sign In link at the top + + * Go to your `profile page `__ and switch on your + scikit-image fork + +It corresponds to steps one and two in +`Travis-CI documentation `__ +(Step three is already done in scikit-image). + +Thus, as soon as you push your code to your fork, it will trigger Travis-CI, +and you will receive an email notification when the process is done. + +Bugs +---- + +Please `report bugs on GitHub `_. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index fd0656db..f8f78c52 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -48,7 +48,7 @@ Incorporating CellProfiler's Sobel edge detector, build and bug fixes. Radon transform, template matching. -- Emmanuelle Guillart +- Emmanuelle Gouillart Total variation noise filtering, integration of CellProfiler's mathematical morphology tools, random walker segmentation, tutorials, and more. @@ -113,7 +113,8 @@ Fixes and tests for Histograms of Oriented Gradients. - Joshua Warner - Multichannel random walker segmentation. + Multichannel random walker segmentation, unified peak finder backend, + n-dimensional array padding, marching cubes, bug and doc fixes. - Petter Strandmark Perimeter calculation in regionprops. @@ -131,8 +132,29 @@ Dense DAISY feature description, circle perimeter drawing. - François Boulogne - Andres Method for circle perimeter, ellipse perimeter drawing. - Circular Hough Transform + Drawing: Andres Method for circle perimeter, ellipse perimeter drawing, + Bezier curve, anti-aliasing. + Circular and elliptical Hough Transforms + Various fixes - Thouis Jones Vectorized operators for arrays of 16-bit ints. + +- Xavier Moles Lopez + Color separation (color deconvolution) for several stainings. + +- Jostein Bø Fløystad + Reconstruction circle mode for Radon transform + Simultaneous Algebraic Reconstruction Technique for inverse Radon transform + +- Matt Terry + Color difference functions + +- Eugene Dvoretsky + Yen threshold implementation. + +- Riaan van den Dool + skimage.io plugin: GDAL + +- Fedor Morozov + Drawing: Wu's anti-aliased circle diff --git a/DEPENDS.txt b/DEPENDS.txt index b2858256..04b745c2 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -2,11 +2,15 @@ Build Requirements ------------------ * `Python >= 2.5 `__ * `Numpy >= 1.6 `__ -* `Cython >= 0.15 `__ +* `Cython >= 0.17 `__ `Matplotlib >= 1.0 `__ is needed to generate the examples in the documentation. +You can use pip to automatically install the base dependencies as follows:: + + $ pip install -r requirements.txt + Runtime requirements -------------------- * `SciPy >= 0.10 `__ @@ -31,10 +35,20 @@ Optional Requirements You can use this scikit with the basic requirements listed above, but some functionality is only available with the following installed: -`PyQt4 `__ +* `PyQt4 `__ The ``qt`` plugin that provides ``imshow(x, fancy=True)`` and `skivi`. -`FreeImage `__ +* `FreeImage `__ The ``freeimage`` plugin provides support for reading various types of image file formats, including multi-page TIFFs. +* `PyAMG `__ + The ``pyamg`` module is used for the fast `cg_mg` mode of random + walker segmentation. + +Testing requirements +-------------------- +* `Nose `__ + A Python Unit Testing Framework +* `Coverage.py `__ + A tool that generates a unit test code coverage report diff --git a/DEVELOPMENT.txt b/DEVELOPMENT.txt deleted file mode 100644 index daffe705..00000000 --- a/DEVELOPMENT.txt +++ /dev/null @@ -1,111 +0,0 @@ -Development process -------------------- - -Here's the long and short of it: - - * 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 'transform-speedups'. - * Commit locally as you progress. - * 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 - -Guidelines ----------- - - * All code should have tests (see `test coverage`_ below for more details). - * All code should be documented, to the same - `standard `_ - as NumPy and SciPy. 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 - 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%. - -To measure the test coverage, install -`coverage.py `__ -(using ``easy_install coverage``) and then run:: - - $ make coverage - -This will print a report with one line for each file in `skimage`, -detailing the test coverage:: - - Name Stmts Exec Cover Missing - ------------------------------------------------------------------------------ - skimage/color/colorconv 77 77 100% - skimage/filter/__init__ 1 1 100% - ... - -Bugs ----- - -Please `report bugs on GitHub `_. diff --git a/RELEASE.txt b/RELEASE.txt index 1131a91a..5772cf77 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -1,23 +1,25 @@ How to make a new release of ``skimage`` ======================================== +- Check ``TODO.txt`` for any outstanding tasks. + - Update release notes. - - To show a list contributors, run ``doc/release/contributors.sh ``, - where ```` is the first commit since the previous release. + - To show a list of contributors and changes, run + ``doc/release/contribs.py ``. - 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 + - Edit ``doc/source/_static/docversions.js`` and commit - 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``. + - Push upstream: ``git push origin gh-pages`` in ``doc/gh-pages``. - Add the version number as a tag in git:: @@ -46,6 +48,8 @@ How to make a new release of ``skimage`` - Update stable and development version numbers in ``_templates/sidebar_versions.html``. - Add release date to ``index.rst`` under "Announcements". + - Add previous stable version documentation path to disallowed paths + in `robots.txt` - Build using ``make gh-pages``. - Push upstream: ``git push`` in ``gh-pages``. diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 00000000..3cbf4b53 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,15 @@ +Version 0.10 +------------ +* Remove deprecated functions in `skimage.filter.rank.*` +* Remove deprecated parameter `epsilon` of `skimage.viewer.LineProfile` +* Remove backwards-compatability of `skimage.measure.regionprops` +* Remove {`ratio`, `sigma`} deprecation warnings of `skimage.segmentation.slic` +* Change default mode of random_walker segmentation to 'cg_mg' > 'cg' > 'bf', + depending on which optional dependencies are available. +* Remove deprecated `out` parameter of `skimage.morphology.binary_*` +* Remove deprecated parameter `depth` in `skimage.segmentation.random_walker` +* Remove deprecated logger function in `skimage/__init__.py` +* Remove deprecated function `filter.median_filter` +* Remove deprecated `skimage.color.is_gray` and `skimage.color.is_rgb` + functions + diff --git a/bento.info b/bento.info index 518888e0..2c39b71a 100644 --- a/bento.info +++ b/bento.info @@ -1,5 +1,5 @@ Name: scikit-image -Version: 0.8.1 +Version: 0.9.0 Summary: Image processing routines for SciPy Url: http://scikit-image.org DownloadUrl: http://github.com/scikit-image/scikit-image @@ -28,7 +28,6 @@ Classifiers: Operating System :: Unix, Operating System :: MacOS -HookFile: bscript UseBackends: Waf Library: @@ -52,6 +51,9 @@ Library: Extension: skimage.measure._moments Sources: skimage/measure/_moments.pyx + Extension: skimage.measure._marching_cubes_cy + Sources: + skimage/measure/_marching_cubes_cy.pyx Extension: skimage.graph._mcp Sources: skimage/graph/_mcp.pyx @@ -91,6 +93,12 @@ Library: Extension: skimage.morphology._greyreconstruct Sources: skimage/morphology/_greyreconstruct.pyx + Extension: skimage.feature.censure_cy + Sources: + skimage/feature/censure_cy.pyx + Extension: skimage.feature._brief_cy + Sources: + skimage/feature/_brief_cy.pyx Extension: skimage.feature.corner_cy Sources: skimage/feature/corner_cy.pyx @@ -109,6 +117,9 @@ Library: Extension: skimage.morphology._skeletonize_cy Sources: skimage/morphology/_skeletonize_cy.pyx + Extension: skimage.transform._radon_transform + Sources: + skimage/transform/_radon_transform.pyx Extension: skimage.transform._warps_cy Sources: skimage/transform/_warps_cy.pyx @@ -121,36 +132,18 @@ Library: Extension: skimage._shared.geometry Sources: skimage/_shared/geometry.pyx - Extension: skimage.filter.rank._core16 + Extension: skimage.filter.rank.generic_cy Sources: - skimage/filter/rank/_core16.pyx - Extension: skimage.filter.rank._crank8 + skimage/filter/rank/generic_cy.pyx + Extension: skimage.filter.rank.percentile_cy Sources: - skimage/filter/rank/_crank8.pyx - Extension: skimage.filter.rank._crank16 + skimage/filter/rank/percentile_cy.pyx + Extension: skimage.filter.rank.core_cy Sources: - skimage/filter/rank/_crank16.pyx - Extension: skimage.filter.rank._core8 + skimage/filter/rank/core_cy.pyx + Extension: skimage.filter.rank.bilateral_cy 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 + skimage/filter/rank/bilateral_cy.pyx Executable: skivi Module: skimage.scripts.skivi diff --git a/bscript b/bscript deleted file mode 100644 index 44b3f3d6..00000000 --- a/bscript +++ /dev/null @@ -1,12 +0,0 @@ -import os.path as op - -from numpy.distutils.misc_util \ - import \ - get_numpy_include_dirs - -from bento.commands import hooks - -@hooks.post_configure -def post_configure(context): - conf = context.waf_context - conf.env.INCLUDES = get_numpy_include_dirs() + [op.join("skimage", "morphology")] diff --git a/check_bento_build.py b/check_bento_build.py index 34b2272a..bf3c5771 100644 --- a/check_bento_build.py +++ b/check_bento_build.py @@ -3,6 +3,7 @@ Check that Cython extensions in setup.py files match those in bento.info. """ import os import re +import sys RE_CYTHON = re.compile("config.add_extension\(\s*['\"]([\S]+)['\"]") @@ -62,12 +63,12 @@ def remove_common_extensions(cy_bento, cy_setup): def print_results(cy_bento, cy_setup): def info(text): - print + print('') print(text) print('-' * len(text)) if not (cy_bento or cy_setup): - print "bento.info and setup.py files match." + print("bento.info and setup.py files match.") if cy_bento: info("Extensions found in 'bento.info' but not in any 'setup.py:") @@ -80,8 +81,8 @@ def print_results(cy_bento, 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) + print(BENTO_TEMPLATE.format(module_path=module_path, + dir_path=dir_path)) if __name__ == '__main__': @@ -93,3 +94,6 @@ if __name__ == '__main__': cy_bento, cy_setup = remove_common_extensions(cy_bento, cy_setup) print_results(cy_bento, cy_setup) + + if cy_setup or cy_bento: + sys.exit(1) diff --git a/doc/Makefile b/doc/Makefile index 7552be6b..a593aa56 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -2,9 +2,10 @@ # # You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = +PYTHON ?= python +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +PAPER ?= # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 @@ -36,14 +37,14 @@ clean: -find ./source/auto_examples/* -type f | grep -v blank | xargs rm -f api: @mkdir -p source/api - python tools/build_modref_templates.py + $(PYTHON) tools/build_modref_templates.py @echo "Build API docs...done." random_gallery: - @cd source && python random_gallery.py + @cd source && $(PYTHON) random_gallery.py coveragetable: - @cd source && python coverage_generator.py + @cd source && $(PYTHON) coverage_generator.py html: api coveragetable random_gallery $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DEST)/html @@ -90,7 +91,7 @@ devhelp: @echo "# ln -s build/devhelp $$HOME/.local/share/devhelp/scikitimage" @echo "# devhelp" -latex: +latex: api $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(DEST)/latex @echo @echo "Build finished; the LaTeX files are in $(DEST)/latex." @@ -120,10 +121,10 @@ doctest: "results in build/doctest/output.txt." gh-pages: - python gh-pages.py + $(PYTHON) gh-pages.py gitwash: - python tools/gitwash/gitwash_dumper.py source scikit-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 \ diff --git a/doc/examples/applications/plot_coins_segmentation.py b/doc/examples/applications/plot_coins_segmentation.py index 0d1d8a34..fa7aa1f4 100644 --- a/doc/examples/applications/plot_coins_segmentation.py +++ b/doc/examples/applications/plot_coins_segmentation.py @@ -3,7 +3,7 @@ Comparing edge-based segmentation and region-based segmentation =============================================================== -In this example, we will see how to segment objects from a background. We use +In this example, we will see how to segment objects from a background. We use the ``coins`` image from ``skimage.data``, which shows several coins outlined against a darker background. """ @@ -108,9 +108,8 @@ closed are not filled correctly, as is the case for one unfilled coin above. Region-based segmentation ========================= -We therefore try a region-based method using the -watershed transform. First, we find an elevation map using the Sobel gradient of the -image. +We therefore try a region-based method using the watershed transform. First, we +find an elevation map using the Sobel gradient of the image. """ @@ -142,7 +141,8 @@ plt.title('markers') """ .. image:: PLOT2RST.current_figure -Finally, we use the watershed transform to fill regions of the elevation map starting from the markers determined above: +Finally, we use the watershed transform to fill regions of the elevation map +starting from the markers determined above: """ segmentation = morphology.watershed(elevation_map, markers) @@ -155,13 +155,16 @@ plt.title('segmentation') """ .. image:: PLOT2RST.current_figure -This last method works even better, and the coins can be segmented and -labeled individually. +This last method works even better, and the coins can be segmented and labeled +individually. """ +from skimage.color import label2rgb + segmentation = ndimage.binary_fill_holes(segmentation - 1) labeled_coins, _ = ndimage.label(segmentation) +image_label_overlay = label2rgb(labeled_coins, image=coins) plt.figure(figsize=(6, 3)) plt.subplot(121) @@ -169,7 +172,7 @@ plt.imshow(coins, cmap=plt.cm.gray, interpolation='nearest') plt.contour(segmentation, [0.5], linewidths=1.2, colors='y') plt.axis('off') plt.subplot(122) -plt.imshow(labeled_coins, cmap=plt.cm.spectral, interpolation='nearest') +plt.imshow(image_label_overlay, interpolation='nearest') plt.axis('off') plt.subplots_adjust(**margins) diff --git a/doc/examples/applications/plot_geometric.py b/doc/examples/applications/plot_geometric.py index 337ecad7..141ef51f 100644 --- a/doc/examples/applications/plot_geometric.py +++ b/doc/examples/applications/plot_geometric.py @@ -7,6 +7,8 @@ In this example, we will see how to use geometric transformations in the context of image processing. """ +from __future__ import print_function + import math import numpy as np import matplotlib.pyplot as plt @@ -31,7 +33,7 @@ First we create a transformation using explicit parameters: tform = tf.SimilarityTransform(scale=1, rotation=math.pi / 2, translation=(0, 1)) -print tform._matrix +print(tform._matrix) """ Alternatively you can define a transformation by the transformation matrix @@ -49,8 +51,8 @@ systems: """ coord = [1, 0] -print tform2(coord) -print tform2.inverse(tform(coord)) +print(tform2(coord)) +print(tform2.inverse(tform(coord))) """ Image warping diff --git a/doc/examples/applications/plot_morphology.py b/doc/examples/applications/plot_morphology.py new file mode 100644 index 00000000..84cf2972 --- /dev/null +++ b/doc/examples/applications/plot_morphology.py @@ -0,0 +1,274 @@ +""" +======================= +Morphological Filtering +======================= + +Morphological image processing is a collection of non-linear operations related +to the shape or morphology of features in an image, such as boundaries, +skeletons, etc. In any given technique, we probe an image with a small shape or +template called a structuring element, which defines the region of interest or +neighborhood around a pixel. + +In this document we outline the following basic morphological operations: + +1. Erosion +2. Dilation +3. Opening +4. Closing +5. White Tophat +6. Black Tophat +7. Skeletonize +8. Convex Hull + + +To get started, let's load an image using ``io.imread``. Note that morphology +functions only work on gray-scale or binary images, so we set ``as_grey=True``. +""" + +import matplotlib.pyplot as plt +from skimage.data import data_dir +from skimage.util import img_as_ubyte +from skimage import io + +plt.gray() +phantom = img_as_ubyte(io.imread(data_dir+'/phantom.png', as_grey=True)) +plt.imshow(phantom) + +""" +.. image:: PLOT2RST.current_figure + +Let's also define a convenience function for plotting comparisons: +""" + +def plot_comparison(original, filtered, filter_name): + + fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8, 4)) + ax1.imshow(original) + ax1.set_title('original') + ax1.axis('off') + ax2.imshow(filtered) + ax2.set_title(filter_name) + ax2.axis('off') + +""" +Erosion +======= + +Morphological ``erosion`` sets a pixel at (i, j) to the *minimum over all +pixels in the neighborhood centered at (i, j)*. The structuring element, +``selem``, passed to ``erosion`` is a boolean array that describes this +neighborhood. Below, we use ``disk`` to create a circular structuring element, +which we use for most of the following examples. +""" + +from skimage.morphology import erosion, dilation, opening, closing, white_tophat +from skimage.morphology import black_tophat, skeletonize, convex_hull_image +from skimage.morphology import disk + +selem = disk(6) +eroded = erosion(phantom, selem) +plot_comparison(phantom, eroded, 'erosion') + +""" +.. image:: PLOT2RST.current_figure + +Notice how the white boundary of the image disappears or gets eroded as we +increase the size of the disk. Also notice the increase in size of the two +black ellipses in the center and the disappearance of the 3 light grey +patches in the lower part of the image. + + +Dilation +======== + +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. +""" + +dilated = dilation(phantom, selem) +plot_comparison(phantom, dilated, 'dilation') + +""" +.. image:: PLOT2RST.current_figure + +Notice how the white boundary of the image thickens, or gets dilated, as we +increase the size of the disk. Also notice the decrease in size of the two +black ellipses in the centre, and the thickening of the light grey circle in +the center and the 3 patches in the lower part of the image. + + +Opening +======= + +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. +""" + +opened = opening(phantom, selem) +plot_comparison(phantom, opened, 'opening') + +""" +.. image:: PLOT2RST.current_figure + +Since ``opening`` an image starts with an erosion operation, light regions that +are *smaller* than the structuring element are removed. The dilation operation +that follows ensures that light regions that are *larger* than the structuring +element retain their original size. Notice how the light and dark shapes in the +center their original thickness but the 3 lighter patches in the bottom get +completely eroded. The size dependence is highlighted by the outer white ring: +The parts of the ring thinner than the structuring element were completely +erased, while the thicker region at the top retains its original thickness. + + +Closing +======= + +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. + +To illustrate this more clearly, let's add a small crack to the white border: +""" + +phantom = img_as_ubyte(io.imread(data_dir+'/phantom.png', as_grey=True)) +phantom[10:30, 200:210] = 0 + +closed = closing(phantom, selem) +plot_comparison(phantom, closed, 'closing') + +""" +.. image:: PLOT2RST.current_figure + +Since ``closing`` an image starts with an dilation operation, dark regions +that are *smaller* than the structuring element are removed. The dilation +operation that follows ensures that dark regions that are *larger* than the +structuring element retain their original size. Notice how the white ellipses +at the bottom get connected because of dilation, but other dark region retain +their original sizes. Also notice how the crack we added is mostly removed. + + +White tophat +============ + +The ``white_tophat`` of an image is defined as the *image minus its +morphological opening*. This operation returns the bright spots of the image +that are smaller than the structuring element. + +To make things interesting, we'll add bright and dark spots to the image: +""" + +phantom = img_as_ubyte(io.imread(data_dir+'/phantom.png', as_grey=True)) +phantom[340:350, 200:210] = 255 +phantom[100:110, 200:210] = 0 + +w_tophat = white_tophat(phantom, selem) +plot_comparison(phantom, w_tophat, 'white tophat') + +""" +.. image:: PLOT2RST.current_figure + +As you can see, the 10-pixel wide white square is highlighted since it is +smaller than the structuring element. Also, the thin, white edges around most +of the ellipse are retained because they're smaller than the structuring +element, but the thicker region at the top disappears. + + +Black tophat +============ + +The ``black_tophat`` of an image is defined as its morphological **closing +minus the original image**. This operation returns the *dark spots of the +image that are smaller than the structuring element*. +""" + +b_tophat = black_tophat(phantom, selem) +plot_comparison(phantom, b_tophat, 'black tophat') + +""" +.. image:: PLOT2RST.current_figure + +As you can see, the 10-pixel wide black square is highlighted since it is +smaller than the structuring element. + + +Duality +------- + +As you should have noticed, many of these operations are simply the reverse +of another operation. This duality can be summarized as follows: + +1. Erosion <-> Dilation +2. Opening <-> Closing +3. White tophat <-> Black tophat + + +Skeletonize +=========== + +Thinning is used to reduce each connected component in a binary image to a +*single-pixel wide skeleton*. It is important to note that this is performed +on binary images only. + +""" + +from skimage import img_as_bool +horse = ~img_as_bool(io.imread(data_dir+'/horse.png', as_grey=True)) + +sk = skeletonize(horse) +plot_comparison(horse, sk, 'skeletonize') + +""" +.. image:: PLOT2RST.current_figure + +As the name suggests, this technique is used to thin the image to 1-pixel wide +skeleton by applying thinning successively. + + +Convex hull +=========== + +The ``convex_hull_image`` is the *set of pixels included in the smallest +convex polygon that surround all white pixels in the input image*. Again note +that this is also performed on binary images. + +""" + +hull1 = convex_hull_image(horse) +plot_comparison(horse, hull1, 'convex hull') + +""" +.. image:: PLOT2RST.current_figure + +As the figure illustrates, ``convex_hull_image`` gives the smallest polygon +which covers the white or True completely in the image. + +If we add a small grain to the image, we can see how the convex hull adapts to +enclose that grain: +""" + +import numpy as np + +horse2 = np.copy(horse) +horse2[45:50, 75:80] = 1 + +hull2 = convex_hull_image(horse2) +plot_comparison(horse2, hull2, 'convex hull') + +""" +.. image:: PLOT2RST.current_figure + + +Additional Resources +==================== + +1. `MathWorks tutorial on morphological processing +`_ +2. `Auckland university's tutorial on Morphological Image Processing +`_ +3. http://en.wikipedia.org/wiki/Mathematical_morphology + +""" + +plt.show() diff --git a/doc/examples/applications/plot_rank_filters.py b/doc/examples/applications/plot_rank_filters.py index c182671d..87dad0d0 100644 --- a/doc/examples/applications/plot_rank_filters.py +++ b/doc/examples/applications/plot_rank_filters.py @@ -3,11 +3,11 @@ Rank filters ============ -Rank filters are non-linear filters using the local greylevels ordering to +Rank filters are non-linear filters using the local gray-level 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. +local gray-level histogram is computed on the neighborhood of a pixel (defined +by a 2-D 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: @@ -26,11 +26,9 @@ Rank filters can be used for several purposes such as: 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`. +In this example, we will see how to filter a gray-level image using some of the +linear and non-linear filters available in skimage. We use the `camera` image +from `skimage.data` for all comparisons. .. [1] Pierre Soille, On morphological operators based on rank filters, Pattern Recognition 35 (2002) 527-535. @@ -40,18 +38,19 @@ image from `skimage.data`. import numpy as np import matplotlib.pyplot as plt +from skimage import img_as_ubyte from skimage import data -ima = data.camera() -hist = np.histogram(ima, bins=np.arange(0, 256)) +noisy_image = img_as_ubyte(data.camera()) +hist = np.histogram(noisy_image, 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.imshow(noisy_image, 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') +plt.title('Histogram of grey values') """ @@ -65,50 +64,56 @@ randomly set to 0. The **median** filter is applied to remove the noise. .. note:: - there are different implementations of median filter : + 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]) +noise = np.random.random(noisy_image.shape) +noisy_image = img_as_ubyte(data.camera()) +noisy_image[noise > 0.99] = 255 +noisy_image[noise < 0.01] = 0 + +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.imshow(noisy_image, vmin=0, vmax=255) +plt.title('Noisy image') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(lo, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('median $r=1$') +plt.imshow(median(noisy_image, disk(1)), vmin=0, vmax=255) +plt.title('Median $r=1$') +plt.axis('off') + plt.subplot(2, 2, 3) -plt.imshow(hi, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('median $r=5$') +plt.imshow(median(noisy_image, disk(5)), vmin=0, vmax=255) +plt.title('Median $r=5$') +plt.axis('off') + plt.subplot(2, 2, 4) -plt.imshow(ext, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('median $r=20$') +plt.imshow(median(noisy_image, disk(20)), vmin=0, vmax=255) +plt.title('Median $r=20$') +plt.axis('off') """ .. 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. +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 bigger sizes are filtered as well, such as the camera tripod. The +median filter is often used for noise removal because borders are preserved and +e.g. salt and pepper noise typically does not distort the gray-level. Image smoothing ================ -The example hereunder shows how a local **mean** smoothes the camera man image. +The example hereunder shows how a local **mean** filter smooths the camera man +image. """ @@ -116,13 +121,17 @@ from skimage.filter.rank import mean fig = plt.figure(figsize=[10, 7]) -loc_mean = mean(nima, disk(10)) +loc_mean = mean(noisy_image, disk(10)) + plt.subplot(1, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('original') +plt.imshow(noisy_image, vmin=0, vmax=255) +plt.title('Original') +plt.axis('off') + plt.subplot(1, 2, 2) -plt.imshow(loc_mean, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('local mean $r=10$') +plt.imshow(loc_mean, vmin=0, vmax=255) +plt.title('Local mean $r=10$') +plt.axis('off') """ @@ -130,35 +139,41 @@ plt.xlabel('local mean $r=10$') 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 +that restricts the local neighborhood to pixel having a gray-level similar to the central one. .. note:: - a different implementation is available for color images in + 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) +noisy_image = img_as_ubyte(data.camera()) -bilat = bilateral_mean(ima.astype(np.uint16), disk(20), s0=10, s1=10) +bilat = bilateral_mean(noisy_image.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.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(bilat, cmap=plt.cm.gray) -plt.xlabel('bilateral mean') +plt.title('Bilateral mean') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(bilat[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') """ @@ -175,7 +190,7 @@ 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. +equalization emphasizes every local gray-level variations. .. [2] http://en.wikipedia.org/wiki/Histogram_equalization .. [3] http://en.wikipedia.org/wiki/Adaptive_histogram_equalization @@ -185,101 +200,112 @@ equalization emphasizes every local greylevel variations. from skimage import exposure from skimage.filter import rank -ima = data.camera() +noisy_image = img_as_ubyte(data.camera()) + # equalize globally and locally -glob = exposure.equalize(ima) * 255 -loc = rank.equalize(ima, disk(20)) +glob = exposure.equalize(noisy_image) * 255 +loc = rank.equalize(noisy_image, disk(20)) # extract histogram for each image -hist = np.histogram(ima, bins=np.arange(0, 256)) +hist = np.histogram(noisy_image, 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.imshow(noisy_image, interpolation='nearest') plt.axis('off') + plt.subplot(322) plt.plot(hist[1][:-1], hist[0], lw=2) -plt.title('histogram of grey values') +plt.title('Histogram of gray values') + plt.subplot(323) -plt.imshow(glob, cmap=plt.cm.gray, interpolation='nearest') +plt.imshow(glob, 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.title('Histogram of gray values') + plt.subplot(325) -plt.imshow(loc, cmap=plt.cm.gray, interpolation='nearest') +plt.imshow(loc, 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') +plt.title('Histogram of gray 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. +Another way to maximize the number of gray-levels used for an image is to apply +a local auto-leveling, i.e. the gray-value of a pixel is proportionally +remapped between local minimum and local maximum. -The following example shows how local autolevel enhances the camara man picture. +The following example shows how local auto-level enhances the camara man +picture. """ from skimage.filter.rank import autolevel -ima = data.camera() -selem = disk(10) +noisy_image = img_as_ubyte(data.camera()) -auto = autolevel(ima.astype(np.uint16), disk(20)) +auto = autolevel(noisy_image.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.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(1, 2, 2) plt.imshow(auto, cmap=plt.cm.gray) -plt.xlabel('local autolevel') +plt.title('Local autolevel') +plt.axis('off') """ .. 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. +This filter is very sensitive to local outliers, see the little white spot in +the left part of the sky. 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 auto-level 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 auto-level +result. """ -from skimage.filter.rank import percentile_autolevel +from skimage.filter.rank import autolevel_percentile 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) +loc_perc_autolevel0 = autolevel_percentile(image, selem=selem, p0=.00, p1=1.0) +loc_perc_autolevel1 = autolevel_percentile(image, selem=selem, p0=.01, p1=.99) +loc_perc_autolevel2 = autolevel_percentile(image, selem=selem, p0=.05, p1=.95) +loc_perc_autolevel3 = autolevel_percentile(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') +ax0.set_title('Original / auto-level') ax1.imshow( np.hstack((loc_perc_autolevel0, loc_perc_autolevel1)), vmin=0, vmax=255) -ax1.set_title('percentile autolevel 0%,1%') +ax1.set_title('Percentile auto-level 0%,1%') ax2.imshow( np.hstack((loc_perc_autolevel2, loc_perc_autolevel3)), vmin=0, vmax=255) -ax2.set_title('percentile autolevel 5% and 10%') +ax2.set_title('Percentile auto-level 5% and 10%') for ax in axes: ax.axis('off') @@ -289,29 +315,35 @@ for ax in axes: .. 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. +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 +from skimage.filter.rank import enhance_contrast -ima = data.camera() +noisy_image = img_as_ubyte(data.camera()) -enh = morph_contr_enh(ima, disk(5)) +enh = enhance_contrast(noisy_image, 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.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(enh, cmap=plt.cm.gray) -plt.xlabel('local morphlogical contrast enhancement') +plt.title('Local morphological contrast enhancement') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(enh[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') """ @@ -322,24 +354,30 @@ percentile *p0* and *p1* instead of the local minimum and maximum. """ -from skimage.filter.rank import percentile_morph_contr_enh +from skimage.filter.rank import enhance_contrast_percentile -ima = data.camera() +noisy_image = img_as_ubyte(data.camera()) -penh = percentile_morph_contr_enh(ima, disk(5), p0=.1, p1=.9) +penh = enhance_contrast_percentile(noisy_image, 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.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(penh, cmap=plt.cm.gray) -plt.xlabel('local percentile morphlogical\n contrast enhancement') +plt.title('Local percentile morphological\n contrast enhancement') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(penh[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') """ @@ -348,20 +386,20 @@ plt.imshow(penh[200:350, 350:450], cmap=plt.cm.gray) 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 Otsu threshold [1]_ method can be applied locally using the local gray- +level 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`. + Local is much slower than global thresholding. A function for global Otsu + thresholding can be found in : `skimage.filter.threshold_otsu`. -.. [1] http://en.wikipedia.org/wiki/Otsu's_method +.. [4] http://en.wikipedia.org/wiki/Otsu's_method """ @@ -382,27 +420,35 @@ 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.title('Original') plt.colorbar() +plt.axis('off') + plt.subplot(2, 2, 2) plt.imshow(t_loc_otsu, cmap=plt.cm.gray) -plt.xlabel('local Otsu ($radius=%d$)' % radius) +plt.title('Local Otsu ($r=%d$)' % radius) plt.colorbar() +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(p8 >= t_loc_otsu, cmap=plt.cm.gray) -plt.xlabel('original>=local Otsu' % t_glob_otsu) +plt.title('Original >= local Otsu' % t_glob_otsu) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(glob_otsu, cmap=plt.cm.gray) -plt.xlabel('global Otsu ($t=%d$)' % t_glob_otsu) +plt.title('Global Otsu ($t=%d$)' % t_glob_otsu) +plt.axis('off') """ .. image:: PLOT2RST.current_figure -The following example shows how local Otsu's threshold handles a global level -shift applied to a synthetic image . +The following example shows how local Otsu thresholding handles a global level +shift applied to a synthetic image. """ @@ -413,13 +459,18 @@ 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.title('Original') +plt.axis('off') + plt.subplot(1, 2, 2) plt.imshow(m >= t, interpolation='nearest') -plt.xlabel('local Otsu ($radius=%d$)' % radius) +plt.title('Local Otsu ($r=%d$)' % radius) +plt.axis('off') """ @@ -428,7 +479,7 @@ plt.xlabel('local Otsu ($radius=%d$)' % radius) Image morphology ================ -Local maximum and local minimum are the base operators for greylevel +Local maximum and local minimum are the base operators for gray-level morphology. .. note:: @@ -436,33 +487,41 @@ morphology. `skimage.dilate` and `skimage.erode` are equivalent filters (see below for comparison). -Here is an example of the classical morphological greylevel filters: opening, +Here is an example of the classical morphological gray-level filters: opening, closing and morphological gradient. """ from skimage.filter.rank import maximum, minimum, gradient -ima = data.camera() +noisy_image = img_as_ubyte(data.camera()) -closing = maximum(minimum(ima, disk(5)), disk(5)) -opening = minimum(maximum(ima, disk(5)), disk(5)) -grad = gradient(ima, disk(5)) +closing = maximum(minimum(noisy_image, disk(5)), disk(5)) +opening = minimum(maximum(noisy_image, disk(5)), disk(5)) +grad = gradient(noisy_image, 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.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 2) plt.imshow(closing, cmap=plt.cm.gray) -plt.xlabel('greylevel closing') +plt.title('Gray-level closing') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(opening, cmap=plt.cm.gray) -plt.xlabel('greylevel opening') +plt.title('Gray-level opening') +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(grad, cmap=plt.cm.gray) -plt.xlabel('morphological gradient') +plt.title('Morphological gradient') +plt.axis('off') """ @@ -471,13 +530,14 @@ plt.xlabel('morphological gradient') Feature extraction =================== -Local histogram can be exploited to compute local entropy, which is related to +Local histograms 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 +filter returns the minimum number of bits needed to encode local gray-level 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. +`skimage.rank.entropy` returns the local entropy on a given structuring +element. The following example shows applies this filter on 8- and 16-bit +images. .. note:: @@ -492,47 +552,36 @@ 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 +image = data.camera() -ent8 = entropy(a8, disk(5)) # pixel value contain 10x the local entropy -ent16 = entropy(a16, disk(5)) # pixel value contain 1000x the local entropy +plt.figure(figsize=(10, 4)) -# 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.subplot(1, 2, 1) +plt.imshow(image, cmap=plt.cm.gray) +plt.title('Image') plt.colorbar() +plt.axis('off') -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.subplot(1, 2, 2) +plt.imshow(entropy(image, disk(5)), cmap=plt.cm.jet) +plt.title('Entropy') plt.colorbar() +plt.axis('off') """ .. 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. +The central part of the `skimage.rank` filters is build on a sliding window +that updates the local gray-level 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. + +In the following we compare the performance of different implementations +available in `skimage`. """ @@ -583,10 +632,10 @@ def ndi_med(image, n): Comparison between -* `rank.maximum` -* `cmorph.dilate` +* `filter.rank.maximum` +* `morphology.dilate` -on increasing structuring element size +on increasing structuring element size: """ @@ -603,18 +652,18 @@ for r in e_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing element size') -plt.ylabel('time (ms)') -plt.xlabel('element radius') +plt.title('Performance with respect to element size') +plt.ylabel('Time (ms)') +plt.title('Element radius') plt.plot(e_range, rec) -plt.legend(['crank.maximum', 'cmorph.dilate']) +plt.legend(['filter.rank.maximum', 'morphology.dilate']) """ -and increasing image size - .. image:: PLOT2RST.current_figure +and increasing image size: + """ r = 9 @@ -623,7 +672,7 @@ 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') + a = (np.random.random((s, s)) * 256).astype(np.uint8) (rc, ms_rc) = cr_max(a, elem) (rcm, ms_rcm) = cm_dil(a, elem) rec.append((ms_rc, ms_rcm)) @@ -631,11 +680,11 @@ for s in s_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing image size') -plt.ylabel('time (ms)') -plt.xlabel('image size') +plt.title('Performance with respect to image size') +plt.ylabel('Time (ms)') +plt.title('Image size') plt.plot(s_range, rec) -plt.legend(['crank.maximum', 'cmorph.dilate']) +plt.legend(['filter.rank.maximum', 'morphology.dilate']) """ @@ -644,11 +693,11 @@ plt.legend(['crank.maximum', 'cmorph.dilate']) Comparison between: -* `rank.median` -* `ctmf.median_filter` -* `ndimage.percentile` +* `filter.rank.median` +* `filter.median_filter` +* `scipy.ndimage.percentile` -on increasing structuring element size +on increasing structuring element size: """ @@ -666,27 +715,29 @@ for r in e_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing element size') +plt.title('Performance with respect to element size') plt.plot(e_range, rec) -plt.legend(['rank.median', 'ctmf.median_filter', 'ndimage.percentile']) -plt.ylabel('time (ms)') -plt.xlabel('element radius') +plt.legend(['filter.rank.median', 'filter.median_filter', + 'scipy.ndimage.percentile']) +plt.ylabel('Time (ms)') +plt.title('Element radius') """ .. image:: PLOT2RST.current_figure -comparison of outcome of the three methods +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') +plt.title('filter.rank.median vs filtermedian_filter vs scipy.ndimage.percentile') +plt.axis('off') """ .. image:: PLOT2RST.current_figure -and increasing image size +and increasing image size: """ @@ -696,7 +747,7 @@ 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') + a = (np.random.random((s, s)) * 256).astype(np.uint8) (rc, ms_rc) = cr_med(a, elem) rctmf, ms_rctmf = ctmf_med(a, r) rndi, ms_ndi = ndi_med(a, r) @@ -705,11 +756,12 @@ for s in s_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing image size') +plt.title('Performance with respect to image size') plt.plot(s_range, rec) -plt.legend(['rank.median', 'ctmf.median_filter', 'ndimage.percentile']) -plt.ylabel('time (ms)') -plt.xlabel('image size') +plt.legend(['filter.rank.median', 'filter.median_filter', + 'scipy.ndimage.percentile']) +plt.ylabel('Time (ms)') +plt.title('Image size') """ .. image:: PLOT2RST.current_figure diff --git a/doc/examples/plot_16bitbilateral.py b/doc/examples/plot_16bitbilateral.py deleted file mode 100644 index 473fcadd..00000000 --- a/doc/examples/plot_16bitbilateral.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -============================== -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_canny.py b/doc/examples/plot_canny.py index f657b9eb..f8269d70 100644 --- a/doc/examples/plot_canny.py +++ b/doc/examples/plot_canny.py @@ -13,12 +13,15 @@ thresholding on the gradient magnitude. The Canny has three adjustable parameters: the width of the Gaussian (the noisier the image, the greater the width), and the low and high threshold for the hysteresis thresholding. + """ import numpy as np import matplotlib.pyplot as plt from scipy import ndimage + from skimage import filter + # Generate noisy image of a square im = np.zeros((128, 128)) im[32:-32, 32:-32] = 1 @@ -52,6 +55,5 @@ plt.title('Canny filter, $\sigma=3$', fontsize=20) plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, bottom=0.02, left=0.02, right=0.98) - plt.show() diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py new file mode 100755 index 00000000..7fb67046 --- /dev/null +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -0,0 +1,148 @@ +""" +======================================== +Circular and Elliptical Hough Transforms +======================================== + +The Hough transform in its simplest form is a `method to detect +straight lines `__ +but it can also be used to detect circles or ellipses. +The algorithm assumes that the edge is detected and it is robust against +noise or missing points. + +Circle detection +================ + +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 +from skimage.util import img_as_ubyte + + +# Load picture and detect edges +image = img_as_ubyte(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) + 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) + + +""" +Ellipse detection +================= + +In this second example, the aim is to detect the edge of a coffee cup. +Basically, this is a projection of a circle, i.e. an ellipse. +The problem to solve is much more difficult because five parameters have to be +determined, instead of three for circles. + + +Algorithm overview +------------------ + +The algorithm takes two different points belonging to the ellipse. It assumes +that it is the main axis. A loop on all the other points determines how much +an ellipse passes to them. A good match corresponds to high accumulator values. + +A full description of the algorithm can be found in reference [1]_. + +References +---------- +.. [1] Xie, Yonghong, and Qiang Ji. "A new efficient ellipse detection + method." Pattern Recognition, 2002. Proceedings. 16th International + Conference on. Vol. 2. IEEE, 2002 +""" + +import matplotlib.pyplot as plt + +from skimage import data, filter, color +from skimage.transform import hough_ellipse +from skimage.draw import ellipse_perimeter + +# Load picture, convert to grayscale and detect edges +image_rgb = data.coffee()[0:220, 160:420] +image_gray = color.rgb2gray(image_rgb) +edges = filter.canny(image_gray, sigma=2.0, + low_threshold=0.55, high_threshold=0.8) + +# Perform a Hough Transform +# The accuracy corresponds to the bin size of a major axis. +# The value is chosen in order to get a single high accumulator. +# The threshold eliminates low accumulators +result = hough_ellipse(edges, accuracy=20, threshold=250, + min_size=100, max_size=120) +result.sort(order='accumulator') + +# Estimated parameters for the ellipse +best = result[-1] +yc = int(best[1]) +xc = int(best[2]) +a = int(best[3]) +b = int(best[4]) +orientation = best[5] + +# Draw the ellipse on the original image +cy, cx = ellipse_perimeter(yc, xc, a, b, orientation) +image_rgb[cy, cx] = (0, 0, 255) +# Draw the edge (white) and the resulting ellipse (red) +edges = color.gray2rgb(edges) +edges[cy, cx] = (250, 0, 0) + +fig2, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(10, 6)) + +ax1.set_title('Original picture') +ax1.imshow(image_rgb) + +ax2.set_title('Edge (white) and result (red)') +ax2.imshow(edges) + +plt.show() diff --git a/doc/examples/plot_circular_hough_transform.py b/doc/examples/plot_circular_hough_transform.py deleted file mode 100755 index d2f8f2ae..00000000 --- a/doc/examples/plot_circular_hough_transform.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -======================== -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_contours.py b/doc/examples/plot_contours.py index 020d3dfc..d23b2ae5 100644 --- a/doc/examples/plot_contours.py +++ b/doc/examples/plot_contours.py @@ -15,13 +15,12 @@ Cubes: A High Resolution 3D Surface Construction Algorithm. Computer Graphics (SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170). """ - -from skimage import data -from skimage import measure - import numpy as np import matplotlib.pyplot as plt +from skimage import measure + + # Construct some test data x, y = np.ogrid[-np.pi:np.pi:100j, -np.pi:np.pi:100j] r = np.sin(np.exp((np.sin(x)**3 + np.cos(y)**2))) @@ -39,4 +38,3 @@ plt.axis('image') plt.xticks([]) plt.yticks([]) plt.show() - diff --git a/doc/examples/plot_convex_hull.py b/doc/examples/plot_convex_hull.py index 5a577c84..31398e6d 100644 --- a/doc/examples/plot_convex_hull.py +++ b/doc/examples/plot_convex_hull.py @@ -13,12 +13,12 @@ A good overview of the algorithm is given on `Steve Eddin's blog `__. """ - import numpy as np import matplotlib.pyplot as plt from skimage.morphology import convex_hull_image + image = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0, 0], @@ -27,9 +27,24 @@ image = np.array( [0, 1, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=float) -chull = convex_hull_image(image) -image[chull] += 1.7 -image -= -1.7 +original_image = np.copy(image) +chull = convex_hull_image(image) +image[chull] += 1 +# image is now: +#[[ 0. 0. 0. 0. 0. 0. 0. 0. 0.] +# [ 0. 0. 0. 0. 2. 0. 0. 0. 0.] +# [ 0. 0. 0. 2. 1. 2. 0. 0. 0.] +# [ 0. 0. 2. 1. 1. 1. 2. 0. 0.] +# [ 0. 2. 1. 1. 1. 1. 1. 2. 0.] +# [ 0. 0. 0. 0. 0. 0. 0. 0. 0.]] + + +fig = plt.subplots(figsize=(10, 6)) +plt.subplot(1, 2, 1) +plt.title('Original picture') +plt.imshow(original_image, cmap=plt.cm.gray, interpolation='nearest') +plt.subplot(1, 2, 2) +plt.title('Transformed picture') plt.imshow(image, cmap=plt.cm.gray, interpolation='nearest') plt.show() diff --git a/doc/examples/plot_corner.py b/doc/examples/plot_corner.py index 30f1f0cf..a3233188 100644 --- a/doc/examples/plot_corner.py +++ b/doc/examples/plot_corner.py @@ -10,7 +10,6 @@ position of corners. .. [2] http://en.wikipedia.org/wiki/Interest_point_detection """ - from matplotlib import pyplot as plt from skimage import data @@ -18,6 +17,7 @@ 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)) diff --git a/doc/examples/plot_daisy.py b/doc/examples/plot_daisy.py index af78103d..fbde5c8d 100644 --- a/doc/examples/plot_daisy.py +++ b/doc/examples/plot_daisy.py @@ -11,7 +11,6 @@ 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 diff --git a/doc/examples/plot_denoise.py b/doc/examples/plot_denoise.py index debb2ea6..200036ae 100644 --- a/doc/examples/plot_denoise.py +++ b/doc/examples/plot_denoise.py @@ -25,13 +25,13 @@ 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 import data, img_as_float from skimage.filter import denoise_tv_chambolle, denoise_bilateral + lena = img_as_float(data.lena()) lena = lena[220:300, 220:320] diff --git a/doc/examples/plot_edge_filter.py b/doc/examples/plot_edge_filter.py new file mode 100644 index 00000000..b43aae38 --- /dev/null +++ b/doc/examples/plot_edge_filter.py @@ -0,0 +1,31 @@ +""" +============== +Edge operators +============== + +Edge operators are used in image processing within edge detection algorithms. +They are discrete differentiation operators, computing an approximation of the +gradient of the image intensity function. + +""" +import matplotlib.pyplot as plt + +from skimage.data import camera +from skimage.filter import roberts, sobel + + +image = camera() +edge_roberts = roberts(image) +edge_sobel = sobel(image) + +fig, (ax0, ax1) = plt.subplots(ncols=2) + +ax0.imshow(edge_roberts, cmap=plt.cm.gray) +ax0.set_title('Roberts Edge Detection') +ax0.axis('off') + +ax1.imshow(edge_sobel, cmap=plt.cm.gray) +ax1.set_title('Sobel Edge Detection') +ax1.axis('off') + +plt.show() diff --git a/doc/examples/plot_entropy.py b/doc/examples/plot_entropy.py index f019d79c..9f208c73 100644 --- a/doc/examples/plot_entropy.py +++ b/doc/examples/plot_entropy.py @@ -1,44 +1,32 @@ """ -=================== +======= Entropy -=================== +======= +Image entropy is a quantity which is used to describe the amount of information +coded in an image. """ +import matplotlib.pyplot as plt + from skimage import data from skimage.filter.rank import entropy from skimage.morphology import disk -import numpy as np -import matplotlib.pyplot as plt +from skimage.util import img_as_ubyte -# 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 +image = img_as_ubyte(data.camera()) -# display results -plt.figure(figsize=(10, 10)) +fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10, 4)) -plt.subplot(2,2,1) -plt.imshow(a8, cmap=plt.cm.gray) -plt.xlabel('8-bit image') -plt.colorbar() +img0 = ax0.imshow(image, cmap=plt.cm.gray) +ax0.set_title('Image') +ax0.axis('off') +plt.colorbar(img0, ax=ax0) -plt.subplot(2,2,2) -plt.imshow(ent8, cmap=plt.cm.jet) -plt.xlabel('entropy*10') -plt.colorbar() +img1 = ax1.imshow(entropy(image, disk(5)), cmap=plt.cm.jet) +ax1.set_title('Entropy') +ax1.axis('off') +plt.colorbar(img1, ax=ax1) -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 3569c4ee..52121e3a 100644 --- a/doc/examples/plot_equalize.py +++ b/doc/examples/plot_equalize.py @@ -17,13 +17,12 @@ that fall within the 2nd and 98th percentiles [2]_. .. [2] http://homepages.inf.ed.ac.uk/rbf/HIPR2/stretch.htm """ +import matplotlib.pyplot as plt +import numpy as np 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. diff --git a/doc/examples/plot_gabor.py b/doc/examples/plot_gabor.py new file mode 100644 index 00000000..db1ff7d6 --- /dev/null +++ b/doc/examples/plot_gabor.py @@ -0,0 +1,135 @@ +""" +============================================= +Gabor filter banks for texture classification +============================================= + +In this example, we will see how to classify textures based on Gabor filter +banks. Frequency and orientation representations of the Gabor filter are similar +to those of the human visual system. + +The images are filtered using the real parts of various different Gabor filter +kernels. The mean and variance of the filtered images are then used as features +for classification, which is based on the least squared error for simplicity. + +""" +from __future__ import print_function + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +from scipy import ndimage as nd + +from skimage import data +from skimage.util import img_as_float +from skimage.filter import gabor_kernel + + +matplotlib.rcParams['font.size'] = 9 + + +def compute_feats(image, kernels): + feats = np.zeros((len(kernels), 2), dtype=np.double) + for k, kernel in enumerate(kernels): + filtered = nd.convolve(image, kernel, mode='wrap') + feats[k, 0] = filtered.mean() + feats[k, 1] = filtered.var() + return feats + + +def match(feats, ref_feats): + min_error = np.inf + min_i = None + for i in range(ref_feats.shape[0]): + error = np.sum((feats - ref_feats[i, :])**2) + if error < min_error: + min_error = error + min_i = i + return min_i + + +# prepare filter bank kernels +kernels = [] +for theta in range(4): + theta = theta / 4. * np.pi + for sigma in (1, 3): + for frequency in (0.05, 0.25): + kernel = np.real(gabor_kernel(frequency, theta=theta, + sigma_x=sigma, sigma_y=sigma)) + kernels.append(kernel) + + +shrink = (slice(0, None, 3), slice(0, None, 3)) +brick = img_as_float(data.load('brick.png'))[shrink] +grass = img_as_float(data.load('grass.png'))[shrink] +wall = img_as_float(data.load('rough-wall.png'))[shrink] +image_names = ('brick', 'grass', 'wall') +images = (brick, grass, wall) + +# prepare reference features +ref_feats = np.zeros((3, len(kernels), 2), dtype=np.double) +ref_feats[0, :, :] = compute_feats(brick, kernels) +ref_feats[1, :, :] = compute_feats(grass, kernels) +ref_feats[2, :, :] = compute_feats(wall, kernels) + +print('Rotated images matched against references using Gabor filter banks:') + +print('original: brick, rotated: 30deg, match result: ', end='') +feats = compute_feats(nd.rotate(brick, angle=190, reshape=False), kernels) +print(image_names[match(feats, ref_feats)]) + +print('original: brick, rotated: 70deg, match result: ', end='') +feats = compute_feats(nd.rotate(brick, angle=70, reshape=False), kernels) +print(image_names[match(feats, ref_feats)]) + +print('original: grass, rotated: 145deg, match result: ', end='') +feats = compute_feats(nd.rotate(grass, angle=145, reshape=False), kernels) +print(image_names[match(feats, ref_feats)]) + + +def power(image, kernel): + # Normalize images for better comparison. + image = (image - image.mean()) / image.std() + return np.sqrt(nd.convolve(image, np.real(kernel), mode='wrap')**2 + + nd.convolve(image, np.imag(kernel), mode='wrap')**2) + +# Plot a selection of the filter bank kernels and their responses. +results = [] +kernel_params = [] +for theta in (0, 1): + theta = theta / 4. * np.pi + for frequency in (0.1, 0.4): + kernel = gabor_kernel(frequency, theta=theta) + params = 'theta=%d,\nfrequency=%.2f' % (theta * 180 / np.pi, frequency) + kernel_params.append(params) + # Save kernel and the power image for each image + results.append((kernel, [power(img, kernel) for img in images])) + +fig, axes = plt.subplots(nrows=5, ncols=4, figsize=(9, 6)) +plt.gray() + +fig.suptitle('Image responses for Gabor filter kernels', fontsize=15) + +axes[0][0].axis('off') + +# Plot original images +for label, img, ax in zip(image_names, images, axes[0][1:]): + ax.imshow(img) + ax.set_title(label) + ax.axis('off') + +for label, (kernel, powers), ax_row in zip(kernel_params, results, axes[1:]): + # Plot Gabor kernel + ax = ax_row[0] + ax.imshow(np.real(kernel), interpolation='nearest') + ax.set_ylabel(label) + ax.set_xticks([]) + ax.set_yticks([]) + + # Plot Gabor responses with the contrast normalized for each filter + vmin = np.min(powers) + vmax = np.max(powers) + for patch, ax in zip(powers, ax_row[1:]): + ax.imshow(patch, vmin=vmin, vmax=vmax) + ax.axis('off') + +plt.show() diff --git a/doc/examples/plot_gabors_from_lena.py b/doc/examples/plot_gabors_from_lena.py index 7d22f676..207230aa 100644 --- a/doc/examples/plot_gabors_from_lena.py +++ b/doc/examples/plot_gabors_from_lena.py @@ -3,8 +3,6 @@ Gabors / Primary Visual Cortex "Simple Cells" from Lena ======================================================= -(under construction) - How to build a (bio-plausible) "sparse" dictionary (or 'codebook', or 'filterbank') for e.g. image classification without any fancy math and with just standard python scientific libraries? @@ -37,7 +35,6 @@ is not rocket science. Interaction, and Functional Architecture in the Cat's Visual Cortex, J. Physiol. 160 pp. 106-154 1962 """ - import numpy as np from scipy.cluster.vq import kmeans2 from scipy import ndimage as ndi diff --git a/doc/examples/plot_glcm.py b/doc/examples/plot_glcm.py index 2c848523..7ee54d57 100644 --- a/doc/examples/plot_glcm.py +++ b/doc/examples/plot_glcm.py @@ -19,10 +19,11 @@ this example) would be to train a classifier, such as logistic regression, to label image patches from new images. """ +import matplotlib.pyplot as plt from skimage.feature import greycomatrix, greycoprops from skimage import data -import matplotlib.pyplot as plt + PATCH_SIZE = 21 diff --git a/doc/examples/plot_hog.py b/doc/examples/plot_hog.py index 81aa202a..4a339a1b 100644 --- a/doc/examples/plot_hog.py +++ b/doc/examples/plot_hog.py @@ -1,4 +1,4 @@ -r''' +""" =============================== Histogram of Oriented Gradients =============================== @@ -77,12 +77,13 @@ References .. [2] David G. Lowe, "Distinctive image features from scale-invariant keypoints," International Journal of Computer Vision, 60, 2 (2004), pp. 91-110. -''' + +""" +import matplotlib.pyplot as plt from skimage.feature import hog from skimage import data, color, exposure -import matplotlib.pyplot as plt image = color.rgb2gray(data.lena()) diff --git a/doc/examples/plot_holes_and_peaks.py b/doc/examples/plot_holes_and_peaks.py index a9c3a7fe..e40b777e 100644 --- a/doc/examples/plot_holes_and_peaks.py +++ b/doc/examples/plot_holes_and_peaks.py @@ -10,6 +10,7 @@ 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 diff --git a/doc/examples/plot_hough_transform.py b/doc/examples/plot_hough_transform.py deleted file mode 100644 index b74132da..00000000 --- a/doc/examples/plot_hough_transform.py +++ /dev/null @@ -1,132 +0,0 @@ -r''' -=============== -Hough transform -=============== - -The Hough transform in its simplest form is a `method to detect -straight lines `__. - -In the following example, we construct an image with a line -intersection. We then use the Hough transform to explore a parameter -space for straight lines that may run through the image. - -Algorithm overview ------------------- - -Usually, lines are parameterised as :math:`y = mx + c`, with a -gradient :math:`m` and y-intercept `c`. However, this would mean that -:math:`m` goes to infinity for vertical lines. Instead, we therefore -construct a segment perpendicular to the line, leading to the origin. -The line is represented by the length of that segment, :math:`r`, and -the angle it makes with the x-axis, :math:`\theta`. - -The Hough transform constructs a histogram array representing the -parameter space (i.e., an :math:`M \times N` matrix, for :math:`M` -different values of the radius and :math:`N` different values of -:math:`\theta`). For each parameter combination, :math:`r` and -:math:`\theta`, we then find the number of non-zero pixels in the -input image that would fall close to the corresponding line, and -increment the array at position :math:`(r, \theta)` appropriately. - -We can think of each non-zero pixel "voting" for potential line -candidates. The local maxima in the resulting histogram indicates the -parameters of the most probably lines. In our example, the maxima -occur at 45 and 135 degrees, corresponding to the normal vector -angles of each line. - -Another approach is the Progressive Probabilistic Hough Transform -[1]_. It is based on the assumption that using a random subset of -voting points give a good approximation to the actual result, and that -lines can be extracted during the voting process by walking along -connected components. This returns the beginning and end of each -line segment, which is useful. - -The function `probabilistic_hough` has three parameters: a general -threshold that is applied to the Hough accumulator, a minimum line -length and the line gap that influences line merging. In the example -below, we find lines longer than 10 with a gap less than 3 pixels. - -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. - -.. [2] Duda, R. O. and P. E. Hart, "Use of the Hough Transformation to - Detect Lines and Curves in Pictures," Comm. ACM, Vol. 15, - pp. 11-15 (January, 1972) - -''' - -from skimage.transform import hough, hough_peaks, probabilistic_hough -from skimage.filter import canny -from skimage import data - -import numpy as np -import matplotlib.pyplot as plt - -# Construct test image - -image = np.zeros((100, 100)) - - -# Classic straight-line Hough transform - -idx = np.arange(25, 75) -image[idx[::-1], idx] = 255 -image[idx, idx] = 255 - -h, theta, d = hough(image) - -plt.figure(figsize=(8, 4)) - -plt.subplot(131) -plt.imshow(image, cmap=plt.cm.gray) -plt.title('Input image') - -plt.subplot(132) -plt.imshow(np.log(1 + h), - extent=[np.rad2deg(theta[-1]), np.rad2deg(theta[0]), - d[-1], d[0]], - cmap=plt.cm.gray, aspect=1/1.5) -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 - -image = data.camera() -edges = canny(image, 2, 1, 25) -lines = probabilistic_hough(edges, threshold=10, line_length=5, line_gap=3) - -plt.figure(figsize=(8, 3)) - -plt.subplot(131) -plt.imshow(image, cmap=plt.cm.gray) -plt.title('Input image') - -plt.subplot(132) -plt.imshow(edges, cmap=plt.cm.gray) -plt.title('Canny edges') - -plt.subplot(133) -plt.imshow(edges * 0) - -for line in lines: - p0, p1 = line - plt.plot((p0[0], p1[0]), (p0[1], p1[1])) - -plt.title('Probabilistic Hough') -plt.axis('image') -plt.show() diff --git a/doc/examples/plot_ihc_color_separation.py b/doc/examples/plot_ihc_color_separation.py new file mode 100644 index 00000000..f89f28fe --- /dev/null +++ b/doc/examples/plot_ihc_color_separation.py @@ -0,0 +1,72 @@ +""" +============================================== +Immunohistochemical staining colors separation +============================================== + +In this example we separate the immunohistochemical (IHC) staining from the +hematoxylin counterstaining. The separation is achieved with the method +described in [1]_, known as "color deconvolution". + +The IHC staining expression of the FHL2 protein is here revealed with +Diaminobenzidine (DAB) which gives a brown color. + + +.. [1] A. C. Ruifrok and D. A. Johnston, "Quantification of histochemical + staining by color deconvolution.," Analytical and quantitative + cytology and histology / the International Academy of Cytology [and] + American Society of Cytology, vol. 23, no. 4, pp. 291-9, Aug. 2001. + +""" +import matplotlib.pyplot as plt + +from skimage import data +from skimage.color import rgb2hed + + +ihc_rgb = data.immunohistochemistry() +ihc_hed = rgb2hed(ihc_rgb) + +fig, axes = plt.subplots(2, 2, figsize=(7, 6)) +ax0, ax1, ax2, ax3 = axes.ravel() + +ax0.imshow(ihc_rgb) +ax0.set_title("Original image") + +ax1.imshow(ihc_hed[:, :, 0], cmap=plt.cm.gray) +ax1.set_title("Hematoxylin") + +ax2.imshow(ihc_hed[:, :, 1], cmap=plt.cm.gray) +ax2.set_title("Eosin") + +ax3.imshow(ihc_hed[:, :, 2], cmap=plt.cm.gray) +ax3.set_title("DAB") + +for ax in axes.ravel(): + ax.axis('off') + +fig.subplots_adjust(hspace=0.3) + + +""" +.. image:: PLOT2RST.current_figure + +Now we can easily manipulate the hematoxylin and DAB "channels": +""" +import numpy as np + +from skimage.exposure import rescale_intensity + +# Rescale hematoxylin and DAB signals and give them a fluorescence look +h = rescale_intensity(ihc_hed[:, :, 0], out_range=(0, 1)) +d = rescale_intensity(ihc_hed[:, :, 2], out_range=(0, 1)) +zdh = np.dstack((np.zeros_like(h), d, h)) + +plt.figure() +plt.imshow(zdh) +plt.title("Stain separated image (rescaled)") +plt.axis('off') +plt.show() + +""" +.. image:: PLOT2RST.current_figure +""" diff --git a/doc/examples/plot_join_segmentations.py b/doc/examples/plot_join_segmentations.py index 02600ba6..8ccc5038 100644 --- a/doc/examples/plot_join_segmentations.py +++ b/doc/examples/plot_join_segmentations.py @@ -8,59 +8,52 @@ 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.color import label2rgb +from skimage import data, img_as_float -from skimage import data - -coins = data.coins() +coins = img_as_float(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 +markers[coins < 30.0 / 255] = background +markers[coins > 150.0 / 255] = 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) +seg2 = slic(coins, n_segments=117, max_iter=160, sigma=1, compactness=0.75, + multichannel=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') + +color1 = label2rgb(seg1, image=coins, bg_label=0) +axes[1].imshow(color1, interpolation='nearest') axes[1].set_title('Sobel+Watershed') -axes[2].imshow(seg2, cmap=random_cmap(seg2), interpolation='nearest') + +color2 = label2rgb(seg2, image=coins, image_alpha=0.5) +axes[2].imshow(color2, interpolation='nearest') axes[2].set_title('SLIC superpixels') -axes[3].imshow(segj, cmap=random_cmap(segj), interpolation='nearest') + +color3 = label2rgb(segj, image=coins, image_alpha=0.5) +axes[3].imshow(color3, interpolation='nearest') axes[3].set_title('Join') for ax in axes: diff --git a/doc/examples/plot_label.py b/doc/examples/plot_label.py index 8c46cb8e..0542d68c 100644 --- a/doc/examples/plot_label.py +++ b/doc/examples/plot_label.py @@ -12,7 +12,6 @@ steps are applied: 4. Measure image regions to filter small objects """ - import numpy as np import matplotlib.pyplot as plt import matplotlib.patches as mpatches @@ -22,6 +21,7 @@ from skimage.filter import threshold_otsu from skimage.segmentation import clear_border from skimage.morphology import label, closing, square from skimage.measure import regionprops +from skimage.color import label2rgb image = data.coins()[50:-50, 50:-50] @@ -38,9 +38,10 @@ clear_border(cleared) label_image = label(cleared) borders = np.logical_xor(bw, cleared) label_image[borders] = -1 +image_label_overlay = label2rgb(label_image, image=image) fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) -ax.imshow(label_image, cmap='jet') +ax.imshow(image_label_overlay) for region in regionprops(label_image, ['Area', 'BoundingBox']): diff --git a/doc/examples/plot_line_hough_transform.py b/doc/examples/plot_line_hough_transform.py new file mode 100644 index 00000000..cd0ae008 --- /dev/null +++ b/doc/examples/plot_line_hough_transform.py @@ -0,0 +1,130 @@ +r""" +============================= +Straight line Hough transform +============================= + +The Hough transform in its simplest form is a `method to detect straight lines +`__. + +In the following example, we construct an image with a line intersection. We +then use the Hough transform to explore a parameter space for straight lines +that may run through the image. + +Algorithm overview +------------------ + +Usually, lines are parameterised as :math:`y = mx + c`, with a gradient +:math:`m` and y-intercept `c`. However, this would mean that :math:`m` goes to +infinity for vertical lines. Instead, we therefore construct a segment +perpendicular to the line, leading to the origin. The line is represented by the +length of that segment, :math:`r`, and the angle it makes with the x-axis, +:math:`\theta`. + +The Hough transform constructs a histogram array representing the parameter +space (i.e., an :math:`M \times N` matrix, for :math:`M` different values of the +radius and :math:`N` different values of :math:`\theta`). For each parameter +combination, :math:`r` and :math:`\theta`, we then find the number of non-zero +pixels in the input image that would fall close to the corresponding line, and +increment the array at position :math:`(r, \theta)` appropriately. + +We can think of each non-zero pixel "voting" for potential line candidates. The +local maxima in the resulting histogram indicates the parameters of the most +probably lines. In our example, the maxima occur at 45 and 135 degrees, +corresponding to the normal vector angles of each line. + +Another approach is the Progressive Probabilistic Hough Transform [1]_. It is +based on the assumption that using a random subset of voting points give a good +approximation to the actual result, and that lines can be extracted during the +voting process by walking along connected components. This returns the beginning +and end of each line segment, which is useful. + +The function `probabilistic_hough` has three parameters: a general threshold +that is applied to the Hough accumulator, a minimum line length and the line gap +that influences line merging. In the example below, we find lines longer than 10 +with a gap less than 3 pixels. + +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. + +.. [2] Duda, R. O. and P. E. Hart, "Use of the Hough Transformation to + Detect Lines and Curves in Pictures," Comm. ACM, Vol. 15, + pp. 11-15 (January, 1972) + +""" + +from skimage.transform import (hough_line, hough_line_peaks, + probabilistic_hough_line) +from skimage.filter import canny +from skimage import data + +import numpy as np +import matplotlib.pyplot as plt + +# Construct test image + +image = np.zeros((100, 100)) + + +# Classic straight-line Hough transform + +idx = np.arange(25, 75) +image[idx[::-1], idx] = 255 +image[idx, idx] = 255 + +h, theta, d = hough_line(image) + +plt.figure(figsize=(8, 4)) + +plt.subplot(131) +plt.imshow(image, cmap=plt.cm.gray) +plt.title('Input image') + +plt.subplot(132) +plt.imshow(np.log(1 + h), + extent=[np.rad2deg(theta[-1]), np.rad2deg(theta[0]), + d[-1], d[0]], + cmap=plt.cm.gray, aspect=1/1.5) +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_line_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 + +image = data.camera() +edges = canny(image, 2, 1, 25) +lines = probabilistic_hough_line(edges, threshold=10, line_length=5, line_gap=3) + +plt.figure(figsize=(8, 3)) + +plt.subplot(131) +plt.imshow(image, cmap=plt.cm.gray) +plt.title('Input image') + +plt.subplot(132) +plt.imshow(edges, cmap=plt.cm.gray) +plt.title('Canny edges') + +plt.subplot(133) +plt.imshow(edges * 0) + +for line in lines: + p0, p1 = line + plt.plot((p0[0], p1[0]), (p0[1], p1[1])) + +plt.title('Probabilistic Hough') +plt.axis('image') +plt.show() diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index 85fd5b95..c0169cc1 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -4,24 +4,159 @@ 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. +Binary Pattern). LBP looks at points surrounding a central point and tests +whether the surrounding points are greater than or less than the central point +(i.e. gives a binary result). + +Before trying out LBP on an image, it helps to look at a schematic of LBPs. +The below code is just used to plot the schematic. +""" +from __future__ import print_function +import numpy as np +import matplotlib.pyplot as plt + + +METHOD = 'uniform' +plt.rcParams['font.size'] = 9 + + +def plot_circle(ax, center, radius, color): + circle = plt.Circle(center, radius, facecolor=color, edgecolor='0.5') + ax.add_patch(circle) + + +def plot_lbp_model(ax, binary_values): + """Draw the schematic for a local binary pattern.""" + # Geometry spec + theta = np.deg2rad(45) + R = 1 + r = 0.15 + w = 1.5 + gray = '0.5' + + # Draw the central pixel. + plot_circle(ax, (0, 0), radius=r, color=gray) + # Draw the surrounding pixels. + for i, facecolor in enumerate(binary_values): + x = R * np.cos(i * theta) + y = R * np.sin(i * theta) + plot_circle(ax, (x, y), radius=r, color=str(facecolor)) + + # Draw the pixel grid. + for x in np.linspace(-w, w, 4): + ax.axvline(x, color=gray) + ax.axhline(x, color=gray) + + # Tweak the layout. + ax.axis('image') + ax.axis('off') + size = w + 0.2 + ax.set_xlim(-size, size) + ax.set_ylim(-size, size) + + +fig, axes = plt.subplots(ncols=5, figsize=(7, 2)) + +titles = ['flat', 'flat', 'edge', 'corner', 'non-uniform'] + +binary_patterns = [np.zeros(8), + np.ones(8), + np.hstack([np.ones(4), np.zeros(4)]), + np.hstack([np.zeros(3), np.ones(5)]), + [1, 0, 0, 1, 1, 1, 0, 0]] + +for ax, values, name in zip(axes, binary_patterns, titles): + plot_lbp_model(ax, values) + ax.set_title(name) + +""" +.. image:: PLOT2RST.current_figure + +The figure above shows example results with black (or white) representing +pixels that are less (or more) intense than the central pixel. When surrounding +pixels are all black or all white, then that image region is flat (i.e. +featureless). Groups of continuous black or white pixels are considered +"uniform" patterns that can be interpreted as corners or edges. If pixels +switch back-and-forth between black and white pixels, the pattern is considered +"non-uniform". + +When using LBP to detect texture, you measure a collection of LBPs over an +image patch and look at the distribution of these LBPs. Lets apply LBP to +a brick texture. """ -import numpy as np -import matplotlib -import matplotlib.pyplot as plt -import scipy.ndimage as nd -import skimage.feature as ft +from skimage.transform import rotate +from skimage.feature import local_binary_pattern from skimage import data - +from skimage.color import label2rgb # settings for LBP -METHOD = 'uniform' -P = 16 -R = 2 -matplotlib.rcParams['font.size'] = 9 +radius = 3 +n_points = 8 * radius + + +def overlay_labels(image, lbp, labels): + mask = np.logical_or.reduce([lbp == each for each in labels]) + return label2rgb(mask, image=image, bg_label=0, alpha=0.5) + + +def highlight_bars(bars, indexes): + for i in indexes: + bars[i].set_facecolor('r') + + +image = data.load('brick.png') +lbp = local_binary_pattern(image, n_points, radius, METHOD) + +def hist(ax, lbp): + n_bins = lbp.max() + 1 + return ax.hist(lbp.ravel(), normed=True, bins=n_bins, range=(0, n_bins), + facecolor='0.5') + +# plot histograms of LBP of textures +fig, (ax_img, ax_hist) = plt.subplots(nrows=2, ncols=3, figsize=(9, 6)) +plt.gray() + +titles = ('edge', 'flat', 'corner') +w = width = radius - 1 +edge_labels = range(n_points // 2 - w, n_points // 2 + w + 1) +flat_labels = list(range(0, w + 1)) + list(range(n_points - w, n_points + 2)) +i_14 = n_points // 4 # 1/4th of the histogram +i_34 = 3 * (n_points // 4) # 3/4th of the histogram +corner_labels = (list(range(i_14 - w, i_14 + w + 1)) + + list(range(i_34 - w, i_34 + w + 1))) + +label_sets = (edge_labels, flat_labels, corner_labels) + +for ax, labels in zip(ax_img, label_sets): + ax.imshow(overlay_labels(image, lbp, labels)) + +for ax, labels, name in zip(ax_hist, label_sets, titles): + counts, _, bars = hist(ax, lbp) + highlight_bars(bars, labels) + ax.set_ylim(ymax=np.max(counts[:-1])) + ax.set_xlim(xmax=n_points + 2) + ax.set_title(name) + +ax_hist[0].set_ylabel('Percentage') +for ax in ax_img: + ax.axis('off') + + +""" +.. image:: PLOT2RST.current_figure + +The above plot highlights flat, edge-like, and corner-like regions of the +image. + +The histogram of the LBP result is a good measure to classify textures. Here, +we test the histogram distributions against each other using the +Kullback-Leibler-Divergence. +""" + +# settings for LBP +radius = 2 +n_points = 8 * radius def kullback_leibler_divergence(p, q): @@ -34,11 +169,12 @@ def kullback_leibler_divergence(p, q): 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)) + lbp = local_binary_pattern(img, n_points, radius, METHOD) + n_bins = lbp.max() + 1 + hist, _ = np.histogram(lbp, normed=True, bins=n_bins, range=(0, n_bins)) for name, ref in refs.items(): - ref_hist, _ = np.histogram(ref, normed=True, bins=P + 2, - range=(0, P + 2)) + ref_hist, _ = np.histogram(ref, normed=True, bins=n_bins, + range=(0, n_bins)) score = kullback_leibler_divergence(hist, ref_hist) if score < best_score: best_score = score @@ -51,19 +187,19 @@ 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) + 'brick': local_binary_pattern(brick, n_points, radius, METHOD), + 'grass': local_binary_pattern(grass, n_points, radius, METHOD), + 'wall': local_binary_pattern(wall, n_points, radius, 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)) +print('Rotated images matched against references using LBP:') +print('original: brick, rotated: 30deg, match result: ', + match(refs, rotate(brick, angle=30, resize=False))) +print('original: brick, rotated: 70deg, match result: ', + match(refs, rotate(brick, angle=70, resize=False))) +print('original: grass, rotated: 145deg, match result: ', + match(refs, rotate(grass, angle=145, resize=False))) # plot histograms of LBP of textures fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(nrows=2, ncols=3, @@ -72,16 +208,20 @@ plt.gray() ax1.imshow(brick) ax1.axis('off') -ax4.hist(refs['brick'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +hist(ax4, refs['brick']) ax4.set_ylabel('Percentage') ax2.imshow(grass) ax2.axis('off') -ax5.hist(refs['grass'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +hist(ax5, refs['grass']) 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)) +hist(ax6, refs['wall']) + +""" +.. image:: PLOT2RST.current_figure +""" plt.show() diff --git a/doc/examples/plot_local_equalize.py b/doc/examples/plot_local_equalize.py index 33b2c2e4..1fd8325f 100644 --- a/doc/examples/plot_local_equalize.py +++ b/doc/examples/plot_local_equalize.py @@ -1,27 +1,32 @@ """ -=============================== +============================ 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. +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. + +References +---------- .. [1] http://en.wikipedia.org/wiki/Histogram_equalization .. [2] http://en.wikipedia.org/wiki/Adaptive_histogram_equalization """ +import numpy as np +import matplotlib.pyplot as plt from skimage import data from skimage.util.dtype import dtype_range +from skimage.util import img_as_ubyte from skimage import exposure from skimage.morphology import disk - -import matplotlib.pyplot as plt - -import numpy as np from skimage.filter import rank @@ -52,7 +57,7 @@ def plot_img_and_hist(img, axes, bins=256): # Load an example image -img = data.moon() +img = img_as_ubyte(data.moon()) # Contrast stretching p2 = np.percentile(img, 2) diff --git a/doc/examples/plot_local_otsu.py b/doc/examples/plot_local_otsu.py index 968ce6e1..3a321780 100644 --- a/doc/examples/plot_local_otsu.py +++ b/doc/examples/plot_local_otsu.py @@ -1,14 +1,16 @@ """ -===================== +==================== 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. +==================== + +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. +.. note: local is much slower than global thresholding .. [1] http://en.wikipedia.org/wiki/Otsu's_method @@ -16,12 +18,12 @@ The example compares the local threshold with the global threshold. 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 +from skimage.morphology import disk +from skimage.filter import threshold_otsu, rank +from skimage.util import img_as_ubyte -p8 = data.page() +p8 = img_as_ubyte(data.page()) radius = 10 selem = disk(radius) @@ -42,8 +44,8 @@ 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.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.xlabel('global Otsu ($t = %d$)' % t_glob_otsu) plt.show() diff --git a/doc/examples/plot_marching_cubes.py b/doc/examples/plot_marching_cubes.py new file mode 100644 index 00000000..5dad1680 --- /dev/null +++ b/doc/examples/plot_marching_cubes.py @@ -0,0 +1,55 @@ +""" +============== +Marching Cubes +============== + +Marching cubes is an algorithm to extract a 2D surface mesh from a 3D volume. +This can be conceptualized as a 3D generalization of isolines on topographical +or weather maps. It works by iterating across the volume, looking for regions +which cross the level of interest. If such regions are found, triangulations +are generated and added to an output mesh. The final result is a set of +vertices and a set of triangular faces. + +The algorithm requires a data volume and an isosurface value. For example, in +CT imaging Hounsfield units of +700 to +3000 represent bone. So, one potential +input would be a reconstructed CT set of data and the value +700, to extract +a mesh for regions of bone or bone-like density. + +This implementation also works correctly on anisotropic datasets, where the +voxel spacing is not equal for every spatial dimension, through use of the +`spacing` kwarg. + +""" +import numpy as np +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d.art3d import Poly3DCollection + +from skimage import measure +from skimage.draw import ellipsoid + +# Generate a level set about zero of two identical ellipsoids in 3D +ellip_base = ellipsoid(6, 10, 16, levelset=True) +ellip_double = np.concatenate((ellip_base[:-1, ...], + ellip_base[2:, ...]), axis=0) + +# Use marching cubes to obtain the surface mesh of these ellipsoids +verts, faces = measure.marching_cubes(ellip_double, 0) + +# Display resulting triangular mesh using Matplotlib. This can also be done +# with mayavi (see skimage.measure.marching_cubes docstring). +fig = plt.figure(figsize=(10, 12)) +ax = fig.add_subplot(111, projection='3d') + +# Fancy indexing: `verts[faces]` to generate a collection of triangles +mesh = Poly3DCollection(verts[faces]) +ax.add_collection3d(mesh) + +ax.set_xlabel("x-axis: a = 6 per ellipsoid") +ax.set_ylabel("y-axis: b = 10") +ax.set_zlabel("z-axis: c = 16") + +ax.set_xlim(0, 24) # a = 6 (times two for 2nd ellipsoid) +ax.set_ylim(0, 20) # b = 10 +ax.set_zlim(0, 32) # c = 16 + +plt.show() diff --git a/doc/examples/plot_marked_watershed.py b/doc/examples/plot_marked_watershed.py index e97280c7..d7f2c354 100644 --- a/doc/examples/plot_marked_watershed.py +++ b/doc/examples/plot_marked_watershed.py @@ -1,7 +1,7 @@ """ -================================ +=============================== Markers for watershed transform -================================ +=============================== The watershed is a classical algorithm used for **segmentation**, that is, for separating different objects in an image. @@ -16,13 +16,14 @@ See Wikipedia_ for more details on the algorithm. 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 +from skimage.util import img_as_ubyte -image = data.camera() + +image = img_as_ubyte(data.camera()) # denoise image denoised = rank.median(image, disk(2)) diff --git a/doc/examples/plot_matching.py b/doc/examples/plot_matching.py new file mode 100644 index 00000000..bae6d6e2 --- /dev/null +++ b/doc/examples/plot_matching.py @@ -0,0 +1,144 @@ +""" +============================ +Robust matching using RANSAC +============================ + +In this simplified example we first generate two synthetic images as if they +were taken from different view points. + +In the next step we find interest points in both images and find +correspondences based on a weighted sum of squared differences of a small +neighborhood around them. Note, that this measure is only robust towards +linear radiometric and not geometric distortions and is thus only usable with +slight view point changes. + +After finding the correspondences we end up having a set of source and +destination coordinates which can be used to estimate the geometric +transformation between both images. However, many of the correspondences are +faulty and simply estimating the parameter set with all coordinates is not +sufficient. Therefore, the RANSAC algorithm is used on top of the normal model +to robustly estimate the parameter set by detecting outliers. + +""" +from __future__ import print_function + +import numpy as np +from matplotlib import pyplot as plt + +from skimage import data +from skimage.util import img_as_float +from skimage.feature import corner_harris, corner_subpix, corner_peaks +from skimage.transform import warp, AffineTransform +from skimage.exposure import rescale_intensity +from skimage.color import rgb2gray +from skimage.measure import ransac + + +# generate synthetic checkerboard image and add gradient for the later matching +checkerboard = img_as_float(data.checkerboard()) +img_orig = np.zeros(list(checkerboard.shape) + [3]) +img_orig[..., 0] = checkerboard +gradient_r, gradient_c = np.mgrid[0:img_orig.shape[0], + 0:img_orig.shape[1]] / float(img_orig.shape[0]) +img_orig[..., 1] = gradient_r +img_orig[..., 2] = gradient_c +img_orig = rescale_intensity(img_orig) +img_orig_gray = rgb2gray(img_orig) + +# warp synthetic image +tform = AffineTransform(scale=(0.9, 0.9), rotation=0.2, translation=(20, -10)) +img_warped = warp(img_orig, tform.inverse, output_shape=(200, 200)) +img_warped_gray = rgb2gray(img_warped) + +# extract corners using Harris' corner measure +coords_orig = corner_peaks(corner_harris(img_orig_gray), threshold_rel=0.001, + min_distance=5) +coords_warped = corner_peaks(corner_harris(img_warped_gray), + threshold_rel=0.001, min_distance=5) + +# determine sub-pixel corner position +coords_orig_subpix = corner_subpix(img_orig_gray, coords_orig, window_size=9) +coords_warped_subpix = corner_subpix(img_warped_gray, coords_warped, + window_size=9) + + +def gaussian_weights(window_ext, sigma=1): + y, x = np.mgrid[-window_ext:window_ext+1, -window_ext:window_ext+1] + g = np.zeros(y.shape, dtype=np.double) + g[:] = np.exp(-0.5 * (x**2 / sigma**2 + y**2 / sigma**2)) + g /= 2 * np.pi * sigma * sigma + return g + + +def match_corner(coord, window_ext=5): + r, c = np.round(coord) + window_orig = img_orig[r-window_ext:r+window_ext+1, + c-window_ext:c+window_ext+1, :] + + # weight pixels depending on distance to center pixel + weights = gaussian_weights(window_ext, 3) + weights = np.dstack((weights, weights, weights)) + + # compute sum of squared differences to all corners in warped image + SSDs = [] + for cr, cc in coords_warped: + window_warped = img_warped[cr-window_ext:cr+window_ext+1, + cc-window_ext:cc+window_ext+1, :] + SSD = np.sum(weights * (window_orig - window_warped)**2) + SSDs.append(SSD) + + # use corner with minimum SSD as correspondence + min_idx = np.argmin(SSDs) + return coords_warped_subpix[min_idx] + + +# find correspondences using simple weighted sum of squared differences +src = [] +dst = [] +for coord in coords_orig_subpix: + src.append(coord) + dst.append(match_corner(coord)) +src = np.array(src) +dst = np.array(dst) + + +# estimate affine transform model using all coordinates +model = AffineTransform() +model.estimate(src, dst) + +# robustly estimate affine transform model with RANSAC +model_robust, inliers = ransac((src, dst), AffineTransform, min_samples=3, + residual_threshold=2, max_trials=100) +outliers = inliers == False + + +# compare "true" and estimated transform parameters +print(tform.scale, tform.translation, tform.rotation) +print(model.scale, model.translation, model.rotation) +print(model_robust.scale, model_robust.translation, model_robust.rotation) + + +# visualize correspondences +img_combined = np.concatenate((img_orig_gray, img_warped_gray), axis=1) + +fig, ax = plt.subplots(nrows=2, ncols=1) +plt.gray() + +ax[0].imshow(img_combined, interpolation='nearest') +ax[0].axis('off') +ax[0].axis((0, 400, 200, 0)) +ax[0].set_title('Correct correspondences') +ax[1].imshow(img_combined, interpolation='nearest') +ax[1].axis('off') +ax[1].axis((0, 400, 200, 0)) +ax[1].set_title('Faulty correspondences') + + +for ax_idx, (m, color) in enumerate(((inliers, 'g'), (outliers, 'r'))): + ax[ax_idx].plot((src[m, 1], dst[m, 1] + 200), (src[m, 0], dst[m, 0]), '-', + color=color) + ax[ax_idx].plot(src[m, 1], src[m, 0], '.', markersize=10, color=color) + ax[ax_idx].plot(dst[m, 1] + 200, dst[m, 0], '.', markersize=10, + color=color) + +plt.show() diff --git a/doc/examples/plot_medial_transform.py b/doc/examples/plot_medial_transform.py index 1421b4ce..f0792e50 100644 --- a/doc/examples/plot_medial_transform.py +++ b/doc/examples/plot_medial_transform.py @@ -3,7 +3,7 @@ Medial axis skeletonization =========================== -The medial axis of an object is the set of all points having more than one +The medial axis of an object is the set of all points having more than one closest point on the object's boundary. It is often called the **topological skeleton**, because it is a 1-pixel wide skeleton of the object, with the same connectivity as the original object. @@ -15,11 +15,11 @@ argument ``return_distance=True``), it is possible to compute the distance to the background for all points of the medial axis with this function. This gives an estimate of the local width of the objects. -For a skeleton with fewer branches, there exists another skeletonization +For a skeleton with fewer branches, there exists another skeletonization algorithm in ``skimage``: ``skimage.morphology.skeletonize``, that computes a skeleton by iterative morphological thinnings. -""" +""" import numpy as np from scipy import ndimage from skimage.morphology import medial_axis @@ -33,7 +33,7 @@ def microstructure(l=256): Parameters ---------- - l: int, optional + l: int, optional linear size of the returned image """ @@ -64,7 +64,5 @@ plt.imshow(dist_on_skel, cmap=plt.cm.spectral, interpolation='nearest') plt.contour(data, [0.5], colors='w') plt.axis('off') -plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, - right=1) +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_otsu.py b/doc/examples/plot_otsu.py index f2335fc0..5221e934 100644 --- a/doc/examples/plot_otsu.py +++ b/doc/examples/plot_otsu.py @@ -14,7 +14,6 @@ the intra-class variance. .. [1] http://en.wikipedia.org/wiki/Otsu's_method """ - import matplotlib.pyplot as plt from skimage.data import camera @@ -42,4 +41,3 @@ plt.title('Thresholded') plt.axis('off') plt.show() - diff --git a/doc/examples/plot_peak_local_max.py b/doc/examples/plot_peak_local_max.py index e8c78661..3a2dccfe 100644 --- a/doc/examples/plot_peak_local_max.py +++ b/doc/examples/plot_peak_local_max.py @@ -1,7 +1,7 @@ """ -=============================================================================== +==================== Finding local maxima -=============================================================================== +==================== The ``peak_local_max`` function returns the coordinates of local peaks (maxima) in an image. A maximum filter is used for finding local maxima. This operation @@ -47,4 +47,3 @@ plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, bottom=0.02, left=0.02, right=0.98) plt.show() - diff --git a/doc/examples/plot_piecewise_affine.py b/doc/examples/plot_piecewise_affine.py index 2dcbd9f1..a0ad7d8f 100644 --- a/doc/examples/plot_piecewise_affine.py +++ b/doc/examples/plot_piecewise_affine.py @@ -4,8 +4,8 @@ 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 diff --git a/doc/examples/plot_polygon.py b/doc/examples/plot_polygon.py index 05ca5359..821c4558 100644 --- a/doc/examples/plot_polygon.py +++ b/doc/examples/plot_polygon.py @@ -5,10 +5,13 @@ Approximate and subdivide polygons This example shows how to approximate (Douglas-Peucker algorithm) and subdivide (B-Splines) polygonal chains. + """ +from __future__ import print_function import numpy as np import matplotlib.pyplot as plt + from skimage.draw import ellipse from skimage.measure import find_contours, approximate_polygon, \ subdivide_polygon @@ -45,7 +48,7 @@ for _ in range(5): # 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) +print("Number of coordinates:", len(hand), len(new_hand), len(appr_hand)) fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(9, 4)) @@ -70,7 +73,7 @@ for contour in find_contours(img, 0): 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) + print("Number of coordinates:", len(contour), len(coords), len(coords2)) ax2.axis((0, 800, 0, 800)) diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py index eb3896f4..c4853c2f 100644 --- a/doc/examples/plot_pyramid.py +++ b/doc/examples/plot_pyramid.py @@ -9,7 +9,6 @@ implement algorithms for denoising, texture discrimination, and scale- invariant detection. """ - import numpy as np import matplotlib.pyplot as plt diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index efd8a776..0dab82e6 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -3,55 +3,197 @@ Radon transform =============== -The radon transform is a technique widely used in tomography to -reconstruct an object from different projections. A projection is, for -example, the scattering data obtained as the output of a tomographic -scan. +In computed tomography, the tomography reconstruction problem is to obtain +a tomographic slice image from a set of projections [1]_. A projection is formed +by drawing a set of parallel rays through the 2D object of interest, assigning +the integral of the object's contrast along each ray to a single pixel in the +projection. A single projection of a 2D object is one dimensional. To +enable computed tomography reconstruction of the object, several projections +must be acquired, each of them corresponding to a different angle between the +rays with respect to the object. A collection of projections at several angles +is called a sinogram, which is a linear transform of the original image. -For more information see: +The inverse Radon transform is used in computed tomography to reconstruct +a 2D image from the measured projections (the sinogram). A practical, exact +implementation of the inverse Radon transform does not exist, but there are +several good approximate algorithms available. - - http://en.wikipedia.org/wiki/Radon_transform - - http://www.clear.rice.edu/elec431/projects96/DSP/bpanalysis.html +As the inverse Radon transform reconstructs the object from a set of +projections, the (forward) Radon transform can be used to simulate a +tomography experiment. -This script performs the radon transform, and reconstructs the -input image based on the resulting sinogram. +This script performs the Radon transform to simulate a tomography experiment +and reconstructs the input image based on the resulting sinogram formed by +the simulation. Two methods for performing the inverse Radon transform +and reconstructing the original image are compared: The Filtered Back +Projection (FBP) and the Simultaneous Algebraic Reconstruction +Technique (SART). +.. seealso:: + + - AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", + http://www.slaney.org/pct/pct-toc.html + - http://en.wikipedia.org/wiki/Radon_transform + +The forward transform +===================== + +As our original image, we will use the Shepp-Logan phantom. When calculating +the Radon transform, we need to decide how many projection angles we wish +to use. As a rule of thumb, the number of projections should be about the +same as the number of pixels there are across the object (to see why this +is so, consider how many unknown pixel values must be determined in the +reconstruction process and compare this to the number of measurements +provided by the projections), and we follow that rule here. Below is the +original image and its Radon transform, often known as its _sinogram_: """ +from __future__ import print_function, division + +import numpy as np import matplotlib.pyplot as plt from skimage.io import imread from skimage import data_dir -from skimage.transform import radon, iradon -from scipy.ndimage import zoom +from skimage.transform import radon, rescale image = imread(data_dir + "/phantom.png", as_grey=True) -image = zoom(image, 0.4) +image = rescale(image, scale=0.4) + +plt.figure(figsize=(8, 4.5)) + +plt.subplot(121) +plt.title("Original") +plt.imshow(image, cmap=plt.cm.Greys_r) + +theta = np.linspace(0., 180., max(image.shape), endpoint=True) +sinogram = radon(image, theta=theta, circle=True) +plt.subplot(122) +plt.title("Radon transform\n(Sinogram)") +plt.xlabel("Projection angle (deg)") +plt.ylabel("Projection position (pixels)") +plt.imshow(sinogram, cmap=plt.cm.Greys_r, + extent=(0, 180, 0, sinogram.shape[0]), aspect='auto') + +plt.subplots_adjust(hspace=0.4, wspace=0.5) +plt.show() + +""" +.. image:: PLOT2RST.current_figure + +Reconstruction with the Filtered Back Projection (FBP) +====================================================== + +The mathematical foundation of the filtered back projection is the Fourier +slice theorem [2]_. It uses Fourier transform of the projection and +interpolation in Fourier space to obtain the 2D Fourier transform of the image, +which is then inverted to form the reconstructed image. The filtered back +projection is among the fastest methods of performing the inverse Radon +transform. The only tunable parameter for the FBP is the filter, which is +applied to the Fourier transformed projections. It may be used to suppress +high frequency noise in the reconstruction. ``skimage`` provides a few +different options for the filter. + +""" + +from skimage.transform import iradon + +reconstruction_fbp = iradon(sinogram, theta=theta, circle=True) +error = reconstruction_fbp - image +print('FBP rms reconstruction error: %.3g' % np.sqrt(np.mean(error**2))) + +imkwargs = dict(vmin=-0.2, vmax=0.2) +plt.figure(figsize=(8, 4.5)) +plt.subplot(121) +plt.title("Reconstruction\nFiltered back projection") +plt.imshow(reconstruction_fbp, cmap=plt.cm.Greys_r) +plt.subplot(122) +plt.title("Reconstruction error\nFiltered back projection") +plt.imshow(reconstruction_fbp - image, cmap=plt.cm.Greys_r, **imkwargs) +plt.show() + +""" +.. image:: PLOT2RST.current_figure + +Reconstruction with the Simultaneous Algebraic Reconstruction Technique +======================================================================= + +Algebraic reconstruction techniques for tomography are based on a +straightforward idea: for a pixelated image the value of a single ray in a +particular projection is simply a sum of all the pixels the ray passes through +on its way through the object. This is a way of expressing the forward Radon +transform. The inverse Radon transform can then be formulated as a (large) set +of linear equations. As each ray passes through a small fraction of the pixels +in the image, this set of equations is sparse, allowing iterative solvers for +sparse linear systems to tackle the system of equations. One iterative method +has been particularly popular, namely Kaczmarz' method [3]_, which has the +property that the solution will approach a least-squares solution of the +equation set. + +The combination of the formulation of the reconstruction problem as a set +of linear equations and an iterative solver makes algebraic techniques +relatively flexible, hence some forms of prior knowledge can be incorporated +with relative ease. + +``skimage`` provides one of the more popular variations of the algebraic +reconstruction techniques: the Simultaneous Algebraic Reconstruction Technique +(SART) [1]_ [4]_. It uses Kaczmarz' method [3]_ as the iterative solver. A good +reconstruction is normally obtained in a single iteration, making the method +computationally effective. Running one or more extra iterations will normally +improve the reconstruction of sharp, high frequency features and reduce the +mean squared error at the expense of increased high frequency noise (the user +will need to decide on what number of iterations is best suited to the problem +at hand. The implementation in ``skimage`` allows prior information of the +form of a lower and upper threshold on the reconstructed values to be supplied +to the reconstruction. + +""" + +from skimage.transform import iradon_sart + +reconstruction_sart = iradon_sart(sinogram, theta=theta) +error = reconstruction_sart - image +print('SART (1 iteration) rms reconstruction error: %.3g' + % np.sqrt(np.mean(error**2))) plt.figure(figsize=(8, 8.5)) plt.subplot(221) -plt.title("Original"); -plt.imshow(image, cmap=plt.cm.Greys_r) - +plt.title("Reconstruction\nSART") +plt.imshow(reconstruction_sart, cmap=plt.cm.Greys_r) plt.subplot(222) -projections = radon(image, theta=[0, 45, 90]) -plt.plot(projections); -plt.title("Projections at\n0, 45 and 90 degrees") -plt.xlabel("Projection axis"); -plt.ylabel("Intensity"); +plt.title("Reconstruction error\nSART") +plt.imshow(reconstruction_sart - image, cmap=plt.cm.Greys_r, **imkwargs) + +# Run a second iteration of SART by supplying the reconstruction +# from the first iteration as an initial estimate +reconstruction_sart2 = iradon_sart(sinogram, theta=theta, + image=reconstruction_sart) +error = reconstruction_sart2 - image +print('SART (2 iterations) rms reconstruction error: %.3g' + % np.sqrt(np.mean(error**2))) -projections = radon(image) plt.subplot(223) -plt.title("Radon transform\n(Sinogram)"); -plt.xlabel("Projection axis"); -plt.ylabel("Intensity"); -plt.imshow(projections) - -reconstruction = iradon(projections) +plt.title("Reconstruction\nSART, 2 iterations") +plt.imshow(reconstruction_sart2, cmap=plt.cm.Greys_r) plt.subplot(224) -plt.title("Reconstruction\nfrom sinogram") -plt.imshow(reconstruction, cmap=plt.cm.Greys_r) - -plt.subplots_adjust(hspace=0.4, wspace=0.5) +plt.title("Reconstruction error\nSART, 2 iterations") +plt.imshow(reconstruction_sart2 - image, cmap=plt.cm.Greys_r, **imkwargs) plt.show() + +""" +.. image:: PLOT2RST.current_figure + + +.. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", + IEEE Press 1988. http://www.slaney.org/pct/pct-toc.html +.. [2] Wikipedia, Radon transform, + http://en.wikipedia.org/wiki/Radon_transform#Relationship_with_the_Fourier_transform +.. [3] S Kaczmarz, "Angenaeherte Aufloesung von Systemen linearer + Gleichungen", Bulletin International de l'Academie Polonaise des + Sciences et des Lettres 35 pp 355--357 (1937) +.. [4] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique + (SART): a superior implementation of the ART algorithm", Ultrasonic + Imaging 6 pp 81--94 (1984) + +""" diff --git a/doc/examples/plot_random_walker_segmentation.py b/doc/examples/plot_random_walker_segmentation.py index 7d9f4fa8..8c960a8b 100644 --- a/doc/examples/plot_random_walker_segmentation.py +++ b/doc/examples/plot_random_walker_segmentation.py @@ -18,13 +18,14 @@ 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 -""" +""" import numpy as np from scipy import ndimage -from skimage.segmentation import random_walker import matplotlib.pyplot as plt +from skimage.segmentation import random_walker + def microstructure(l=256): """ diff --git a/doc/examples/plot_rank_mean.py b/doc/examples/plot_rank_mean.py new file mode 100644 index 00000000..6f16c440 --- /dev/null +++ b/doc/examples/plot_rank_mean.py @@ -0,0 +1,52 @@ +""" +============ +Mean filters +============ + +This example compares the following mean filters of the rank filter package: + + * **local mean**: all pixels belonging to the structuring element to compute + average gray level. + * **percentile mean**: only use values between percentiles p0 and p1 + (here 10% and 90%). + * **bilateral mean**: only use pixels of the structuring element having a gray + level situated inside g-s0 and g+s1 (here g-500 and g+500) + +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 higher image +frequencies remain untouched. + +""" +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data +from skimage.morphology import disk +from skimage.filter import rank + + +image = (data.coins()).astype(np.uint16) * 16 +selem = disk(20) + +percentile_result = rank.mean_percentile(image, selem=selem, p0=.1, p1=.9) +bilateral_result = rank.mean_bilateral(image, selem=selem, s0=500, s1=500) +normal_result = rank.mean(image, selem=selem) + + +fig, axes = plt.subplots(nrows=3, figsize=(8, 10)) +ax0, ax1, ax2 = axes + +ax0.imshow(np.hstack((image, percentile_result))) +ax0.set_title('Percentile mean') +ax0.axis('off') + +ax1.imshow(np.hstack((image, bilateral_result))) +ax1.set_title('Bilateral mean') +ax1.axis('off') + +ax2.imshow(np.hstack((image, normal_result))) +ax2.set_title('Local mean') +ax2.axis('off') + +plt.show() diff --git a/doc/examples/plot_ransac.py b/doc/examples/plot_ransac.py new file mode 100644 index 00000000..2de76619 --- /dev/null +++ b/doc/examples/plot_ransac.py @@ -0,0 +1,55 @@ +""" +========================================= +Robust line model estimation using RANSAC +========================================= + +In this example we see how to robustly fit a line model to faulty data using +the RANSAC algorithm. + +""" +import numpy as np +from matplotlib import pyplot as plt + +from skimage.measure import LineModel, ransac + + +np.random.seed(seed=1) + +# generate coordinates of line +x = np.arange(-200, 200) +y = 0.2 * x + 20 +data = np.column_stack([x, y]) + +# add faulty data +faulty = np.array(30 * [(180., -100)]) +faulty += 5 * np.random.normal(size=faulty.shape) +data[:faulty.shape[0]] = faulty + +# add gaussian noise to coordinates +noise = np.random.normal(size=data.shape) +data += 0.5 * noise +data[::2] += 5 * noise[::2] +data[::4] += 20 * noise[::4] + +# fit line using all data +model = LineModel() +model.estimate(data) + +# robustly fit line only using inlier data with RANSAC algorithm +model_robust, inliers = ransac(data, LineModel, min_samples=2, + residual_threshold=1, max_trials=1000) +outliers = inliers == False + +# generate coordinates of estimated models +line_x = np.arange(-250, 250) +line_y = model.predict_y(line_x) +line_y_robust = model_robust.predict_y(line_x) + +plt.plot(data[inliers, 0], data[inliers, 1], '.b', alpha=0.6, + label='Inlier data') +plt.plot(data[outliers, 0], data[outliers, 1], '.r', alpha=0.6, + label='Outlier data') +plt.plot(line_x, line_y, '-k', label='Line model from all data') +plt.plot(line_x, line_y_robust, '-b', label='Robust line model') +plt.legend(loc='lower left') +plt.show() diff --git a/doc/examples/plot_regional_maxima.py b/doc/examples/plot_regional_maxima.py index 9d4de9b1..f7b125f1 100644 --- a/doc/examples/plot_regional_maxima.py +++ b/doc/examples/plot_regional_maxima.py @@ -11,14 +11,15 @@ 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 scipy.ndimage import gaussian_filter +import matplotlib.pyplot as plt 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()) diff --git a/doc/examples/plot_regionprops.py b/doc/examples/plot_regionprops.py index 90b40a89..f675d11c 100644 --- a/doc/examples/plot_regionprops.py +++ b/doc/examples/plot_regionprops.py @@ -4,8 +4,8 @@ Measure region properties ========================= This example shows how to measure properties of labelled image regions. -""" +""" import math import matplotlib.pyplot as plt import numpy as np @@ -13,48 +13,34 @@ import numpy as np from skimage.draw import ellipse from skimage.morphology import label from skimage.measure import regionprops -from scipy.ndimage import geometric_transform +from skimage.transform import rotate -ANGLE = 0.2 - -def rotate(xy): - x, y = xy - out_x = math.cos(ANGLE) * x - math.sin(ANGLE) * y - out_y = math.sin(ANGLE) * x + math.cos(ANGLE) * y - return (out_x, out_y) - -image = np.zeros((600, 600), 'int') +image = np.zeros((600, 600)) rr, cc = ellipse(300, 350, 100, 220) image[rr,cc] = 1 -image = geometric_transform(image, rotate) +image = rotate(image, angle=15, order=0) label_img = label(image) -props = regionprops(label_img, [ - 'BoundingBox', - 'Centroid', - 'Orientation', - 'MajorAxisLength', - 'MinorAxisLength' -]) +regions = regionprops(label_img) plt.imshow(image) -for prop in props: - x0 = prop['Centroid'][1] - y0 = prop['Centroid'][0] - x1 = x0 + math.cos(prop['Orientation']) * 0.5 * prop['MajorAxisLength'] - y1 = y0 - math.sin(prop['Orientation']) * 0.5 * prop['MajorAxisLength'] - x2 = x0 - math.sin(prop['Orientation']) * 0.5 * prop['MinorAxisLength'] - y2 = y0 - math.cos(prop['Orientation']) * 0.5 * prop['MinorAxisLength'] +for props in regions: + y0, x0 = props.centroid + orientation = props.orientation + x1 = x0 + math.cos(orientation) * 0.5 * props.major_axis_length + y1 = y0 - math.sin(orientation) * 0.5 * props.major_axis_length + x2 = x0 - math.sin(orientation) * 0.5 * props.minor_axis_length + y2 = y0 - math.cos(orientation) * 0.5 * props.minor_axis_length plt.plot((x0, x1), (y0, y1), '-r', linewidth=2.5) plt.plot((x0, x2), (y0, y2), '-r', linewidth=2.5) plt.plot(x0, y0, '.g', markersize=15) - minr, minc, maxr, maxc = prop['BoundingBox'] + minr, minc, maxr, maxc = props.bbox bx = (minc, maxc, maxc, minc, minc) by = (minr, minr, maxr, maxr, minr) plt.plot(bx, by, '-b', linewidth=2.5) diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index 418a0903..55bb6082 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -58,6 +58,7 @@ of Quickshift, while ``n_segments`` chooses the number of centers for kmeans. Pascal Fua, and Sabine Suesstrunk, SLIC Superpixels Compared to State-of-the-art Superpixel Methods, TPAMI, May 2012. """ +from __future__ import print_function import matplotlib.pyplot as plt import numpy as np diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 25a48ebe..2ae842a5 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -1,27 +1,34 @@ """ -=========== -Fill shapes -=========== +====== +Shapes +====== -This example shows how to fill several different shapes: +This example shows how to draw several different shapes: * line +* Bezier curve * polygon * circle * ellipse """ - +import math +import numpy as np import matplotlib.pyplot as plt -from skimage.draw import line, polygon, circle, circle_perimeter, ellipse -import numpy as np +from skimage.draw import (line, polygon, circle, + circle_perimeter, + ellipse, ellipse_perimeter, + bezier_curve) -img = np.zeros((500, 500, 3), 'uint8') +fig, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(10, 6)) + + +img = np.zeros((500, 500, 3), dtype=np.double) # draw line rr, cc = line(120, 123, 20, 400) -img[rr,cc,0] = 255 +img[rr, cc, 0] = 255 # fill polygon poly = np.array(( @@ -31,20 +38,61 @@ poly = np.array(( (220, 590), (300, 300), )) -rr, cc = polygon(poly[:,0], poly[:,1], img.shape) -img[rr,cc,1] = 255 +rr, cc = polygon(poly[:, 0], poly[:, 1], img.shape) +img[rr, cc, 1] = 1 # fill circle rr, cc = circle(200, 200, 100, img.shape) -img[rr,cc,:] = (255, 255, 0) +img[rr, cc, :] = (1, 1, 0) # fill ellipse rr, cc = ellipse(300, 300, 100, 200, img.shape) -img[rr,cc,2] = 255 +img[rr, cc, 2] = 1 # circle -rr, cc = circle_perimeter(120, 400, 50) -img[rr, cc, :] = (255, 0, 255) +rr, cc = circle_perimeter(120, 400, 15) +img[rr, cc, :] = (1, 0, 0) + +# Bezier curve +rr, cc = bezier_curve(70, 100, 10, 10, 150, 100, 1) +img[rr, cc, :] = (1, 0, 0) + +# ellipses +rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 4.) +img[rr, cc, :] = (1, 0, 1) +rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=-math.pi / 4.) +img[rr, cc, :] = (0, 0, 1) +rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 2.) +img[rr, cc, :] = (1, 1, 1) + +ax1.imshow(img) +ax1.set_title('No anti-aliasing') +ax1.axis('off') + +""" + +Anti-aliased drawing for: +* line +* circle + +""" + +from skimage.draw import line_aa, circle_perimeter_aa + + +img = np.zeros((100, 100), dtype=np.double) + +# anti-aliased line +rr, cc, val = line_aa(12, 12, 20, 50) +img[rr, cc] = val + +# anti-aliased circle +rr, cc, val = circle_perimeter_aa(60, 40, 30) +img[rr, cc] = val + + +ax2.imshow(img, cmap=plt.cm.gray, interpolation='nearest') +ax2.set_title('Anti-aliasing') +ax2.axis('off') -plt.imshow(img) plt.show() diff --git a/doc/examples/plot_skeleton.py b/doc/examples/plot_skeleton.py index 65a0ee48..a0bfd267 100644 --- a/doc/examples/plot_skeleton.py +++ b/doc/examples/plot_skeleton.py @@ -29,10 +29,12 @@ 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 -rs, cs = draw.bresenham(10, 150, 250, 280) -for i in range(20): image[rs+i, cs] = 1 +rs, cs = draw.line(250, 150, 10, 280) +for i in range(10): + image[rs + i, cs] = 1 +rs, cs = draw.line(10, 150, 250, 280) +for i in range(20): + image[rs + i, cs] = 1 # foreground object 3 ir, ic = np.indices(image.shape) diff --git a/doc/examples/plot_ssim.py b/doc/examples/plot_ssim.py index 60419692..9fe0c930 100644 --- a/doc/examples/plot_ssim.py +++ b/doc/examples/plot_ssim.py @@ -1,4 +1,4 @@ -''' +""" =========================== Structural similarity index =========================== @@ -20,12 +20,13 @@ but with very different mean structural similarity indices. Transactions on Image Processing, vol. 13, no. 4, pp. 600-612, Apr. 2004. -''' +""" +import numpy as np +import matplotlib.pyplot as plt -from skimage import data, color, io, exposure, img_as_float +from skimage import data, img_as_float from skimage.measure import structural_similarity as ssim -import numpy as np img = img_as_float(data.camera()) rows, cols = img.shape @@ -33,13 +34,13 @@ rows, cols = img.shape noise = np.ones_like(img) * 0.2 * (img.max() - img.min()) noise[np.random.random(size=noise.shape) > 0.5] *= -1 + def mse(x, y): return np.linalg.norm(x - y) img_noise = img + noise img_const = img + abs(noise) -import matplotlib.pyplot as plt f, (ax0, ax1, ax2) = plt.subplots(1, 3) mse_none = mse(img, img) diff --git a/doc/examples/plot_swirl.py b/doc/examples/plot_swirl.py index 18947dcb..98226a20 100644 --- a/doc/examples/plot_swirl.py +++ b/doc/examples/plot_swirl.py @@ -1,4 +1,4 @@ -r""" +""" ===== Swirl ===== @@ -8,7 +8,7 @@ effect. This example describes the implementation of this transform in ``skimage``, as well as the underlying warp mechanism. Image warping -````````````` +------------- When applying a geometric transformation on an image, we typically make use of a reverse mapping, i.e., for each pixel in the output image, we compute its corresponding position in the input. The reason is that, if we were to do it @@ -19,7 +19,7 @@ image, and even if that position is non-integer, we may use interpolation to compute the corresponding image value. Performing a reverse mapping -```````````````````````````` +---------------------------- To perform a geometric warp in ``skimage``, you simply need to provide the reverse mapping to the ``skimage.transform.warp`` function. E.g., consider the case where we would like to shift an image 50 pixels to the left. The reverse @@ -35,16 +35,16 @@ The corresponding call to warp is:: warp(image, shift_left) The swirl transformation -```````````````````````` +------------------------ Consider the coordinate :math:`(x, y)` in the output image. The reverse mapping for the swirl transformation first computes, relative to a center :math:`(x_0, y_0)`, its polar coordinates, .. math:: - \theta = \arctan(y/x) + \\theta = \\arctan(y/x) - \rho = \sqrt{(x - x_0)^2 + (y - y_0)^2}, + \\rho = \sqrt{(x - x_0)^2 + (y - y_0)^2}, and then transforms them according to @@ -56,19 +56,20 @@ and then transforms them according to s = \mathtt{strength} - \theta' = \phi + s \, e^{-\rho / r + \theta} + \\theta' = \phi + s \, e^{-\\rho / r + \\theta} where ``strength`` is a parameter for the amount of swirl, ``radius`` indicates the swirl extent in pixels, and ``rotation`` adds a rotation angle. The transformation of ``radius`` into :math:`r` is to ensure that the -transformation decays to :math:`\approx 1/1000^{\mathsf{th}}` within the +transformation decays to :math:`\\approx 1/1000^{\mathsf{th}}` within the specified radius. + """ +import matplotlib.pyplot as plt from skimage import data from skimage.transform import swirl -import matplotlib.pyplot as plt image = data.checkerboard() swirled = swirl(image, rotation=0, strength=10, radius=120, order=2) diff --git a/doc/examples/plot_template.py b/doc/examples/plot_template.py index 65b4571b..08581625 100644 --- a/doc/examples/plot_template.py +++ b/doc/examples/plot_template.py @@ -17,13 +17,15 @@ the template. .. [1] J. P. Lewis, "Fast Normalized Cross-Correlation", Industrial Light and Magic. -""" +""" import numpy as np import matplotlib.pyplot as plt + from skimage import data from skimage.feature import match_template + image = data.coins() coin = image[170:220, 75:130] @@ -53,4 +55,3 @@ ax3.autoscale(False) ax3.plot(x, y, 'o', markeredgecolor='r', markerfacecolor='none', markersize=10) plt.show() - diff --git a/doc/examples/plot_view_as_blocks.py b/doc/examples/plot_view_as_blocks.py index b70a0522..c58a74a6 100644 --- a/doc/examples/plot_view_as_blocks.py +++ b/doc/examples/plot_view_as_blocks.py @@ -12,8 +12,8 @@ blocks. Then, on each block, we either pool the mean, the max or the median value of that block. The results are displayed altogether, along with a spline interpolation of order 3 rescaling of the original `lena` image. -""" +""" import numpy as np from scipy import ndimage as ndi from matplotlib import pyplot as plt diff --git a/doc/examples/plot_watershed.py b/doc/examples/plot_watershed.py index 50857b2c..f003c122 100644 --- a/doc/examples/plot_watershed.py +++ b/doc/examples/plot_watershed.py @@ -24,11 +24,13 @@ See Wikipedia_ for more details on the algorithm. .. _Wikipedia: http://en.wikipedia.org/wiki/Watershed_(image_processing) """ - import numpy as np - import matplotlib.pyplot as plt -from skimage.morphology import watershed, is_local_maximum +from scipy import ndimage + +from skimage.morphology import watershed +from skimage.feature import peak_local_max + # Generate an initial image with two overlapping circles x, y = np.indices((80, 80)) @@ -40,9 +42,9 @@ image = np.logical_or(mask_circle1, mask_circle2) # Now we want to separate the two objects in image # Generate the markers as local maxima of the distance to the background -from scipy import ndimage distance = ndimage.distance_transform_edt(image) -local_maxi = is_local_maximum(distance, image, np.ones((3, 3))) +local_maxi = peak_local_max(distance, indices=False, footprint=np.ones((3, 3)), + labels=image) markers = ndimage.label(local_maxi)[0] labels = watershed(-distance, markers, mask=image) @@ -50,8 +52,11 @@ fig, axes = plt.subplots(ncols=3, figsize=(8, 2.7)) ax0, ax1, ax2 = axes ax0.imshow(image, cmap=plt.cm.gray, interpolation='nearest') +ax0.set_title('Overlapping objects') ax1.imshow(-distance, cmap=plt.cm.jet, interpolation='nearest') +ax1.set_title('Distances') ax2.imshow(labels, cmap=plt.cm.spectral, interpolation='nearest') +ax2.set_title('Separated objects') for ax in axes: ax.axis('off') diff --git a/doc/ext/docscrape.py b/doc/ext/docscrape.py index ad5998cc..b90b49c8 100644 --- a/doc/ext/docscrape.py +++ b/doc/ext/docscrape.py @@ -9,6 +9,7 @@ import pydoc from StringIO import StringIO from warnings import warn + class Reader(object): """A line-based string reader. @@ -369,7 +370,7 @@ class NumpyDocString(object): idx = self['index'] out = [] out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.iteritems(): + for section, references in idx.items(): if section == 'default': continue out += [' :%s: %s' % (section, ', '.join(references))] diff --git a/doc/ext/docscrape_sphinx.py b/doc/ext/docscrape_sphinx.py index 9f4350d4..9e66c4be 100644 --- a/doc/ext/docscrape_sphinx.py +++ b/doc/ext/docscrape_sphinx.py @@ -2,6 +2,7 @@ import re, inspect, textwrap, pydoc import sphinx from docscrape import NumpyDocString, FunctionDoc, ClassDoc + class SphinxDocString(NumpyDocString): def __init__(self, docstring, config={}): self.use_plots = config.get('use_plots', False) @@ -127,7 +128,7 @@ class SphinxDocString(NumpyDocString): return out out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.iteritems(): + for section, references in idx.items(): if section == 'default': continue elif section == 'refguide': diff --git a/doc/ext/plot2rst.py b/doc/ext/plot2rst.py index 00560be4..3b26aeb9 100644 --- a/doc/ext/plot2rst.py +++ b/doc/ext/plot2rst.py @@ -187,7 +187,7 @@ def generate_example_galleries(app): def generate_examples_and_gallery(example_dir, rst_dir, cfg): """Generate rst from examples and create gallery to showcase examples.""" if not example_dir.exists: - print "No example directory found at", example_dir + print("No example directory found at", example_dir) return rst_dir.makedirs() @@ -225,12 +225,12 @@ def write_gallery(gallery_index, src_dir, rst_dir, cfg, depth=0): index_name = cfg.plot2rst_index_name + cfg.source_suffix gallery_template = src_dir.pjoin(index_name) if not os.path.exists(gallery_template): - print src_dir - print 80*'_' - print ('Example directory %s does not have a %s file' + print(src_dir) + print(80*'_') + print('Example directory %s does not have a %s file' % (src_dir, index_name)) - print 'Skipping this directory' - print 80*'_' + print('Skipping this directory') + print(80*'_') return gallery_description = file(gallery_template).read() @@ -252,11 +252,11 @@ def write_gallery(gallery_index, src_dir, rst_dir, cfg, depth=0): 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 + print("Exception raised while running:") + print("%s in %s" % (src_name, src_dir)) + print('~' * 60) traceback.print_exc() - print '~' * 60 + print('~' * 60) continue link_name = sub_dir.pjoin(src_name) @@ -354,8 +354,8 @@ def write_example(src_name, src_dir, rst_dir, cfg): if not thumb_path.exists: if cfg.plot2rst_default_thumb is None: - print "WARNING: No plots found and default thumbnail not defined." - print "Specify 'plot2rst_default_thumb' in Sphinx config file." + print("WARNING: No plots found and default thumbnail not defined.") + print("Specify 'plot2rst_default_thumb' in Sphinx config file.") else: shutil.copy(cfg.plot2rst_default_thumb, thumb_path) diff --git a/doc/ext/plot_directive.py b/doc/ext/plot_directive.py index dc86552b..4a32b6f1 100644 --- a/doc/ext/plot_directive.py +++ b/doc/ext/plot_directive.py @@ -132,6 +132,7 @@ except ImportError: def format_template(template, **kw): return jinja.from_string(template, **kw) + import matplotlib import matplotlib.cbook as cbook matplotlib.use('Agg') @@ -234,7 +235,7 @@ def mark_plot_labels(app, document): the "htmlonly" (or "latexonly") node to the actual figure node itself. """ - for name, explicit in document.nametypes.iteritems(): + for name, explicit in document.nametypes.items(): if not explicit: continue labelid = document.nameids[name] diff --git a/doc/gh-pages.py b/doc/gh-pages.py index ab29959e..af992158 100644 --- a/doc/gh-pages.py +++ b/doc/gh-pages.py @@ -48,7 +48,7 @@ def sh2(cmd): out = p.communicate()[0] retcode = p.returncode if retcode: - print out.rstrip() + print(out.rstrip()) raise CalledProcessError(retcode, cmd) else: return out.rstrip() @@ -85,6 +85,10 @@ if __name__ == '__main__': for l in setup_lines: if l.startswith('VERSION'): tag = l.split("'")[1] + + # Rename to, e.g., 0.9.x + tag = '.'.join(tag.split('.')[:-1] + ['x']) + break if "dev" in tag: @@ -123,12 +127,12 @@ if __name__ == '__main__': sh('git add %s' % tag) sh2('git commit -m"Updated doc release: %s"' % tag) - print 'Most recent commit:' + print('Most recent commit:') sys.stdout.flush() sh('git --no-pager log --oneline HEAD~1..') finally: cd(startdir) - print - print 'Now verify the build in: %r' % dest - print "If everything looks good, run 'git push' inside doc/gh-pages." + print('') + print('Now verify the build in: %r' % dest) + print("If everything looks good, run 'git push' inside doc/gh-pages.") diff --git a/doc/logo/scikit_image_logo.py b/doc/logo/scikit_image_logo.py index de460344..85ba5199 100644 --- a/doc/logo/scikit_image_logo.py +++ b/doc/logo/scikit_image_logo.py @@ -1,80 +1,57 @@ """ Script to draw skimage logo using Scipy logo as stencil. The easiest -starting point is the `plot_colorized_logo`; the "if-main" demonstrates its use. +starting point is the `plot_colorized_logo`. Original snake image from pixabay [1]_ .. [1] http://pixabay.com/en/snake-green-toxic-close-yellow-3237/ """ -import numpy as np +import sys +if len(sys.argv) != 2 or sys.argv[1] != '--no-plot': + print("Run with '--no-plot' flag to generate logo silently.") +else: + import matplotlib as mpl + mpl.use('Agg') import matplotlib.pyplot as plt -import scipy.misc + +import numpy as np import skimage.io as sio -import skimage.filter as imfilt +from skimage import img_as_float +from skimage.color import gray2rgb, rgb2gray +from skimage.exposure import rescale_intensity +from skimage.filter import sobel import scipy_logo - # Utility functions # ================= -def get_edges(img): - edge = np.empty(img.shape) - if len(img.shape) == 3: - for i in range(3): - edge[:, :, i] = imfilt.sobel(img[:, :, i]) - else: - edge = imfilt.sobel(img) - edge = rescale_intensity(edge) - return edge +def colorize(image, color, whiten=False): + """Return colorized image from gray scale image. -def rescale_intensity(img): - i_range = float(img.max() - img.min()) - img = (img - img.min()) / i_range * 255 - return np.uint8(img) - -def colorize(img, color, whiten=False): - """Return colorized image from gray scale image - - Parameters - ---------- - img : N x M array - grayscale image - color : length-3 sequence of floats - RGB color spec. Float values should be between 0 and 1. - whiten : bool - If True, a color value less than 1 increases the image intensity. + The colorized image has values from ranging between black at the lowest + intensity to `color` at the highest. If `whiten=True`, then the color + ranges from `color` to white. """ color = np.asarray(color)[np.newaxis, np.newaxis, :] - img = img[:, :, np.newaxis] + image = image[:, :, np.newaxis] if whiten: # truncate and stretch intensity range to enhance contrast - img = np.clip(img, 80, 255) - img = rescale_intensity(img) - return np.uint8(color * (255 - img) + img) + image = rescale_intensity(image, in_range=(0.3, 1)) + return color * (1 - image) + image else: - return np.uint8(img * color) + return image * color def prepare_axes(ax): plt.sca(ax) ax.xaxis.set_visible(False) ax.yaxis.set_visible(False) - for spine in ax.spines.itervalues(): + for spine in ax.spines.values(): spine.set_visible(False) -_rgb_stack = np.ones((1, 1, 3), dtype=bool) -def gray2rgb(arr): - """Return RGB image from a grayscale image. - - Expand h x w image to h x w x 3 image where color channels are simply copies - of the grayscale image. - """ - return arr[:, :, np.newaxis] * _rgb_stack - - # Logo generating classes # ======================= @@ -82,21 +59,17 @@ class LogoBase(object): def __init__(self): self.logo = scipy_logo.ScipyLogo(radius=self.radius) - self.mask_1 = self.logo.get_mask(self.img.shape, 'upper left') - self.mask_2 = self.logo.get_mask(self.img.shape, 'lower right') - self.edges = get_edges(self.img) + self.mask_1 = self.logo.get_mask(self.image.shape, 'upper left') + self.mask_2 = self.logo.get_mask(self.image.shape, 'lower right') + + edges = np.array([sobel(img) for img in self.image.T]).T # truncate and stretch intensity range to enhance contrast - self.edges = np.clip(self.edges, 0, 100) - self.edges = rescale_intensity(self.edges) + self.edges = rescale_intensity(edges, in_range=(0, 0.4)) - - def _crop_image(self, img): + def _crop_image(self, image): w = 2 * self.radius x, y = self.origin - return img[y:y+w, x:x+w] - - def get_canvas(self): - return 255 * np.ones(self.img.shape, dtype=np.uint8) + return image[y:y + w, x:x + w] def plot_curve(self, **kwargs): self.logo.plot_snake_curve(**kwargs) @@ -104,15 +77,13 @@ class LogoBase(object): class SnakeLogo(LogoBase): - def __init__(self): - self.radius = 250 - self.origin = (420, 0) - img = sio.imread('data/snake_pixabay.jpg') - img = self._crop_image(img) + radius = 250 + origin = (420, 0) - img = img.astype(float) * 1.1 - img[img > 255] = 255 - self.img = img.astype(np.uint8) + def __init__(self): + image = sio.imread('data/snake_pixabay.jpg') + image = self._crop_image(image) + self.image = img_as_float(image) LogoBase.__init__(self) @@ -120,107 +91,75 @@ class SnakeLogo(LogoBase): snake_color = SnakeLogo() snake = SnakeLogo() # turn RGB image into gray image -snake.img = np.mean(snake.img, axis=2) -snake.edges = np.mean(snake.edges, axis=2) +snake.image = rgb2gray(snake.image) +snake.edges = rgb2gray(snake.edges) # Demo plotting functions # ======================= -def plot_colorized_logo(logo, color, edges='light', switch=False, whiten=False): - """Convenience function to plot artificially colored logo. +def plot_colorized_logo(logo, color, edges='light', whiten=False): + """Convenience function to plot artificially-colored logo. + + The upper-left half of the logo is an edge filtered image, while the + lower-right half is unfiltered. Parameters ---------- - logo : subclass of LogoBase - color : length-3 sequence of floats + logo : LogoBase instance + color : length-3 sequence of floats or 2 length-3 sequences RGB color spec. Float values should be between 0 and 1. edges : {'light'|'dark'} Specifies whether Sobel edges are drawn light or dark - switch : bool - If False, the image is drawn on the southeast half of the Scipy curve - and the edge image is drawn on northwest half. - whiten : bool + whiten : bool or 2 bools If True, a color value less than 1 increases the image intensity. """ if not hasattr(color[0], '__iter__'): - color = [color] * 2 + color = [color] * 2 # use same color for upper-left & lower-right if not hasattr(whiten, '__iter__'): - whiten = [whiten] * 2 - img = gray2rgb(logo.get_canvas()) + whiten = [whiten] * 2 # use same setting for upper-left & lower-right + + image = gray2rgb(np.ones_like(logo.image)) mask_img = gray2rgb(logo.mask_2) mask_edge = gray2rgb(logo.mask_1) - if switch: - mask_img, mask_edge = mask_edge, mask_img + + # Compose image with colorized image and edge-image. if edges == 'dark': - lg_edge = colorize(255 - logo.edges, color[0], whiten=whiten[0]) + logo_edge = colorize(1 - logo.edges, color[0], whiten=whiten[0]) else: - lg_edge = colorize(logo.edges, color[0], whiten=whiten[0]) - lg_img = colorize(logo.img, color[1], whiten=whiten[1]) - img[mask_img] = lg_img[mask_img] - img[mask_edge] = lg_edge[mask_edge] - logo.plot_curve(lw=5, color='w') - plt.imshow(img) + logo_edge = colorize(logo.edges, color[0], whiten=whiten[0]) + logo_img = colorize(logo.image, color[1], whiten=whiten[1]) + image[mask_img] = logo_img[mask_img] + image[mask_edge] = logo_edge[mask_edge] - -def red_light_edges(logo, **kwargs): - plot_colorized_logo(logo, (1, 0, 0), edges='light', **kwargs) - - -def red_dark_edges(logo, **kwargs): - plot_colorized_logo(logo, (1, 0, 0), edges='dark', **kwargs) - -def blue_light_edges(logo, **kwargs): - plot_colorized_logo(logo, (0.35, 0.55, 0.85), edges='light', **kwargs) - - -def blue_dark_edges(logo, **kwargs): - plot_colorized_logo(logo, (0.35, 0.55, 0.85), edges='dark', **kwargs) - - -def green_orange_light_edges(logo, **kwargs): - colors = ((0.6, 0.8, 0.3), (1, 0.5, 0.1)) - plot_colorized_logo(logo, colors, edges='light', **kwargs) - -def green_orange_dark_edges(logo, **kwargs): - colors = ((0.6, 0.8, 0.3), (1, 0.5, 0.1)) - plot_colorized_logo(logo, colors, edges='dark', **kwargs) + logo.plot_curve(lw=5, color='w') # plot snake curve on current axes + plt.imshow(image) if __name__ == '__main__': - - import sys - plot = False - if len(sys.argv) < 2 or sys.argv[1] != '--no-plot': - plot = True - - print "Run with '--no-plot' flag to generate logo silently." + # Colors to use for the logo: + red = (1, 0, 0) + blue = (0.35, 0.55, 0.85) + green_orange = ((0.6, 0.8, 0.3), (1, 0.5, 0.1)) def plot_all(): - plotters = (red_light_edges, red_dark_edges, - blue_light_edges, blue_dark_edges, - green_orange_light_edges, green_orange_dark_edges) - - f, axes_array = plt.subplots(nrows=2, ncols=len(plotters)) - for plot, ax_col in zip(plotters, axes_array.T): - prepare_axes(ax_col[0]) - plot(snake) - prepare_axes(ax_col[1]) - plot(snake, whiten=True) + color_list = [red, blue, green_orange] + edge_list = ['light', 'dark'] + f, axes = plt.subplots(nrows=len(edge_list), ncols=len(color_list)) + for axes_row, edges in zip(axes, edge_list): + for ax, color in zip(axes_row, color_list): + prepare_axes(ax) + plot_colorized_logo(snake, color, edges=edges) plt.tight_layout() - def plot_snake(): - + def plot_official_logo(): f, ax = plt.subplots() prepare_axes(ax) - green_orange_dark_edges(snake, whiten=(False, True)) + plot_colorized_logo(snake, green_orange, edges='dark', + whiten=(False, True)) plt.savefig('green_orange_snake.png', bbox_inches='tight') - if plot: - plot_all() - - plot_snake() - - if plot: - plt.show() + plot_all() + plot_official_logo() + plt.show() diff --git a/doc/logo/scipy_logo.py b/doc/logo/scipy_logo.py index c9de053a..c57cd95b 100644 --- a/doc/logo/scipy_logo.py +++ b/doc/logo/scipy_logo.py @@ -3,10 +3,11 @@ Code used to trace Scipy logo. """ import numpy as np import matplotlib.pyplot as plt -import skimage.io as imgio -from scipy.misc import lena import matplotlib.nxutils as nx +from skimage import io +from skimage import data + class SymmetricAnchorPoint(object): """Anchor point in a parametric curve with symmetric handles @@ -185,7 +186,7 @@ class ScipyLogo(object): def plot_image(self, **kwargs): ax = kwargs.pop('ax', plt.gca()) - img = imgio.imread('data/scipy.png') + img = io.imread('data/scipy.png') ax.imshow(img, **kwargs) def get_mask(self, shape, region): @@ -236,9 +237,7 @@ def plot_snake_overlay(): logo = ScipyLogo((670, 250), 250) logo.plot_snake_curve() logo.plot_circle() - img = imgio.imread('data/snake_pixabay.jpg') - #mask = logo.get_mask(img.shape, 'upper left') - #img[mask] = 255 + img = io.imread('data/snake_pixabay.jpg') plt.imshow(img) @@ -247,9 +246,7 @@ def plot_lena_overlay(): logo = ScipyLogo((300, 300), 180) logo.plot_snake_curve() logo.plot_circle() - img = lena() - #mask = logo.get_mask(img.shape, 'upper left') - #img[mask] = 255 + img = data.lena() plt.imshow(img) @@ -259,4 +256,3 @@ if __name__ == '__main__': plot_lena_overlay() plt.show() - diff --git a/doc/make.bat b/doc/make.bat index e16c8d40..5f117648 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -27,6 +27,14 @@ if "%1" == "help" ( goto end ) +for %%x in (html htmlhelp latex qthelp) do ( + if "%1" == "%%x" ( + md source\api 2>NUL + python tools/build_modref_templates.py + ) +) + + if "%1" == "clean" ( for /d %%i in (build\*) do rmdir /q /s %%i del /q /s build\* @@ -34,6 +42,7 @@ if "%1" == "clean" ( ) if "%1" == "html" ( + cd source && python random_gallery.py && python coverage_generator.py && cd .. %SPHINXBUILD% -b html %ALLSPHINXOPTS% build/html echo. echo.Build finished. The HTML pages are in build/html. diff --git a/doc/release/contribs.py b/doc/release/contribs.py new file mode 100755 index 00000000..08a59fc9 --- /dev/null +++ b/doc/release/contribs.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +import subprocess +import sys +import string +import shlex + +if len(sys.argv) != 2: + print "Usage: ./contributors.py tag-of-previous-release" + sys.exit(-1) + +tag = sys.argv[1] + +def call(cmd): + return subprocess.check_output(shlex.split(cmd)).split('\n') + +tag_date = call("git show --format='%%ci' %s" % tag)[0] +print "Release %s was on %s" % (tag, tag_date) + +merges = call("git log --since='%s' --merges --format='>>>%%B' --reverse" % tag_date) +merges = [m for m in merges if m.strip()] +merges = '\n'.join(merges).split('>>>') +merges = [m.split('\n')[:2] for m in merges] +merges = [m for m in merges if len(m) == 2 and m[1].strip()] + +print "\nIt contained the following %d merges:" % len(merges) +print +for (merge, message) in merges: + if merge.startswith('Merge pull request #'): + PR = ' (%s)' % merge.split()[3] + else: + PR = '' + + print '- ' + message + PR + + +print "\nMade by the following committers [alphabetical by last name]:\n" + +authors = call("git log --since='%s' --format=%%aN" % tag_date) +authors = [a.strip() for a in authors if a.strip()] + +def key(author): + author = [v for v in author.split() if v[0] in string.letters] + return author[-1] + +authors = sorted(set(authors), key=key) + +for a in authors: + print '-', a diff --git a/doc/release/contributors.sh b/doc/release/contributors.sh deleted file mode 100755 index 023928d3..00000000 --- a/doc/release/contributors.sh +++ /dev/null @@ -1,2 +0,0 @@ -git log $1..HEAD --format='- %aN' | sed 's/@/\-at\-/' | sed 's/<>//' | sort -u - diff --git a/doc/release/release_0.9.txt b/doc/release/release_0.9.txt new file mode 100644 index 00000000..1b6d890a --- /dev/null +++ b/doc/release/release_0.9.txt @@ -0,0 +1,130 @@ +Announcement: scikits-image 0.9.0 +================================= + +We're happy to announce the release of scikit-image v0.9.0! + +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 +------------ + +`scikit-image` now runs without translation under both Python 2 and 3. + +In addition to several bug fixes, speed improvements and examples, the 204 pull +requests merged for this release include the following new features (PR number +in brackets): + +Segmentation: + +- 3D support in SLIC segmentation (#546) +- SLIC voxel spacing (#719) +- Generalized anisotropic spacing support for random_walker (#775) +- Yen threshold method (#686) + +Transforms and filters: + +- SART algorithm for tomography reconstruction (#584) +- Gabor filters (#371) +- Hough transform for ellipses (#597) +- Fast resampling of nD arrays (#511) +- Rotation axis center for Radon transforms with inverses. (#654) +- Reconstruction circle in inverse Radon transform (#567) +- Pixelwise image adjustment curves and methods (#505) + +Feature detection: + +- [experimental API] BRIEF feature descriptor (#591) +- [experimental API] Censure (STAR) Feature Detector (#668) +- Octagon structural element (#669) +- Add non rotation invariant uniform LBPs (#704) + +Color and noise: + +- Add deltaE color comparison and lab2lch conversion (#665) +- Isotropic denoising (#653) +- Generator to add various types of random noise to images (#625) +- Color deconvolution for immunohistochemical images (#441) +- Color label visualization (#485) + +Drawing and visualization: + +- Wu's anti-aliased circle, line, bezier curve (#709) +- Linked image viewers and docked plugins (#575) +- Rotated ellipse + bezier curve drawing (#510) +- PySide & PyQt4 compatibility in skimage-viewer (#551) + +Other: + +- Python 3 support without 2to3. (#620) +- 3D Marching Cubes (#469) +- Line, Circle, Ellipse total least squares fitting and RANSAC algorithm (#440) +- N-dimensional array padding (#577) +- Add a wrapper around `scipy.ndimage.gaussian_filter` with useful default behaviors. (#712) +- Predefined structuring elements for 3D morphology (#484) + + +API changes +----------- + +The following backward-incompatible API changes were made between 0.8 and 0.9: + +- No longer wrap ``imread`` output in an ``Image`` class +- Change default value of `sigma` parameter in ``skimage.segmentation.slic`` + to 0 +- ``hough_circle`` now returns a stack of arrays that are the same size as the + input image. Set the ``full_output`` flag to True for the old behavior. +- The following functions were deprecated over two releases: + `skimage.filter.denoise_tv_chambolle`, + `skimage.morphology.is_local_maximum`, `skimage.transform.hough`, + `skimage.transform.probabilistic_hough`,`skimage.transform.hough_peaks`. + Their functionality still exists, but under different names. + + +Contributors to this release +---------------------------- + +This release was made possible by the collaborative efforts of many +contributors, both new and old. They are listed in alphabetical order by +surname: + +- Ankit Agrawal +- K.-Michael Aye +- Chris Beaumont +- François Boulogne +- Luis Pedro Coelho +- Marianne Corvellec +- Olivier Debeir +- Ferdinand Deger +- Kemal Eren +- Jostein Bø Fløystad +- Christoph Gohlke +- Emmanuelle Gouillart +- Christian Horea +- Thouis (Ray) Jones +- Almar Klein +- Xavier Moles Lopez +- Alexis Mignon +- Juan Nunez-Iglesias +- Zachary Pincus +- Nicolas Pinto +- Davin Potts +- Malcolm Reynolds +- Umesh Sharma +- Johannes Schönberger +- Chintak Sheth +- Kirill Shklovsky +- Steven Silvester +- Matt Terry +- Riaan van den Dool +- Stéfan van der Walt +- Josh Warner +- Adam Wisniewski +- Yang Zetian +- Tony S Yu diff --git a/doc/source/_static/docversions.js b/doc/source/_static/docversions.js index fde9437b..b4fd531f 100644 --- a/doc/source/_static/docversions.js +++ b/doc/source/_static/docversions.js @@ -1,17 +1,21 @@ -function insert_version_links() { - var labels = ['dev', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; +var versions = ['dev', '0.9.x', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; - for (i = 0; i < labels.length; i++){ +function insert_version_links() { + for (i = 0; i < versions.length; i++){ open_list = '
  • ' if (typeof(DOCUMENTATION_OPTIONS) !== 'undefined') { - if ((DOCUMENTATION_OPTIONS['VERSION'] == labels[i]) || + if ((DOCUMENTATION_OPTIONS['VERSION'] == versions[i]) || (DOCUMENTATION_OPTIONS['VERSION'].match(/dev$/) && (i == 0))) { open_list = '
  • ' } } document.write(open_list); document.write('skimage VERSION
  • \n' - .replace('VERSION', labels[i]) - .replace('URL', 'http://scikit-image.org/docs/' + labels[i])); + .replace('VERSION', versions[i]) + .replace('URL', 'http://scikit-image.org/docs/' + versions[i])); } } + +function stable_version() { + return versions[1]; +} diff --git a/doc/source/api_changes.txt b/doc/source/api_changes.txt index 57486723..1f755635 100644 --- a/doc/source/api_changes.txt +++ b/doc/source/api_changes.txt @@ -1,9 +1,23 @@ +Version 0.9 +----------- +- No longer wrap ``imread`` output in an ``Image`` class +- Change default value of `sigma` parameter in ``skimage.segmentation.slic`` + to 0 +- ``hough_circle`` now returns a stack of arrays that are the same size as the + input image. Set the ``full_output`` flag to True for the old behavior. +- The following functions were deprecated over two releases: + `skimage.filter.denoise_tv_chambolle`, + `skimage.morphology.is_local_maximum`, `skimage.transform.hough`, + `skimage.transform.probabilistic_hough`,`skimage.transform.hough_peaks`. + Their functionality still exists, but under different names. + +Version 0.4 +----------- +- Switch mask and radius arguments for ``median_filter`` + Version 0.3 ----------- - Remove ``as_grey``, ``dtype`` keyword from ImageCollection - Remove ``dtype`` from imread - Generalise ImageCollection to accept a load_func -Version 0.4 ------------ -- Switch mask and radius arguments for median_filter diff --git a/doc/source/conf.py b/doc/source/conf.py index 5071fab1..c2179b41 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -26,9 +26,26 @@ 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', 'plot2rst', 'sphinx.ext.intersphinx'] +# Determine if the matplotlib has a recent enough version of the +# plot_directive, otherwise use the local fork. +try: + from matplotlib.sphinxext import plot_directive +except ImportError: + use_matplotlib_plot_directive = False +else: + try: + use_matplotlib_plot_directive = (plot_directive.__version__ >= 2) + except AttributeError: + use_matplotlib_plot_directive = False + +if use_matplotlib_plot_directive: + extensions.append('matplotlib.sphinxext.plot_directive') +else: + extensions.append('plot_directive') + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -42,8 +59,8 @@ source_suffix = '.txt' master_doc = 'index' # General information about the project. -project = u'skimage' -copyright = u'2011, the scikit-image team' +project = 'skimage' +copyright = '2013, 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 @@ -185,13 +202,13 @@ htmlhelp_basename = 'scikitimagedoc' #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('contents', 'scikitimage.tex', u'The Image Scikit Documentation', - u'SciPy Developers', 'manual'), + ('contents', 'scikit-image.tex', u'The scikit-image Documentation', + u'scikit-image development team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -203,13 +220,32 @@ latex_documents = [ #latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +latex_preamble = r''' +\usepackage{enumitem} +\setlistdepth{100} + +\usepackage{amsmath} +\DeclareUnicodeCharacter{00A0}{\nobreakspace} + +% In the parameters section, place a newline after the Parameters header +\usepackage{expdlist} +\let\latexdescription=\description +\def\description{\latexdescription{}{} \breaklabel} + +% Make Examples/etc section headers smaller and more compact +\makeatletter +\titleformat{\paragraph}{\normalsize\py@HeaderFamily}% + {\py@TitleColor}{0em}{\py@TitleColor}{\py@NormalColor} +\titlespacing*{\paragraph}{0pt}{1ex}{0pt} +\makeatother + +''' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +latex_use_modindex = False # ----------------------------------------------------------------------------- # Numpy extensions @@ -243,7 +279,7 @@ matplotlib.rcParams.update({ """ plot_include_source = True -plot_formats = [('png', 100)] +plot_formats = [('png', 100), ('pdf', 100)] plot2rst_index_name = 'README' plot2rst_rcparams = {'image.cmap' : 'gray', diff --git a/doc/source/contribute.txt b/doc/source/contribute.txt index 5e62b585..f3404b89 100644 --- a/doc/source/contribute.txt +++ b/doc/source/contribute.txt @@ -1,2 +1,2 @@ .. include:: ../../TASKS.txt -.. include:: ../../DEVELOPMENT.txt +.. include:: ../../CONTRIBUTING.txt diff --git a/doc/source/coverage_generator.py b/doc/source/coverage_generator.py index 83fd5d86..1112c021 100755 --- a/doc/source/coverage_generator.py +++ b/doc/source/coverage_generator.py @@ -49,13 +49,13 @@ def calculate_coverage(reader): partial_items, total_items - (partial_items + done_items + na_items), na_items) - + return list(i / total_items for i in counts) def read_table_titles(reader): r"""Create a dictionary with keys as section names and values as a list of table names - + return (dict) """ section_titles = [] @@ -69,39 +69,39 @@ def read_table_titles(reader): # Extract names of the tables for name in row[1:]: if len(name) > 0: - names.append(name) + names.append(name) else: break section_titles.append(row[0]) table_names[row[0]] = names except csv.Error, e: sys.exit('line %d: %s' % (reader.line_num, e)) - + return section_titles,table_names def table_seperator(stream,lengths,character="-"): r"""Write out table row seperator - + :Input: - *stream* (io/stream) Stream where output is put - *lengths* (list) A list of the lengths of the columns - *character* (string) Character to be filled between +, defaults to "-". - + """ stream.write("+") stream.write('+'.join([character*(length+2) for length in lengths])) stream.write("+") - + def table_row(stream,data,lengths,num_columns=None): r"""Write out table row data - + :Input: - *stream* (io/stream) Stream where output is put - *data* (list) List of strings containing data - *lengths* (list) A list of the lengths of the columns - - *num_columns* (string) Number of columns, defaults to the length of the + - *num_columns* (string) Number of columns, defaults to the length of the data array - + """ if num_columns is None: num_columns = len(data) @@ -115,11 +115,11 @@ def table_row(stream,data,lengths,num_columns=None): else: entry = MISSING_STRING stream.write(" " + entry + " "*(lengths[i] - len(entry)) + " |") - + def generate_table(reader,stream,table_name=None, column_titles=["Functionality","Matlab","Scipy","Scipy"]): r"""Generate a reST grid table based on the CSV data in reader - + Reads CSV data from *reader* until an empty line is found and generates a reST table based on the data into *stream*. A table name can be given for a section and table label. All rows are read in and checked for maximum @@ -127,13 +127,13 @@ def generate_table(reader,stream,table_name=None, widths so that the table can be constructed. If a row contains less than the maximum number of columns a string is inserted that defaults to the string *MISSING_STRING* which is a global parameter. - + :Input: - reader (csv.reader) The CSV reader to read in from - stream (iostream) Output target - table_name (string) Optional name of table, defaults to *None* - column_titles (list) List of column titles - + """ # Find number of columns and column widths, base number of columns is # determined by the headers @@ -141,7 +141,6 @@ def generate_table(reader,stream,table_name=None, data = [column_titles] try: for row in reader: - # print row if len(row[0]) == 0: break data.append([entry.expandtabs() for entry in row]) @@ -153,7 +152,7 @@ def generate_table(reader,stream,table_name=None, for row in data: for i in xrange(len(row)): column_lengths[i] = max(column_lengths[i],len(row[i])) - + # Output table header stream.write(table_name + "\n") if table_name is not None: @@ -167,7 +166,7 @@ def generate_table(reader,stream,table_name=None, stream.write("\n") table_seperator(stream,column_lengths,character="=") stream.write("\n") - + # Output table data for row in data[1:]: table_row(stream,row,column_lengths,num_columns) @@ -175,28 +174,28 @@ def generate_table(reader,stream,table_name=None, table_seperator(stream,column_lengths,character='-') stream.write("\n") stream.write("\n\n") - + def generate_page(csv_path,stream,page_title="Coverage Tables"): r"""Generate coverage table page - + Generates all reST for all tables contained in the CSV file at *csv_path* and output it to *stream*. - + :Input: - *csv_path* (path) Path to CSV file - *stream* (iostream) Output stream - - *page_title* (string) Optional page title, defaults to + - *page_title* (string) Optional page title, defaults to ``Coverage Tables``. """ # Open reader csv_file = open(csv_path,'U') - + # Sniffer does not seem to work all the time even when an Excel # spread sheet is being used # dialect = csv.Sniffer().sniff(csv_file.read(1024)) # csv_file.seek(0) # reader = csv.reader(csv_file, dialect) - + reader = csv.reader(csv_file) item_counts = calculate_coverage(reader) csv_file.seek(0) @@ -254,21 +253,21 @@ if __name__ == "__main__": output_path = './coverage_table.txt' if len(sys.argv) > 1: if sys.argv[1][:5].lower() == "help": - print "Coverage Table Generator: coverage_generator.py" - print " Usage: coverage_generator.py [csv] [output]" - print " csv - Path to csv file, defaults to ./coverage.csv" - print " output - Ouput path, defaults to ./coverage_table.txt" - print "" + print("Coverage Table Generator: coverage_generator.py") + print(" Usage: coverage_generator.py [csv] [output]") + print(" csv - Path to csv file, defaults to ./coverage.csv") + print(" output - Ouput path, defaults to ./coverage_table.txt") + print('') sys.exit(0) if len(sys.argv) == 2: csv_path = os.path.abspath(sys.argv[1]) if len(sys.argv) == 3: output_path = os.path.abspath(sys.argv[2]) - + output = open(output_path,'w') generate_page(csv_path,output) output.close() - + print("Generated %s from %s." % (output_path,csv_path)) diff --git a/doc/source/install.txt b/doc/source/install.txt index 7c886549..b78c1269 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -6,8 +6,15 @@ Pre-built installation are kindly provided by Christoph Gohlke. The latest stable release is also included as part of the `Enthought Python -Distribution (EPD) `__ and `Python(x,y) -`__. +Distribution (EPD) `__, `Python(x,y) +`__ and +`Anaconda `__. + +On Debian and Ubuntu, a Debian package ``python-skimage`` can be found in +`the Neurodebian repository `__. Follow `the +instructions `__ to +add Neurodebian to your system package manager, then look for +``python-skimage`` in the package manager. On systems that support setuptools, the package can be installed from the `Python packaging index `__ using @@ -28,36 +35,51 @@ Installation from source Obtain the source from the git-repository at `http://github.com/scikit-image/scikit-image -`_. - -by running - -:: +`_ by running:: git clone http://github.com/scikit-image/scikit-image.git -in a terminal (You will need to have git installed on your machine). +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/scikit-image/scikit-image/zipball/master `_. -The SciKit can be installed globally using - -:: +The SciKit can be installed globally using:: python setup.py install -or locally using - -:: +or locally using:: python setup.py install --prefix=${HOME} If you prefer, you can use it without installing, by simply adding -this path to your PYTHONPATH variable and compiling extensions +this path to your ``PYTHONPATH`` variable and compiling extensions in-place:: python setup.py build_ext -i +Building with bento +------------------- + +``scikit-image`` can also be built using `bento +`__. Bento depends on `WAF +`__ for compilation. + +Follow the `Bento installation instructions +`__ and `download the WAF +source `__. + +Tell Bento where to find WAF by setting the ``WAFDIR`` environment variable:: + + export WAFDIR= + +From the ``scikit-image`` source directory:: + + bentomaker configure + bentomaker build -j # (add -i for in-place build) + bentomaker install # (when not builing in-place) + +Depending on file permissions, the install commands may need to be run as sudo. + .. include:: ../../DEPENDS.txt diff --git a/doc/source/plots/hough_tf.py b/doc/source/plots/hough_tf.py index 8b745864..df1dcd31 100644 --- a/doc/source/plots/hough_tf.py +++ b/doc/source/plots/hough_tf.py @@ -1,17 +1,18 @@ import numpy as np import matplotlib.pyplot as plt -from skimage.transform import hough +from skimage.transform import hough_line +from skimage.draw import line img = np.zeros((100, 150), dtype=bool) img[30, :] = 1 img[:, 65] = 1 img[35:45, 35:50] = 1 -for i in range(90): - img[i, i] = 1 +rr, cc = line(60, 130, 80, 10) +img[rr, cc] = 1 img += np.random.random(img.shape) > 0.95 -out, angles, d = hough(img) +out, angles, d = hough_line(img) plt.subplot(1, 2, 1) @@ -20,8 +21,8 @@ plt.title('Input image') plt.subplot(1, 2, 2) plt.imshow(out, cmap=plt.cm.bone, - extent=(d[0], d[-1], - np.rad2deg(angles[0]), np.rad2deg(angles[-1]))) + extent=(np.rad2deg(angles[-1]), np.rad2deg(angles[0]), + d[-1], d[0])) plt.title('Hough transform') plt.xlabel('Angle (degree)') plt.ylabel('Distance (pixel)') diff --git a/doc/source/themes/scikit-image/static/css/custom.css b/doc/source/themes/scikit-image/static/css/custom.css index 85a174df..0a2d3136 100644 --- a/doc/source/themes/scikit-image/static/css/custom.css +++ b/doc/source/themes/scikit-image/static/css/custom.css @@ -41,6 +41,12 @@ h6 { font-size: 13px; line-height: 15px; } +blockquote { + border-left: 0; +} +dt { + font-weight: normal; +} .logo { float: left; @@ -73,6 +79,10 @@ h6 { padding-left: 15px; } +#current { + font-weight: bold; +} + .headerlink { margin-left: 10px; color: #ddd; @@ -222,3 +232,8 @@ p.admonition-title { width: 200px; text-align: center !important; } + +/* misc */ +div.math { + text-align: center; +} diff --git a/doc/source/user_guide/data_types.txt b/doc/source/user_guide/data_types.txt index c3b87b2e..43ea8e9b 100644 --- a/doc/source/user_guide/data_types.txt +++ b/doc/source/user_guide/data_types.txt @@ -34,9 +34,9 @@ violates these assumptions about the dtype range:: >>> from skimage import img_as_float >>> image = np.arange(0, 50, 10, dtype=np.uint8) - >>> print image.astype(np.float) # These float values are out of range. + >>> print(image.astype(np.float)) # These float values are out of range. [ 0. 10. 20. 30. 40.] - >>> print img_as_float(image) + >>> print(img_as_float(image)) [ 0. 0.03921569 0.07843137 0.11764706 0.15686275] diff --git a/doc/source/user_guide/getting_help.txt b/doc/source/user_guide/getting_help.txt index ea444856..7d56fea4 100644 --- a/doc/source/user_guide/getting_help.txt +++ b/doc/source/user_guide/getting_help.txt @@ -24,7 +24,7 @@ Contributing examples to the gallery can be done on github (see Search field ------------ -The ``quick search`` field located in the sidebar of the html +The ``quick search`` field located in the navigation bar of the html documentation can be used to search for specific keywords (segmentation, rescaling, denoising, etc.). diff --git a/doc/tools/apigen.py b/doc/tools/apigen.py index 49c7e6e3..f1a94b6a 100644 --- a/doc/tools/apigen.py +++ b/doc/tools/apigen.py @@ -176,7 +176,7 @@ class ApiDocWriter(object): ''' Parse module defined in *uri* ''' filename = self._uri2path(uri) if filename is None: - print filename, 'erk' + print(filename, 'erk') # nothing that we could handle here. return ([],[]) f = open(filename, 'rt') @@ -260,7 +260,7 @@ class ApiDocWriter(object): # get the names of all classes and functions functions, classes = self._parse_module_with_import(uri) if not len(functions) and not len(classes) and DEBUG: - print 'WARNING: Empty -', uri # dbg + print('WARNING: Empty -', uri) # dbg return '' # Make a shorter version of the uri that omits the package name for @@ -449,7 +449,7 @@ class ApiDocWriter(object): relpath = (outdir + os.path.sep).replace(relative_to + os.path.sep, '') else: relpath = outdir - print "outdir: ", relpath + print("outdir: ", relpath) idx = open(path,'wt') w = idx.write w('.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n') diff --git a/doc/tools/build_modref_templates.py b/doc/tools/build_modref_templates.py index 8dfc6124..b4db115d 100644 --- a/doc/tools/build_modref_templates.py +++ b/doc/tools/build_modref_templates.py @@ -13,7 +13,7 @@ from distutils.version import LooseVersion as V #***************************************************************************** def abort(error): - print '*WARNING* API documentation not generated: %s'%error + print('*WARNING* API documentation not generated: %s' % error) exit() if __name__ == '__main__': @@ -35,7 +35,7 @@ if __name__ == '__main__': # are not (re)generated. This avoids automatic generation of documentation # for older or newer versions if such versions are installed on the system. - installed_version = V(module.version.version) + installed_version = V(module.__version__) setup_lines = open('../setup.py').readlines() version = 'vUndefined' @@ -54,4 +54,4 @@ if __name__ == '__main__': ] docwriter.write_api_docs(outdir) docwriter.write_index(outdir, 'api', relative_to='source/api') - print '%d files written' % len(docwriter.written_modules) + print('%d files written' % len(docwriter.written_modules)) diff --git a/doc/tools/plot_pr.py b/doc/tools/plot_pr.py index 09a9f91e..5f9b4aa6 100644 --- a/doc/tools/plot_pr.py +++ b/doc/tools/plot_pr.py @@ -1,14 +1,15 @@ -import urllib import json -import copy +import urllib +import dateutil.parser from collections import OrderedDict +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +import numpy as np import matplotlib.pyplot as plt from matplotlib.ticker import FuncFormatter +from matplotlib.transforms import blended_transform_factory -import dateutil.parser -from dateutil.relativedelta import relativedelta -from datetime import datetime, timedelta cache = '_pr_cache.txt' @@ -22,16 +23,16 @@ cache = '_pr_cache.txt' 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.3', u'2011-10-10 03:28:47 -0700'), ('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')]) + ('0.6', u'2012-06-24 21:37:05 -0700'), + ('0.7', u'2012-09-29 18:08:49 -0700'), + ('0.8', u'2013-03-04 20:46:09 +0100')]) month_duration = 24 -for r in releases: - releases[r] = dateutil.parser.parse(releases[r]) def fetch_PRs(user='scikit-image', repo='scikit-image', state='open'): params = {'state': state, @@ -46,12 +47,12 @@ def fetch_PRs(user='scikit-image', repo='scikit-image', state='open'): 'repo': repo, 'params': urllib.urlencode(params)} - fetch_status = 'Fetching page %(page)d (state=%(state)s)' % params + \ - ' from %(user)s/%(repo)s...' % config - print fetch_status + fetch_status = ('Fetching page %(page)d (state=%(state)s)' % params + + ' from %(user)s/%(repo)s...' % config) + print(fetch_status) f = urllib.urlopen( - 'https://api.github.com/repos/%(user)s/%(repo)s/pulls?%(params)s' \ + 'https://api.github.com/repos/%(user)s/%(repo)s/pulls?%(params)s' % config ) @@ -61,15 +62,40 @@ def fetch_PRs(user='scikit-image', repo='scikit-image', state='open'): if 'message' in page_data and page_data['message'] == "Not Found": page_data = [] - print 'Warning: Repo not found (%(user)s/%(repo)s)' % config + print('Warning: Repo not found (%(user)s/%(repo)s)' % config) else: data.extend(page_data) return data + +def seconds_from_epoch(dates): + seconds = [(dt - epoch).total_seconds() for dt in dates] + return seconds + + +def get_month_bins(dates): + now = datetime.now(tz=dates[0].tzinfo) + this_month = datetime(year=now.year, month=now.month, day=1, + tzinfo=dates[0].tzinfo) + + bins = [this_month - relativedelta(months=i) + for i in reversed(range(-1, month_duration))] + return seconds_from_epoch(bins) + + +def date_formatter(value, _): + dt = epoch + timedelta(seconds=value) + return dt.strftime('%Y/%m') + + +for r in releases: + releases[r] = dateutil.parser.parse(releases[r]) + + try: PRs = json.loads(open(cache, 'r').read()) - print 'Loaded PRs from cache...' + print('Loaded PRs from cache...') except IOError: PRs = fetch_PRs(user='stefanv', repo='scikits.image', state='closed') @@ -81,53 +107,47 @@ except IOError: cf.flush() nrs = [pr['number'] for pr in PRs] -print 'Processing %d pull requests...' % len(nrs) +print('Processing %d pull requests...' % len(nrs)) dates = [dateutil.parser.parse(pr['created_at']) for pr in PRs] epoch = datetime(2009, 1, 1, tzinfo=dates[0].tzinfo) -def seconds_from_epoch(dates): - seconds = [(dt - epoch).total_seconds() for dt in dates] - return seconds - dates_f = seconds_from_epoch(dates) +bins = get_month_bins(dates) -def date_formatter(value, _): - dt = epoch + timedelta(seconds=value) - return dt.strftime('%Y/%m') +fig, ax = plt.subplots(figsize=(7, 5)) -plt.figure(figsize=(7, 5)) +n, bins, _ = ax.hist(dates_f, bins=bins, color='blue', alpha=0.6) -now = datetime.now(tz=dates[0].tzinfo) -this_month = datetime(year=now.year, month=now.month, day=1, - tzinfo=dates[0].tzinfo) - -bins = [this_month - relativedelta(months=i) \ - for i in reversed(range(-1, month_duration))] -bins = seconds_from_epoch(bins) -plt.hist(dates_f, bins=bins) - -ax = plt.gca() ax.xaxis.set_major_formatter(FuncFormatter(date_formatter)) -ax.set_xticks(bins[:-1]) +ax.set_xticks(bins[2:-1:3]) # Date label every 3 months. labels = ax.get_xticklabels() for l in labels: l.set_rotation(40) l.set_size(10) +mixed_transform = blended_transform_factory(ax.transData, ax.transAxes) for version, date in releases.items(): date = seconds_from_epoch([date])[0] - plt.axvline(date, color='r', label=version) + ax.axvline(date, color='black', linestyle=':', label=version) + ax.text(date, 1, version, color='r', va='bottom', ha='center', + transform=mixed_transform) -plt.title('Pull request activity').set_y(1.05) -plt.xlabel('Date') -plt.ylabel('PRs created') -plt.legend(loc=2, title='Release') -plt.subplots_adjust(top=0.875, bottom=0.225) +ax.set_title('Pull request activity').set_y(1.05) +ax.set_xlabel('Date') +ax.set_ylabel('PRs per month', color='blue') +fig.subplots_adjust(top=0.875, bottom=0.225) -plt.savefig('PRs.png') +cumulative = np.cumsum(n) +cumulative += len(dates) - cumulative[-1] + +ax2 = ax.twinx() +ax2.plot(bins[1:], cumulative, color='black', linewidth=2) +ax2.set_ylabel('Total PRs', color='black') + +fig.savefig('PRs.png') plt.show() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..4a4aaa53 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +cython>=0.17 +matplotlib>=1.0 +numpy>=1.6 diff --git a/setup.py b/setup.py index da93dadb..87e45dc8 100644 --- a/setup.py +++ b/setup.py @@ -17,11 +17,11 @@ MAINTAINER_EMAIL = 'stefan@sun.ac.za' URL = 'http://scikit-image.org' LICENSE = 'Modified BSD' DOWNLOAD_URL = 'http://github.com/scikit-image/scikit-image' -VERSION = '0.8.1' +VERSION = '0.9.0' PYTHON_VERSION = (2, 5) DEPENDENCIES = { 'numpy': (1, 6), - 'Cython': (0, 15), + 'Cython': (0, 17), } @@ -30,10 +30,7 @@ import sys import re import setuptools from numpy.distutils.core import setup -try: - from distutils.command.build_py import build_py_2to3 as build_py -except ImportError: - from distutils.command.build_py import build_py +from distutils.command.build_py import build_py def configuration(parent_package='', top_path=None): diff --git a/skimage/__init__.py b/skimage/__init__.py index daac238c..c73a79b7 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -38,8 +38,6 @@ util Utility Functions ----------------- -get_log - Returns the ``skimage`` log. Use this to print debug output. img_as_float Convert an image to floating point format, with values in [0, 1]. img_as_uint @@ -54,6 +52,7 @@ img_as_ubyte import os.path as _osp import imp as _imp import functools as _functools +from skimage._shared.utils import deprecated as _deprecated pkg_dir = _osp.abspath(_osp.dirname(__file__)) data_dir = _osp.join(pkg_dir, 'data') @@ -62,6 +61,7 @@ try: from .version import version as __version__ except ImportError: __version__ = "unbuilt-dev" +del version try: @@ -88,6 +88,56 @@ test_verbose = _functools.partial(test, verbose=True) test_verbose.__doc__ = test.__doc__ +class _Log(Warning): + pass + + +class _FakeLog(object): + def __init__(self, name): + """ + Parameters + ---------- + name : str + Name of the log. + repeat : bool + Whether to print repeating messages more than once (False by + default). + """ + self._name = name + + import warnings + warnings.simplefilter("always", _Log) + + self._warnings = warnings + + def _warn(self, msg, wtype): + self._warnings.warn('%s: %s' % (wtype, msg), _Log) + + def debug(self, msg): + self._warn(msg, 'DEBUG') + + def info(self, msg): + self._warn(msg, 'INFO') + + def warning(self, msg): + self._warn(msg, 'WARNING') + + warn = warning + + def error(self, msg): + self._warn(msg, 'ERROR') + + def critical(self, msg): + self._warn(msg, 'CRITICAL') + + def addHandler(*args): + pass + + def setLevel(*args): + pass + + +@_deprecated() def get_log(name=None): """Return a console logger. @@ -105,39 +155,12 @@ def get_log(name=None): http://docs.python.org/library/logging.html """ - import logging - if name is None: name = 'skimage' else: name = 'skimage.' + name - log = logging.getLogger(name) - return log + return _FakeLog(name) -def _setup_log(): - """Configure root logger. - - """ - import logging - import sys - - formatter = logging.Formatter( - '%(name)s: %(levelname)s: %(message)s' - ) - - try: - handler = logging.StreamHandler(stream=sys.stdout) - except TypeError: - handler = logging.StreamHandler(strm=sys.stdout) - handler.setFormatter(formatter) - - log = get_log() - log.addHandler(handler) - log.setLevel(logging.WARNING) - log.propagate = False - -_setup_log() - from .util.dtype import * diff --git a/skimage/_build.py b/skimage/_build.py index 771b04b9..38239e4b 100644 --- a/skimage/_build.py +++ b/skimage/_build.py @@ -8,7 +8,8 @@ import subprocess try: WindowsError except NameError: - WindowsError = None + class WindowsError(Exception): + pass def cython(pyx_files, working_path=''): diff --git a/skimage/_shared/six.py b/skimage/_shared/six.py new file mode 100644 index 00000000..8a877b17 --- /dev/null +++ b/skimage/_shared/six.py @@ -0,0 +1,423 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2013 Benjamin Peterson +# +# 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: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# 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. + +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.3.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" + _iterlists = "lists" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + _iterlists = "iterlists" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +def iterkeys(d, **kw): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)(**kw)) + +def itervalues(d, **kw): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)(**kw)) + +def iteritems(d, **kw): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)(**kw)) + +def iterlists(d, **kw): + """Return an iterator over the (key, [values]) pairs of a dictionary.""" + return iter(getattr(d, _iterlists)(**kw)) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + unichr = chr + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + unichr = unichr + int2byte = chr + def byte2int(bs): + return ord(bs[0]) + def indexbytes(buf, i): + return ord(buf[i]) + def iterbytes(buf): + return (ord(byte) for byte in buf) + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + return meta("NewBase", bases, {}) diff --git a/skimage/_shared/transform.pxd b/skimage/_shared/transform.pxd index ccb16ff3..4978e843 100644 --- a/skimage/_shared/transform.pxd +++ b/skimage/_shared/transform.pxd @@ -1,5 +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) +cdef float integrate(float[:, ::1] 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 index 8ce2ab67..9bdc6824 100644 --- a/skimage/_shared/transform.pyx +++ b/skimage/_shared/transform.pyx @@ -5,8 +5,8 @@ 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): +cdef float integrate(float[:, ::1] 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. diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 4075ddb4..b7071521 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -1,8 +1,19 @@ import warnings import functools +import sys + +from . import six -__all__ = ['deprecated'] +__all__ = ['deprecated', 'get_bound_method_class'] + + +class skimage_deprecation(Warning): + """Create our own deprecation class, since Python >= 2.7 + silences deprecations by default. + + """ + pass class deprecated(object): @@ -25,19 +36,38 @@ class deprecated(object): def __call__(self, func): - msg = "Call to deprecated function `%s`." % func.__name__ + alt_msg = '' if self.alt_func is not None: - msg = msg + " Use `%s` instead." % self.alt_func + alt_msg = ' Use ``%s`` instead.' % self.alt_func + + msg = 'Call to deprecated function ``%s``.' % func.__name__ + msg += alt_msg @functools.wraps(func) def wrapped(*args, **kwargs): if self.behavior == 'warn': + func_code = six.get_function_code(func) + warnings.simplefilter('always', skimage_deprecation) warnings.warn_explicit(msg, - category=DeprecationWarning, - filename=func.func_code.co_filename, - lineno=func.func_code.co_firstlineno + 1) + category=skimage_deprecation, + filename=func_code.co_filename, + lineno=func_code.co_firstlineno + 1) elif self.behavior == 'raise': - raise DeprecationWarning(msg) + raise skimage_deprecation(msg) return func(*args, **kwargs) + # modify doc string to display deprecation warning + doc = '**Deprecated function**.' + alt_msg + if wrapped.__doc__ is None: + wrapped.__doc__ = doc + else: + wrapped.__doc__ = doc + '\n\n ' + wrapped.__doc__ + return wrapped + + +def get_bound_method_class(m): + """Return the class for a bound method. + + """ + return m.im_class if sys.version < '3' else m.__self__.__class__ diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index 08b5f739..1c61020d 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -1 +1,107 @@ -from .colorconv import * +from .colorconv import (convert_colorspace, + guess_spatial_dimensions, + rgb2hsv, + hsv2rgb, + rgb2xyz, + xyz2rgb, + rgb2rgbcie, + rgbcie2rgb, + rgb2grey, + rgb2gray, + gray2rgb, + xyz2lab, + lab2xyz, + lab2rgb, + rgb2lab, + rgb2hed, + hed2rgb, + lab2lch, + lch2lab, + separate_stains, + combine_stains, + rgb_from_hed, + hed_from_rgb, + rgb_from_hdx, + hdx_from_rgb, + rgb_from_fgx, + fgx_from_rgb, + rgb_from_bex, + bex_from_rgb, + rgb_from_rbd, + rbd_from_rgb, + rgb_from_gdx, + gdx_from_rgb, + rgb_from_hax, + hax_from_rgb, + rgb_from_bro, + bro_from_rgb, + rgb_from_bpx, + bpx_from_rgb, + rgb_from_ahx, + ahx_from_rgb, + rgb_from_hpx, + hpx_from_rgb, + is_rgb, + is_gray) + +from .colorlabel import color_dict, label2rgb + +from .delta_e import (deltaE_cie76, + deltaE_ciede94, + deltaE_ciede2000, + deltaE_cmc, + ) + + +__all__ = ['convert_colorspace', + 'guess_spatial_dimensions', + 'rgb2hsv', + 'hsv2rgb', + 'rgb2xyz', + 'xyz2rgb', + 'rgb2rgbcie', + 'rgbcie2rgb', + 'rgb2grey', + 'rgb2gray', + 'gray2rgb', + 'xyz2lab', + 'lab2xyz', + 'lab2rgb', + 'rgb2lab', + 'rgb2hed', + 'hed2rgb', + 'lab2lch', + 'lch2lab', + 'separate_stains', + 'combine_stains', + 'rgb_from_hed', + 'hed_from_rgb', + 'rgb_from_hdx', + 'hdx_from_rgb', + 'rgb_from_fgx', + 'fgx_from_rgb', + 'rgb_from_bex', + 'bex_from_rgb', + 'rgb_from_rbd', + 'rbd_from_rgb', + 'rgb_from_gdx', + 'gdx_from_rgb', + 'rgb_from_hax', + 'hax_from_rgb', + 'rgb_from_bro', + 'bro_from_rgb', + 'rgb_from_bpx', + 'bpx_from_rgb', + 'rgb_from_ahx', + 'ahx_from_rgb', + 'rgb_from_hpx', + 'hpx_from_rgb', + 'is_rgb', + 'is_gray', + 'color_dict', + 'label2rgb', + 'deltaE_cie76', + 'deltaE_ciede94', + 'deltaE_ciede2000', + 'deltaE_cmc', + ] diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 4186682d..7562b650 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -26,10 +26,17 @@ Supported color spaces Derived from the RGB CIE color space. Chosen such that ``x == y == z == 1/3`` at the whitepoint, and all color matching functions are greater than zero everywhere. +* LAB CIE : Lightness, a, b + Colorspace derived from XYZ CIE that is intended to be more + perceptually uniform +* LCH CIE : Lightness, Chroma, Hue + Defined in terms of LAB CIE. C and H are the polar representation of + a and b. The polar angle C is defined to be on (0, 2*pi) :author: Nicolas Pinto (rgb2hsv) :author: Ralf Gommers (hsv2rgb) :author: Travis Oliphant (XYZ and RGB CIE functions) +:author: Matt Terry (lab2lch) :license: modified BSD @@ -43,18 +50,44 @@ References from __future__ import division -__all__ = ['convert_colorspace', 'rgb2hsv', 'hsv2rgb', 'rgb2xyz', 'xyz2rgb', - 'rgb2rgbcie', 'rgbcie2rgb', 'rgb2grey', 'rgb2gray', 'gray2rgb', - 'xyz2lab', 'lab2xyz', 'lab2rgb', 'rgb2lab', 'is_rgb', 'is_gray' - ] - -__docformat__ = "restructuredtext en" - import numpy as np from scipy import linalg from ..util import dtype +from skimage._shared.utils import deprecated +def guess_spatial_dimensions(image): + """Make an educated guess about whether an image has a channels dimension. + + Parameters + ---------- + image : ndarray + The input image. + + Returns + ------- + spatial_dims : int or None + The number of spatial dimensions of `image`. If ambiguous, the value + is `None`. + + Raises + ------ + ValueError + If the image array has less than two or more than four dimensions. + """ + if image.ndim == 2: + return 2 + if image.ndim == 3 and image.shape[-1] != 3: + return 3 + if image.ndim == 3 and image.shape[-1] == 3: + return None + if image.ndim == 4 and image.shape[-1] == 3: + return 3 + else: + raise ValueError("Expected 2D, 3D, or 4D array, got %iD." % image.ndim) + + +@deprecated() def is_rgb(image): """Test whether the image is RGB or RGBA. @@ -67,6 +100,7 @@ def is_rgb(image): return (image.ndim == 3 and image.shape[2] in (3, 4)) +@deprecated() def is_gray(image): """Test whether the image is gray (i.e. has only one color band). @@ -76,7 +110,7 @@ def is_gray(image): Input image. """ - return image.ndim == 2 + return image.ndim in (2, 3) and not is_rgb(image) def convert_colorspace(arr, fromspace, tospace): @@ -133,8 +167,9 @@ def _prepare_colorarray(arr): """ arr = np.asanyarray(arr) - if arr.ndim != 3 or arr.shape[2] != 3: - msg = "the input array must be have a shape == (.,.,3))" + if arr.ndim not in [3, 4] or arr.shape[-1] != 3: + msg = ("the input array must be have a shape == (.., ..,[ ..,] 3)), " + + "got (" + (", ".join(map(str, arr.shape))) + ")") raise ValueError(msg) return dtype.img_as_float(arr) @@ -312,6 +347,90 @@ gray_from_rgb = np.array([[0.2125, 0.7154, 0.0721], # CIE LAB constants for Observer= 2A, Illuminant= D65 lab_ref_white = np.array([0.95047, 1., 1.08883]) + +# Haematoxylin-Eosin-DAB colorspace +# From original Ruifrok's paper: A. C. Ruifrok and D. A. Johnston, +# "Quantification of histochemical staining by color deconvolution.," +# Analytical and quantitative cytology and histology / the International +# Academy of Cytology [and] American Society of Cytology, vol. 23, no. 4, +# pp. 291-9, Aug. 2001. +rgb_from_hed = np.array([[0.65, 0.70, 0.29], + [0.07, 0.99, 0.11], + [0.27, 0.57, 0.78]]) +hed_from_rgb = linalg.inv(rgb_from_hed) + +# Following matrices are adapted form the Java code written by G.Landini. +# The original code is available at: +# http://www.dentistry.bham.ac.uk/landinig/software/cdeconv/cdeconv.html + +# Hematoxylin + DAB +rgb_from_hdx = np.array([[0.650, 0.704, 0.286], + [0.268, 0.570, 0.776], + [0.0, 0.0, 0.0]]) +rgb_from_hdx[2, :] = np.cross(rgb_from_hdx[0, :], rgb_from_hdx[1, :]) +hdx_from_rgb = linalg.inv(rgb_from_hdx) + +# Feulgen + Light Green +rgb_from_fgx = np.array([[0.46420921, 0.83008335, 0.30827187], + [0.94705542, 0.25373821, 0.19650764], + [0.0, 0.0, 0.0]]) +rgb_from_fgx[2, :] = np.cross(rgb_from_fgx[0, :], rgb_from_fgx[1, :]) +fgx_from_rgb = linalg.inv(rgb_from_fgx) + +# Giemsa: Methyl Blue + Eosin +rgb_from_bex = np.array([[0.834750233, 0.513556283, 0.196330403], + [0.092789, 0.954111, 0.283111], + [0.0, 0.0, 0.0]]) +rgb_from_bex[2, :] = np.cross(rgb_from_bex[0, :], rgb_from_bex[1, :]) +bex_from_rgb = linalg.inv(rgb_from_bex) + +# FastRed + FastBlue + DAB +rgb_from_rbd = np.array([[0.21393921, 0.85112669, 0.47794022], + [0.74890292, 0.60624161, 0.26731082], + [0.268, 0.570, 0.776]]) +rbd_from_rgb = linalg.inv(rgb_from_rbd) + +# Methyl Green + DAB +rgb_from_gdx = np.array([[0.98003, 0.144316, 0.133146], + [0.268, 0.570, 0.776], + [0.0, 0.0, 0.0]]) +rgb_from_gdx[2, :] = np.cross(rgb_from_gdx[0, :], rgb_from_gdx[1, :]) +gdx_from_rgb = linalg.inv(rgb_from_gdx) + +# Hematoxylin + AEC +rgb_from_hax = np.array([[0.650, 0.704, 0.286], + [0.2743, 0.6796, 0.6803], + [0.0, 0.0, 0.0]]) +rgb_from_hax[2, :] = np.cross(rgb_from_hax[0, :], rgb_from_hax[1, :]) +hax_from_rgb = linalg.inv(rgb_from_hax) + +# Blue matrix Anilline Blue + Red matrix Azocarmine + Orange matrix Orange-G +rgb_from_bro = np.array([[0.853033, 0.508733, 0.112656], + [0.09289875, 0.8662008, 0.49098468], + [0.10732849, 0.36765403, 0.9237484]]) +bro_from_rgb = linalg.inv(rgb_from_bro) + +# Methyl Blue + Ponceau Fuchsin +rgb_from_bpx = np.array([[0.7995107, 0.5913521, 0.10528667], + [0.09997159, 0.73738605, 0.6680326], + [0.0, 0.0, 0.0]]) +rgb_from_bpx[2, :] = np.cross(rgb_from_bpx[0, :], rgb_from_bpx[1, :]) +bpx_from_rgb = linalg.inv(rgb_from_bpx) + +# Alcian Blue + Hematoxylin +rgb_from_ahx = np.array([[0.874622, 0.457711, 0.158256], + [0.552556, 0.7544, 0.353744], + [0.0, 0.0, 0.0]]) +rgb_from_ahx[2, :] = np.cross(rgb_from_ahx[0, :], rgb_from_ahx[1, :]) +ahx_from_rgb = linalg.inv(rgb_from_ahx) + +# Hematoxylin + PAS +rgb_from_hpx = np.array([[0.644211, 0.716556, 0.266844], + [0.175411, 0.972178, 0.154589], + [0.0, 0.0, 0.0]]) +rgb_from_hpx[2, :] = np.cross(rgb_from_hpx[0, :], rgb_from_hpx[1, :]) +hpx_from_rgb = linalg.inv(rgb_from_hpx) + #------------------------------------------------------------- # The conversion functions that make use of the matrices above #------------------------------------------------------------- @@ -333,12 +452,12 @@ def _convert(matrix, arr): The converted array. """ arr = _prepare_colorarray(arr) - arr = np.swapaxes(arr, 0, 2) + arr = np.swapaxes(arr, 0, -1) oldshape = arr.shape arr = np.reshape(arr, (3, -1)) out = np.dot(matrix, arr) out.shape = oldshape - out = np.swapaxes(out, 2, 0) + out = np.swapaxes(out, -1, 0) return np.ascontiguousarray(out) @@ -393,17 +512,19 @@ def rgb2xyz(rgb): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Returns ------- out : ndarray - The image in XYZ format, in a 3-D array of shape (.., .., 3). + The image in XYZ format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). Notes ----- @@ -548,23 +669,24 @@ def gray2rgb(image): Parameters ---------- image : array_like - Input image of shape ``(M, N)``. + Input image of shape ``(M, N [, P])``. Returns ------- rgb : ndarray - RGB image of shape ``(M, N, 3)``. + RGB image of shape ``(M, N, [, P], 3)``. Raises ------ ValueError - If the input is not 2-dimensional. + If the input is not a 2- or 3-dimensional image. """ - if is_rgb(image): + if np.squeeze(image).ndim == 3 and image.shape[2] in (3, 4): return image - elif is_gray(image): - return np.dstack((image, image, image)) + elif image.ndim != 1 and np.squeeze(image).ndim in (1, 2, 3): + image = image[..., np.newaxis] + return np.concatenate(3 * (image,), axis=-1) else: raise ValueError("Input image expected to be RGB, RGBA or gray.") @@ -575,17 +697,19 @@ def xyz2lab(xyz): Parameters ---------- xyz : array_like - The image in XYZ format, in a 3-D array of shape (.., .., 3). + The image in XYZ format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Returns ------- out : ndarray - The image in CIE-LAB format, in a 3-D array of shape (.., .., 3). + The image in CIE-LAB format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Raises ------ ValueError - If `xyz` is not a 3-D array of shape (.., .., 3). + If `xyz` is not a 3-D array of shape (.., ..,[ ..,] 3). Notes ----- @@ -615,14 +739,14 @@ def xyz2lab(xyz): 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] + 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]) + return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1) def lab2xyz(lab): @@ -679,17 +803,19 @@ def rgb2lab(rgb): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Returns ------- out : ndarray - The image in Lab format, in a 3-D array of shape (.., .., 3). + The image in Lab format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). Notes ----- @@ -721,3 +847,291 @@ def lab2rgb(lab): This function uses lab2xyz and xyz2rgb. """ return xyz2rgb(lab2xyz(lab)) + + +def rgb2hed(rgb): + """RGB to Haematoxylin-Eosin-DAB (HED) 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 HED format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `rgb` is not a 3-D array of shape (.., .., 3). + + + References + ---------- + .. [1] A. C. Ruifrok and D. A. Johnston, "Quantification of histochemical + staining by color deconvolution.," Analytical and quantitative + cytology and histology / the International Academy of Cytology [and] + American Society of Cytology, vol. 23, no. 4, pp. 291-9, Aug. 2001. + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2hed + >>> ihc = data.immunohistochemistry() + >>> ihc_hed = rgb2hed(ihc) + """ + return separate_stains(rgb, hed_from_rgb) + + +def hed2rgb(hed): + """Haematoxylin-Eosin-DAB (HED) to RGB color space conversion. + + Parameters + ---------- + hed : array_like + The image in the HED color space, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in RGB, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `hed` is not a 3-D array of shape (.., .., 3). + + References + ---------- + .. [1] A. C. Ruifrok and D. A. Johnston, "Quantification of histochemical + staining by color deconvolution.," Analytical and quantitative + cytology and histology / the International Academy of Cytology [and] + American Society of Cytology, vol. 23, no. 4, pp. 291-9, Aug. 2001. + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2hed, hed2rgb + >>> ihc = data.immunohistochemistry() + >>> ihc_hed = rgb2hed(ihc) + >>> ihc_rgb = hed2rgb(ihc_hed) + """ + return combine_stains(hed, rgb_from_hed) + + +def separate_stains(rgb, conv_matrix): + """RGB to stain color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3-D array of shape (.., .., 3). + conv_matrix: ndarray + The stain separation matrix as described by G. Landini [1]_. + + Returns + ------- + out : ndarray + The image in stain color space, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `rgb` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + Stain separation matrices available in the ``color`` module and their + respective colorspace: + + * ``hed_from_rgb``: Hematoxylin + Eosin + DAB + * ``hdx_from_rgb``: Hematoxylin + DAB + * ``fgx_from_rgb``: Feulgen + Light Green + * ``bex_from_rgb``: Giemsa stain : Methyl Blue + Eosin + * ``rbd_from_rgb``: FastRed + FastBlue + DAB + * ``gdx_from_rgb``: Methyl Green + DAB + * ``hax_from_rgb``: Hematoxylin + AEC + * ``bro_from_rgb``: Blue matrix Anilline Blue + Red matrix Azocarmine\ + + Orange matrix Orange-G + * ``bpx_from_rgb``: Methyl Blue + Ponceau Fuchsin + * ``ahx_from_rgb``: Alcian Blue + Hematoxylin + * ``hpx_from_rgb``: Hematoxylin + PAS + + References + ---------- + .. [1] http://www.dentistry.bham.ac.uk/landinig/software/cdeconv/cdeconv.html + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import separate_stains, hdx_from_rgb + >>> ihc = data.immunohistochemistry() + >>> ihc_hdx = separate_stains(ihc, hdx_from_rgb) + """ + rgb = dtype.img_as_float(rgb) + 2 + stains = np.dot(np.reshape(-np.log(rgb), (-1, 3)), conv_matrix) + return np.reshape(stains, rgb.shape) + + +def combine_stains(stains, conv_matrix): + """Stain to RGB color space conversion. + + Parameters + ---------- + stains : array_like + The image in stain color space, in a 3-D array of shape (.., .., 3). + conv_matrix: ndarray + The stain separation matrix as described by G. Landini [1]_. + + Returns + ------- + out : ndarray + The image in RGB format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `stains` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + Stain combination matrices available in the ``color`` module and their + respective colorspace: + + * ``rgb_from_hed``: Hematoxylin + Eosin + DAB + * ``rgb_from_hdx``: Hematoxylin + DAB + * ``rgb_from_fgx``: Feulgen + Light Green + * ``rgb_from_bex``: Giemsa stain : Methyl Blue + Eosin + * ``rgb_from_rbd``: FastRed + FastBlue + DAB + * ``rgb_from_gdx``: Methyl Green + DAB + * ``rgb_from_hax``: Hematoxylin + AEC + * ``rgb_from_bro``: Blue matrix Anilline Blue + Red matrix Azocarmine\ + + Orange matrix Orange-G + * ``rgb_from_bpx``: Methyl Blue + Ponceau Fuchsin + * ``rgb_from_ahx``: Alcian Blue + Hematoxylin + * ``rgb_from_hpx``: Hematoxylin + PAS + + References + ---------- + .. [1] http://www.dentistry.bham.ac.uk/landinig/software/cdeconv/cdeconv.html + + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import (separate_stains, combine_stains, + ... hdx_from_rgb, rgb_from_hdx) + >>> ihc = data.immunohistochemistry() + >>> ihc_hdx = separate_stains(ihc, hdx_from_rgb) + >>> ihc_rgb = combine_stains(ihc_hdx, rgb_from_hdx) + """ + from ..exposure import rescale_intensity + + stains = dtype.img_as_float(stains) + logrgb2 = np.dot(-np.reshape(stains, (-1, 3)), conv_matrix) + rgb2 = np.exp(logrgb2) + return rescale_intensity(np.reshape(rgb2 - 2, stains.shape), in_range=(-1, 1)) + + +def lab2lch(lab): + """CIE-LAB to CIE-LCH color space conversion. + + LCH is the cylindrical representation of the LAB (Cartesian) colorspace + + Parameters + ---------- + lab : array_like + The N-D image in CIE-LAB format. The last (`N+1`th) dimension must have + at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` color + channels. Subsequent elements are copied. + + Returns + ------- + out : ndarray + The image in LCH format, in a N-D array with same shape as input `lab`. + + Raises + ------ + ValueError + If `lch` does not have at least 3 color channels (i.e. l, a, b). + + Notes + ----- + The Hue is expressed as an angle between (0, 2*pi) + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2lab, lab2lch + >>> lena = data.lena() + >>> lena_lab = rgb2lab(lena) + >>> lena_lch = lab2lch(lena_lab) + """ + lch = _prepare_lab_array(lab) + + a, b = lch[..., 1], lch[..., 2] + lch[..., 1], lch[..., 2] = _cart2polar_2pi(a, b) + return lch + + +def _cart2polar_2pi(x, y): + """convert cartesian coordiantes to polar (uses non-standard theta range!) + + NON-STANDARD RANGE! Maps to (0, 2*pi) rather than usual (-pi, +pi) + """ + r, t = np.hypot(x, y), np.arctan2(y, x) + t += np.where(t < 0., 2 * np.pi, 0) + return r, t + + +def lch2lab(lch): + """CIE-LCH to CIE-LAB color space conversion. + + LCH is the cylindrical representation of the LAB (Cartesian) colorspace + + Parameters + ---------- + lch : array_like + The N-D image in CIE-LCH format. The last (`N+1`th) dimension must have + at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` color + channels. Subsequent elements are copied. + + Returns + ------- + out : ndarray + The image in LAB format, with same shape as input `lch`. + + Raises + ------ + ValueError + If `lch` does not have at least 3 color channels (i.e. l, c, h). + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2lab, lch2lab + >>> lena = data.lena() + >>> lena_lab = rgb2lab(lena) + >>> lena_lch = lab2lch(lena_lab) + >>> lena_lab2 = lch2lab(lena_lch) + """ + lch = _prepare_lab_array(lch) + + c, h = lch[..., 1], lch[..., 2] + lch[..., 1], lch[..., 2] = c * np.cos(h), c * np.sin(h) + return lch + + +def _prepare_lab_array(arr): + """Ensure input for lab2lch, lch2lab are well-posed. + + Arrays must be in floating point and have at least 3 elements in + last dimension. Return a new array. + """ + arr = np.asarray(arr) + shape = arr.shape + if shape[-1] < 3: + raise ValueError('Input array has less than 3 color channels') + return dtype.img_as_float(arr, force_copy=True) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py new file mode 100644 index 00000000..8d7787aa --- /dev/null +++ b/skimage/color/colorlabel.py @@ -0,0 +1,134 @@ +import warnings +import itertools + +import numpy as np + +from skimage import img_as_float +from skimage._shared import six +from skimage._shared.six.moves import zip +from .colorconv import rgb2gray, gray2rgb +from . import rgb_colors + + +__all__ = ['color_dict', 'label2rgb', 'DEFAULT_COLORS'] + + +DEFAULT_COLORS = ('red', 'blue', 'yellow', 'magenta', 'green', + 'indigo', 'darkorange', 'cyan', 'pink', 'yellowgreen') + + +color_dict = rgb_colors.__dict__ + + +def _rgb_vector(color): + """Return RGB color as (1, 3) array. + + This RGB array gets multiplied by masked regions of an RGB image, which are + partially flattened by masking (i.e. dimensions 2D + RGB -> 1D + RGB). + + Parameters + ---------- + color : str or array + Color name in `color_dict` or RGB float values between [0, 1]. + """ + if isinstance(color, six.string_types): + color = color_dict[color] + # Slice to handle RGBA colors. + return np.array(color[:3]) + + +def _match_label_with_color(label, colors, bg_label, bg_color): + """Return `unique_labels` and `color_cycle` for label array and color list. + + Colors are cycled for normal labels, but the background color should only + be used for the background. + """ + # Temporarily set background color; it will be removed later. + if bg_color is None: + bg_color = (0, 0, 0) + bg_color = _rgb_vector([bg_color]) + + unique_labels = list(set(label.flat)) + # Ensure that the background label is in front to match call to `chain`. + if bg_label in unique_labels: + unique_labels.remove(bg_label) + unique_labels.insert(0, bg_label) + + # Modify labels and color cycle so background color is used only once. + color_cycle = itertools.cycle(colors) + color_cycle = itertools.chain(bg_color, color_cycle) + + return unique_labels, color_cycle + + +def label2rgb(label, image=None, colors=None, alpha=0.3, + bg_label=-1, bg_color=None, image_alpha=1): + """Return an RGB image where color-coded labels are painted over the image. + + Parameters + ---------- + label : array + Integer array of labels with the same shape as `image`. + image : array + Image used as underlay for labels. If the input is an RGB image, it's + converted to grayscale before coloring. + colors : list + List of colors. If the number of labels exceeds the number of colors, + then the colors are cycled. + alpha : float [0, 1] + Opacity of colorized labels. Ignored if image is `None`. + bg_label : int + Label that's treated as the background. + bg_color : str or array + Background color. Must be a name in `color_dict` or RGB float values + between [0, 1]. + image_alpha : float [0, 1] + Opacity of the image. + """ + if colors is None: + colors = DEFAULT_COLORS + colors = [_rgb_vector(c) for c in colors] + + if image is None: + image = np.zeros(label.shape + (3,), dtype=np.float64) + # Opacity doesn't make sense if no image exists. + alpha = 1 + else: + if not image.shape[:2] == label.shape: + raise ValueError("`image` and `label` must be the same shape") + + if image.min() < 0: + warnings.warn("Negative intensities in `image` are not supported") + + image = img_as_float(rgb2gray(image)) + image = gray2rgb(image) * image_alpha + (1 - image_alpha) + + # Ensure that all labels are non-negative so we can index into + # `label_to_color` correctly. + offset = min(label.min(), bg_label) + if offset != 0: + label = label - offset # Make sure you don't modify the input array. + bg_label -= offset + + new_type = np.min_scalar_type(label.max()) + if new_type == np.bool: + new_type = np.uint8 + label = label.astype(new_type) + + unique_labels, color_cycle = _match_label_with_color(label, colors, + bg_label, bg_color) + + if len(unique_labels) == 0: + return image + + dense_labels = range(max(unique_labels) + 1) + label_to_color = np.array([c for i, c in zip(dense_labels, color_cycle)]) + + result = label_to_color[label] * alpha + image * (1 - alpha) + + # Remove background label if its color was not specified. + remove_background = bg_label in unique_labels and bg_color is None + if remove_background: + result[label == bg_label] = image[label == bg_label] + + return result diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py new file mode 100644 index 00000000..9119ecf4 --- /dev/null +++ b/skimage/color/delta_e.py @@ -0,0 +1,339 @@ +""" +Functions for calculating the "distance" between colors. + +Implicit in these definitions of "distance" is the notion of "Just Noticeable +Distance" (JND). This represents the distance between colors where a human can +perceive different colors. Humans are more sensitive to certain colors than +others, which different deltaE metrics correct for with varying degrees of +sophistication. + +The literature often mentions 1 as the minimum distance for visual +differentiation, but more recent studies (Mahy 1994) peg JND at 2.3 + +The delta-E notation comes from the German word for "Sensation" (Empfindung). + +Reference +--------- +http://en.wikipedia.org/wiki/Color_difference + +""" +from __future__ import division + +import numpy as np + +from skimage.color.colorconv import lab2lch, _cart2polar_2pi + + +def deltaE_cie76(lab1, lab2): + """Euclidean distance between two points in Lab color space + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + + Returns + ------- + dE : array_like + distance between colors `lab1` and `lab2` + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] A. R. Robertson, "The CIE 1976 color-difference formulae," + Color Res. Appl. 2, 7-11 (1977). + """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + L1, a1, b1 = np.rollaxis(lab1, -1)[:3] + L2, a2, b2 = np.rollaxis(lab2, -1)[:3] + return np.sqrt((L2 - L1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2) + + +def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): + """Color difference according to CIEDE 94 standard + + Accommodates perceptual non-uniformities through the use of application + specific scale factors (`kH`, `kC`, `kL`, `k1`, and `k2`). + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + kH : float, optional + Hue scale + kC : float, optional + Chroma scale + kL : float, optional + Lightness scale + k1 : float, optional + first scale parameter + k2 : float, optional + second scale parameter + + Returns + ------- + dE : array_like + color difference between `lab1` and `lab2` + + Notes + ----- + deltaE_ciede94 is not symmetric with respect to lab1 and lab2. CIEDE94 + defines the scales for the lightness, hue, and chroma in terms of the first + color. Consequently, the first color should be regarded as the "reference" + color. + + `kL`, `k1`, `k2` depend on the application and default to the values + suggested for graphic arts + + ========== ============== ========== + Parameter Graphic Arts Textiles + ========== ============== ========== + `kL` 1.000 2.000 + `k1` 0.045 0.048 + `k2` 0.015 0.014 + ========== ============== ========== + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + """ + L1, C1 = np.rollaxis(lab2lch(lab1), -1)[:2] + L2, C2 = np.rollaxis(lab2lch(lab2), -1)[:2] + + dL = L1 - L2 + dC = C1 - C2 + dH2 = get_dH2(lab1, lab2) + + SL = 1 + SC = 1 + k1 * C1 + SH = 1 + k2 * C1 + + dE2 = (dL / (kL * SL)) ** 2 + dE2 += (dC / (kC * SC)) ** 2 + dE2 += dH2 / (kH * SH) ** 2 + return np.sqrt(dE2) + + +def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): + """Color difference as given by the CIEDE 2000 standard. + + CIEDE 2000 is a major revision of CIDE94. The perceptual calibration is + largely based on experience with automotive paint on smooth surfaces. + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + kL : float (range), optional + lightness scale factor, 1 for "acceptably close"; 2 for "imperceptible" + see deltaE_cmc + kC : float (range), optional + chroma scale factor, usually 1 + kH : float (range), optional + hue scale factor, usually 1 + + Returns + ------- + deltaE : array_like + The distance between `lab1` and `lab2` + + Notes + ----- + CIEDE 2000 assumes parametric weighting factors for the lightness, chroma, + and hue (`kL`, `kC`, `kH` respectively). These default to 1. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf + (doi:10.1364/AO.33.008069) + .. [3] M. Melgosa, J. Quesada, and E. Hita, "Uniformity of some recent + color metrics tested with an accurate color-difference tolerance + dataset," Appl. Opt. 33, 8069-8077 (1994). + """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + unroll = False + if lab1.ndim == 1 and lab2.ndim == 1: + unroll = True + if lab1.ndim == 1: + lab1 = lab1[None, :] + if lab2.ndim == 1: + lab2 = lab2[None, :] + L1, a1, b1 = np.rollaxis(lab1, -1)[:3] + L2, a2, b2 = np.rollaxis(lab2, -1)[:3] + + # distort `a` based on average chroma + # then convert to lch coordines from distorted `a` + # all subsequence calculations are in the new coordiantes + # (often denoted "prime" in the literature) + Cbar = 0.5 * (np.hypot(a1, b1) + np.hypot(a2, b2)) + c7 = Cbar ** 7 + G = 0.5 * (1 - np.sqrt(c7 / (c7 + 25 ** 7))) + scale = 1 + G + C1, h1 = _cart2polar_2pi(a1 * scale, b1) + C2, h2 = _cart2polar_2pi(a2 * scale, b2) + # recall that c, h are polar coordiantes. c==r, h==theta + + # cide2000 has four terms to delta_e: + # 1) Luminance term + # 2) Hue term + # 3) Chroma term + # 4) hue Rotation term + + # lightness term + Lbar = 0.5 * (L1 + L2) + tmp = (Lbar - 50) ** 2 + SL = 1 + 0.015 * tmp / np.sqrt(20 + tmp) + L_term = (L2 - L1) / (kL * SL) + + # chroma term + Cbar = 0.5 * (C1 + C2) # new coordiantes + SC = 1 + 0.045 * Cbar + C_term = (C2 - C1) / (kC * SC) + + # hue term + h_diff = h2 - h1 + h_sum = h1 + h2 + CC = C1 * C2 + + dH = h_diff.copy() + dH[h_diff > np.pi] -= 2 * np.pi + dH[h_diff < -np.pi] += 2 * np.pi + dH[CC == 0.] = 0. # if r == 0, dtheta == 0 + dH_term = 2 * np.sqrt(CC) * np.sin(dH / 2) + + Hbar = h_sum.copy() + mask = np.logical_and(CC != 0., np.abs(h_diff) > np.pi) + Hbar[mask * (h_sum < 2 * np.pi)] += 2 * np.pi + Hbar[mask * (h_sum >= 2 * np.pi)] -= 2 * np.pi + Hbar[CC == 0.] *= 2 + Hbar *= 0.5 + + T = (1 - + 0.17 * np.cos(Hbar - np.deg2rad(30)) + + 0.24 * np.cos(2 * Hbar) + + 0.32 * np.cos(3 * Hbar + np.deg2rad(6)) - + 0.20 * np.cos(4 * Hbar - np.deg2rad(63)) + ) + SH = 1 + 0.015 * Cbar * T + + H_term = dH_term / (kH * SH) + + # hue rotation + c7 = Cbar ** 7 + Rc = 2 * np.sqrt(c7 / (c7 + 25 ** 7)) + dtheta = np.deg2rad(30) * np.exp(-((np.rad2deg(Hbar) - 275) / 25) ** 2) + R_term = -np.sin(2 * dtheta) * Rc * C_term * H_term + + # put it all together + dE2 = L_term ** 2 + dE2 += C_term ** 2 + dE2 += H_term ** 2 + dE2 += R_term + ans = np.sqrt(dE2) + if unroll: + ans = ans[0] + return ans + + +def deltaE_cmc(lab1, lab2, kL=1, kC=1): + """Color difference from the CMC l:c standard. + + This color difference was developed by the Colour Measurement Committee + (CMC) of the Society of Dyers and Colourists (United Kingdom). It is + intended for use in the textile industry. + + The scale factors `kL`, `kC` set the weight given to differences in + lightness and chroma relative to differences in hue. The usual values are + ``kL=2``, ``kC=1`` for "acceptability" and ``kL=1``, ``kC=1`` for + "imperceptibility". Colors with ``dE > 1`` are "different" for the given + scale factors. + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + + Returns + ------- + dE : array_like + distance between colors `lab1` and `lab2` + + Notes + ----- + deltaE_cmc the defines the scales for the lightness, hue, and chroma + in terms of the first color. Consequently + ``deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1)`` + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + .. [3] F. J. J. Clarke, R. McDonald, and B. Rigg, "Modification to the + JPC79 colour-difference formula," J. Soc. Dyers Colour. 100, 128-132 + (1984). + """ + L1, C1, h1 = np.rollaxis(lab2lch(lab1), -1)[:3] + L2, C2, h2 = np.rollaxis(lab2lch(lab2), -1)[:3] + + dC = C1 - C2 + dL = L1 - L2 + dH2 = get_dH2(lab1, lab2) + + T = np.where(np.logical_and(np.rad2deg(h1) >= 164, np.rad2deg(h1) <= 345), + 0.56 + 0.2 * np.abs(np.cos(h1 + np.deg2rad(168))), + 0.36 + 0.4 * np.abs(np.cos(h1 + np.deg2rad(35))) + ) + c1_4 = C1 ** 4 + F = np.sqrt(c1_4 / (c1_4 + 1900)) + + SL = np.where(L1 < 16, 0.511, 0.040975 * L1 / (1. + 0.01765 * L1)) + SC = 0.638 + 0.0638 * C1 / (1. + 0.0131 * C1) + SH = SC * (F * T + 1 - F) + + dE2 = (dL / (kL * SL)) ** 2 + dE2 += (dC / (kC * SC)) ** 2 + dE2 += dH2 / (SH ** 2) + return np.sqrt(dE2) + + +def get_dH2(lab1, lab2): + """squared hue difference term occurring in deltaE_cmc and deltaE_ciede94 + + Despite its name, "dH" is not a simple difference of hue values. We avoid + working directly with the hue value, since differencing angles is + troublesome. The hue term is usually written as: + c1 = sqrt(a1**2 + b1**2) + c2 = sqrt(a2**2 + b2**2) + term = (a1-a2)**2 + (b1-b2)**2 - (c1-c2)**2 + dH = sqrt(term) + + However, this has poor roundoff properties when a or b is dominant. + Instead, ab is a vector with elements a and b. The same dH term can be + re-written as: + |ab1-ab2|**2 - (|ab1| - |ab2|)**2 + and then simplified to: + 2*|ab1|*|ab2| - 2*dot(ab1, ab2) + """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + a1, b1 = np.rollaxis(lab1, -1)[1:3] + a2, b2 = np.rollaxis(lab2, -1)[1:3] + + # magnitude of (a, b) is the chroma + C1 = np.hypot(a1, b1) + C2 = np.hypot(a2, b2) + + term = (C1 * C2) - (a1 * a2 + b1 * b2) + return 2*term diff --git a/skimage/color/rgb_colors.py b/skimage/color/rgb_colors.py new file mode 100644 index 00000000..23046105 --- /dev/null +++ b/skimage/color/rgb_colors.py @@ -0,0 +1,146 @@ +aliceblue = (0.941, 0.973, 1) +antiquewhite = (0.98, 0.922, 0.843) +aqua = (0, 1, 1) +aquamarine = (0.498, 1, 0.831) +azure = (0.941, 1, 1) +beige = (0.961, 0.961, 0.863) +bisque = (1, 0.894, 0.769) +black = (0, 0, 0) +blanchedalmond = (1, 0.922, 0.804) +blue = (0, 0, 1) +blueviolet = (0.541, 0.169, 0.886) +brown = (0.647, 0.165, 0.165) +burlywood = (0.871, 0.722, 0.529) +cadetblue = (0.373, 0.62, 0.627) +chartreuse = (0.498, 1, 0) +chocolate = (0.824, 0.412, 0.118) +coral = (1, 0.498, 0.314) +cornflowerblue = (0.392, 0.584, 0.929) +cornsilk = (1, 0.973, 0.863) +crimson = (0.863, 0.0784, 0.235) +cyan = (0, 1, 1) +darkblue = (0, 0, 0.545) +darkcyan = (0, 0.545, 0.545) +darkgoldenrod = (0.722, 0.525, 0.0431) +darkgray = (0.663, 0.663, 0.663) +darkgreen = (0, 0.392, 0) +darkgrey = (0.663, 0.663, 0.663) +darkkhaki = (0.741, 0.718, 0.42) +darkmagenta = (0.545, 0, 0.545) +darkolivegreen = (0.333, 0.42, 0.184) +darkorange = (1, 0.549, 0) +darkorchid = (0.6, 0.196, 0.8) +darkred = (0.545, 0, 0) +darksalmon = (0.914, 0.588, 0.478) +darkseagreen = (0.561, 0.737, 0.561) +darkslateblue = (0.282, 0.239, 0.545) +darkslategray = (0.184, 0.31, 0.31) +darkslategrey = (0.184, 0.31, 0.31) +darkturquoise = (0, 0.808, 0.82) +darkviolet = (0.58, 0, 0.827) +deeppink = (1, 0.0784, 0.576) +deepskyblue = (0, 0.749, 1) +dimgray = (0.412, 0.412, 0.412) +dimgrey = (0.412, 0.412, 0.412) +dodgerblue = (0.118, 0.565, 1) +firebrick = (0.698, 0.133, 0.133) +floralwhite = (1, 0.98, 0.941) +forestgreen = (0.133, 0.545, 0.133) +fuchsia = (1, 0, 1) +gainsboro = (0.863, 0.863, 0.863) +ghostwhite = (0.973, 0.973, 1) +gold = (1, 0.843, 0) +goldenrod = (0.855, 0.647, 0.125) +gray = (0.502, 0.502, 0.502) +green = (0, 0.502, 0) +greenyellow = (0.678, 1, 0.184) +grey = (0.502, 0.502, 0.502) +honeydew = (0.941, 1, 0.941) +hotpink = (1, 0.412, 0.706) +indianred = (0.804, 0.361, 0.361) +indigo = (0.294, 0, 0.51) +ivory = (1, 1, 0.941) +khaki = (0.941, 0.902, 0.549) +lavender = (0.902, 0.902, 0.98) +lavenderblush = (1, 0.941, 0.961) +lawngreen = (0.486, 0.988, 0) +lemonchiffon = (1, 0.98, 0.804) +lightblue = (0.678, 0.847, 0.902) +lightcoral = (0.941, 0.502, 0.502) +lightcyan = (0.878, 1, 1) +lightgoldenrodyellow = (0.98, 0.98, 0.824) +lightgray = (0.827, 0.827, 0.827) +lightgreen = (0.565, 0.933, 0.565) +lightgrey = (0.827, 0.827, 0.827) +lightpink = (1, 0.714, 0.757) +lightsalmon = (1, 0.627, 0.478) +lightseagreen = (0.125, 0.698, 0.667) +lightskyblue = (0.529, 0.808, 0.98) +lightslategray = (0.467, 0.533, 0.6) +lightslategrey = (0.467, 0.533, 0.6) +lightsteelblue = (0.69, 0.769, 0.871) +lightyellow = (1, 1, 0.878) +lime = (0, 1, 0) +limegreen = (0.196, 0.804, 0.196) +linen = (0.98, 0.941, 0.902) +magenta = (1, 0, 1) +maroon = (0.502, 0, 0) +mediumaquamarine = (0.4, 0.804, 0.667) +mediumblue = (0, 0, 0.804) +mediumorchid = (0.729, 0.333, 0.827) +mediumpurple = (0.576, 0.439, 0.859) +mediumseagreen = (0.235, 0.702, 0.443) +mediumslateblue = (0.482, 0.408, 0.933) +mediumspringgreen = (0, 0.98, 0.604) +mediumturquoise = (0.282, 0.82, 0.8) +mediumvioletred = (0.78, 0.0824, 0.522) +midnightblue = (0.098, 0.098, 0.439) +mintcream = (0.961, 1, 0.98) +mistyrose = (1, 0.894, 0.882) +moccasin = (1, 0.894, 0.71) +navajowhite = (1, 0.871, 0.678) +navy = (0, 0, 0.502) +oldlace = (0.992, 0.961, 0.902) +olive = (0.502, 0.502, 0) +olivedrab = (0.42, 0.557, 0.137) +orange = (1, 0.647, 0) +orangered = (1, 0.271, 0) +orchid = (0.855, 0.439, 0.839) +palegoldenrod = (0.933, 0.91, 0.667) +palegreen = (0.596, 0.984, 0.596) +palevioletred = (0.686, 0.933, 0.933) +papayawhip = (1, 0.937, 0.835) +peachpuff = (1, 0.855, 0.725) +peru = (0.804, 0.522, 0.247) +pink = (1, 0.753, 0.796) +plum = (0.867, 0.627, 0.867) +powderblue = (0.69, 0.878, 0.902) +purple = (0.502, 0, 0.502) +red = (1, 0, 0) +rosybrown = (0.737, 0.561, 0.561) +royalblue = (0.255, 0.412, 0.882) +saddlebrown = (0.545, 0.271, 0.0745) +salmon = (0.98, 0.502, 0.447) +sandybrown = (0.98, 0.643, 0.376) +seagreen = (0.18, 0.545, 0.341) +seashell = (1, 0.961, 0.933) +sienna = (0.627, 0.322, 0.176) +silver = (0.753, 0.753, 0.753) +skyblue = (0.529, 0.808, 0.922) +slateblue = (0.416, 0.353, 0.804) +slategray = (0.439, 0.502, 0.565) +slategrey = (0.439, 0.502, 0.565) +snow = (1, 0.98, 0.98) +springgreen = (0, 1, 0.498) +steelblue = (0.275, 0.51, 0.706) +tan = (0.824, 0.706, 0.549) +teal = (0, 0.502, 0.502) +thistle = (0.847, 0.749, 0.847) +tomato = (1, 0.388, 0.278) +turquoise = (0.251, 0.878, 0.816) +violet = (0.933, 0.51, 0.933) +wheat = (0.961, 0.871, 0.702) +white = (1, 1, 1) +whitesmoke = (0.961, 0.961, 0.961) +yellow = (1, 1, 0) +yellowgreen = (0.604, 0.804, 0.196) diff --git a/skimage/color/tests/ciede2000_test_data.txt b/skimage/color/tests/ciede2000_test_data.txt new file mode 100644 index 00000000..b7e3fd57 --- /dev/null +++ b/skimage/color/tests/ciede2000_test_data.txt @@ -0,0 +1,38 @@ +# input, intermediate, and output values for CIEDE2000 dE function +# data taken from "The CIEDE2000 Color-Difference Formula: Implementation Notes, ..." http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf +# tab delimited data +# pair 1 L1 a1 b1 ap1 cp1 hp1 hbar1 G T SL SC SH RT dE 2 L2 a2 b2 ap2 cp2 hp2 +1 1 50.0000 2.6772 -79.7751 2.6774 79.8200 271.9222 270.9611 0.0001 0.6907 1.0000 4.6578 1.8421 -1.7042 2.0425 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +2 1 50.0000 3.1571 -77.2803 3.1573 77.3448 272.3395 271.1698 0.0001 0.6843 1.0000 4.6021 1.8216 -1.7070 2.8615 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +3 1 50.0000 2.8361 -74.0200 2.8363 74.0743 272.1944 271.0972 0.0001 0.6865 1.0000 4.5285 1.8074 -1.7060 3.4412 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +4 1 50.0000 -1.3802 -84.2814 -1.3803 84.2927 269.0618 269.5309 0.0001 0.7357 1.0000 4.7584 1.9217 -1.6809 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +5 1 50.0000 -1.1848 -84.8006 -1.1849 84.8089 269.1995 269.5997 0.0001 0.7335 1.0000 4.7700 1.9218 -1.6822 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +6 1 50.0000 -0.9009 -85.5211 -0.9009 85.5258 269.3964 269.6982 0.0001 0.7303 1.0000 4.7862 1.9217 -1.6840 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +7 1 50.0000 0.0000 0.0000 0.0000 0.0000 0.0000 126.8697 0.5000 1.2200 1.0000 1.0562 1.0229 0.0000 2.3669 2 50.0000 -1.0000 2.0000 -1.5000 2.5000 126.8697 +8 1 50.0000 -1.0000 2.0000 -1.5000 2.5000 126.8697 126.8697 0.5000 1.2200 1.0000 1.0562 1.0229 0.0000 2.3669 2 50.0000 0.0000 0.0000 0.0000 0.0000 0.0000 +9 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 269.9854 0.4998 0.7212 1.0000 1.1681 1.0404 -0.0022 7.1792 2 50.0000 -2.4900 0.0009 -3.7346 3.7346 179.9862 +10 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 269.9847 0.4998 0.7212 1.0000 1.1681 1.0404 -0.0022 7.1792 2 50.0000 -2.4900 0.0010 -3.7346 3.7346 179.9847 +11 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 89.9839 0.4998 0.6175 1.0000 1.1681 1.0346 0.0000 7.2195 2 50.0000 -2.4900 0.0011 -3.7346 3.7346 179.9831 +12 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 89.9831 0.4998 0.6175 1.0000 1.1681 1.0346 0.0000 7.2195 2 50.0000 -2.4900 0.0012 -3.7346 3.7346 179.9816 +13 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 180.0328 0.4998 0.9779 1.0000 1.1121 1.0365 0.0000 4.8045 2 50.0000 0.0009 -2.4900 0.0013 2.4900 270.0311 +14 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 180.0345 0.4998 0.9779 1.0000 1.1121 1.0365 0.0000 4.8045 2 50.0000 0.0010 -2.4900 0.0015 2.4900 270.0345 +15 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 0.0362 0.4998 1.3197 1.0000 1.1121 1.0493 0.0000 4.7461 2 50.0000 0.0011 -2.4900 0.0016 2.4900 270.0380 +16 1 50.0000 2.5000 0.0000 3.7496 3.7496 0.0000 315.0000 0.4998 0.8454 1.0000 1.1406 1.0396 -0.0001 4.3065 2 50.0000 0.0000 -2.5000 0.0000 2.5000 270.0000 +17 1 50.0000 2.5000 0.0000 3.4569 3.4569 0.0000 346.2470 0.3827 1.4453 1.1608 1.9547 1.4599 -0.0003 27.1492 2 73.0000 25.0000 -18.0000 34.5687 38.9743 332.4939 +18 1 50.0000 2.5000 0.0000 3.4954 3.4954 0.0000 51.7766 0.3981 0.6447 1.0640 1.7498 1.1612 0.0000 22.8977 2 61.0000 -5.0000 29.0000 -6.9907 29.8307 103.5532 +19 1 50.0000 2.5000 0.0000 3.5514 3.5514 0.0000 272.2362 0.4206 0.6521 1.0251 1.9455 1.2055 -0.8219 31.9030 2 56.0000 -27.0000 -3.0000 -38.3556 38.4728 184.4723 +20 1 50.0000 2.5000 0.0000 3.5244 3.5244 0.0000 11.9548 0.4098 1.1031 1.0400 1.9120 1.3353 0.0000 19.4535 2 58.0000 24.0000 15.0000 33.8342 37.0102 23.9095 +21 1 50.0000 2.5000 0.0000 3.7494 3.7494 0.0000 3.5056 0.4997 1.2616 1.0000 1.1923 1.0808 0.0000 1.0000 2 50.0000 3.1736 0.5854 4.7596 4.7954 7.0113 +22 1 50.0000 2.5000 0.0000 3.7493 3.7493 0.0000 0.0000 0.4997 1.3202 1.0000 1.1956 1.0861 0.0000 1.0000 2 50.0000 3.2972 0.0000 4.9450 4.9450 0.0000 +23 1 50.0000 2.5000 0.0000 3.7497 3.7497 0.0000 5.8190 0.4999 1.2197 1.0000 1.1486 1.0604 0.0000 1.0000 2 50.0000 1.8634 0.5757 2.7949 2.8536 11.6380 +24 1 50.0000 2.5000 0.0000 3.7493 3.7493 0.0000 1.9603 0.4997 1.2883 1.0000 1.1946 1.0836 0.0000 1.0000 2 50.0000 3.2592 0.3350 4.8879 4.8994 3.9206 +25 1 60.2574 -34.0099 36.2677 -34.0678 49.7590 133.2085 132.0835 0.0017 1.3010 1.1427 3.2946 1.9951 0.0000 1.2644 2 60.4626 -34.1751 39.4387 -34.2333 52.2238 130.9584 +26 1 63.0109 -31.0961 -5.8663 -32.6194 33.1427 190.1951 188.8221 0.0490 0.9402 1.1831 2.4549 1.4560 0.0000 1.2630 2 62.8187 -29.7946 -4.0864 -31.2542 31.5202 187.4490 +27 1 61.2901 3.7196 -5.3901 5.5668 7.7487 315.9240 310.0313 0.4966 0.6952 1.1586 1.3092 1.0717 -0.0032 1.8731 2 61.4292 2.2480 -4.9620 3.3644 5.9950 304.1385 +28 1 35.0831 -44.1164 3.7933 -44.3939 44.5557 175.1161 176.4290 0.0063 1.0168 1.2148 2.9105 1.6476 0.0000 1.8645 2 35.0232 -40.0716 1.5901 -40.3237 40.3550 177.7418 +29 1 22.7233 20.0904 -46.6940 20.1424 50.8532 293.3339 291.3809 0.0026 0.3636 1.4014 3.1597 1.2617 -1.2537 2.0373 2 23.0331 14.9730 -42.5619 15.0118 45.1317 289.4279 +30 1 36.4612 47.8580 18.3852 47.9197 51.3256 20.9901 21.8781 0.0013 0.9239 1.1943 3.3888 1.7357 0.0000 1.4146 2 36.2715 50.5065 21.2231 50.5716 54.8444 22.7660 +31 1 90.8027 -2.0831 1.4410 -3.1245 3.4408 155.2410 167.1011 0.4999 1.1546 1.6110 1.1329 1.0511 0.0000 1.4441 2 91.1528 -1.6435 0.0447 -2.4651 2.4655 178.9612 +32 1 90.9257 -0.5406 -0.9208 -0.8109 1.2270 228.6315 218.4363 0.5000 1.3916 1.5930 1.0620 1.0288 0.0000 1.5381 2 88.6381 -0.8985 -0.7239 -1.3477 1.5298 208.2412 +33 1 6.7747 -0.2908 -2.4247 -0.4362 2.4636 259.8025 263.0049 0.4999 0.9556 1.6517 1.1057 1.0337 -0.0004 0.6377 2 5.8714 -0.0985 -2.2286 -0.1477 2.2335 266.2073 +34 1 2.0776 0.0795 -1.1350 0.1192 1.1412 275.9978 268.0910 0.5000 0.7826 1.7246 1.0383 1.0100 0.0000 0.9082 2 0.9033 -0.0636 -0.5514 -0.0954 0.5596 260.18421 diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 962fa9fc..fbec9ba6 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -14,26 +14,48 @@ Authors import os.path import numpy as np -from numpy.testing import * +from numpy.testing import (assert_equal, + assert_almost_equal, + assert_array_almost_equal, + assert_raises, + TestCase, + ) -from skimage import img_as_float +from skimage import img_as_float, img_as_ubyte from skimage.io import imread -from skimage.color import ( - rgb2hsv, hsv2rgb, - rgb2xyz, xyz2rgb, - rgb2rgbcie, rgbcie2rgb, - convert_colorspace, - rgb2grey, gray2rgb, - xyz2lab, lab2xyz, - lab2rgb, rgb2lab, - is_rgb, is_gray - ) +from skimage.color import (rgb2hsv, hsv2rgb, + rgb2xyz, xyz2rgb, + rgb2hed, hed2rgb, + separate_stains, + combine_stains, + rgb2rgbcie, rgbcie2rgb, + convert_colorspace, + rgb2grey, gray2rgb, + xyz2lab, lab2xyz, + lab2rgb, rgb2lab, + is_rgb, is_gray, + lab2lch, lch2lab, + guess_spatial_dimensions + ) from skimage import data_dir, data import colorsys +def test_guess_spatial_dimensions(): + im1 = np.zeros((5, 5)) + im2 = np.zeros((5, 5, 5)) + im3 = np.zeros((5, 5, 3)) + im4 = np.zeros((5, 5, 5, 3)) + im5 = np.zeros((5,)) + assert_equal(guess_spatial_dimensions(im1), 2) + assert_equal(guess_spatial_dimensions(im2), 3) + assert_equal(guess_spatial_dimensions(im3), None) + assert_equal(guess_spatial_dimensions(im4), 3) + assert_raises(ValueError, guess_spatial_dimensions, im5) + + class TestColorconv(TestCase): img_rgb = imread(os.path.join(data_dir, 'color.png')) @@ -121,6 +143,32 @@ class TestColorconv(TestCase): img_rgb = img_as_float(self.img_rgb) assert_array_almost_equal(xyz2rgb(rgb2xyz(img_rgb)), img_rgb) + # RGB<->HED roundtrip with ubyte image + def test_hed_rgb_roundtrip(self): + img_rgb = img_as_ubyte(self.img_rgb) + assert_equal(img_as_ubyte(hed2rgb(rgb2hed(img_rgb))), img_rgb) + + # RGB<->HED roundtrip with float image + def test_hed_rgb_float_roundtrip(self): + img_rgb = img_as_float(self.img_rgb) + assert_array_almost_equal(hed2rgb(rgb2hed(img_rgb)), img_rgb) + + # RGB<->HDX roundtrip with ubyte image + def test_hdx_rgb_roundtrip(self): + from skimage.color.colorconv import hdx_from_rgb, rgb_from_hdx + img_rgb = self.img_rgb + conv = combine_stains(separate_stains(img_rgb, hdx_from_rgb), + rgb_from_hdx) + assert_equal(img_as_ubyte(conv), img_rgb) + + # RGB<->HDX roundtrip with ubyte image + def test_hdx_rgb_roundtrip(self): + from skimage.color.colorconv import hdx_from_rgb, rgb_from_hdx + img_rgb = img_as_float(self.img_rgb) + conv = combine_stains(separate_stains(img_rgb, hdx_from_rgb), + rgb_from_hdx) + assert_array_almost_equal(conv, img_rgb) + # RGB to RGB CIE def test_rgb2rgbcie_conversion(self): gt = np.array([[[ 0.1488856 , 0.18288098, 0.19277574], @@ -202,6 +250,43 @@ class TestColorconv(TestCase): img_rgb = img_as_float(self.img_rgb) assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb) + def test_lab_lch_roundtrip(self): + rgb = img_as_float(self.img_rgb) + lab = rgb2lab(rgb) + lab2 = lch2lab(lab2lch(lab)) + assert_array_almost_equal(lab2, lab) + + def test_rgb_lch_roundtrip(self): + rgb = img_as_float(self.img_rgb) + lab = rgb2lab(rgb) + lch = lab2lch(lab) + lab2 = lch2lab(lch) + rgb2 = lab2rgb(lab2) + assert_array_almost_equal(rgb, rgb2) + + def test_lab_lch_0d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch2 = lab2lch(lab0[None, None, :]) + assert_array_almost_equal(lch0, lch2[0, 0, :]) + + def test_lab_lch_1d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch1 = lab2lch(lab0[None, :]) + assert_array_almost_equal(lch0, lch1[0, :]) + + def test_lab_lch_3d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch3 = lab2lch(lab0[None, None, None, :]) + assert_array_almost_equal(lch0, lch3[0, 0, 0, :]) + + def _get_lab0(self): + rgb = img_as_float(self.img_rgb[:1, :1, :]) + return rgb2lab(rgb)[0, 0, :] + + def test_gray2rgb(): x = np.array([0, 0.5, 1]) assert_raises(ValueError, gray2rgb, x) @@ -238,4 +323,5 @@ def test_is_rgb(): if __name__ == "__main__": + from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/color/tests/test_colorlabel.py b/skimage/color/tests/test_colorlabel.py new file mode 100644 index 00000000..dcfbe4ea --- /dev/null +++ b/skimage/color/tests/test_colorlabel.py @@ -0,0 +1,95 @@ +import itertools + +import numpy as np +from numpy import testing +from skimage.color.colorlabel import label2rgb +from numpy.testing import (assert_array_almost_equal as assert_close, + assert_array_equal) + + +def test_shape_mismatch(): + image = np.ones((3, 3)) + label = np.ones((2, 2)) + testing.assert_raises(ValueError, label2rgb, image, label) + + +def test_rgb(): + image = np.ones((1, 3)) + label = np.arange(3).reshape(1, -1) + colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] + # Set alphas just in case the defaults change + rgb = label2rgb(label, image=image, colors=colors, alpha=1, image_alpha=1) + assert_close(rgb, [colors]) + + +def test_alpha(): + image = np.random.uniform(size=(3, 3)) + label = np.random.randint(0, 9, size=(3, 3)) + # If we set `alpha = 0`, then rgb should match image exactly. + rgb = label2rgb(label, image=image, alpha=0, image_alpha=1) + assert_close(rgb[..., 0], image) + assert_close(rgb[..., 1], image) + assert_close(rgb[..., 2], image) + + +def test_no_input_image(): + label = np.arange(3).reshape(1, -1) + colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] + rgb = label2rgb(label, colors=colors) + assert_close(rgb, [colors]) + + +def test_image_alpha(): + image = np.random.uniform(size=(1, 3)) + label = np.arange(3).reshape(1, -1) + colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] + # If we set `image_alpha = 0`, then rgb should match label colors exactly. + rgb = label2rgb(label, image=image, colors=colors, alpha=1, image_alpha=0) + assert_close(rgb, [colors]) + + +def test_color_names(): + image = np.ones((1, 3)) + label = np.arange(3).reshape(1, -1) + cnames = ['red', 'lime', 'blue'] + colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] + # Set alphas just in case the defaults change + rgb = label2rgb(label, image=image, colors=cnames, alpha=1, image_alpha=1) + assert_close(rgb, [colors]) + + +def test_bg_and_color_cycle(): + image = np.zeros((1, 10)) # dummy image + label = np.arange(10).reshape(1, -1) + colors = [(1, 0, 0), (0, 0, 1)] + bg_color = (0, 0, 0) + rgb = label2rgb(label, image=image, bg_label=0, bg_color=bg_color, + colors=colors, alpha=1) + assert_close(rgb[0, 0], bg_color) + for pixel, color in zip(rgb[0, 1:], itertools.cycle(colors)): + assert_close(pixel, color) + + +def test_label_consistency(): + """Assert that the same labels map to the same colors.""" + label_1 = np.arange(5).reshape(1, -1) + label_2 = np.array([2, 4]) + colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (1, 0, 1)] + # Set alphas just in case the defaults change + rgb_1 = label2rgb(label_1, colors=colors) + rgb_2 = label2rgb(label_2, colors=colors) + for label_id in label_2.flat: + assert_close(rgb_1[label_1 == label_id], rgb_2[label_2 == label_id]) + +def test_leave_labels_alone(): + labels = np.array([-1, 0, 1]) + labels_saved = labels.copy() + + label2rgb(labels) + label2rgb(labels, bg_label=1) + assert_array_equal(labels, labels_saved) + + +if __name__ == '__main__': + testing.run_module_suite() + diff --git a/skimage/color/tests/test_delta_e.py b/skimage/color/tests/test_delta_e.py new file mode 100644 index 00000000..84f13b48 --- /dev/null +++ b/skimage/color/tests/test_delta_e.py @@ -0,0 +1,167 @@ +"""Test for correctness of color distance functions""" +from os.path import abspath, dirname, join as pjoin + +import numpy as np +from numpy.testing import assert_allclose + +from skimage.color import (deltaE_cie76, + deltaE_ciede94, + deltaE_ciede2000, + deltaE_cmc) + + +def test_ciede2000_dE(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_ciede2000(lab1, lab2) + + assert_allclose(dE2, data['dE'], rtol=1.e-4) + + +def load_ciede2000_data(): + dtype = [('pair', int), + ('1', int), + ('L1', float), + ('a1', float), + ('b1', float), + ('a1_prime', float), + ('C1_prime', float), + ('h1_prime', float), + ('hbar_prime', float), + ('G', float), + ('T', float), + ('SL', float), + ('SC', float), + ('SH', float), + ('RT', float), + ('dE', float), + ('2', int), + ('L2', float), + ('a2', float), + ('b2', float), + ('a2_prime', float), + ('C2_prime', float), + ('h2_prime', float), + ] + + # note: ciede_test_data.txt contains several intermediate quantities + path = pjoin(dirname(abspath(__file__)), 'ciede2000_test_data.txt') + return np.loadtxt(path, dtype=dtype) + + +def test_cie76(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_cie76(lab1, lab2) + oracle = np.array([ + 4.00106328, 6.31415011, 9.1776999, 2.06270077, 2.36957073, + 2.91529271, 2.23606798, 2.23606798, 4.98000036, 4.9800004, + 4.98000044, 4.98000049, 4.98000036, 4.9800004, 4.98000044, + 3.53553391, 36.86800781, 31.91002977, 30.25309901, 27.40894015, + 0.89242934, 0.7972, 0.8583065, 0.82982507, 3.1819238, + 2.21334297, 1.53890382, 4.60630929, 6.58467989, 3.88641412, + 1.50514845, 2.3237848, 0.94413208, 1.31910843 + ]) + assert_allclose(dE2, oracle, rtol=1.e-8) + + +def test_ciede94(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_ciede94(lab1, lab2) + oracle = np.array([ + 1.39503887, 1.93410055, 2.45433566, 0.68449187, 0.6695627, + 0.69194527, 2.23606798, 2.03163832, 4.80069441, 4.80069445, + 4.80069449, 4.80069453, 4.80069441, 4.80069445, 4.80069449, + 3.40774352, 34.6891632, 29.44137328, 27.91408781, 24.93766082, + 0.82213163, 0.71658427, 0.8048753, 0.75284394, 1.39099471, + 1.24808929, 1.29795787, 1.82045088, 2.55613309, 1.42491303, + 1.41945261, 2.3225685, 0.93853308, 1.30654464 + ]) + assert_allclose(dE2, oracle, rtol=1.e-8) + + +def test_cmc(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_cmc(lab1, lab2) + oracle = np.array([ + 1.73873611, 2.49660844, 3.30494501, 0.85735576, 0.88332927, + 0.97822692, 3.50480874, 2.87930032, 6.5783807, 6.57838075, + 6.5783808, 6.57838086, 6.67492321, 6.67492326, 6.67492331, + 4.66852997, 42.10875485, 39.45889064, 38.36005919, 33.93663807, + 1.14400168, 1.00600419, 1.11302547, 1.05335328, 1.42822951, + 1.2548143, 1.76838061, 2.02583367, 3.08695508, 1.74893533, + 1.90095165, 1.70258148, 1.80317207, 2.44934417 + ]) + + assert_allclose(dE2, oracle, rtol=1.e-8) + + +def test_single_color_cie76(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_cie76(lab1, lab2) + + +def test_single_color_ciede94(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_ciede94(lab1, lab2) + + +def test_single_color_ciede2000(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_ciede2000(lab1, lab2) + + +def test_single_color_cmc(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_cmc(lab1, lab2) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index d2fba7db..ecd261f7 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -12,6 +12,21 @@ from ..io import imread from skimage import data_dir +__all__ = ['load', + 'camera', + 'lena', + 'text', + 'checkerboard', + 'coins', + 'moon', + 'page', + 'horse', + 'clock', + 'immunohistochemistry', + 'chelsea', + 'coffee'] + + def load(f): """Load an image file located in the data directory. @@ -116,6 +131,19 @@ def page(): return load("page.png") +def horse(): + """Black and white silhouette of a horse. + + This image was downloaded from + `openclipart ` + + Released into public domain and drawn and uploaded by Andreas Preuss + (marauder). + + """ + return load("horse.png") + + def clock(): """Motion blurred clock. @@ -127,3 +155,48 @@ def clock(): """ return load("clock_motion.png") + + +def immunohistochemistry(): + """Immunohistochemical (IHC) staining with hematoxylin counterstaining. + + This picture shows colonic glands where the IHC expression of FHL2 protein + is revealed with DAB. Hematoxylin counterstaining is applied to enhance the + negative parts of the tissue. + + This image was acquired at the Center for Microscopy And Molecular Imaging + (CMMI). + + No known copyright restrictions. + + """ + return load("ihc.png") + + +def chelsea(): + """Chelsea the cat. + + An example with texture, prominent edges in horizontal and diagonal + directions, as well as features of differing scales. + + Notes + ----- + No copyright restrictions. CC0 by the photographer (Stefan van der Walt). + + """ + return load("chelsea.png") + + +def coffee(): + """Coffee cup. + + This photograph is courtesy of Pikolo Espresso Bar. + It contains several elliptical shapes as well as varying texture (smooth + porcelain to course wood grain). + + Notes + ----- + No copyright restrictions. CC0 by the photographer (Rachel Michetti). + + """ + return load("coffee.png") diff --git a/skimage/data/chelsea.png b/skimage/data/chelsea.png new file mode 100644 index 00000000..fae212d2 Binary files /dev/null and b/skimage/data/chelsea.png differ diff --git a/skimage/data/coffee.png b/skimage/data/coffee.png new file mode 100644 index 00000000..f8350bf7 Binary files /dev/null and b/skimage/data/coffee.png differ diff --git a/skimage/data/horse.png b/skimage/data/horse.png new file mode 100644 index 00000000..59f48822 Binary files /dev/null and b/skimage/data/horse.png differ diff --git a/skimage/data/ihc.png b/skimage/data/ihc.png new file mode 100644 index 00000000..59df35fd Binary files /dev/null and b/skimage/data/ihc.png differ diff --git a/skimage/data/tests/test_data.py b/skimage/data/tests/test_data.py index f660c69c..49cd4b5a 100644 --- a/skimage/data/tests/test_data.py +++ b/skimage/data/tests/test_data.py @@ -34,11 +34,21 @@ def test_page(): data.page() -def test_page(): +def test_clock(): """ Test that "clock" image can be loaded. """ data.clock() +def test_chelsea(): + """ Test that "chelsea" image can be loaded. """ + data.chelsea() + + +def test_coffee(): + """ Test that "coffee" image can be loaded. """ + data.coffee() + + 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 4aa6bf35..5f788ea7 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -1,5 +1,18 @@ -from ._draw import line, polygon, ellipse, ellipse_perimeter, \ - circle, circle_perimeter, set_color +from .draw import circle, ellipse, set_color +from .draw3d import ellipsoid, ellipsoid_stats +from ._draw import (line, line_aa, polygon, ellipse_perimeter, + circle_perimeter, circle_perimeter_aa, + _bezier_segment, bezier_curve) - -bresenham = line +__all__ = ['line', + 'line_aa', + 'bezier_curve', + 'polygon', + 'ellipse', + 'ellipse_perimeter', + 'ellipsoid', + 'ellipsoid_stats', + 'circle', + 'circle_perimeter', + 'circle_perimeter_aa', + 'set_color'] diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 4316cadc..7300a2a1 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -6,7 +6,7 @@ import math import numpy as np cimport numpy as cnp -from libc.math cimport sqrt +from libc.math cimport sqrt, sin, cos, floor, ceil from skimage._shared.geometry cimport point_in_polygon @@ -27,9 +27,29 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. - """ + Notes + ----- + Anti-aliased line generator is available with `line_aa`. - cdef cnp.ndarray[cnp.intp_t, ndim=1, mode="c"] rr, cc + Examples + -------- + >>> from skimage.draw import line + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = line(1, 1, 8, 8) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + + """ cdef char steep = 0 cdef Py_ssize_t dx = abs(x2 - x) @@ -51,8 +71,8 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): sx, sy = sy, sx d = (2 * dy) - dx - rr = np.zeros(int(dx) + 1, dtype=np.intp) - cc = np.zeros(int(dx) + 1, dtype=np.intp) + cdef Py_ssize_t[::1] rr = np.zeros(int(dx) + 1, dtype=np.intp) + cdef Py_ssize_t[::1] cc = np.zeros(int(dx) + 1, dtype=np.intp) for i in range(dx): if steep: @@ -70,7 +90,100 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): rr[dx] = y2 cc[dx] = x2 - return rr, cc + return np.asarray(rr), np.asarray(cc) + + +def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): + """Generate anti-aliased line pixel coordinates. + + Parameters + ---------- + y1, x1 : int + Starting position (row, column). + y2, x2 : int + End position (row, column). + + Returns + ------- + rr, cc, val : (N,) ndarray (int, int, float) + Indices of pixels (`rr`, `cc`) and intensity values (`val`). + ``img[rr, cc] = val``. + + References + ---------- + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf + + Examples + -------- + >>> from skimage.draw import line_aa + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc, val = line_aa(1, 1, 8, 8) + >>> img[rr, cc] = val * 255 + >>> img + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 255, 56, 0, 0, 0, 0, 0, 0, 0], + [ 0, 56, 255, 56, 0, 0, 0, 0, 0, 0], + [ 0, 0, 56, 255, 56, 0, 0, 0, 0, 0], + [ 0, 0, 0, 56, 255, 56, 0, 0, 0, 0], + [ 0, 0, 0, 0, 56, 255, 56, 0, 0, 0], + [ 0, 0, 0, 0, 0, 56, 255, 56, 0, 0], + [ 0, 0, 0, 0, 0, 0, 56, 255, 56, 0], + [ 0, 0, 0, 0, 0, 0, 0, 56, 255, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + cdef list rr = list() + cdef list cc = list() + cdef list val = list() + + cdef int dx = abs(x1 - x2) + cdef int dy = abs(y1 - y2) + cdef int err = dx - dy + cdef int x, y, e, ed, sign_x, sign_y + + if x1 < x2: + sign_x = 1 + else: + sign_x = -1 + + if y1 < y2: + sign_y = 1 + else: + sign_y = -1 + + if dx + dy == 0: + ed = 1 + else: + ed = (sqrt(dx*dx + dy*dy)) + + x, y = x1, y1 + while True: + cc.append(x) + rr.append(y) + val.append(1. * abs(err - dx + dy) / (ed)) + e = err + if 2 * e >= -dx: + if x == x2: + break + if e + dy < ed: + cc.append(x) + rr.append(y + sign_y) + val.append(1. * abs(e + dy) / (ed)) + err -= dy + x += sign_x + if 2 * e <= dy: + if y == y2: + break + if dx - e < ed: + cc.append(x) + rr.append(y) + val.append(abs(dx - e) / (ed)) + err += dx + y += sign_y + + return (np.array(rr, dtype=np.intp), + np.array(cc, dtype=np.intp), + 1. - np.array(val, dtype=np.float)) def polygon(y, x, shape=None): @@ -83,7 +196,7 @@ def polygon(y, x, shape=None): x : (N,) ndarray X-coordinates of vertices of polygon. shape : tuple, optional - image shape which is used to determine maximum extents of output pixel + Image shape which is used to determine maximum extents of output pixel coordinates. This is useful for polygons which exceed the image size. By default the full extents of the polygon are used. @@ -94,13 +207,33 @@ def polygon(y, x, shape=None): May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. + Examples + -------- + >>> from skimage.draw import polygon + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> x = np.array([1, 7, 4, 1]) + >>> y = np.array([1, 2, 8, 1]) + >>> rr, cc = polygon(y, x) + >>> img[rr, cc] = 1 + >>> img + 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, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 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]], dtype=uint8) + """ 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 maxr = int(ceil(y.max())) cdef Py_ssize_t minc = int(max(0, x.min())) - cdef Py_ssize_t maxc = int(math.ceil(x.max())) + cdef Py_ssize_t maxc = int(ceil(x.max())) # make sure output coordinates do not exceed image size if shape is not None: @@ -111,8 +244,8 @@ def polygon(y, x, shape=None): # make contigous arrays for r, c coordinates cdef cnp.ndarray contiguous_rdata, contiguous_cdata - contiguous_rdata = np.ascontiguousarray(y, 'double') - contiguous_cdata = np.ascontiguousarray(x, 'double') + contiguous_rdata = np.ascontiguousarray(y, dtype=np.double) + contiguous_cdata = np.ascontiguousarray(x, dtype=np.double) cdef cnp.double_t* rptr = contiguous_rdata.data cdef cnp.double_t* cptr = contiguous_cdata.data @@ -129,82 +262,6 @@ def polygon(y, x, shape=None): return np.array(rr, dtype=np.intp), np.array(cc, dtype=np.intp) -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. - 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 - ------- - rr, cc : ndarray of int - Pixel coordinates of ellipse. - May be used to directly index into an array, e.g. - ``img[rr, cc] = 1``. - - """ - - 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) - - cdef Py_ssize_t r, c - - # output coordinate arrays - cdef list rr = list() - cdef list cc = list() - - for r in range(minr, maxr+1): - for c in range(minc, maxc+1): - if sqrt(((r - cy) / yradius)**2 + ((c - cx) / xradius)**2) < 1: - rr.append(r) - cc.append(c) - - return np.array(rr, dtype=np.intp), np.array(cc, dtype=np.intp) - - -def circle(double cy, double cx, double radius, shape=None): - """Generate coordinates of pixels within circle. - - Parameters - ---------- - cy, cx : double - Centre coordinate of circle. - radius: double - 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 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. @@ -216,13 +273,13 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, radius: int Radius of circle. method : {'bresenham', 'andres'}, optional - bresenham : Bresenham method + bresenham : Bresenham method (default) andres : Andres method - Returns ------- rr, cc : (N,) ndarray of int + Bresenham and Andres' method: Indices of pixels that belong to the circle perimeter. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. @@ -233,12 +290,32 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, 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. + Anti-aliased circle generator is available with `circle_perimeter_aa`. 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. + plotter", IBM Systems journal, 4 (1965) 25-30. + .. [2] E. Andres, "Discrete circles, rings and spheres", Computers & + Graphics, 18 (1994) 695-706. + + Examples + -------- + >>> from skimage.draw import circle_perimeter + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = circle_perimeter(4, 4, 3) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 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]], dtype=uint8) """ @@ -248,6 +325,10 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, cdef Py_ssize_t x = 0 cdef Py_ssize_t y = radius cdef Py_ssize_t d = 0 + + cdef double dceil = 0 + cdef double dceil_prev = 0 + cdef char cmethod if method == 'bresenham': d = 3 - 2 * radius @@ -280,32 +361,128 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, d = d + 2 * (y - x - 1) y = y - 1 x = x + 1 + return (np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx) - return np.array(rr, dtype=np.intp) + cy, np.array(cc, dtype=np.intp) + cx + +def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): + """Generate anti-aliased circle perimeter coordinates. + + Parameters + ---------- + cy, cx : int + Centre coordinate of circle. + radius: int + Radius of circle. + + Returns + ------- + rr, cc, val : (N,) ndarray (int, int, float) + Indices of pixels (`rr`, `cc`) and intensity values (`val`). + ``img[rr, cc] = val``. + + Notes + ----- + Wu's method draws anti-aliased circle. This implementation doesn't use + lookup table optimization. + + References + ---------- + .. [1] X. Wu, "An efficient antialiasing technique", In ACM SIGGRAPH + Computer Graphics, 25 (1991) 143-152. + + Examples + -------- + >>> from skimage.draw import circle_perimeter_aa + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc, val = circle_perimeter_aa(4, 4, 3) + >>> img[rr, cc] = val * 255 + >>> img + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 60, 211, 255, 211, 60, 0, 0, 0], + [ 0, 60, 194, 43, 0, 43, 194, 60, 0, 0], + [ 0, 211, 43, 0, 0, 0, 43, 211, 0, 0], + [ 0, 255, 0, 0, 0, 0, 0, 255, 0, 0], + [ 0, 211, 43, 0, 0, 0, 43, 211, 0, 0], + [ 0, 60, 194, 43, 0, 43, 194, 60, 0, 0], + [ 0, 0, 60, 211, 255, 211, 60, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + + cdef Py_ssize_t x = 0 + cdef Py_ssize_t y = radius + cdef Py_ssize_t d = 0 + + cdef double dceil = 0 + cdef double dceil_prev = 0 + + cdef list rr = [y, x, y, x, -y, -x, -y, -x] + cdef list cc = [x, y, -x, -y, x, y, -x, -y] + cdef list val = [1] * 8 + + while y > x + 1: + x += 1 + dceil = sqrt(radius**2 - x**2) + dceil = ceil(dceil) - dceil + if dceil < dceil_prev: + y -= 1 + rr.extend([y, y - 1, x, x, y, y - 1, x, x]) + cc.extend([x, x, y, y - 1, -x, -x, -y, 1 - y]) + + rr.extend([-y, 1 - y, -x, -x, -y, 1 - y, -x, -x]) + cc.extend([x, x, y, y - 1, -x, -x, -y, 1 - y]) + + val.extend([1 - dceil, dceil] * 8) + dceil_prev = dceil + + return (np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx, + np.array(val, dtype=np.float)) def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, - Py_ssize_t xradius): + Py_ssize_t xradius, double orientation=0): """Generate ellipse perimeter coordinates. Parameters ---------- cy, cx : int Centre coordinate of ellipse. - yradius, xradius: int - Main radial values. + yradius, xradius : int + Minor and major semi-axes. ``(x/xradius)**2 + (y/yradius)**2 = 1``. + orientation : double, optional (default 0) + Major axis orientation in clockwise direction as radians. Returns ------- rr, cc : (N,) ndarray of int - Indices of pixels that belong to the circle perimeter. + Indices of pixels that belong to the ellipse 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". + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf + + Examples + -------- + >>> from skimage.draw import ellipse_perimeter + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = ellipse_perimeter(5, 5, 3, 4) + >>> img[rr, cc] = 1 + >>> img + array([[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], + [0, 0, 1, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 1, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) """ @@ -313,87 +490,347 @@ def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, 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 + # Compute useful values + cdef Py_ssize_t xd = xradius**2 + cdef Py_ssize_t yd = yradius**2 - 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 + cdef Py_ssize_t x, y, e2, err - 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 + cdef int ix0, ix1, iy0, iy1, ixd, iyd + cdef double sin_angle, xa, ya, za, a, b - # Second set of points: - x = 0 - y = yradius + if orientation == 0: + x = -xradius + y = 0 + e2 = yd + err = x*(2 * e2 + x) + e2 + while x <= 0: + # Quadrant 1 + px.append(cx - x) + py.append(cy + y) + # Quadrant 2 + px.append(cx + x) + py.append(cy + y) + # Quadrant 3 + px.append(cx + x) + py.append(cy - y) + # Quadrant 4 + px.append(cx - x) + py.append(cy - y) + # Adjust x and y + e2 = 2 * err + if e2 >= (2 * x + 1) * yd: + x += 1 + err += (2 * x + 1) * yd + if e2 <= (2 * y + 1) * xd: + y += 1 + err += (2 * y + 1) * xd + while y < yradius: + y += 1 + px.append(cx) + py.append(cy + y) + px.append(cx) + py.append(cy - y) - err = 0 - xstop = 0 - ystop = twoasquared * yradius - xchange = yradius * yradius - ychange = xradius * xradius * (1 - 2 * yradius) + else: + sin_angle = sin(orientation) + za = (xd - yd) * sin_angle + xa = sqrt(xd - za * sin_angle) + ya = sqrt(yd + za * sin_angle) - 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 + a = xa + 0.5 + b = ya + 0.5 + za = za * a * b / (xa * ya) - return np.array(py, dtype=np.intp) + cy, np.array(px, dtype=np.intp) + cx + ix0 = int(cx - a) + iy0 = int(cy - b) + ix1 = int(cx + a) + iy1 = int(cy + b) + + xa = ix1 - ix0 + ya = iy1 - iy0 + za = 4 * za * cos(orientation) + w = xa * ya + if w != 0: + w = (w - za) / (w + w) + ixd = int(floor(xa * w + 0.5)) + iyd = int(floor(ya * w + 0.5)) + + # Draw the 4 quadrants + rr, cc = _bezier_segment(iy0 + iyd, ix0, iy0, ix0, iy0, ix0 + ixd, 1-w) + py.extend(rr) + px.extend(cc) + rr, cc = _bezier_segment(iy0 + iyd, ix0, iy1, ix0, iy1, ix1 - ixd, w) + py.extend(rr) + px.extend(cc) + rr, cc = _bezier_segment(iy1 - iyd, ix1, iy1, ix1, iy1, ix1 - ixd, 1-w) + py.extend(rr) + px.extend(cc) + rr, cc = _bezier_segment(iy1 - iyd, ix1, iy0, ix1, iy0, ix0 + ixd, w) + py.extend(rr) + px.extend(cc) + + return np.array(py, dtype=np.intp), np.array(px, dtype=np.intp) -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. +def _bezier_segment(Py_ssize_t y0, Py_ssize_t x0, + Py_ssize_t y1, Py_ssize_t x1, + Py_ssize_t y2, Py_ssize_t x2, + double weight): + """Generate Bezier segment coordinates. 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. + y0, x0 : int + Coordinates of the first control point. + y1, x1 : int + Coordinates of the middle control point. + y2, x2 : int + Coordinates of the last control point. + weight : double + Middle control point weight, it describes the line tension. Returns ------- - img : (M, N, D) ndarray - The updated image. + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the Bezier curve. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + Notes + ----- + The algorithm is the rational quadratic algorithm presented in + reference [1]_. + + References + ---------- + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf """ + # Pixels + cdef list px = list() + cdef list py = list() - 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 + # Steps + cdef double sx = x2 - x1 + cdef double sy = y2 - y1 + + cdef double dx = x0 - x2 + cdef double dy = y0 - y2 + cdef double xx = x0 - x1 + cdef double yy = y0 - y1 + cdef double xy = xx * sy + yy * sx + cdef double cur = xx * sy - yy * sx + cdef double err + + cdef bint test1, test2 + + # if it's not a straight line + if cur != 0 and weight > 0: + if (sx * sx + sy * sy > xx * xx + yy * yy): + # Swap point 0 and point 2 + # to start from the longer part + x2 = x0 + x0 -= (dx) + y2 = y0 + y0 -= (dy) + cur = -cur + xx = 2 * (4 * weight * sx * xx + dx * dx) + yy = 2 * (4 * weight * sy * yy + dy * dy) + # Set steps + if x0 < x2: + sx = 1 + else: + sx = -1 + if y0 < y2: + sy = 1 + else: + sy = -1 + xy = -2 * sx * sy * (2 * weight * xy + dx * dy) + + if cur * sx * sy < 0: + xx = -xx + yy = -yy + xy = -xy + cur = -cur + + dx = 4 * weight * (x1 - x0) * sy * cur + xx / 2 + xy + dy = 4 * weight * (y0 - y1) * sx * cur + yy / 2 + xy + + # Flat ellipse, algo fails + if (weight < 0.5 and (dy > xy or dx < xy)): + cur = (weight + 1) / 2 + weight = sqrt(weight) + xy = 1. / (weight + 1) + # subdivide curve in half + sx = floor((x0 + 2 * weight * x1 + x2) * xy * 0.5 + 0.5) + sy = floor((y0 + 2 * weight * y1 + y2) * xy * 0.5 + 0.5) + dx = floor((weight * x1 + x0) * xy + 0.5) + dy = floor((y1 * weight + y0) * xy + 0.5) + return _bezier_segment(y0, x0, (dy), (dx), + (sy), (sx), cur) + + err = dx + dy - xy + while dy <= xy and dx >= xy: + px.append(x0) + py.append(y0) + if x0 == x2 and y0 == y2: + # The job is done! + return np.array(py, dtype=np.intp), np.array(px, dtype=np.intp) + + # Save boolean values + test1 = 2 * err > dy + test2 = 2 * (err + yy) < -dy + # Move (x0,y0) to the next position + if 2 * err < dx or test2: + y0 += (sy) + dy += xy + dx += xx + err += dx + if 2 * err > dx or test1: + x0 += (sx) + dx += xy + dy += yy + err += dy + + # Plot line + rr, cc = line(x0, y0, x2, y2) + px.extend(rr) + py.extend(cc) + + return np.array(py, dtype=np.intp), np.array(px, dtype=np.intp) + + +def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, + Py_ssize_t y1, Py_ssize_t x1, + Py_ssize_t y2, Py_ssize_t x2, + double weight): + """Generate Bezier curve coordinates. + + Parameters + ---------- + y0, x0 : int + Coordinates of the first control point. + y1, x1 : int + Coordinates of the middle control point. + y2, x2 : int + Coordinates of the last control point. + weight : double + Middle control point weight, it describes the line tension. + + Returns + ------- + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the Bezier curve. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Notes + ----- + The algorithm is the rational quadratic algorithm presented in + reference [1]_. + + References + ---------- + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf + + Examples + -------- + >>> import numpy as np + >>> from skimage.draw import bezier_curve + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = bezier_curve(1, 5, 5, -2, 8, 8, 2) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + # Pixels + cdef list px = list() + cdef list py = list() + + cdef int x, y + cdef double xx, yy, ww, t, q + x = x0 - 2 * x1 + x2 + y = y0 - 2 * y1 + y2 + + xx = x0 - x1 + yy = y0 - y1 + + if xx * (x2 - x1) > 0: + if yy * (y2 - y1): + if abs(xx * y) > abs(yy * x): + x0 = x2 + x2 = (xx + x1) + y0 = y2 + y2 = (yy + y1) + if (x0 == x2) or (weight == 1.): + t = (x0 - x1) / x + else: + q = sqrt(4. * weight * weight * (x0 - x1) * (x2 - x1) + (x2 - x0) * floor(x2 - x0)) + if (x1 < x0): + q = -q + t = (2. * weight * (x0 - x1) - x0 + x2 + q) / (2. * (1. - weight) * (x2 - x0)) + + q = 1. / (2. * t * (1. - t) * (weight - 1.) + 1.0) + xx = (t * t * (x0 - 2. * weight * x1 + x2) + 2. * t * (weight * x1 - x0) + x0) * q + yy = (t * t * (y0 - 2. * weight * y1 + y2) + 2. * t * (weight * y1 - y0) + y0) * q + ww = t * (weight - 1.) + 1. + ww *= ww * q + weight = ((1. - t) * (weight - 1.) + 1.) * sqrt(q) + x = (xx + 0.5) + y = (yy + 0.5) + yy = (xx - x0) * (y1 - y0) / (x1 - x0) + y0 + + rr, cc = _bezier_segment(y0, x0, (yy + 0.5), x, y, x, ww) + px.extend(rr) + py.extend(cc) + + yy = (xx - x2) * (y1 - y2) / (x1 - x2) + y2 + y1 = (yy + 0.5) + x0 = x1 = x + y0 = y + if (y0 - y1) * floor(y2 - y1) > 0: + if (y0 == y2) or (weight == 1): + t = (y0 - y1) / (y0 - 2. * y1 + y2) + else: + q = sqrt(4. * weight * weight * (y0 - y1) * (y2 - y1) + (y2 - y0) * floor(y2 - y0)) + if y1 < y0: + q = -q + t = (2. * weight * (y0 - y1) - y0 + y2 + q) / (2. * (1. - weight) * (y2 - y0)) + q = 1. / (2. * t * (1. - t) * (weight - 1.) + 1.) + xx = (t * t * (x0 - 2. * weight * x1 + x2) + 2. * t * (weight * x1 - x0) + x0) * q + yy = (t * t * (y0 - 2. * weight * y1 + y2) + 2. * t * (weight * y1 - y0) + y0) * q + ww = t * (weight - 1.) + 1. + ww *= ww * q + weight = ((1. - t) * (weight - 1.) + 1.) * sqrt(q) + x = (xx + 0.5) + y = (yy + 0.5) + xx = (x1 - x0) * (yy - y0) / (y1 - y0) + x0 + + rr, cc = _bezier_segment(y0, x0, y, (xx + 0.5), y, x, ww) + px.extend(rr) + py.extend(cc) + + xx = (x1 - x2) * (yy - y2) / (y1 - y2) + x2 + x1 = (xx + 0.5) + x0 = x + y0 = y1 = y + + rr, cc = _bezier_segment(y0, x0, y1, x1, y2, x2, weight * weight) + px.extend(rr) + py.extend(cc) + return np.array(px, dtype=np.intp), np.array(py, dtype=np.intp) diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py new file mode 100644 index 00000000..cbf3ced2 --- /dev/null +++ b/skimage/draw/draw.py @@ -0,0 +1,156 @@ +# coding: utf-8 +import numpy as np + + +def _coords_inside_image(rr, cc, shape): + mask = (rr >= 0) & (rr < shape[0]) & (cc >= 0) & (cc < shape[1]) + return rr[mask], cc[mask] + + +def ellipse(cy, cx, yradius, xradius, shape=None): + """Generate coordinates of pixels within ellipse. + + Parameters + ---------- + cy, cx : double + 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 + ------- + rr, cc : ndarray of int + Pixel coordinates of ellipse. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Examples + -------- + >>> from skimage.draw import ellipse + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = ellipse(5, 5, 3, 4) + >>> img[rr, cc] = 1 + >>> img + 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, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 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]], dtype=uint8) + + """ + + dr = 1 / float(yradius) + dc = 1 / float(xradius) + + r, c = np.ogrid[-1:1:dr, -1:1:dc] + rr, cc = np.nonzero(r ** 2 + c ** 2 < 1) + + rr.flags.writeable = True + cc.flags.writeable = True + rr += cy - yradius + cc += cx - xradius + + if shape is not None: + _coords_inside_image(rr, cc, shape) + + return rr, cc + + +def circle(cy, cx, radius, shape=None): + """Generate coordinates of pixels within circle. + + Parameters + ---------- + cy, cx : double + Centre coordinate of circle. + radius: double + 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 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() + + Examples + -------- + >>> from skimage.draw import circle + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = circle(4, 4, 5) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + + """ + + return ellipse(cy, cx, radius, radius, shape) + + +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. + + Examples + -------- + >>> from skimage.draw import line, set_color + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = line(1, 1, 20, 20) + >>> set_color(img, (rr, cc), 1) + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]], dtype=uint8) + + """ + + rr, cc = coords + rr, cc = _coords_inside_image(rr, cc, img.shape) + img[rr, cc] = color diff --git a/skimage/draw/draw3d.py b/skimage/draw/draw3d.py new file mode 100644 index 00000000..db2f0d39 --- /dev/null +++ b/skimage/draw/draw3d.py @@ -0,0 +1,115 @@ +# coding: utf-8 +import numpy as np +from scipy.special import (ellipkinc as ellip_F, ellipeinc as ellip_E) + + +def ellipsoid(a, b, c, spacing=(1., 1., 1.), levelset=False): + """ + Generates ellipsoid with semimajor axes aligned with grid dimensions + on grid with specified `spacing`. + + Parameters + ---------- + a : float + Length of semimajor axis aligned with x-axis. + b : float + Length of semimajor axis aligned with y-axis. + c : float + Length of semimajor axis aligned with z-axis. + spacing : tuple of floats, length 3 + Spacing in (x, y, z) spatial dimensions. + levelset : bool + If True, returns the level set for this ellipsoid (signed level + set about zero, with positive denoting interior) as np.float64. + False returns a binarized version of said level set. + + Returns + ------- + ellip : (N, M, P) array + Ellipsoid centered in a correctly sized array for given `spacing`. + Boolean dtype unless `levelset=True`, in which case a float array is + returned with the level set above 0.0 representing the ellipsoid. + + """ + if (a <= 0) or (b <= 0) or (c <= 0): + raise ValueError('Parameters a, b, and c must all be > 0') + + offset = np.r_[1, 1, 1] * np.r_[spacing] + + # Calculate limits, and ensure output volume is odd & symmetric + low = np.ceil((- np.r_[a, b, c] - offset)) + high = np.floor((np.r_[a, b, c] + offset + 1)) + + for dim in range(3): + if (high[dim] - low[dim]) % 2 == 0: + low[dim] -= 1 + num = np.arange(low[dim], high[dim], spacing[dim]) + if 0 not in num: + low[dim] -= np.max(num[num < 0]) + + # Generate (anisotropic) spatial grid + x, y, z = np.mgrid[low[0]:high[0]:spacing[0], + low[1]:high[1]:spacing[1], + low[2]:high[2]:spacing[2]] + + if not levelset: + arr = ((x / float(a)) ** 2 + + (y / float(b)) ** 2 + + (z / float(c)) ** 2) <= 1 + else: + arr = ((x / float(a)) ** 2 + + (y / float(b)) ** 2 + + (z / float(c)) ** 2) - 1 + + return arr + + +def ellipsoid_stats(a, b, c): + """ + Calculates analytical surface area and volume for ellipsoid with + semimajor axes aligned with grid dimensions of specified `spacing`. + + Parameters + ---------- + a : float + Length of semimajor axis aligned with x-axis. + b : float + Length of semimajor axis aligned with y-axis. + c : float + Length of semimajor axis aligned with z-axis. + + Returns + ------- + vol : float + Calculated volume of ellipsoid. + surf : float + Calculated surface area of ellipsoid. + + """ + if (a <= 0) or (b <= 0) or (c <= 0): + raise ValueError('Parameters a, b, and c must all be > 0') + + # Calculate volume & surface area + # Surface calculation requires a >= b >= c and a != c. + abc = [a, b, c] + abc.sort(reverse=True) + a = abc[0] + b = abc[1] + c = abc[2] + + # Volume + vol = 4 / 3. * np.pi * a * b * c + + # Analytical ellipsoid surface area + phi = np.arcsin((1. - (c ** 2 / (a ** 2.))) ** 0.5) + d = float((a ** 2 - c ** 2) ** 0.5) + m = (a ** 2 * (b ** 2 - c ** 2) / + float(b ** 2 * (a ** 2 - c ** 2))) + F = ellip_F(phi, m) + E = ellip_E(phi, m) + + surf = 2 * np.pi * (c ** 2 + + b * c ** 2 / d * F + + b * d * E) + + return vol, surf diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index ef09747c..2d739f0f 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -1,7 +1,22 @@ -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_equal, assert_equal import numpy as np -from skimage.draw import line, polygon, circle, circle_perimeter, ellipse, ellipse_perimeter +from skimage.draw import (set_color, line, line_aa, polygon, + circle, circle_perimeter, circle_perimeter_aa, + ellipse, ellipse_perimeter, + _bezier_segment, bezier_curve) + + +def test_set_color(): + img = np.zeros((10, 10)) + + rr, cc = line(0, 0, 0, 30) + set_color(img, (rr, cc), 1) + + img_ = np.zeros((10, 10)) + img_[0, :] = 1 + + assert_array_equal(img, img_) def test_line_horizontal(): @@ -51,6 +66,43 @@ def test_line_diag(): assert_array_equal(img, img_) +def test_line_aa_horizontal(): + img = np.zeros((10, 10)) + + rr, cc, val = line_aa(0, 0, 0, 9) + img[rr, cc] = val + + img_ = np.zeros((10, 10)) + img_[0, :] = 1 + + assert_array_equal(img, img_) + + +def test_line_aa_vertical(): + img = np.zeros((10, 10)) + + rr, cc, val = line_aa(0, 0, 9, 0) + img[rr, cc] = val + + img_ = np.zeros((10, 10)) + img_[:, 0] = 1 + + assert_array_equal(img, img_) + + +def test_line_aa_diagonal(): + img = np.zeros((10, 10)) + + rr, cc, val = line_aa(0, 0, 9, 6) + img[rr, cc] = 1 + + # Check that each pixel belonging to line, + # also belongs to line_aa + r, c = line(0, 0, 9, 6) + for x, y in zip(r, c): + assert_equal(img[r, c], 1) + + def test_polygon_rectangle(): img = np.zeros((10, 10), 'uint8') poly = np.array(((1, 1), (4, 1), (4, 4), (1, 4), (1, 1))) @@ -181,6 +233,7 @@ def test_circle_perimeter_bresenham(): ) 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') @@ -212,6 +265,39 @@ def test_circle_perimeter_andres(): ) assert_array_equal(img, img_) + +def test_circle_perimeter_aa(): + img = np.zeros((15, 15), 'uint8') + rr, cc, val = circle_perimeter_aa(7, 7, 0) + img[rr, cc] = 1 + assert(np.sum(img) == 1) + assert(img[7][7] == 1) + + img = np.zeros((17, 17), 'uint8') + rr, cc, val = circle_perimeter_aa(8, 8, 7) + img[rr, cc] = val * 255 + img_ = np.array( + [[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 82, 180, 236, 255, 236, 180, 82, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 189, 172, 74, 18, 0, 18, 74, 172, 189, 0, 0, 0, 0], + [ 0, 0, 0, 229, 25, 0, 0, 0, 0, 0, 0, 0, 25, 229, 0, 0, 0], + [ 0, 0, 189, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 189, 0, 0], + [ 0, 82, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 172, 82, 0], + [ 0, 180, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 180, 0], + [ 0, 236, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 236, 0], + [ 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0], + [ 0, 236, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 236, 0], + [ 0, 180, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 180, 0], + [ 0, 82, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 172, 82, 0], + [ 0, 0, 189, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 189, 0, 0], + [ 0, 0, 0, 229, 25, 0, 0, 0, 0, 0, 0, 0, 25, 229, 0, 0, 0], + [ 0, 0, 0, 0, 189, 172, 74, 18, 0, 18, 74, 172, 189, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 82, 180, 236, 255, 236, 180, 82, 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') @@ -238,15 +324,40 @@ def test_ellipse(): assert_array_equal(img, img_) -def test_ellipse_perimeter(): + +def test_ellipse_perimeter_dot_zeroangle(): + # dot, angle == 0 img = np.zeros((30, 15), 'uint8') - rr, cc = ellipse_perimeter(15, 7, 0, 0) + rr, cc = ellipse_perimeter(15, 7, 0, 0, 0) img[rr, cc] = 1 assert(np.sum(img) == 1) assert(img[15][7] == 1) + +def test_ellipse_perimeter_dot_nzeroangle(): + # dot, angle != 0 img = np.zeros((30, 15), 'uint8') - rr, cc = ellipse_perimeter(15, 7, 14, 6) + rr, cc = ellipse_perimeter(15, 7, 0, 0, 1) + img[rr, cc] = 1 + assert(np.sum(img) == 1) + assert(img[15][7] == 1) + + +def test_ellipse_perimeter_flat_zeroangle(): + # flat ellipse + img = np.zeros((20, 18), 'uint8') + img_ = np.zeros((20, 18), 'uint8') + rr, cc = ellipse_perimeter(6, 7, 0, 5, 0) + img[rr, cc] = 1 + rr, cc = line(6, 2, 6, 12) + img_[rr, cc] = 1 + assert_array_equal(img, img_) + + +def test_ellipse_perimeter_zeroangle(): + # angle == 0 + img = np.zeros((30, 15), 'uint8') + rr, cc = ellipse_perimeter(15, 7, 14, 6, 0) img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -283,6 +394,193 @@ def test_ellipse_perimeter(): assert_array_equal(img, img_) + +def test_ellipse_perimeter_nzeroangle(): + # angle != 0 + img = np.zeros((30, 25), 'uint8') + rr, cc = ellipse_perimeter(15, 11, 12, 6, 1.1) + 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, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 1, 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, 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, 1, 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, 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], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 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, 0, 0, 0, 0, 0, 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, 1, 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, 0, 0, 0, 0, 0, 1, 1, 1, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 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_bezier_segment_straight(): + image = np.zeros((200, 200), dtype=int) + x0 = 50 + y0 = 50 + x1 = 150 + y1 = 50 + x2 = 150 + y2 = 150 + rr, cc = _bezier_segment(x0, y0, x1, y1, x2, y2, 0) + image[rr, cc] = 1 + + image2 = np.zeros((200, 200), dtype=int) + rr, cc = line(x0, y0, x2, y2) + image2[rr, cc] = 1 + assert_array_equal(image, image2) + + +def test_bezier_segment_curved(): + img = np.zeros((25, 25), 'uint8') + x1, y1 = 20, 20 + x2, y2 = 20, 2 + x3, y3 = 2, 2 + rr, cc = _bezier_segment(x1, y1, x2, y2, x3, y3, 1) + 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, 0, 0, 0, 0], + [0, 0, 0, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 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_equal(img[x1, y1], 1) + assert_equal(img[x3, y3], 1) + assert_array_equal(img, img_) + + +def test_bezier_curve_straight(): + image = np.zeros((200, 200), dtype=int) + x0 = 50 + y0 = 50 + x1 = 150 + y1 = 50 + x2 = 150 + y2 = 150 + rr, cc = bezier_curve(x0, y0, x1, y1, x2, y2, 0) + image [rr, cc] = 1 + + image2 = np.zeros((200, 200), dtype=int) + rr, cc = line(x0, y0, x2, y2) + image2 [rr, cc] = 1 + assert_array_equal(image, image2) + + +def test_bezier_curved_weight_eq_1(): + img = np.zeros((23, 8), 'uint8') + x1, y1 = (1, 1) + x2, y2 = (11, 11) + x3, y3 = (21, 1) + rr, cc = bezier_curve(x1, y1, x2, y2, x3, y3, 1) + img[rr, cc] = 1 + assert_equal(img[x1, y1], 1) + assert_equal(img[x3, y3], 1) + img_ = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [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, 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, 0, 1, 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, 0, 1, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0]] + ) + assert_equal(img, img_) + + +def test_bezier_curved_weight_neq_1(): + img = np.zeros((23, 10), 'uint8') + x1, y1 = (1, 1) + x2, y2 = (11, 11) + x3, y3 = (21, 1) + rr, cc = bezier_curve(x1, y1, x2, y2, x3, y3, 2) + img[rr, cc] = 1 + assert_equal(img[x1, y1], 1) + assert_equal(img[x3, y3], 1) + img_ = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 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]] + ) + assert_equal(img, img_) + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/draw/tests/test_draw3d.py b/skimage/draw/tests/test_draw3d.py new file mode 100644 index 00000000..2e1198eb --- /dev/null +++ b/skimage/draw/tests/test_draw3d.py @@ -0,0 +1,104 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_allclose + +from skimage.draw import ellipsoid, ellipsoid_stats + + +def test_ellipsoid_bool(): + test = ellipsoid(2, 2, 2)[1:-1, 1:-1, 1:-1] + test_anisotropic = ellipsoid(2, 2, 4, spacing=(1., 1., 2.)) + test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1] + + expected = np.array([[[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, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], + + [[0, 0, 1, 0, 0], + [0, 1, 1, 1, 0], + [1, 1, 1, 1, 1], + [0, 1, 1, 1, 0], + [0, 0, 1, 0, 0]], + + [[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]], + + [[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]]]) + + assert_array_equal(test, expected.astype(bool)) + assert_array_equal(test_anisotropic, expected.astype(bool)) + + +def test_ellipsoid_levelset(): + test = ellipsoid(2, 2, 2, levelset=True)[1:-1, 1:-1, 1:-1] + test_anisotropic = ellipsoid(2, 2, 4, spacing=(1., 1., 2.), + levelset=True) + test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1] + + expected = np.array([[[ 2. , 1.25, 1. , 1.25, 2. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 1. , 0.25, 0. , 0.25, 1. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 2. , 1.25, 1. , 1.25, 2. ]], + + [[ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25]], + + [[ 1. , 0.25, 0. , 0.25, 1. ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0. , -0.75, -1. , -0.75, 0. ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 1. , 0.25, 0. , 0.25, 1. ]], + + [[ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25]], + + [[ 2. , 1.25, 1. , 1.25, 2. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 1. , 0.25, 0. , 0.25, 1. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 2. , 1.25, 1. , 1.25, 2. ]]]) + + assert_allclose(test, expected) + assert_allclose(test_anisotropic, expected) + + +def test_ellipsoid_stats(): + # Test comparison values generated by Wolfram Alpha + vol, surf = ellipsoid_stats(6, 10, 16) + assert(round(1280 * np.pi, 4) == round(vol, 4)) + assert(1383.28 == round(surf, 2)) + + # Test when a <= b <= c does not hold + vol, surf = ellipsoid_stats(16, 6, 10) + assert(round(1280 * np.pi, 4) == round(vol, 4)) + assert(1383.28 == round(surf, 2)) + + # Larger test to ensure reliability over broad range + vol, surf = ellipsoid_stats(17, 27, 169) + assert(round(103428 * np.pi, 4) == round(vol, 4)) + assert(37426.3 == round(surf, 1)) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/exposure/__init__.py b/skimage/exposure/__init__.py index ae75c982..b873c339 100644 --- a/skimage/exposure/__init__.py +++ b/skimage/exposure/__init__.py @@ -1,3 +1,15 @@ -from .exposure import histogram, equalize, equalize_hist -from .exposure import rescale_intensity, cumulative_distribution +from .exposure import histogram, equalize, equalize_hist, \ + rescale_intensity, cumulative_distribution, \ + adjust_gamma, adjust_sigmoid, adjust_log + from ._adapthist import equalize_adapthist + +__all__ = ['histogram', + 'equalize', + 'equalize_hist', + 'equalize_adapthist', + 'rescale_intensity', + 'cumulative_distribution', + 'adjust_gamma', + 'adjust_sigmoid', + 'adjust_log'] diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index c7f1e712..fd5d53dd 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -1,23 +1,28 @@ +import warnings import numpy as np 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.util.dtype import dtype_range, dtype_limits from skimage._shared.utils import deprecated __all__ = ['histogram', 'cumulative_distribution', 'equalize', - 'rescale_intensity'] + 'rescale_intensity', 'adjust_gamma', + 'adjust_log', 'adjust_sigmoid'] def histogram(image, nbins=256): """Return histogram of image. + Unlike `numpy.histogram`, this function returns the centers of bins and does not rebin integer arrays. For integer arrays, each integer value has its own bin, which improves speed and intensity-resolution. + The histogram is computed on the flattened image: for color images, the + function should be used separately on each channel to obtain a histogram + for each color channel. + Parameters ---------- image : array @@ -32,7 +37,20 @@ def histogram(image, nbins=256): The values of the histogram. bin_centers : array The values at the center of the bins. + + Examples + -------- + >>> from skimage import data + >>> hist = histogram(data.camera()) + >>> import matplotlib.pyplot as plt + >>> plt.plot(hist[1], hist[0]) # doctest: +ELLIPSIS + [...] """ + sh = image.shape + if len(sh) == 3 and sh[-1] < 4: + warnings.warn("This might be a color image. The histogram will be " + "computed on the flattened image. You can instead " + "apply this function to each color channel.") # For integer types, histogramming with bincount is more efficient. if np.issubdtype(image.dtype, np.integer): @@ -197,3 +215,140 @@ def rescale_intensity(image, in_range=None, out_range=None): image = (image - imin) / float(imax - imin) return dtype(image * (omax - omin) + omin) + + +def _assert_non_negative(image): + + if np.any(image < 0): + raise ValueError('Image Correction methods work correctly only on ' + 'images with non-negative values. Use ' + 'skimage.exposure.rescale_intensity.') + + +def adjust_gamma(image, gamma=1, gain=1): + """Performs Gamma Correction on the input image. + + Also known as Power Law Transform. + This function transforms the input image pixelwise according to the + equation ``O = I**gamma`` after scaling each pixel to the range 0 to 1. + + Parameters + ---------- + image : ndarray + Input image. + gamma : float + Non negative real number. Default value is 1. + gain : float + The constant multiplier. Default value is 1. + + Returns + ------- + out : ndarray + Gamma corrected output image. + + Notes + ----- + For gamma greater than 1, the histogram will shift towards left and + the output image will be darker than the input image. + + For gamma less than 1, the histogram will shift towards right and + the output image will be brighter than the input image. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Gamma_correction + + """ + _assert_non_negative(image) + dtype = image.dtype.type + + if gamma < 0: + return "Gamma should be a non-negative real number" + + scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0]) + + out = ((image / scale) ** gamma) * scale * gain + return dtype(out) + + +def adjust_log(image, gain=1, inv=False): + """Performs Logarithmic correction on the input image. + + This function transforms the input image pixelwise according to the + equation ``O = gain*log(1 + I)`` after scaling each pixel to the range 0 to 1. + For inverse logarithmic correction, the equation is ``O = gain*(2**I - 1)``. + + Parameters + ---------- + image : ndarray + Input image. + gain : float + The constant multiplier. Default value is 1. + inv : float + If True, it performs inverse logarithmic correction, + else correction will be logarithmic. Defaults to False. + + Returns + ------- + out : ndarray + Logarithm corrected output image. + + References + ---------- + .. [1] http://www.ece.ucsb.edu/Faculty/Manjunath/courses/ece178W03/EnhancePart1.pdf + + """ + _assert_non_negative(image) + dtype = image.dtype.type + scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0]) + + if inv: + out = (2 ** (image / scale) - 1) * scale * gain + return dtype(out) + + out = np.log2(1 + image / scale) * scale * gain + return dtype(out) + + +def adjust_sigmoid(image, cutoff=0.5, gain=10, inv=False): + """Performs Sigmoid Correction on the input image. + + Also known as Contrast Adjustment. + This function transforms the input image pixelwise according to the + equation ``O = 1/(1 + exp*(gain*(cutoff - I)))`` after scaling each pixel + to the range 0 to 1. + + Parameters + ---------- + image : ndarray + Input image. + cutoff : float + Cutoff of the sigmoid function that shifts the characteristic curve + in horizontal direction. Default value is 0.5. + gain : float + The constant multiplier in exponential's power of sigmoid function. + Default value is 10. + inv : bool + If True, returns the negative sigmoid correction. Defaults to False. + + Returns + ------- + out : ndarray + Sigmoid corrected output image. + + References + ---------- + .. [1] Gustav J. Braun, "Image Lightness Rescaling Using Sigmoidal Contrast + Enhancement Functions" http://www.cis.rit.edu/fairchild/PDFs/PAP07.pdf + + """ + _assert_non_negative(image) + dtype = image.dtype.type + scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0]) + + if inv: + out = (1 - 1 / (1 + np.exp(gain * (cutoff - image/scale)))) * scale + return dtype(out) + + out = (1 / (1 + np.exp(gain * (cutoff - image/scale)))) * scale + return dtype(out) diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 086739de..2b696b32 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -1,5 +1,8 @@ +import warnings + import numpy as np from numpy.testing import assert_array_almost_equal as assert_close +from numpy.testing import assert_array_equal import skimage from skimage import data from skimage import exposure @@ -112,6 +115,10 @@ def test_adapthist_color(): '''Test an RGB color uint16 image ''' img = skimage.img_as_uint(data.lena()) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + hist, bin_centers = exposure.histogram(img) + assert len(w) > 0 adapted = exposure.equalize_adapthist(img, clip_limit=0.01) assert_almost_equal = np.testing.assert_almost_equal assert adapted.min() == 0 @@ -169,3 +176,163 @@ def norm_brightness_err(img1, img2): if __name__ == '__main__': from numpy import testing testing.run_module_suite() + + +# Test Gamma Correction +# ===================== + +def test_adjust_gamma_one(): + """Same image should be returned for gamma equal to one""" + image = np.random.uniform(0, 255, (8, 8)) + result = exposure.adjust_gamma(image, 1) + assert_array_equal(result, image) + + +def test_adjust_gamma_zero(): + """White image should be returned for gamma equal to zero""" + image = np.random.uniform(0, 255, (8, 8)) + result = exposure.adjust_gamma(image, 0) + dtype = image.dtype.type + assert_array_equal(result, dtype_range[dtype][1]) + + +def test_adjust_gamma_less_one(): + """Verifying the output with expected results for gamma + correction with gamma equal to half""" + image = np.arange(0, 255, 4, np.uint8).reshape(8,8) + expected = np.array([[ 0, 31, 45, 55, 63, 71, 78, 84], + [ 90, 95, 100, 105, 110, 115, 119, 123], + [127, 131, 135, 139, 142, 146, 149, 153], + [156, 159, 162, 165, 168, 171, 174, 177], + [180, 183, 186, 188, 191, 194, 196, 199], + [201, 204, 206, 209, 211, 214, 216, 218], + [221, 223, 225, 228, 230, 232, 234, 236], + [238, 241, 243, 245, 247, 249, 251, 253]], dtype=np.uint8) + + result = exposure.adjust_gamma(image, 0.5) + assert_array_equal(result, expected) + + +def test_adjust_gamma_greater_one(): + """Verifying the output with expected results for gamma + correction with gamma equal to two""" + image = np.arange(0, 255, 4, np.uint8).reshape(8,8) + expected = np.array([[ 0, 0, 0, 0, 1, 1, 2, 3], + [ 4, 5, 6, 7, 9, 10, 12, 14], + [ 16, 18, 20, 22, 25, 27, 30, 33], + [ 36, 39, 42, 45, 49, 52, 56, 60], + [ 64, 68, 72, 76, 81, 85, 90, 95], + [100, 105, 110, 116, 121, 127, 132, 138], + [144, 150, 156, 163, 169, 176, 182, 189], + [196, 203, 211, 218, 225, 233, 241, 249]], dtype=np.uint8) + + result = exposure.adjust_gamma(image, 2) + assert_array_equal(result, expected) + + +# Test Logarithmic Correction +# =========================== + +def test_adjust_log(): + """Verifying the output with expected results for logarithmic + correction with multiplier constant multiplier equal to unity""" + image = np.arange(0, 255, 4, np.uint8).reshape(8,8) + expected = np.array([[ 0, 5, 11, 16, 22, 27, 33, 38], + [ 43, 48, 53, 58, 63, 68, 73, 77], + [ 82, 86, 91, 95, 100, 104, 109, 113], + [117, 121, 125, 129, 133, 137, 141, 145], + [149, 153, 157, 160, 164, 168, 172, 175], + [179, 182, 186, 189, 193, 196, 199, 203], + [206, 209, 213, 216, 219, 222, 225, 228], + [231, 234, 238, 241, 244, 246, 249, 252]], dtype=np.uint8) + + result = exposure.adjust_log(image, 1) + assert_array_equal(result, expected) + + +def test_adjust_inv_log(): + """Verifying the output with expected results for inverse logarithmic + correction with multiplier constant multiplier equal to unity""" + image = np.arange(0, 255, 4, np.uint8).reshape(8,8) + expected = np.array([[ 0, 2, 5, 8, 11, 14, 17, 20], + [ 23, 26, 29, 32, 35, 38, 41, 45], + [ 48, 51, 55, 58, 61, 65, 68, 72], + [ 76, 79, 83, 87, 90, 94, 98, 102], + [106, 110, 114, 118, 122, 126, 130, 134], + [138, 143, 147, 151, 156, 160, 165, 170], + [174, 179, 184, 188, 193, 198, 203, 208], + [213, 218, 224, 229, 234, 239, 245, 250]], dtype=np.uint8) + + result = exposure.adjust_log(image, 1, True) + assert_array_equal(result, expected) + + +# Test Sigmoid Correction +# ======================= + +def test_adjust_sigmoid_cutoff_one(): + """Verifying the output with expected results for sigmoid correction + with cutoff equal to one and gain of 5""" + image = np.arange(0, 255, 4, np.uint8).reshape(8,8) + expected = np.array([[ 1, 1, 1, 2, 2, 2, 2, 2], + [ 3, 3, 3, 4, 4, 4, 5, 5], + [ 5, 6, 6, 7, 7, 8, 9, 10], + [ 10, 11, 12, 13, 14, 15, 16, 18], + [ 19, 20, 22, 24, 25, 27, 29, 32], + [ 34, 36, 39, 41, 44, 47, 50, 54], + [ 57, 61, 64, 68, 72, 76, 80, 85], + [ 89, 94, 99, 104, 108, 113, 118, 123]], dtype=np.uint8) + + result = exposure.adjust_sigmoid(image, 1, 5) + assert_array_equal(result, expected) + + +def test_adjust_sigmoid_cutoff_zero(): + """Verifying the output with expected results for sigmoid correction + with cutoff equal to zero and gain of 10""" + image = np.arange(0, 255, 4, np.uint8).reshape(8,8) + expected = np.array([[127, 137, 147, 156, 166, 175, 183, 191], + [198, 205, 211, 216, 221, 225, 229, 232], + [235, 238, 240, 242, 244, 245, 247, 248], + [249, 250, 250, 251, 251, 252, 252, 253], + [253, 253, 253, 253, 254, 254, 254, 254], + [254, 254, 254, 254, 254, 254, 254, 254], + [254, 254, 254, 254, 254, 254, 254, 254], + [254, 254, 254, 254, 254, 254, 254, 254]], dtype=np.uint8) + + result = exposure.adjust_sigmoid(image, 0, 10) + assert_array_equal(result, expected) + + +def test_adjust_sigmoid_cutoff_half(): + """Verifying the output with expected results for sigmoid correction + with cutoff equal to half and gain of 10""" + image = np.arange(0, 255, 4, np.uint8).reshape(8,8) + expected = np.array([[ 1, 1, 2, 2, 3, 3, 4, 5], + [ 5, 6, 7, 9, 10, 12, 14, 16], + [ 19, 22, 25, 29, 34, 39, 44, 50], + [ 57, 64, 72, 80, 89, 99, 108, 118], + [128, 138, 148, 158, 167, 176, 184, 192], + [199, 205, 211, 217, 221, 226, 229, 233], + [236, 238, 240, 242, 244, 246, 247, 248], + [249, 250, 250, 251, 251, 252, 252, 253]], dtype=np.uint8) + + result = exposure.adjust_sigmoid(image, 0.5, 10) + assert_array_equal(result, expected) + + +def test_adjust_inv_sigmoid_cutoff_half(): + """Verifying the output with expected results for inverse sigmoid + correction with cutoff equal to half and gain of 10""" + image = np.arange(0, 255, 4, np.uint8).reshape(8,8) + expected = np.array([[253, 253, 252, 252, 251, 251, 250, 249], + [249, 248, 247, 245, 244, 242, 240, 238], + [235, 232, 229, 225, 220, 215, 210, 204], + [197, 190, 182, 174, 165, 155, 146, 136], + [126, 116, 106, 96, 87, 78, 70, 62], + [ 55, 49, 43, 37, 33, 28, 25, 21], + [ 18, 16, 14, 12, 10, 8, 7, 6], + [ 5, 4, 4, 3, 3, 2, 2, 1]], dtype=np.uint8) + + result = exposure.adjust_sigmoid(image, 0.5, 10, True) + assert_array_equal(result, expected) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 9f1e5f92..4a6518d6 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -2,7 +2,24 @@ from ._daisy import daisy from ._hog import hog from .texture import greycomatrix, greycoprops, local_binary_pattern from .peak import peak_local_max -from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, - corner_foerstner, corner_subpix, corner_peaks) +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 + + +__all__ = ['daisy', + 'hog', + 'greycomatrix', + 'greycoprops', + 'local_binary_pattern', + 'peak_local_max', + 'corner_kitchen_rosenfeld', + 'corner_harris', + 'corner_shi_tomasi', + 'corner_foerstner', + 'corner_subpix', + 'corner_peaks', + 'corner_moravec', + 'match_template'] diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py new file mode 100644 index 00000000..ecc2ec11 --- /dev/null +++ b/skimage/feature/_brief.py @@ -0,0 +1,228 @@ +import numpy as np +from scipy.ndimage.filters import gaussian_filter + +from ..util import img_as_float +from .util import _mask_border_keypoints, pairwise_hamming_distance + +from ._brief_cy import _brief_loop + + +def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, + sample_seed=1, variance=2): + """**Experimental function**. + + Extract BRIEF Descriptor about given keypoints for a given image. + + Parameters + ---------- + image : 2D ndarray + Input image. + keypoints : (P, 2) ndarray + Array of keypoint locations in the format (row, col). + descriptor_size : int + Size of BRIEF descriptor about each keypoint. Sizes 128, 256 and 512 + preferred by the authors. Default is 256. + mode : string + Probability distribution for sampling location of decision pixel-pairs + around keypoints. Default is 'normal' otherwise uniform. + patch_size : int + Length of the two dimensional square patch sampling region around + the keypoints. Default is 49. + sample_seed : int + Seed for sampling the decision pixel-pairs. From a square window with + length patch_size, pixel pairs are sampled using the `mode` parameter + to build the descriptors using intensity comparison. The value of + `sample_seed` should be the same for the images to be matched while + building the descriptors. Default is 1. + variance : float + Variance of the Gaussian Low Pass filter applied on the image to + alleviate noise sensitivity. Default is 2. + + Returns + ------- + descriptors : (Q, `descriptor_size`) ndarray of dtype bool + 2D ndarray of binary descriptors of size `descriptor_size` about Q + keypoints after filtering out border keypoints with value at an index + (i, j) either being True or False representing the outcome + of Intensity comparison about ith keypoint on jth decision pixel-pair. + keypoints : (Q, 2) ndarray + Location i.e. (row, col) of keypoints after removing out those that + are near border. + + References + ---------- + .. [1] Michael Calonder, Vincent Lepetit, Christoph Strecha and Pascal Fua + "BRIEF : Binary robust independent elementary features", + http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf + + Examples + -------- + >>> import numpy as np + >>> from skimage.feature.corner import corner_peaks, corner_harris + >>> from skimage.feature import pairwise_hamming_distance, brief, match_keypoints_brief + >>> square1 = np.zeros([8, 8], dtype=np.int32) + >>> square1[2:6, 2:6] = 1 + >>> square1 + array([[0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) + >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) + >>> keypoints1 + array([[2, 2], + [2, 5], + [5, 2], + [5, 5]]) + >>> descriptors1, keypoints1 = brief(square1, keypoints1, patch_size=5) + >>> keypoints1 + array([[2, 2], + [2, 5], + [5, 2], + [5, 5]]) + >>> square2 = np.zeros([9, 9], dtype=np.int32) + >>> square2[2:7, 2:7] = 1 + >>> square2 + array([[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], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 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]], dtype=int32) + >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) + >>> keypoints2 + array([[2, 2], + [2, 6], + [6, 2], + [6, 6]]) + >>> descriptors2, keypoints2 = brief(square2, keypoints2, patch_size=5) + >>> keypoints2 + array([[2, 2], + [2, 6], + [6, 2], + [6, 6]]) + >>> pairwise_hamming_distance(descriptors1, descriptors2) + array([[ 0.03125 , 0.3203125, 0.3671875, 0.6171875], + [ 0.3203125, 0.03125 , 0.640625 , 0.375 ], + [ 0.375 , 0.6328125, 0.0390625, 0.328125 ], + [ 0.625 , 0.3671875, 0.34375 , 0.0234375]]) + >>> match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2) + array([[[ 2, 2], + [ 2, 2]], + + [[ 2, 5], + [ 2, 6]], + + [[ 5, 2], + [ 6, 2]], + + [[ 5, 5], + [ 6, 6]]]) + + """ + np.random.seed(sample_seed) + + image = np.squeeze(image) + if image.ndim != 2: + raise ValueError("Only 2-D gray-scale images supported.") + + image = img_as_float(image) + + # Gaussian Low pass filtering to alleviate noise + # sensitivity + image = gaussian_filter(image, variance) + + image = np.ascontiguousarray(image) + + keypoints = np.array(keypoints + 0.5, dtype=np.intp, order='C') + + # Removing keypoints that are within (patch_size / 2) distance from the + # image border + keypoints = keypoints[_mask_border_keypoints(image, keypoints, patch_size // 2)] + keypoints = np.ascontiguousarray(keypoints) + + descriptors = np.zeros((keypoints.shape[0], descriptor_size), dtype=bool, + order='C') + + # Sampling pairs of decision pixels in patch_size x patch_size window + if mode == 'normal': + + samples = (patch_size / 5.0) * np.random.randn(descriptor_size * 8) + samples = np.array(samples, dtype=np.int32) + samples = samples[(samples < (patch_size // 2)) + & (samples > - (patch_size - 2) // 2)] + + pos1 = samples[:descriptor_size * 2] + pos1 = pos1.reshape(descriptor_size, 2) + pos2 = samples[descriptor_size * 2:descriptor_size * 4] + pos2 = pos2.reshape(descriptor_size, 2) + + else: + + samples = np.random.randint(-(patch_size - 2) // 2, + (patch_size // 2) + 1, + (descriptor_size * 2, 2)) + pos1, pos2 = np.split(samples, 2) + + pos1 = np.ascontiguousarray(pos1) + pos2 = np.ascontiguousarray(pos2) + + _brief_loop(image, descriptors.view(np.uint8), keypoints, pos1, pos2) + + return descriptors, keypoints + + +def match_keypoints_brief(keypoints1, descriptors1, keypoints2, + descriptors2, threshold=0.15): + """**Experimental function**. + + Match keypoints described using BRIEF descriptors in one image to + those in second image. + + Parameters + ---------- + keypoints1 : (M, 2) ndarray + M Keypoints from the first image described using skimage.feature.brief + descriptors1 : (M, P) ndarray + BRIEF descriptors of size P about M keypoints in the first image. + keypoints2 : (N, 2) ndarray + N Keypoints from the second image described using skimage.feature.brief + descriptors2 : (N, P) ndarray + BRIEF descriptors of size P about N keypoints in the second image. + threshold : float in range [0, 1] + Maximum allowable hamming distance between descriptors of two keypoints + in separate images to be regarded as a match. Default is 0.15. + + Returns + ------- + match_keypoints_brief : (Q, 2, 2) ndarray + Location of Q matched keypoint pairs from two images. + + """ + if (keypoints1.shape[0] != descriptors1.shape[0] + or keypoints2.shape[0] != descriptors2.shape[0]): + raise ValueError("The number of keypoints and number of described " + "keypoints do not match. Make the optional parameter " + "return_keypoints True to get described keypoints.") + + if descriptors1.shape[1] != descriptors2.shape[1]: + raise ValueError("Descriptor sizes for matching keypoints in both " + "the images should be equal.") + + # Get hamming distances between keeypoints1 and keypoints2 + distance = pairwise_hamming_distance(descriptors1, descriptors2) + + temp = distance > threshold + row_check = np.any(~temp, axis=1) + matched_keypoints2 = keypoints2[np.argmin(distance, axis=1)] + matched_keypoint_pairs = np.zeros((np.sum(row_check), 2, 2), dtype=np.intp) + matched_keypoint_pairs[:, 0, :] = keypoints1[row_check] + matched_keypoint_pairs[:, 1, :] = matched_keypoints2[row_check] + + return matched_keypoint_pairs diff --git a/skimage/feature/_brief_cy.pyx b/skimage/feature/_brief_cy.pyx new file mode 100644 index 00000000..c53d85fc --- /dev/null +++ b/skimage/feature/_brief_cy.pyx @@ -0,0 +1,24 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp + + +def _brief_loop(double[:, ::1] image, char[:, ::1] descriptors, + Py_ssize_t[:, ::1] keypoints, + int[:, ::1] pos0, int[:, ::1] pos1): + + cdef Py_ssize_t k, d, kr, kc, pr0, pr1, pc0, pc1 + + for p in range(pos0.shape[0]): + pr0 = pos0[p, 0] + pc0 = pos0[p, 1] + pr1 = pos1[p, 0] + pc1 = pos1[p, 1] + for k in range(keypoints.shape[0]): + kr = keypoints[k, 0] + kc = keypoints[k, 1] + if image[kr + pr0, kc + pc0] < image[kr + pr1, kc + pc1]: + descriptors[k, p] = True diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 9fa018b7..431a5986 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -117,9 +117,9 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), #create new integral image for this orientation # isolate orientations in this range - temp_ori = np.where(orientation < 180 / orientations * (i + 1), + temp_ori = np.where(orientation < 180.0 / orientations * (i + 1), orientation, -1) - temp_ori = np.where(orientation >= 180 / orientations * i, + temp_ori = np.where(orientation >= 180.0 / orientations * i, temp_ori, -1) # select magnitudes for those orientations cond2 = temp_ori > -1 @@ -142,10 +142,10 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), 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(int(centre[0] - dx), - int(centre[1] - dy), - int(centre[0] + dx), - int(centre[1] + dy)) + rr, cc = draw.line(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] """ diff --git a/skimage/feature/_template.pyx b/skimage/feature/_template.pyx index 03695959..855ece23 100644 --- a/skimage/feature/_template.pyx +++ b/skimage/feature/_template.pyx @@ -50,9 +50,9 @@ from skimage.transform import integral def match_template(cnp.ndarray[float, ndim=2, mode="c"] image, cnp.ndarray[float, ndim=2, mode="c"] template): - 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[:, ::1] corr + cdef float[:, ::1] image_sat + cdef float[:, ::1] image_sqr_sat cdef float template_mean = np.mean(template) cdef float template_ssd cdef float inv_area @@ -94,4 +94,4 @@ def match_template(cnp.ndarray[float, ndim=2, mode="c"] image, den = sqrt((window_sqr_sum - window_mean_sqr) * template_ssd) corr[r, c] /= den - return corr + return np.asarray(corr) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index f98ed4ca..ec83fa65 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -8,15 +8,9 @@ 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): +def _glcm_loop(cnp.uint8_t[:, ::1] image, double[:] distances, + double[:] angles, Py_ssize_t levels, + cnp.uint32_t[:, :, :, ::1] out): """Perform co-occurrence matrix accumulation. Parameters @@ -81,7 +75,7 @@ cdef inline int _bit_rotate_right(int value, int length): return (value >> 1) | ((value & 1) << (length - 1)) -def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, +def _local_binary_pattern(double[:, ::1] image, int P, float R, char method='D'): """Gray scale and rotation invariant LBP (Local Binary Patterns). @@ -92,16 +86,17 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, image : (N, M) double array Graylevel image. P : int - Number of circularly symmetric neighbour set points (quantization of the - angular space). + 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 : {'D', 'R', 'U', 'N', 'V'} Method to determine the pattern. * 'D': 'default' * 'R': 'ror' * 'U': 'uniform' + * 'N': 'nri_uniform' * 'V': 'var' Returns @@ -111,30 +106,35 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, """ # texture weights - cdef cnp.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype=np.int32) + cdef int[::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) + rr = - R * np.sin(2 * np.pi * np.arange(P, dtype=np.double) / P) + cc = R * np.cos(2 * np.pi * np.arange(P, dtype=np.double) / P) + cdef double[::1] rp = np.round(rr, 5) + cdef double[::1] cp = np.round(cc, 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) + # pre-allocate arrays for computation + cdef double[::1] texture = np.zeros(P, dtype=np.double) + cdef char[::1] signed_texture = np.zeros(P, dtype=np.int8) + cdef int[::1] rotation_chain = np.zeros(P, dtype=np.int32) output_shape = (image.shape[0], image.shape[1]) - cdef cnp.ndarray[double, ndim=2] output = np.zeros(output_shape, np.double) + cdef double[:, ::1] output = np.zeros(output_shape, dtype=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 + cdef Py_ssize_t rot_index, n_ones + cdef cnp.int8_t first_zero, first_one + 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) + texture[i] = bilinear_interpolation(&image[0, 0], rows, cols, + r + rp[i], c + cp[i], + 'C', 0) # signed / thresholded texture for i in range(P): if texture[i] - image[r, c] >= 0: @@ -145,24 +145,83 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, lbp = 0 # if method == 'uniform' or method == 'var': - if method == 'U' or method == 'V': + if method == 'U' or method == 'N' 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 method == 'N': + # Uniform local binary patterns are defined as patterns + # with at most 2 value changes (from 0 to 1 or from 1 to + # 0). Uniform patterns can be caraterized by their number + # `n_ones` of 1. The possible values for `n_ones` range + # from 0 to P. + # Here is an example for P = 4: + # n_ones=0: 0000 + # n_ones=1: 0001, 1000, 0100, 0010 + # n_ones=2: 0011, 1001, 1100, 0110 + # n_ones=3: 0111, 1011, 1101, 1110 + # n_ones=4: 1111 + # + # For a pattern of size P there are 2 constant patterns + # corresponding to n_ones=0 and n_ones=P. For each other + # value of `n_ones` , i.e n_ones=[1..P-1], there are P + # possible patterns which are related to each other through + # circular permutations. The total number of uniform + # patterns is thus (2 + P * (P - 1)). + # Given any pattern (uniform or not) we must be able to + # associate a unique code: + # 1. Constant patterns patterns (with n_ones=0 and + # n_ones=P) and non uniform patterns are given fixed + # code values. + # 2. Other uniform patterns are indexed considering the + # value of n_ones, and an index called 'rot_index' + # reprenting the number of circular right shifts + # required to obtain the pattern starting from a + # reference position (corresponding to all zeros stacked + # on the right). This number of rotations (or circular + # right shifts) 'rot_index' is efficiently computed by + # considering the positions of the first 1 and the first + # 0 found in the pattern. - 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 + if changes <= 2: + # We have a uniform pattern + n_ones = 0 # determies the number of ones + first_one = -1 # position was the first one + first_zero = -1 # position of the first zero + for i in range(P): + if signed_texture[i]: + n_ones += 1 + if first_one == -1: + first_one = i + else: + if first_zero == -1: + first_zero = i + if n_ones == 0: + lbp = 0 + elif n_ones == P: + lbp = P * (P - 1) + 1 + else: + if first_one == 0: + rot_index = n_ones - first_zero + else: + rot_index = P - first_one + lbp = 1 + (n_ones - 1) * P + rot_index + else: # changes > 2 + lbp = P * (P - 1) + 2 + else: # method != 'N' + if changes <= 2: + for i in range(P): + lbp += signed_texture[i] else: - lbp = np.nan + 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): @@ -181,4 +240,4 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, output[r, c] = lbp - return output + return np.asarray(output) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py new file mode 100644 index 00000000..4bb7fdda --- /dev/null +++ b/skimage/feature/censure.py @@ -0,0 +1,234 @@ +import numpy as np +from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve + +from skimage.transform import integral_image +from skimage.feature.corner import _compute_auto_correlation +from skimage.util import img_as_float +from skimage.morphology import octagon, star +from skimage.feature.util import _mask_border_keypoints + +from skimage.feature.censure_cy import _censure_dob_loop + + +# The paper(Reference [1]) mentions the sizes of the Octagon shaped filter +# kernel for the first seven scales only. The sizes of the later scales +# have been extrapolated based on the following statement in the paper. +# "These octagons scale linearly and were experimentally chosen to correspond +# to the seven DOBs described in the previous section." +OCTAGON_OUTER_SHAPE = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), + (15, 10), (15, 11), (15, 12), (17, 13), (17, 14)] +OCTAGON_INNER_SHAPE = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5), + (7, 5), (7, 6), (9, 6), (9, 7)] + +# The sizes for the STAR shaped filter kernel for different scales have been +# taken from the OpenCV implementation. +STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] +STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), + (9, 6), (11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] + + +def _filter_image(image, min_scale, max_scale, mode): + + response = np.zeros((image.shape[0], image.shape[1], + max_scale - min_scale + 1), dtype=np.double) + + if mode == 'dob': + + # make response[:, :, i] contiguous memory block + item_size = response.itemsize + response.strides = (item_size * response.shape[0], item_size, + item_size * response.shape[0] * response.shape[1]) + + integral_img = integral_image(image) + + for i in range(max_scale - min_scale + 1): + n = min_scale + i + + # Constant multipliers for the outer region and the inner region + # of the bi-level filters with the constraint of keeping the + # DC bias 0. + inner_weight = (1.0 / (2 * n + 1)**2) + outer_weight = (1.0 / (12 * n**2 + 4 * n)) + + _censure_dob_loop(n, integral_img, response[:, :, i], + inner_weight, outer_weight) + + # NOTE : For the Octagon shaped filter, we implemented and evaluated the + # slanted integral image based image filtering but the performance was + # more or less equal to image filtering using + # scipy.ndimage.filters.convolve(). Hence we have decided to use the + # later for a much cleaner implementation. + elif mode == 'octagon': + # TODO : Decide the shapes of Octagon filters for scales > 7 + + for i in range(max_scale - min_scale + 1): + mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1] + mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1] + response[:, :, i] = convolve(image, + _octagon_filter_kernel(mo, no, mi, ni)) + + elif mode == 'star': + + for i in range(max_scale - min_scale + 1): + m = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][0]] + n = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][1]] + response[:, :, i] = convolve(image, _star_filter_kernel(m, n)) + + return response + + +def _octagon_filter_kernel(mo, no, mi, ni): + outer = (mo + 2 * no)**2 - 2 * no * (no + 1) + inner = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) + outer_weight = 1.0 / (outer - inner) + inner_weight = 1.0 / inner + c = ((mo + 2 * no) - (mi + 2 * ni)) // 2 + outer_oct = octagon(mo, no) + inner_oct = np.zeros((mo + 2 * no, mo + 2 * no)) + inner_oct[c: -c, c: -c] = octagon(mi, ni) + bfilter = (outer_weight * outer_oct - + (outer_weight + inner_weight) * inner_oct) + return bfilter + + +def _star_filter_kernel(m, n): + c = m + m // 2 - n - n // 2 + outer_star = star(m) + inner_star = np.zeros_like(outer_star) + inner_star[c: -c, c: -c] = star(n) + outer_weight = 1.0 / (np.sum(outer_star - inner_star)) + inner_weight = 1.0 / np.sum(inner_star) + bfilter = (outer_weight * outer_star - + (outer_weight + inner_weight) * inner_star) + return bfilter + + +def _suppress_lines(feature_mask, image, sigma, line_threshold): + Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + feature_mask[(Axx + Ayy) * (Axx + Ayy) + > line_threshold * (Axx * Ayy - Axy * Axy)] = False + + +def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', + non_max_threshold=0.15, line_threshold=10): + """**Experimental function**. + + Extracts CenSurE keypoints along with the corresponding scale using + either Difference of Boxes, Octagon or STAR bi-level filter. + + Parameters + ---------- + image : 2D ndarray + Input image. + min_scale : int + Minimum scale to extract keypoints from. + max_scale : int + Maximum scale to extract keypoints from. The keypoints will be + extracted from all the scales except the first and the last i.e. + from the scales in the range [min_scale + 1, max_scale - 1]. + mode : {'DoB', 'Octagon', 'STAR'} + Type of bi-level filter used to get the scales of the input image. + Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes + represent the shape of the bi-level filters i.e. box(square), octagon + and star respectively. For instance, a bi-level octagon filter consists + of a smaller inner octagon and a larger outer octagon with the filter + weights being uniformly negative in both the inner octagon while + uniformly positive in the difference region. Use STAR and Octagon for + better features and DoB for better performance. + non_max_threshold : float + Threshold value used to suppress maximas and minimas with a weak + magnitude response obtained after Non-Maximal Suppression. + line_threshold : float + Threshold for rejecting interest points which have ratio of principal + curvatures greater than this value. + + Returns + ------- + keypoints : (N, 2) array + Location of the extracted keypoints in the ``(row, col)`` format. + scales : (N, 1) array + The corresponding scale of the N extracted keypoints. + + References + ---------- + .. [1] Motilal Agrawal, Kurt Konolige and Morten Rufus Blas + "CenSurE: Center Surround Extremas for Realtime Feature + Detection and Matching", + http://link.springer.com/content/pdf/10.1007%2F978-3-540-88693-8_8.pdf + + .. [2] Adam Schmidt, Marek Kraft, Michal Fularz and Zuzanna Domagala + "Comparative Assessment of Point Feature Detectors and + Descriptors in the Context of Robot Navigation" + http://www.jamris.org/01_2013/saveas.php?QUEST=JAMRIS_No01_2013_P_11-20.pdf + + """ + + # (1) First we generate the required scales on the input grayscale image + # using a bi-level filter and stack them up in `filter_response`. + # (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on the + # filter_response to suppress points that are neither minima or maxima in + # 3 x 3 x 3 neighbourhood. We obtain a boolean ndarray `feature_mask` + # containing all the minimas and maximas in `filter_response` as True. + # (3) Then we suppress all the points in the `feature_mask` for which the + # corresponding point in the image at a particular scale has the ratio of + # principal curvatures greater than `line_threshold`. + # (4) Finally, we remove the border keypoints and return the keypoints + # along with its corresponding scale. + + image = np.squeeze(image) + if image.ndim != 2: + raise ValueError("Only 2-D gray-scale images supported.") + + mode = mode.lower() + if mode not in ('dob', 'octagon', 'star'): + raise ValueError('Mode must be one of "DoB", "Octagon", "STAR".') + + if min_scale < 1 or max_scale < 1 or max_scale - min_scale < 2: + raise ValueError('The scales must be >= 1 and the number of scales ' + 'should be >= 3.') + + image = img_as_float(image) + image = np.ascontiguousarray(image) + + # Generating all the scales + filter_response = _filter_image(image, min_scale, max_scale, mode) + + # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 + # neighbourhood to zero + minimas = minimum_filter(filter_response, (3, 3, 3)) == filter_response + maximas = maximum_filter(filter_response, (3, 3, 3)) == filter_response + + feature_mask = minimas | maximas + feature_mask[filter_response < non_max_threshold] = False + + for i in range(1, max_scale - min_scale): + # sigma = (window_size - 1) / 6.0, so the window covers > 99% of the + # kernel's distribution + # window_size = 7 + 2 * (min_scale - 1 + i) + # Hence sigma = 1 + (min_scale - 1 + i)/ 3.0 + _suppress_lines(feature_mask[:, :, i], image, + (1 + (min_scale + i - 1) / 3.0), line_threshold) + + rows, cols, scales = np.nonzero(feature_mask[..., 1:max_scale - min_scale]) + keypoints = np.column_stack([rows, cols]) + scales = scales + min_scale + 1 + + if mode == 'dob': + return keypoints, scales + + cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool) + + if mode == 'octagon': + for i in range(min_scale + 1, max_scale): + c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 \ + + OCTAGON_OUTER_SHAPE[i - 1][1] + cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ + & (scales == i) + elif mode == 'star': + for i in range(min_scale + 1, max_scale): + c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] \ + + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 + cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ + & (scales == i) + + return keypoints[cumulative_mask], scales[cumulative_mask] diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx new file mode 100644 index 00000000..1c352fde --- /dev/null +++ b/skimage/feature/censure_cy.pyx @@ -0,0 +1,72 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + + +def _censure_dob_loop(Py_ssize_t n, + double[:, ::1] integral_img, + double[:, ::1] filtered_image, + double inner_weight, double outer_weight): + # This function calculates the value in the DoB filtered image using + # integral images. If r = right. l = left, u = up, d = down, the sum of + # pixel values in the rectangle formed by (u, l), (u, r), (d, r), (d, l) + # is calculated as I(d, r) + I(u - 1, l - 1) - I(u - 1, r) - I(d, l - 1). + + cdef Py_ssize_t i, j + cdef double inner, outer + cdef Py_ssize_t n2 = 2 * n + cdef double total_weight = inner_weight + outer_weight + + # top-left pixel + inner = (integral_img[n2 + n, n2 + n] + + integral_img[n2 - n - 1, n2 - n - 1] + - integral_img[n2 + n, n2 - n - 1] + - integral_img[n2 - n - 1, n2 + n]) + + outer = integral_img[2 * n2, 2 * n2] + + filtered_image[n2, n2] = (outer_weight * outer + - total_weight * inner) + + # left column + for i in range(n2 + 1, integral_img.shape[0] - n2): + inner = (integral_img[i + n, n2 + n] + + integral_img[i - n - 1, n2 - n - 1] + - integral_img[i + n, n2 - n - 1] + - integral_img[i - n - 1, n2 + n]) + + outer = (integral_img[i + n2, 2 * n2] + - integral_img[i - n2 - 1, 2 * n2]) + + filtered_image[i, n2] = (outer_weight * outer + - total_weight * inner) + + # top row + for j in range(n2 + 1, integral_img.shape[1] - n2): + inner = (integral_img[n2 + n, j + n] + + integral_img[n2 - n - 1, j - n - 1] + - integral_img[n2 + n, j - n - 1] + - integral_img[n2 - n - 1, j + n]) + + outer = (integral_img[2 * n2, j + n2] + - integral_img[2 * n2, j - n2 - 1]) + + filtered_image[n2, j] = (outer_weight * outer + - total_weight * inner) + + # remaining block + for i in range(n2 + 1, integral_img.shape[0] - n2): + for j in range(n2 + 1, integral_img.shape[1] - n2): + inner = (integral_img[i + n, j + n] + + integral_img[i - n - 1, j - n - 1] + - integral_img[i + n, j - n - 1] + - integral_img[i - n - 1, j + n]) + + outer = (integral_img[i + n2, j + n2] + + integral_img[i - n2 - 1, j - n2 - 1] + - integral_img[i + n2, j - n2 - 1] + - integral_img[i - n2 - 1, j + n2]) + + filtered_image[i, j] = (outer_weight * outer + - total_weight * inner) diff --git a/skimage/feature/corner.py b/skimage/feature/corner.py index 12d3fb70..53b2241e 100644 --- a/skimage/feature/corner.py +++ b/skimage/feature/corner.py @@ -91,8 +91,13 @@ def corner_kitchen_rosenfeld(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) + numerator = (imxx * imy**2 + imyy * imx**2 - 2 * imxy * imx * imy) + denominator = (imx**2 + imy**2) + + response = np.zeros_like(image, dtype=np.double) + + mask = denominator != 0 + response[mask] = numerator[mask] / denominator[mask] return response @@ -136,8 +141,8 @@ def corner_harris(image, method='k', k=0.05, eps=1e-6, sigma=1): References ---------- - ..[1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/harris.htm - ..[2] http://en.wikipedia.org/wiki/Corner_detection + .. [1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/harris.htm + .. [2] http://en.wikipedia.org/wiki/Corner_detection Examples -------- @@ -206,8 +211,8 @@ def corner_shi_tomasi(image, sigma=1): References ---------- - ..[1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/harris.htm - ..[2] http://en.wikipedia.org/wiki/Corner_detection + .. [1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/harris.htm + .. [2] http://en.wikipedia.org/wiki/Corner_detection Examples -------- @@ -272,9 +277,8 @@ def corner_foerstner(image, sigma=1): References ---------- - ..[1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/\ - foerstner87.fast.pdf - ..[2] http://en.wikipedia.org/wiki/Corner_detection + .. [1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/foerstner87.fast.pdf + .. [2] http://en.wikipedia.org/wiki/Corner_detection Examples -------- @@ -311,8 +315,13 @@ def corner_foerstner(image, sigma=1): # trace traceA = Axx + Ayy - w = detA / traceA - q = 4 * detA / traceA**2 + w = np.zeros_like(image, dtype=np.double) + q = np.zeros_like(image, dtype=np.double) + + mask = traceA != 0 + + w[mask] = detA[mask] / traceA[mask] + q[mask] = 4 * detA[mask] / traceA[mask]**2 return w, q @@ -338,9 +347,9 @@ def corner_subpix(image, corners, window_size=11, alpha=0.99): References ---------- - ..[1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/\ - foerstner87.fast.pdf - ..[2] http://en.wikipedia.org/wiki/Corner_detection + .. [1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/\ + foerstner87.fast.pdf + .. [2] http://en.wikipedia.org/wiki/Corner_detection """ @@ -489,7 +498,7 @@ def corner_peaks(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, threshold_abs=threshold_abs, threshold_rel=threshold_rel, exclude_border=exclude_border, - indices=False, num_peaks=np.inf, + indices=False, num_peaks=num_peaks, footprint=footprint, labels=labels) if min_distance > 0: coords = np.transpose(peaks.nonzero()) diff --git a/skimage/feature/corner_cy.pyx b/skimage/feature/corner_cy.pyx index 71c748f8..7d558e52 100644 --- a/skimage/feature/corner_cy.pyx +++ b/skimage/feature/corner_cy.pyx @@ -20,7 +20,7 @@ def corner_moravec(image, Py_ssize_t window_size=1): ---------- image : ndarray Input image. - window_size : int, optional + window_size : int, optional (default 1) Window size. Returns @@ -59,16 +59,8 @@ def corner_moravec(image, Py_ssize_t window_size=1): 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[:, ::1] cimage = np.ascontiguousarray(img_as_float(image)) + cdef double[:, ::1] out = np.zeros(image.shape, dtype=np.double) cdef double msum, min_msum cdef Py_ssize_t r, c, br, bc, mr, mc, a, b @@ -81,11 +73,10 @@ def corner_moravec(image, Py_ssize_t window_size=1): 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 + msum += (cimage[r + mr, c + mc] + - cimage[br + mr, bc + mc]) ** 2 min_msum = min(msum, min_msum) - out_data[r * cols + c] = min_msum + out[r, c] = min_msum - return out + return np.asarray(out) diff --git a/skimage/feature/peak.py b/skimage/feature/peak.py index 9c7a934f..3268831a 100644 --- a/skimage/feature/peak.py +++ b/skimage/feature/peak.py @@ -33,8 +33,8 @@ def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, 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` + If True, the output will be an array representing peak coordinates. + If False, the output will be a boolean array 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`, @@ -130,11 +130,12 @@ def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, image *= mask if exclude_border: - # Remove the image borders - image[:min_distance] = 0 - image[-min_distance:] = 0 - image[:, :min_distance] = 0 - image[:, -min_distance:] = 0 + # zero out the image borders + for i in range(image.ndim): + image = image.swapaxes(0, i) + image[:min_distance] = 0 + image[-min_distance:] = 0 + image = image.swapaxes(0, i) # find top peak candidates above a threshold peak_threshold = max(np.max(image.ravel()) * threshold_rel, threshold_abs) @@ -150,5 +151,6 @@ def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, if indices is True: return coordinates else: - out[coordinates[:, 0], coordinates[:, 1]] = True + nd_indices = tuple(coordinates.T) + out[nd_indices] = True return out diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index e769621d..7df64c32 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -13,11 +13,17 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['corner_cy.pyx'], working_path=base_path) + cython(['censure_cy.pyx'], working_path=base_path) + cython(['_brief_cy.pyx'], working_path=base_path) cython(['_texture.pyx'], working_path=base_path) cython(['_template.pyx'], working_path=base_path) config.add_extension('corner_cy', sources=['corner_cy.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('censure_cy', sources=['censure_cy.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_brief_cy', sources=['_brief_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'], diff --git a/skimage/feature/template.py b/skimage/feature/template.py index 51ca90b4..5cb4d382 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -34,14 +34,14 @@ def match_template(image, template, pad_input=False): -------- >>> template = np.zeros((3, 3)) >>> template[1, 1] = 1 - >>> print template + >>> print(template) [[ 0. 0. 0.] [ 0. 1. 0.] [ 0. 0. 0.]] >>> image = np.zeros((6, 6)) >>> image[1, 1] = 1 >>> image[4, 4] = -1 - >>> print image + >>> print(image) [[ 0. 0. 0. 0. 0. 0.] [ 0. 1. 0. 0. 0. 0.] [ 0. 0. 0. 0. 0. 0.] @@ -49,13 +49,13 @@ def match_template(image, template, pad_input=False): [ 0. 0. 0. 0. -1. 0.] [ 0. 0. 0. 0. 0. 0.]] >>> result = match_template(image, template) - >>> print np.round(result, 3) + >>> print(np.round(result, 3)) [[ 1. -0.125 0. 0. ] [-0.125 -0.125 0. 0. ] [ 0. 0. 0.125 0.125] [ 0. 0. 0.125 -1. ]] >>> result = match_template(image, template, pad_input=True) - >>> print np.round(result, 3) + >>> print(np.round(result, 3)) [[-0.125 -0.125 -0.125 0. 0. 0. ] [-0.125 1. -0.125 0. 0. 0. ] [-0.125 -0.125 -0.125 0. 0. 0. ] diff --git a/skimage/feature/tests/_test_brief.py b/skimage/feature/tests/_test_brief.py new file mode 100644 index 00000000..1d26cbbd --- /dev/null +++ b/skimage/feature/tests/_test_brief.py @@ -0,0 +1,83 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage import data +from skimage import transform as tf +from skimage.color import rgb2gray +from skimage.feature import (brief, match_keypoints_brief, corner_peaks, + corner_harris) + + +def test_brief_color_image_unsupported_error(): + """Brief descriptors can be evaluated on gray-scale images only.""" + img = np.zeros((20, 20, 3)) + keypoints = [[7, 5], [11, 13]] + assert_raises(ValueError, brief, img, keypoints) + + +def test_match_keypoints_brief_lena_translation(): + """Test matched keypoints between lena image and its translated version.""" + img = data.lena() + img = rgb2gray(img) + img.shape + tform = tf.SimilarityTransform(scale=1, rotation=0, translation=(15, 20)) + translated_img = tf.warp(img, tform) + + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) + + keypoints2 = corner_peaks(corner_harris(translated_img), min_distance=5) + descriptors2, keypoints2 = brief(translated_img, keypoints2, + descriptor_size=512) + + matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, + keypoints2, descriptors2, + threshold=0.10) + + assert_array_equal(matched_keypoints[:, 0, :], matched_keypoints[:, 1, :] + + [20, 15]) + + +def test_match_keypoints_brief_lena_rotation(): + """Verify matched keypoints result between lena image and its rotated + version with the expected keypoint pairs.""" + img = data.lena() + img = rgb2gray(img) + img.shape + tform = tf.SimilarityTransform(scale=1, rotation=0.10, translation=(0, 0)) + rotated_img = tf.warp(img, tform) + + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) + + keypoints2 = corner_peaks(corner_harris(rotated_img), min_distance=5) + descriptors2, keypoints2 = brief(rotated_img, keypoints2, + descriptor_size=512) + + matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, + keypoints2, descriptors2, + threshold=0.07) + + expected = np.array([[[263, 272], + [234, 298]], + + [[271, 120], + [258, 146]], + + [[323, 164], + [305, 195]], + + [[414, 70], + [405, 111]], + + [[435, 181], + [415, 223]], + + [[454, 176], + [435, 221]]]) + + assert_array_equal(matched_keypoints, expected) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/_test_censure.py b/skimage/feature/tests/_test_censure.py new file mode 100644 index 00000000..4cd2ad68 --- /dev/null +++ b/skimage/feature/tests/_test_censure.py @@ -0,0 +1,89 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage.data import moon +from skimage.feature import keypoints_censure + + +def test_keypoints_censure_color_image_unsupported_error(): + """Censure keypoints can be extracted from gray-scale images only.""" + img = np.zeros((20, 20, 3)) + assert_raises(ValueError, keypoints_censure, img) + + +def test_keypoints_censure_mode_validity_error(): + """Mode argument in keypoints_censure can be either DoB, Octagon or + STAR.""" + img = np.zeros((20, 20)) + assert_raises(ValueError, keypoints_censure, img, mode='dummy') + + +def test_keypoints_censure_scale_range_error(): + """Difference between the the max_scale and min_scale parameters in + keypoints_censure should be greater than or equal to two.""" + img = np.zeros((20, 20)) + assert_raises(ValueError, keypoints_censure, img, min_scale=1, max_scale=2) + + +def test_keypoints_censure_moon_image_dob(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for DoB filter.""" + img = moon() + actual_kp_dob, actual_scale = keypoints_censure(img, 1, 7, 'DoB', 0.15) + expected_kp_dob = np.array([[ 21, 497], + [ 36, 46], + [119, 350], + [185, 177], + [287, 250], + [357, 239], + [463, 116], + [464, 132], + [467, 260]]) + expected_scale = np.array([3, 4, 4, 2, 2, 3, 2, 2, 2]) + + assert_array_equal(expected_kp_dob, actual_kp_dob) + assert_array_equal(expected_scale, actual_scale) + + +def test_keypoints_censure_moon_image_octagon(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for Octagon filter.""" + img = moon() + actual_kp_octagon, actual_scale = keypoints_censure(img, 1, 7, 'Octagon', + 0.15) + expected_kp_octagon = np.array([[ 21, 496], + [ 35, 46], + [287, 250], + [356, 239], + [463, 116]]) + + expected_scale = np.array([3, 4, 2, 2, 2]) + + assert_array_equal(expected_kp_octagon, actual_kp_octagon) + assert_array_equal(expected_scale, actual_scale) + + +def test_keypoints_censure_moon_image_star(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for STAR filter.""" + img = moon() + actual_kp_star, actual_scale = keypoints_censure(img, 1, 7, 'STAR', 0.15) + expected_kp_star = np.array([[ 21, 497], + [ 36, 46], + [117, 356], + [185, 177], + [260, 227], + [287, 250], + [357, 239], + [451, 281], + [463, 116], + [467, 260]]) + + expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) + + assert_array_equal(expected_kp_star, actual_kp_star) + assert_array_equal(expected_scale, actual_scale) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_corner.py b/skimage/feature/tests/test_corner.py index a7ee01ff..66ef41c6 100644 --- a/skimage/feature/tests/test_corner.py +++ b/skimage/feature/tests/test_corner.py @@ -5,7 +5,8 @@ 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) + corner_subpix, peak_local_max, corner_peaks, + corner_kitchen_rosenfeld, corner_foerstner) def test_square_image(): @@ -100,6 +101,19 @@ def test_subpix(): assert_array_equal(subpix[0], (24.5, 24.5)) +def test_num_peaks(): + """For a bunch of different values of num_peaks, check that + peak_local_max returns exactly the right amount of peaks. Test + is run on Lena in order to produce a sufficient number of corners""" + + lena_corners = corner_harris(data.lena()) + + for i in range(20): + n = np.random.random_integers(20) + results = peak_local_max(lena_corners, num_peaks=n) + assert (results.shape[0] == n) + + def test_corner_peaks(): response = np.zeros((5, 5)) response[2:4, 2:4] = 1 @@ -111,6 +125,21 @@ def test_corner_peaks(): assert len(corners) == 4 +def test_blank_image_nans(): + """Some of the corner detectors had a weakness in terms of returning + NaN when presented with regions of constant intensity. This should + be fixed by now. We test whether each detector returns something + finite in the case of constant input""" + + detectors = [corner_moravec, corner_harris, corner_shi_tomasi, + corner_kitchen_rosenfeld, corner_foerstner] + constant_image = np.zeros((20, 20)) + + for det in detectors: + response = det(constant_image) + assert np.all(np.isfinite(response)) + + 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 cfef2da0..b37513a5 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -4,7 +4,10 @@ from skimage import data from skimage import feature from skimage import img_as_float from skimage import draw -from numpy.testing import * +from numpy.testing import (assert_raises, + assert_almost_equal, + ) + def test_histogram_of_oriented_gradients(): img = img_as_float(data.lena()[:256, :].mean(axis=2)) @@ -14,16 +17,19 @@ def test_histogram_of_oriented_gradients(): 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 @@ -90,6 +96,7 @@ def test_hog_basic_orientations_and_data_types(): assert_almost_equal(actual, desired, decimal=2) + def test_hog_orientations_circle(): # scenario: # 1) create image with blurred circle in the middle @@ -129,6 +136,7 @@ def test_hog_orientations_circle(): desired = np.mean(hog_matrix) assert_almost_equal(actual, desired, decimal=1) + 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 3ef1f12d..1a3e91f2 100644 --- a/skimage/feature/tests/test_peak.py +++ b/skimage/feature/tests/test_peak.py @@ -117,6 +117,155 @@ def test_indices_with_labels(): assert (result == np.transpose(expected.nonzero())).all() +def test_ndarray_indices_false(): + nd_image = np.zeros((5,5,5)) + nd_image[2,2,2] = 1 + peaks = peak.peak_local_max(nd_image, min_distance=1, indices=False) + assert (peaks == nd_image.astype(np.bool)).all() + + +def test_ndarray_exclude_border(): + nd_image = np.zeros((5,5,5)) + nd_image[[1,0,0],[0,1,0],[0,0,1]] = 1 + nd_image[3,0,0] = 1 + nd_image[2,2,2] = 1 + expected = np.zeros_like(nd_image, dtype=np.bool) + expected[2,2,2] = True + result = peak.peak_local_max(nd_image, min_distance=2, indices=False) + assert (result == expected).all() + + +def test_empty(): + image = np.zeros((10, 20)) + labels = np.zeros((10, 20), int) + result = peak.peak_local_max(image, labels=labels, + footprint=np.ones((3, 3), bool), + min_distance=1, threshold_rel=0, + indices=False, exclude_border=False) + assert np.all(~ result) + + +def test_one_point(): + image = np.zeros((10, 20)) + labels = np.zeros((10, 20), int) + image[5, 5] = 1 + labels[5, 5] = 1 + result = peak.peak_local_max(image, labels=labels, + footprint=np.ones((3, 3), bool), + min_distance=1, threshold_rel=0, + indices=False, exclude_border=False) + assert np.all(result == (labels == 1)) + + +def test_adjacent_and_same(): + image = np.zeros((10, 20)) + labels = np.zeros((10, 20), int) + image[5, 5:6] = 1 + labels[5, 5:6] = 1 + result = peak.peak_local_max(image, labels=labels, + footprint=np.ones((3, 3), bool), + min_distance=1, threshold_rel=0, + indices=False, exclude_border=False) + assert np.all(result == (labels == 1)) + + +def test_adjacent_and_different(): + image = np.zeros((10, 20)) + labels = np.zeros((10, 20), int) + image[5, 5] = 1 + image[5, 6] = .5 + labels[5, 5:6] = 1 + expected = (image == 1) + result = peak.peak_local_max(image, labels=labels, + footprint=np.ones((3, 3), bool), + min_distance=1, threshold_rel=0, + indices=False, exclude_border=False) + assert np.all(result == expected) + result = peak.peak_local_max(image, labels=labels, + min_distance=1, threshold_rel=0, + indices=False, exclude_border=False) + assert np.all(result == expected) + + +def test_not_adjacent_and_different(): + 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 = peak.peak_local_max(image, labels=labels, + footprint=np.ones((3, 3), bool), + min_distance=1, threshold_rel=0, + indices=False, exclude_border=False) + assert np.all(result == expected) + + +def test_two_objects(): + 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 = peak.peak_local_max(image, labels=labels, + footprint=np.ones((3, 3), bool), + min_distance=1, threshold_rel=0, + indices=False, exclude_border=False) + assert np.all(result == expected) + + +def test_adjacent_different_objects(): + 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 = peak.peak_local_max(image, labels=labels, + footprint=np.ones((3, 3), bool), + min_distance=1, threshold_rel=0, + indices=False, exclude_border=False) + assert np.all(result == expected) + + +def test_four_quadrants(): + 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, footprint=footprint, + min_distance=1, threshold_rel=0, + indices=False, exclude_border=False) + assert np.all(result == expected) + + +def test_disk(): + '''regression test of img-1194, footprint = [1] + Test peak.peak_local_max when every point is a local maximum + ''' + np.random.seed(31) + image = np.random.uniform(size=(10, 20)) + footprint = np.array([[1]]) + result = peak.peak_local_max(image, labels=np.ones((10, 20)), + footprint=footprint, + min_distance=1, threshold_rel=0, + indices=False, exclude_border=False) + assert np.all(result) + result = peak.peak_local_max(image, footprint=footprint) + assert np.all(result) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py index d48a14f7..e4fb6acb 100644 --- a/skimage/feature/tests/test_texture.py +++ b/skimage/feature/tests/test_texture.py @@ -199,5 +199,16 @@ class TestLBP(): np.testing.assert_array_almost_equal(lbp, ref) + def test_nri_uniform(self): + lbp = local_binary_pattern(self.image, 8, 1, 'nri_uniform') + ref = np.array([[ 0, 54, 0, 57, 12, 57], + [34, 0, 58, 58, 3, 22], + [58, 57, 15, 50, 0, 47], + [10, 3, 40, 42, 35, 0], + [57, 7, 57, 58, 0, 56], + [ 9, 58, 0, 57, 7, 14]]) + np.testing.assert_array_almost_equal(lbp, ref) + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/feature/tests/test_util.py b/skimage/feature/tests/test_util.py new file mode 100644 index 00000000..6e25f51a --- /dev/null +++ b/skimage/feature/tests/test_util.py @@ -0,0 +1,32 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.feature.util import pairwise_hamming_distance + + +def test_pairwise_hamming_distance_range(): + """Values of all the pairwise hamming distances should be in the range + [0, 1].""" + a = np.random.random_sample((10, 50)) > 0.5 + b = np.random.random_sample((20, 50)) > 0.5 + dist = pairwise_hamming_distance(a, b) + assert np.all((0 <= dist) & (dist <= 1)) + + +def test_pairwise_hamming_distance_value(): + """The result of pairwise_hamming_distance of two fixed sets of boolean + vectors should be same as expected.""" + np.random.seed(10) + a = np.random.random_sample((4, 100)) > 0.5 + np.random.seed(20) + b = np.random.random_sample((3, 100)) > 0.5 + result = pairwise_hamming_distance(a, b) + expected = np.array([[0.5 , 0.49, 0.44], + [0.44, 0.53, 0.52], + [0.4 , 0.55, 0.5 ], + [0.47, 0.48, 0.57]]) + assert_array_equal(result, expected) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 7655b82a..f9bf6c9f 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -248,6 +248,8 @@ def local_binary_pattern(image, P, R, method='default'): * 'uniform': improved rotation invariance with uniform patterns and finer quantization of the angular space which is gray scale and rotation invariant. + * 'nri_uniform': non rotation-invariant uniform patterns variant + which is only gray scale invariant [2]. * 'var': rotation invariant variance measures of the contrast of local image texture which is rotation but not gray scale invariant. @@ -263,14 +265,19 @@ def local_binary_pattern(image, P, R, method='default'): Timo Ojala, Matti Pietikainen, Topi Maenpaa. http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ Articoliriferimento/LBP.pdf, 2002. + .. [2] Face recognition with local binary patterns. + Timo Ahonen, Abdenour Hadid, Matti Pietikainen, + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.214.6851, + 2004. """ methods = { 'default': ord('D'), 'ror': ord('R'), 'uniform': ord('U'), + 'nri_uniform': ord('N'), 'var': ord('V') } - image = np.array(image, dtype='double', copy=True) + image = np.ascontiguousarray(image, dtype=np.double) output = _local_binary_pattern(image, P, R, methods[method.lower()]) return output diff --git a/skimage/feature/util.py b/skimage/feature/util.py new file mode 100644 index 00000000..a5267d44 --- /dev/null +++ b/skimage/feature/util.py @@ -0,0 +1,38 @@ + + +def _mask_border_keypoints(image, keypoints, dist): + """Removes keypoints that are within dist pixels from the image border.""" + width = image.shape[0] + height = image.shape[1] + + keypoints_filtering_mask = ((dist - 1 < keypoints[:, 0]) & + (keypoints[:, 0] < width - dist + 1) & + (dist - 1 < keypoints[:, 1]) & + (keypoints[:, 1] < height - dist + 1)) + + return keypoints_filtering_mask + + +def pairwise_hamming_distance(array1, array2): + """**Experimental function**. + + Calculate hamming dissimilarity measure between two sets of + vectors. + + Parameters + ---------- + array1 : (P1, D) array + P1 vectors of size D. + array2 : (P2, D) array + P2 vectors of size D. + + Returns + ------- + distance : (P1, P2) array of dtype float + 2D ndarray with value at an index (i, j) representing the hamming + distance in the range [0, 1] between ith vector in array1 and jth + vector in array2. + + """ + distance = (array1[:, None] != array2[None]).mean(axis=2) + return distance diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index f32c3b23..cef9b2e2 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,9 +1,42 @@ -from .lpi_filter import * +from .lpi_filter import inverse, wiener, LPIFilter2D from .ctmf import median_filter +from ._gaussian import gaussian_filter from ._canny import canny from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, - hprewitt, vprewitt) -from ._denoise import denoise_tv_chambolle, tv_denoise + hprewitt, vprewitt, roberts, roberts_positive_diagonal, + roberts_negative_diagonal) +from ._denoise import denoise_tv_chambolle from ._denoise_cy import denoise_bilateral, denoise_tv_bregman from ._rank_order import rank_order +from ._gabor import gabor_kernel, gabor_filter from .thresholding import threshold_otsu, threshold_adaptive +from . import rank + + +__all__ = ['inverse', + 'wiener', + 'LPIFilter2D', + 'median_filter', + 'gaussian_filter', + 'canny', + 'sobel', + 'hsobel', + 'vsobel', + 'scharr', + 'hscharr', + 'vscharr', + 'prewitt', + 'hprewitt', + 'vprewitt', + 'roberts', + 'roberts_positive_diagonal', + 'roberts_negative_diagonal', + 'denoise_tv_chambolle', + 'denoise_bilateral', + 'denoise_tv_bregman', + 'rank_order', + 'gabor_kernel', + 'gabor_filter', + 'threshold_otsu', + 'threshold_adaptive', + 'rank'] diff --git a/skimage/filter/_canny.py b/skimage/filter/_canny.py index 8be77894..185bfd40 100644 --- a/skimage/filter/_canny.py +++ b/skimage/filter/_canny.py @@ -1,4 +1,5 @@ -'''canny.py - Canny Edge detector +""" +canny.py - Canny Edge detector Reference: Canny, J., A Computational Approach To Edge Detection, IEEE Trans. Pattern Analysis and Machine Intelligence, 8:679-714, 1986 @@ -9,13 +10,13 @@ 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 import scipy.ndimage as ndi from scipy.ndimage import (gaussian_filter, generate_binary_structure, binary_erosion, label) +from skimage import dtype_limits def smooth_with_function_and_mask(image, function, mask): @@ -24,13 +25,11 @@ def smooth_with_function_and_mask(image, function, mask): Parameters ---------- image : array - The image to smooth - + Image you want to smooth. function : callable - A function that takes an image and returns a smoothed image - + A function that does image smoothing. mask : array - Mask with 1's for significant pixels, 0 for masked pixels + Mask with 1's for significant pixels, 0's for masked pixels. Notes ------ @@ -50,31 +49,28 @@ def smooth_with_function_and_mask(image, function, mask): return output_image -def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): - '''Edge filter an image using the Canny algorithm. +def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): + """Edge filter an image using the Canny algorithm. Parameters ----------- - image : array_like, dtype=float - The greyscale input image to detect edges on; should be normalized to - 0.0 to 1.0. - + image : 2D array + Greyscale input image to detect edges on; can be of any dtype. sigma : float - The standard deviation of the Gaussian filter - + Standard deviation of the Gaussian filter. low_threshold : float - The lower bound for hysterisis thresholding (linking edges) - + Lower bound for hysteresis thresholding (linking edges). + If None, low_threshold is set to 10% of dtype's max. high_threshold : float - The upper bound for hysterisis thresholding (linking edges) - + Upper bound for hysteresis thresholding (linking edges). + If None, high_threshold is set to 20% of dtype's max. mask : array, dtype=bool, optional - An optional mask to limit the application of Canny to a certain area. + Mask to limit the application of Canny to a certain area. Returns ------- - output : array (image) - The binary edge map. + output : 2D array (image) + The binary edge map. See also -------- @@ -107,7 +103,7 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): Canny, J., A Computational Approach To Edge Detection, IEEE Trans. Pattern Analysis and Machine Intelligence, 8:679-714, 1986 - William Green' Canny tutorial + William Green's Canny tutorial http://dasl.mem.drexel.edu/alumni/bGreen/www.pages.drexel.edu/_weg22/can_tut.html Examples @@ -116,12 +112,12 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): >>> # Generate noisy image of a square >>> im = np.zeros((256, 256)) >>> im[64:-64, 64:-64] = 1 - >>> im += 0.2*np.random.random(im.shape) + >>> im += 0.2 * np.random.random(im.shape) >>> # 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) - ''' + """ # # The steps involved: @@ -154,7 +150,13 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): # if image.ndim != 2: - raise TypeError("The input 'image' must be a two dimensional array.") + raise TypeError("The input 'image' must be a two-dimensional array.") + + if low_threshold is None: + low_threshold = 0.1 * dtype_limits(image)[1] + + if high_threshold is None: + high_threshold = 0.2 * dtype_limits(image)[1] if mask is None: mask = np.ones(image.shape, dtype=bool) @@ -254,7 +256,8 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): # Segment the low-mask, then only keep low-segments that have # some high_mask component in them # - labels, count = label(low_mask, np.ndarray((3, 3), bool)) + strel = np.ones((3, 3), bool) + labels, count = label(low_mask, strel) if count == 0: return low_mask diff --git a/skimage/filter/_ctmf.pyx b/skimage/filter/_ctmf.pyx index e3e845e4..70a59aa3 100644 --- a/skimage/filter/_ctmf.pyx +++ b/skimage/filter/_ctmf.pyx @@ -731,13 +731,8 @@ cdef int c_median_filter(Py_ssize_t rows, return 0 -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, +def median_filter(cnp.uint8_t[:, ::1] data, cnp.uint8_t[:, ::1] mask, + cnp.uint8_t[:, ::1] output, int radius, cnp.int32_t percent): """Median filter with octagon shape and masking. @@ -773,12 +768,10 @@ def median_filter(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, 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[0, 0], + &mask[0, 0], + &output[0, 0]): raise MemoryError('Failed to allocate scratchpad memory') diff --git a/skimage/filter/_denoise.py b/skimage/filter/_denoise.py index 87850759..043399e0 100644 --- a/skimage/filter/_denoise.py +++ b/skimage/filter/_denoise.py @@ -1,6 +1,5 @@ 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): @@ -37,7 +36,7 @@ def _denoise_tv_chambolle_3d(im, weight=100, eps=2.e-4, n_iter_max=200): >>> 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) + >>> res = denoise_tv_chambolle(mask, weight=100) """ @@ -127,7 +126,7 @@ def _denoise_tv_chambolle_2d(im, weight=50, eps=2.e-4, n_iter_max=200): >>> 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) + >>> denoised_lena = denoise_tv_chambolle(lena, weight=60) """ @@ -227,7 +226,7 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, >>> 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) + >>> denoised_lena = denoise_tv_chambolle(lena, weight=60) 3D example on synthetic data: @@ -235,7 +234,7 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, >>> 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) + >>> res = denoise_tv_chambolle(mask, weight=100) """ @@ -250,14 +249,10 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, 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) + 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 index 5a85e84a..0c4f2539 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/filter/_denoise_cy.pyx @@ -99,38 +99,54 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, image = np.atleast_3d(img_as_float(image)) + # if image.max() is 0, then dist_scale can have an unverified value + # and color_lut[(dist * dist_scale)] may cause a segmentation fault + # so we verify we have a positive image and that the max is not 0.0. + if image.min() < 0.0: + raise ValueError("Image must contain only positive values") + 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() + double max_value - 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[:, :, ::1] cimage + double[:, :, ::1] out - 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) + double* color_lut + double* range_lut 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)) + double dist_scale + double* values + double* centres + double* total_values if sigma_range is None: csigma_range = image.std() else: csigma_range = sigma_range + max_value = image.max() + + if max_value == 0.0: + raise ValueError("The maximum value found in the image was 0.") + + cimage = np.ascontiguousarray(image) + + out = np.zeros((rows, cols, dims), dtype=np.double) + color_lut = _compute_color_lut(bins, csigma_range, max_value) + range_lut = _compute_range_lut(win_size, sigma_spatial) + dist_scale = bins / dims / max_value + values = malloc(dims * sizeof(double)) + centres = malloc(dims * sizeof(double)) + total_values = malloc(dims * sizeof(double)) + if mode not in ('constant', 'wrap', 'reflect', 'nearest'): raise ValueError("Invalid mode specified. Please use " "`constant`, `nearest`, `wrap` or `reflect`.") @@ -138,11 +154,10 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, 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] + centres[d] = cimage[r, c, d] for wr in range(-window_ext, window_ext + 1): rr = wr + r kr = wr + window_ext @@ -154,7 +169,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, # distance between centre stack and current position dist = 0 for d in range(dims): - value = get_pixel3d(image_data, rows, cols, dims, + value = get_pixel3d(&cimage[0, 0, 0], rows, cols, dims, rr, cc, d, cmode, cval) values[d] = value dist += (centres[d] - value)**2 @@ -168,7 +183,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, total_values[d] += values[d] * weight total_weight += weight for d in range(dims): - out_data[pixel_addr + d] = total_values[d] / total_weight + out[r, c, d] = total_values[d] / total_weight free(color_lut) free(range_lut) @@ -176,10 +191,11 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, free(centres) free(total_values) - return np.squeeze(out) + return np.squeeze(np.asarray(out)) -def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): +def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, + char isotropic=True): """Perform total-variation denoising using split-Bregman optimization. Total-variation denoising (also know as total-variation regularization) @@ -201,8 +217,10 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): SUM((u(n) - u(n-1))**2) < eps - max_iter: int, optional + max_iter : int, optional Maximal number of iterations used for the optimization. + isotropic : boolean, optional + Switch between isotropic and anisotropic TV denoising. Returns ------- @@ -218,6 +236,7 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): .. [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 + .. [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf """ @@ -233,21 +252,17 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): Py_ssize_t total = rows * cols * dims - shape_ext = (rows2, cols2, dims) + shape_ext = (rows2, cols2, dims) + u = np.zeros(shape_ext, dtype=np.double) - 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) + cdef: + double[:, :, ::1] cimage = np.ascontiguousarray(image) + double[:, :, ::1] cu = u - 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[:, :, ::1] dx = np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] dy = np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] bx = np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] by = np.zeros(shape_ext, dtype=np.double) double ux, uy, uprev, unew, bxx, byy, dxx, dyy, s int i = 0 @@ -271,19 +286,19 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): for r in range(1, rows + 1): for c in range(1, cols + 1): - uprev = u[r, c, k] + uprev = cu[r, c, k] # forward derivatives - ux = u[r, c + 1, k] - uprev - uy = u[r + 1, c, k] - uprev + ux = cu[r, c + 1, k] - uprev + uy = cu[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] + + cu[r + 1, c, k] + + cu[r - 1, c, k] + + cu[r, c + 1, k] + + cu[r, c - 1, k] + dx[r, c - 1, k] - dx[r, c, k] @@ -296,7 +311,7 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): + by[r, c, k] ) + weight * cimage[r - 1, c - 1, k] ) / norm - u[r, c, k] = unew + cu[r, c, k] = unew # update root mean square error rmse += (unew - uprev)**2 @@ -304,9 +319,27 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): 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) + # d_subproblem after reference [4] + if isotropic: + s = sqrt((ux + bxx)**2 + (uy + byy)**2) + dxx = s * lam * (ux + bxx) / (s * lam + 1) + dyy = s * lam * (uy + byy) / (s * lam + 1) + + else: + s = ux + bxx + if s > 1 / lam: + dxx = s - 1/lam + elif s < -1 / lam: + dxx = s + 1 / lam + else: + dxx = 0 + s = uy + byy + if s > 1 / lam: + dyy = s - 1 / lam + elif s < -1 / lam: + dyy = s + 1 / lam + else: + dyy = 0 dx[r, c, k] = dxx dy[r, c, k] = dyy @@ -317,4 +350,4 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): rmse = sqrt(rmse / total) i += 1 - return np.squeeze(u[1:-1, 1:-1]) + return np.squeeze(np.asarray(u[1:-1, 1:-1])) diff --git a/skimage/filter/_gabor.py b/skimage/filter/_gabor.py new file mode 100644 index 00000000..f766ac74 --- /dev/null +++ b/skimage/filter/_gabor.py @@ -0,0 +1,121 @@ +import numpy as np +from scipy import ndimage + + +__all__ = ['gabor_kernel', 'gabor_filter'] + + +def _sigma_prefactor(bandwidth): + b = bandwidth + # See http://www.cs.rug.nl/~imaging/simplecell.html + return 1.0 / np.pi * np.sqrt(np.log(2)/2.0) * (2.0**b + 1) / (2.0**b - 1) + + +def gabor_kernel(frequency, theta=0, bandwidth=1, sigma_x=None, sigma_y=None, + offset=0): + """Return complex 2D Gabor filter kernel. + + Frequency and orientation representations of the Gabor filter are similar + to those of the human visual system. It is especially suitable for texture + classification using Gabor filter banks. + + Parameters + ---------- + frequency : float + Frequency of the harmonic function. + theta : float + Orientation in radians. If 0, the harmonic is in the x-direction. + bandwidth : float + The bandwidth captured by the filter. For fixed bandwidth, `sigma_x` + and `sigma_y` will decrease with increasing frequency. This value is + ignored if `sigma_x` and `sigma_y` are set by the user. + sigma_x, sigma_y : float + Standard deviation in x- and y-directions. These directions apply to + the kernel *before* rotation. If `theta = pi/2`, then the kernel is + rotated 90 degrees so that `sigma_x` controls the *vertical* direction. + offset : float, optional + Phase offset of harmonic function in radians. + + Returns + ------- + g : complex array + Complex filter kernel. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Gabor_filter + .. [2] http://mplab.ucsd.edu/tutorials/gabor.pdf + + """ + if sigma_x is None: + sigma_x = _sigma_prefactor(bandwidth) / frequency + if sigma_y is None: + sigma_y = _sigma_prefactor(bandwidth) / frequency + + n_stds = 3 + x0 = np.ceil(max(np.abs(n_stds * sigma_x * np.cos(theta)), + np.abs(n_stds * sigma_y * np.sin(theta)), 1)) + y0 = np.ceil(max(np.abs(n_stds * sigma_y * np.cos(theta)), + np.abs(n_stds * sigma_x * np.sin(theta)), 1)) + y, x = np.mgrid[-y0:y0+1, -x0:x0+1] + + rotx = x * np.cos(theta) + y * np.sin(theta) + roty = -x * np.sin(theta) + y * np.cos(theta) + + g = np.zeros(y.shape, dtype=np.complex) + g[:] = np.exp(-0.5 * (rotx**2 / sigma_x**2 + roty**2 / sigma_y**2)) + g /= 2 * np.pi * sigma_x * sigma_y + g *= np.exp(1j * (2 * np.pi * frequency * rotx + offset)) + + return g + + +def gabor_filter(image, frequency, theta=0, bandwidth=1, sigma_x=None, + sigma_y=None, offset=0, mode='reflect', cval=0): + """Return real and imaginary responses to Gabor filter. + + The real and imaginary parts of the Gabor filter kernel are applied to the + image and the response is returned as a pair of arrays. + + Frequency and orientation representations of the Gabor filter are similar + to those of the human visual system. It is especially suitable for texture + classification using Gabor filter banks. + + Parameters + ---------- + image : array + Input image. + frequency : float + Frequency of the harmonic function. + theta : float + Orientation in radians. If 0, the harmonic is in the x-direction. + bandwidth : float + The bandwidth captured by the filter. For fixed bandwidth, `sigma_x` + and `sigma_y` will decrease with increasing frequency. This value is + ignored if `sigma_x` and `sigma_y` are set by the user. + sigma_x, sigma_y : float + Standard deviation in x- and y-directions. These directions apply to + the kernel *before* rotation. If `theta = pi/2`, then the kernel is + rotated 90 degrees so that `sigma_x` controls the *vertical* direction. + offset : float, optional + Phase offset of harmonic function in radians. + + Returns + ------- + real, imag : arrays + Filtered images using the real and imaginary parts of the Gabor filter + kernel. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Gabor_filter + .. [2] http://mplab.ucsd.edu/tutorials/gabor.pdf + + """ + + g = gabor_kernel(frequency, theta, bandwidth, sigma_x, sigma_y, offset) + + filtered_real = ndimage.convolve(image, np.real(g), mode=mode, cval=cval) + filtered_imag = ndimage.convolve(image, np.imag(g), mode=mode, cval=cval) + + return filtered_real, filtered_imag diff --git a/skimage/filter/_gaussian.py b/skimage/filter/_gaussian.py new file mode 100644 index 00000000..af63ba10 --- /dev/null +++ b/skimage/filter/_gaussian.py @@ -0,0 +1,105 @@ +import collections as coll +import numpy as np +from scipy import ndimage +import warnings + +from ..util import img_as_float +from ..color import guess_spatial_dimensions + +__all__ = ['gaussian_filter'] + + +def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, + multichannel=None): + """ + Multi-dimensional Gaussian filter + + Parameters + ---------- + + image : array-like + input image (grayscale or color) to filter. + sigma : scalar or sequence of scalars + standard deviation for Gaussian kernel. The standard + deviations of the Gaussian filter are given for each axis as a + sequence, or as a single number, in which case it is equal for + all axes. + output : array, optional + The ``output`` parameter passes an array in which to store the + filter output. + 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'. Default is 'nearest'. + cval : scalar, optional + Value to fill past edges of input if `mode` is 'constant'. Default + is 0.0 + multichannel : bool, optional (default: None) + Whether the last axis of the image is to be interpreted as multiple + channels. If True, each channel is filtered separately (channels are + not mixed together). Only 3 channels are supported. If `None`, + the function will attempt to guess this, and raise a warning if + ambiguous, when the array has shape (M, N, 3). + + Returns + ------- + + filtered_image : ndarray + the filtered array + + Notes + ----- + + This function is a wrapper around :func:`scipy.ndimage.gaussian_filter`. + + Integer arrays are converted to float. + + The multi-dimensional filter is implemented as a sequence of + one-dimensional convolution filters. The intermediate arrays are + stored in the same data type as the output. Therefore, for output + types with a limited precision, the results may be imprecise + because intermediate results may be stored with insufficient + precision. + + Examples + -------- + + >>> a = np.zeros((3, 3)) + >>> a[1, 1] = 1 + >>> a + array([[ 0., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 0.]]) + >>> gaussian_filter(a, sigma=0.4) # mild smoothing + array([[ 0.00163116, 0.03712502, 0.00163116], + [ 0.03712502, 0.84496158, 0.03712502], + [ 0.00163116, 0.03712502, 0.00163116]]) + >>> gaussian_filter(a, sigma=1) # more smooting + array([[ 0.05855018, 0.09653293, 0.05855018], + [ 0.09653293, 0.15915589, 0.09653293], + [ 0.05855018, 0.09653293, 0.05855018]]) + >>> # Several modes are possible for handling boundaries + >>> gaussian_filter(a, sigma=1, mode='reflect') + array([[ 0.08767308, 0.12075024, 0.08767308], + [ 0.12075024, 0.16630671, 0.12075024], + [ 0.08767308, 0.12075024, 0.08767308]]) + >>> # For RGB images, each is filtered separately + >>> from skimage.data import lena + >>> image = lena() + >>> filtered_lena = gaussian_filter(image, sigma=1, multichannel=True) + """ + spatial_dims = guess_spatial_dimensions(image) + if spatial_dims is None and multichannel is None: + msg = ("Images with dimensions (M, N, 3) are interpreted as 2D+RGB" + + " by default. Use `multichannel=False` to interpret as " + + " 3D image with last dimension of length 3.") + warnings.warn(RuntimeWarning(msg)) + multichannel = True + if multichannel: + # do not filter across channels + if not isinstance(sigma, coll.Iterable): + sigma = [sigma] * (image.ndim - 1) + if len(sigma) != image.ndim: + sigma = np.concatenate((np.asarray(sigma), [0])) + image = img_as_float(image) + return ndimage.gaussian_filter(image, sigma, mode=mode, cval=cval) diff --git a/skimage/filter/_rank_order.py b/skimage/filter/_rank_order.py index f878702f..cdd992ff 100644 --- a/skimage/filter/_rank_order.py +++ b/skimage/filter/_rank_order.py @@ -8,7 +8,7 @@ Copyright (c) 2009-2011 Broad Institute All rights reserved. Original author: Lee Kamentstky """ -import numpy +import numpy as np def rank_order(image): @@ -47,14 +47,14 @@ def rank_order(image): (array([0, 1, 2, 1], dtype=uint32), array([-1. , 2.5, 3.1])) """ flat_image = image.ravel() - sort_order = flat_image.argsort().astype(numpy.uint32) + sort_order = flat_image.argsort().astype(np.uint32) flat_image = flat_image[sort_order] - sort_rank = numpy.zeros_like(sort_order) + sort_rank = np.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) + np.cumsum(is_different, out=sort_rank[1:]) + original_values = np.zeros((sort_rank[-1] + 1,), image.dtype) original_values[0] = flat_image[0] original_values[1:] = flat_image[1:][is_different] - int_image = numpy.zeros_like(sort_order) + int_image = np.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 94f273e1..e39aa8bc 100644 --- a/skimage/filter/ctmf.py +++ b/skimage/filter/ctmf.py @@ -1,4 +1,4 @@ -'''ctmf.py - constant time per pixel median filtering with an octagonal shape +"""ctmf.py - constant time per pixel median filtering with an octagonal shape Reference: S. Perreault and P. Hebert, "Median Filtering in Constant Time", IEEE Transactions on Image Processing, September 2007. @@ -9,86 +9,101 @@ Copyright (c) 2003-2009 Massachusetts Institute of Technology Copyright (c) 2009-2011 Broad Institute All rights reserved. Original author: Lee Kamentsky -''' +""" +import warnings import numpy as np from . import _ctmf from ._rank_order import rank_order +from .._shared.utils import deprecated +@deprecated('filter.rank.median') def median_filter(image, radius=2, mask=None, percent=50): - '''Masked median filter with octagon shape. + """Masked median filter with octagon shape. Parameters ---------- - image : (M,N) ndarray, dtype uint8 + image : (M, N) ndarray Input image. - radius : {int, 2}, optional - The radius of a circle inscribed into the filtering - octagon. Must be at least 2. Default radius is 2. - mask : (M,N) ndarray, dtype uint8, optional - A value of 1 indicates a significant pixel, 0 - that a pixel is masked. By default, all pixels - are considered. - percent : {int, 50}, optional + radius : int + Radius (in pixels) of a circle inscribed into the filtering + octagon. Must be at least 2. Default radius is 2. + mask : (M, N) ndarray + Mask with 1's for significant pixels, 0's for masked pixels. + By default, all pixels are considered significant. + percent : int The unmasked pixels within the octagon are sorted, and the - value at the `percent`-th index chosen. For example, the - default value of 50 chooses the median pixel. + value at `percent` percent of the index range is chosen. + Default value of 50 gives the median pixel. Returns ------- - out : (M,N) ndarray, dtype uint8 - Filtered array. In areas where the median filter does - not overlap the mask, the filtered result is underfined, but + out : (M, N) ndarray + Filtered array. In areas where the median filter does + not overlap the mask, the filtered result is undefined, but in practice, it will be the lowest value in the valid area. - ''' + Notes + ----- + Because of the histogram implementation, the number of unique values + for the output is limited to 256. + + Examples + -------- + >>> a = np.ones((5, 5)) + >>> a[2, 2] = 10 # introduce outlier + >>> b = median_filter(a) + >>> b[2, 2] # the median filter is good at removing outliers + 1.0 + """ if image.ndim != 2: - raise TypeError("The input 'image' must be a two dimensional array.") + raise TypeError("Input 'image' must be a two-dimensional array.") if radius < 2: - raise ValueError("The input 'radius' must be >= 2.") + raise ValueError("Input 'radius' must be >= 2.") if mask is None: mask = np.ones(image.shape, dtype=np.bool) mask = np.ascontiguousarray(mask, dtype=np.bool) if np.all(~ mask): + warnings.warn('Mask is all over image! Returning copy of input image.') return image.copy() - # - # Normalize the ranked image to 0-255 - # + if (not np.issubdtype(image.dtype, np.int) or np.min(image) < 0 or np.max(image) > 255): - ranked_image, translation = rank_order(image[mask]) - max_ranked_image = np.max(ranked_image) - if max_ranked_image == 0: - return image - if max_ranked_image > 255: - ranked_image = ranked_image * 255 // max_ranked_image + ranked_values, translation = rank_order(image[mask]) + max_ranked_values = np.max(ranked_values) + if max_ranked_values == 0: + warnings.warn('Particular case? Returning copy of input image.') + return image.copy() + if max_ranked_values > 255: + ranked_values = ranked_values * 255 // max_ranked_values was_ranked = True else: - ranked_image = image[mask] + ranked_values = image[mask] was_ranked = False - input = np.zeros(image.shape, np.uint8) - input[mask] = ranked_image + ranked_image = np.zeros(image.shape, np.uint8) + ranked_image[mask] = ranked_values mask.dtype = np.uint8 output = np.zeros(image.shape, np.uint8) - _ctmf.median_filter(input, mask, output, radius, percent) + _ctmf.median_filter(ranked_image, mask, output, radius, percent) if was_ranked: # # The translation gives the original value at each ranking. # We rescale the output to the original ranking and then # use the translation to look up the original value in the image. # - if max_ranked_image > 255: + if max_ranked_values > 255: result = translation[output.astype(np.uint32) * - max_ranked_image // 255] + max_ranked_values // 255] else: result = translation[output] else: result = output return result + diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index fcd9f548..764c7d34 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -1,4 +1,4 @@ -"""edges.py - Edge filters +""" Sobel and Prewitt filters originally part of CellProfiler, code licensed under both GPL and BSD licenses. @@ -16,6 +16,26 @@ from scipy.ndimage import convolve, binary_erosion, generate_binary_structure EROSION_SELEM = generate_binary_structure(2, 2) +HSOBEL_WEIGHTS = np.array([[ 1, 2, 1], + [ 0, 0, 0], + [-1,-2,-1]]) / 4.0 +VSOBEL_WEIGHTS = HSOBEL_WEIGHTS.T + +HSCHARR_WEIGHTS = np.array([[ 3, 10, 3], + [ 0, 0, 0], + [-3, -10, -3]]) / 16.0 +VSCHARR_WEIGHTS = HSCHARR_WEIGHTS.T + +HPREWITT_WEIGHTS = np.array([[ 1, 1, 1], + [ 0, 0, 0], + [-1,-1,-1]]) / 3.0 +VPREWITT_WEIGHTS = HPREWITT_WEIGHTS.T + +ROBERTS_PD_WEIGHTS = np.array([[1, 0], + [0, -1]], dtype=np.double) +ROBERTS_ND_WEIGHTS = np.array([[0, 1], + [-1, 0]], dtype=np.double) + def _mask_filter_result(result, mask): """Return result after masking. @@ -48,7 +68,7 @@ def sobel(image, mask=None): Returns ------- - output : ndarray + output : 2-D array The Sobel edge map. Notes @@ -77,7 +97,7 @@ def hsobel(image, mask=None): Returns ------- - output : ndarray + output : 2-D array The Sobel edge map. Notes @@ -91,10 +111,7 @@ def hsobel(image, mask=None): """ image = img_as_float(image) - result = np.abs(convolve(image, - np.array([[ 1, 2, 1], - [ 0, 0, 0], - [-1,-2,-1]]).astype(float) / 4.0)) + result = np.abs(convolve(image, HSOBEL_WEIGHTS)) return _mask_filter_result(result, mask) @@ -112,7 +129,7 @@ def vsobel(image, mask=None): Returns ------- - output : ndarray + output : 2-D array The Sobel edge map. Notes @@ -126,10 +143,7 @@ def vsobel(image, mask=None): """ image = img_as_float(image) - result = np.abs(convolve(image, - np.array([[1, 0, -1], - [2, 0, -2], - [1, 0, -1]]).astype(float) / 4.0)) + result = np.abs(convolve(image, VSOBEL_WEIGHTS)) return _mask_filter_result(result, mask) @@ -147,7 +161,7 @@ def scharr(image, mask=None): Returns ------- - output : ndarray + output : 2-D array The Scharr edge map. Notes @@ -179,7 +193,7 @@ def hscharr(image, mask=None): Returns ------- - output : ndarray + output : 2-D array The Scharr edge map. Notes @@ -198,10 +212,7 @@ def hscharr(image, mask=None): """ 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)) + result = np.abs(convolve(image, HSCHARR_WEIGHTS)) return _mask_filter_result(result, mask) @@ -219,7 +230,7 @@ def vscharr(image, mask=None): Returns ------- - output : ndarray + output : 2-D array The Scharr edge map. Notes @@ -238,10 +249,7 @@ def vscharr(image, mask=None): """ 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)) + result = np.abs(convolve(image, VSCHARR_WEIGHTS)) return _mask_filter_result(result, mask) @@ -259,7 +267,7 @@ def prewitt(image, mask=None): Returns ------- - output : ndarray + output : 2-D array The Prewitt edge map. Notes @@ -284,7 +292,7 @@ def hprewitt(image, mask=None): Returns ------- - output : ndarray + output : 2-D array The Prewitt edge map. Notes @@ -298,10 +306,7 @@ def hprewitt(image, mask=None): """ image = img_as_float(image) - result = np.abs(convolve(image, - np.array([[ 1, 1, 1], - [ 0, 0, 0], - [-1,-1,-1]]).astype(float) / 3.0)) + result = np.abs(convolve(image, HPREWITT_WEIGHTS)) return _mask_filter_result(result, mask) @@ -319,7 +324,7 @@ def vprewitt(image, mask=None): Returns ------- - output : ndarray + output : 2-D array The Prewitt edge map. Notes @@ -333,8 +338,94 @@ def vprewitt(image, mask=None): """ image = img_as_float(image) - result = np.abs(convolve(image, - np.array([[1, 0, -1], - [1, 0, -1], - [1, 0, -1]]).astype(float) / 3.0)) + result = np.abs(convolve(image, VPREWITT_WEIGHTS)) + return _mask_filter_result(result, mask) + + +def roberts(image, mask=None): + """Find the edge magnitude using Roberts' cross operator. + + 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 : 2-D array + The Roberts' Cross edge map. + """ + return np.sqrt(roberts_positive_diagonal(image, mask)**2 + + roberts_negative_diagonal(image, mask)**2) + + +def roberts_positive_diagonal(image, mask=None): + """Find the cross edges of an image using Roberts' cross operator. + + The kernel is applied to the input image to produce separate measurements + of the gradient component one orientation. + + 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 : 2-D array + The Robert's edge map. + + Notes + ----- + We use the following kernel and return the absolute value of the + result at each point:: + + 1 0 + 0 -1 + + """ + image = img_as_float(image) + result = np.abs(convolve(image, ROBERTS_PD_WEIGHTS)) + return _mask_filter_result(result, mask) + + +def roberts_negative_diagonal(image, mask=None): + """Find the cross edges of an image using the Roberts' Cross operator. + + The kernel is applied to the input image to produce separate measurements + of the gradient component one orientation. + + 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 : 2-D array + The Robert's edge map. + + Notes + ----- + We use the following kernel and return the absolute value of the + result at each point:: + + 0 1 + -1 0 + + """ + image = img_as_float(image) + result = np.abs(convolve(image, ROBERTS_ND_WEIGHTS)) return _mask_filter_result(result, mask) diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index 2af74d7a..5b5705f2 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -3,9 +3,6 @@ :license: modified BSD """ -__all__ = ['inverse', 'wiener', 'LPIFilter2D'] -__docformat__ = 'restructuredtext en' - import numpy as np from scipy.fftpack import ifftshift @@ -69,9 +66,10 @@ class LPIFilter2D(object): -------- Gaussian filter: - - >>> def filt_func(r, c): - ... return np.exp(-np.hypot(r, c)/1) + Use a 1-D gaussian in each direction without normalization + coefficients. + >>> def filt_func(r, c, sigma = 1): + ... return np.exp(-np.hypot(r, c)/sigma) >>> filter = LPIFilter2D(filt_func) """ diff --git a/skimage/filter/rank/README.rst b/skimage/filter/rank/README.rst index cdf8205c..e5c5a9ad 100644 --- a/skimage/filter/rank/README.rst +++ b/skimage/filter/rank/README.rst @@ -23,10 +23,10 @@ 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. +images and 2 to 16-bit for 16-bit images depending on the maximum value of the +image. -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. +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 index 30d936db..cfd034f1 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -1,3 +1,70 @@ -from .rank import * -from .percentile_rank import * -from .bilateral_rank import * +from .generic import (autolevel, bottomhat, equalize, gradient, maximum, mean, + subtract_mean, median, minimum, modal, enhance_contrast, + pop, threshold, tophat, noise_filter, entropy, otsu) +from .percentile import (autolevel_percentile, gradient_percentile, + mean_percentile, subtract_mean_percentile, + enhance_contrast_percentile, percentile, + pop_percentile, threshold_percentile) +from .bilateral import mean_bilateral, pop_bilateral + +from skimage._shared.utils import deprecated + + +percentile_autolevel = deprecated('autolevel_percentile')(autolevel_percentile) + +percentile_gradient = deprecated('gradient_percentile')(gradient_percentile) + +percentile_mean = deprecated('mean_percentile')(mean_percentile) +bilateral_mean = deprecated('mean_bilateral')(mean_bilateral) + +meansubtraction = deprecated('subtract_mean')(subtract_mean) +percentile_mean_subtraction = deprecated('subtract_mean_percentile')\ + (subtract_mean_percentile) + +morph_contr_enh = deprecated('enhance_contrast')(enhance_contrast) +percentile_morph_contr_enh = deprecated('enhance_contrast_percentile')\ + (enhance_contrast_percentile) + +percentile_pop = deprecated('pop_percentile')(pop_percentile) +bilateral_pop = deprecated('pop_bilateral')(pop_bilateral) + +percentile_threshold = deprecated('threshold_percentile')(threshold_percentile) + + +__all__ = ['autolevel', + 'autolevel_percentile', + 'bottomhat', + 'equalize', + 'gradient', + 'gradient_percentile', + 'maximum', + 'mean', + 'mean_percentile', + 'mean_bilateral', + 'subtract_mean', + 'subtract_mean_percentile', + 'median', + 'minimum', + 'modal', + 'enhance_contrast', + 'enhance_contrast_percentile', + 'pop', + 'pop_percentile', + 'pop_bilateral', + 'threshold', + 'threshold_percentile', + 'tophat', + 'noise_filter', + 'entropy', + 'otsu' + 'percentile', + # Deprecated + 'percentile_autolevel', + 'percentile_gradient', + 'percentile_mean', + 'percentile_mean_subtraction', + 'percentile_morph_contr_enh', + 'percentile_pop', + 'percentile_threshold', + 'bilateral_mean', + 'bilateral_pop'] diff --git a/skimage/filter/rank/_core16.pxd b/skimage/filter/rank/_core16.pxd deleted file mode 100644 index 5586aea1..00000000 --- a/skimage/filter/rank/_core16.pxd +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 36bc500d..00000000 --- a/skimage/filter/rank/_core16.pyx +++ /dev/null @@ -1,255 +0,0 @@ -#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 deleted file mode 100644 index d3b6d8c2..00000000 --- a/skimage/filter/rank/_core8.pxd +++ /dev/null @@ -1,25 +0,0 @@ -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/_crank16.pyx b/skimage/filter/rank/_crank16.pyx deleted file mode 100644 index 232d6812..00000000 --- a/skimage/filter/rank/_crank16.pyx +++ /dev/null @@ -1,422 +0,0 @@ -#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 deleted file mode 100644 index e431e42b..00000000 --- a/skimage/filter/rank/_crank16_bilateral.pyx +++ /dev/null @@ -1,82 +0,0 @@ -#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 deleted file mode 100644 index 0ab71353..00000000 --- a/skimage/filter/rank/_crank16_percentiles.pyx +++ /dev/null @@ -1,330 +0,0 @@ -#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 deleted file mode 100644 index cb7febac..00000000 --- a/skimage/filter/rank/_crank8.pyx +++ /dev/null @@ -1,483 +0,0 @@ -#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 deleted file mode 100644 index d085957e..00000000 --- a/skimage/filter/rank/_crank8_percentiles.pyx +++ /dev/null @@ -1,294 +0,0 @@ -#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.py similarity index 53% rename from skimage/filter/rank/bilateral_rank.pyx rename to skimage/filter/rank/bilateral.py index 04f6cb35..f1b10fec 100644 --- a/skimage/filter/rank/bilateral_rank.pyx +++ b/skimage/filter/rank/bilateral.py @@ -3,19 +3,16 @@ 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 +* 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. +Result image is 8-/16-bit or double with respect to the input image and the +rank filter operation. References ---------- @@ -28,50 +25,27 @@ References 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 + +from . import bilateral_cy +from .generic import _handle_input -__all__ = ['bilateral_mean', 'bilateral_pop'] +__all__ = ['mean_bilateral', 'pop_bilateral'] -def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, s0, s1): - selem = img_as_ubyte(selem) - image = np.ascontiguousarray(image) +def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1, + out_dtype=None): - if mask is None: - mask = np.ones(image.shape, dtype=np.uint8) - else: - mask = np.ascontiguousarray(mask) - mask = img_as_ubyte(mask) + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype) - 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.") + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin, s0=s0, s1=s1) return out -def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, +def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, shift_y=False, s0=10, s1=10): """Apply a flat kernel bilateral filter. @@ -81,43 +55,38 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, 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] + 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 + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray 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) + 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. + Define the [s0, s1] interval around the greyvalue of the center pixel + to be considered for computing the value. Returns ------- - out : uint16 array - The result of the local bilateral mean. + out : ndarray (same dtype as input image) + Output image. See also -------- - skimage.filter.denoise_bilateral() for a gaussian bilateral filter. - - Notes - ----- - - * input image are 16-bit only + skimage.filter.denoise_bilateral for a gaussian bilateral filter. Examples -------- @@ -128,13 +97,14 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, >>> 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, + return _apply(bilateral_cy._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, +def pop_bilateral(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 @@ -142,32 +112,27 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint16). As the algorithm uses max. 12bit histogram, - an exception will be raised if image has a value > 4095 + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray 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) + 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. + Define the [s0, s1] interval around the greyvalue of the center pixel + 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 + out : ndarray (same dtype as input image) + Output image. Examples -------- @@ -175,10 +140,10 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, >>> 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) + ... [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], @@ -188,5 +153,5 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(None, _crank16_bilateral.pop, image, selem, out=out, + return _apply(bilateral_cy._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/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx new file mode 100644 index 00000000..de2b3e53 --- /dev/null +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -0,0 +1,70 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log + +from .core_cy cimport dtype_t, dtype_t_out, _core + + +cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t bilat_pop = 0 + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + 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 double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t bilat_pop = 0 + + if pop: + for i in range(max_bin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + return bilat_pop + else: + return 0 + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): + + _core(_kernel_mean[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): + + _core(_kernel_pop[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) diff --git a/skimage/filter/rank/core_cy.pxd b/skimage/filter/rank/core_cy.pxd new file mode 100644 index 00000000..2e97e50a --- /dev/null +++ b/skimage/filter/rank/core_cy.pxd @@ -0,0 +1,28 @@ +from numpy cimport uint8_t, uint16_t, double_t + + +ctypedef fused dtype_t: + uint8_t + uint16_t + +ctypedef fused dtype_t_out: + uint8_t + uint16_t + double_t + + +cdef dtype_t _max(dtype_t a, dtype_t b) +cdef dtype_t _min(dtype_t a, dtype_t b) + + +cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, + Py_ssize_t, Py_ssize_t, double, + double, Py_ssize_t, Py_ssize_t), + dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin) except * diff --git a/skimage/filter/rank/_core8.pyx b/skimage/filter/rank/core_cy.pyx similarity index 51% rename from skimage/filter/rank/_core8.pyx rename to skimage/filter/rank/core_cy.pyx index eca47891..02c2c8d0 100644 --- a/skimage/filter/rank/_core8.pyx +++ b/skimage/filter/rank/core_cy.pyx @@ -9,29 +9,29 @@ cimport numpy as cnp from libc.stdlib cimport malloc, free -cdef inline dtype_t uint8_max(dtype_t a, dtype_t b): +cdef inline dtype_t _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): +cdef inline dtype_t _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, +cdef inline void histogram_increment(Py_ssize_t* histo, double* pop, dtype_t value): histo[value] += 1 pop[0] += 1 -cdef inline void histogram_decrement(Py_ssize_t * histo, float * pop, +cdef inline void histogram_decrement(Py_ssize_t* histo, double* 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): +cdef inline char is_in_mask(Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t c, + char* 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 @@ -42,14 +42,17 @@ cdef inline dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, 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 *: +cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, + Py_ssize_t, Py_ssize_t, double, + double, Py_ssize_t, Py_ssize_t), + dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin) except *: """Compute histogram for each pixel neighborhood, apply kernel function and use kernel function return value for output image. """ @@ -59,8 +62,8 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, 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 + cdef Py_ssize_t centre_r = (selem.shape[0] / 2) + shift_y + cdef Py_ssize_t centre_c = (selem.shape[1] / 2) + shift_x # check that structuring element center is inside the element bounding box assert centre_r >= 0 @@ -68,54 +71,56 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, assert centre_r < srows assert centre_c < scols - # define pointers to the data + # add 1 to ensure maximum value is included in histogram -> range(max_bin) + max_bin += 1 - cdef dtype_t * out_data = out.data - cdef dtype_t * image_data = image.data - cdef dtype_t * mask_data = mask.data + cdef Py_ssize_t mid_bin = max_bin / 2 + + # define pointers to the data + cdef char* mask_data = &mask[0, 0] # 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 + # number of pixels actually inside the neighborhood (double) + cdef double pop = 0 # the current local histogram distribution - cdef Py_ssize_t * histo = malloc(256 * sizeof(Py_ssize_t)) + cdef Py_ssize_t* histo = malloc(max_bin * sizeof(Py_ssize_t)) + for i in range(max_bin): + histo[i] = 0 # 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 + 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 + num_se_n = num_se_s = num_se_e = num_se_w = 0 + + 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 + cdef char[:, :] t_e = (np.diff(t, axis=1) < 0).view(np.uint8) t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) - t_w = np.diff(t, axis=1) == 1 + cdef char[:, :] t_w = (np.diff(t, axis=1) > 0).view(np.uint8) t = np.vstack((selem, np.zeros((1, selem.shape[1])))) - t_s = np.diff(t, axis=0) == -1 + cdef char[:, :] t_s = (np.diff(t, axis=0) < 0).view(np.uint8) 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 + cdef char[:, :] t_n = (np.diff(t, axis=0) > 0).view(np.uint8) for r in range(srows): for c in range(scols): @@ -136,92 +141,41 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, 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]) + histogram_increment(histo, &pop, image[rr, cc]) r = 0 c = 0 - # kernel ------------------------------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + out[r, c] = kernel(histo, pop, image[r, c], max_bin, mid_bin, 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]) + histogram_increment(histo, &pop, image[rr, 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]) + histogram_decrement(histo, &pop, image[rr, cc]) - # kernel ----------------------------------------------------------- - out_data[r * cols + c] = \ - kernel(histo, pop, image_data[r * cols + c], p0, p1, s0, s1) - # kernel ----------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) - 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 + r += 1 # pass to the next row if r >= rows: break @@ -230,21 +184,55 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, 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]) + histogram_increment(histo, &pop, image[rr, 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]) + histogram_decrement(histo, &pop, image[rr, cc]) - # kernel --------------------------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], - p0, p1, s0, s1) - # kernel --------------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) + + # ---> 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[rr, 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[rr, cc]) + + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) + + 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[rr, 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[rr, cc]) + + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) # release memory allocated by malloc - free(se_e_r) free(se_e_c) free(se_w_r) @@ -253,5 +241,4 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, free(se_n_c) free(se_s_r) free(se_s_c) - free(histo) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 94fc3130..50c9a370 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -1,11 +1,740 @@ +"""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, for 16-bit input images, the number of +histogram bins is determined from the maximum value present in the image. + +Result image is 8-/16-bit or double with respect to the input image and the +rank filter operation. + +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 warnings import numpy as np +from skimage import img_as_ubyte + +from . import generic_cy -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)) +__all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', + 'subtract_mean', 'median', 'minimum', 'modal', 'enhance_contrast', + 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] + + +def _handle_input(image, selem, out, mask, out_dtype=None): + + if image.dtype not in (np.uint8, np.uint16): + image = img_as_ubyte(image) + + selem = np.ascontiguousarray(img_as_ubyte(selem > 0)) + image = np.ascontiguousarray(image) + + if mask is None: + mask = np.ones(image.shape, dtype=np.uint8) else: - return 1 + mask = img_as_ubyte(mask) + mask = np.ascontiguousarray(mask) + + if out is None: + if out_dtype is None: + out_dtype = image.dtype + out = np.empty_like(image, dtype=out_dtype) + + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + + is_8bit = image.dtype in (np.uint8, np.int8) + + if is_8bit: + max_bin = 255 + else: + max_bin = max(4, image.max()) + + bitdepth = int(np.log2(max_bin)) + if bitdepth > 10: + warnings.warn("Bitdepth of %d may result in bad rank filter " + "performance due to large number of bins." % bitdepth) + + return image, selem, out, mask, max_bin + + +def _apply(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): + + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype) + + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin) + + return out + + +def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Autolevel image using local histogram. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + 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(generic_cy._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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 + ------- + bottomhat : ndarray (same dtype as input image) + The result of the local bottomhat. + + """ + + return _apply(generic_cy._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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + 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(generic_cy._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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + """ + + return _apply(generic_cy._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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + See also + -------- + skimage.morphology.dilation + + Note + ---- + * the lower algorithm complexity makes the rank.maximum() more efficient + for larger images and structuring elements + + """ + + return _apply(generic_cy._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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + 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(generic_cy._mean, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def subtract_mean(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Return image subtracted from its local mean. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + """ + + return _apply(generic_cy._subtract_mean, 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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + 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(generic_cy._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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + See also + -------- + skimage.morphology.erosion + + Note + ---- + * the lower algorithm complexity makes the rank.minimum() more efficient + for larger images and structuring elements + + """ + + return _apply(generic_cy._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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + """ + + return _apply(generic_cy._modal, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def enhance_contrast(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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 + Output image. + out : ndarray (same dtype as input image) + The result of the local enhance_contrast. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import enhance_contrast + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = enhance_contrast(ima, disk(20)) + + """ + + return _apply(generic_cy._enhance_contrast, 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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + 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(generic_cy._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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + 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(generic_cy._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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + """ + + return _apply(generic_cy._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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (same dtype as input image) + Output image. + + """ + + # 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(generic_cy._noise_filter, 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 [1]_ 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 (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 : ndarray (double) + Output image. + + References + ---------- + .. [1] 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 + >>> a8 = data.camera() + >>> ent8 = entropy(a8, disk(5)) + + """ + + return _apply(generic_cy._entropy, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, + out_dtype=np.double) + + +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 + 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 : ndarray (same dtype as input image) + Output image. + + References + ---------- + .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method + + 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(generic_cy._otsu, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx new file mode 100644 index 00000000..dcf6e361 --- /dev/null +++ b/skimage/filter/rank/generic_cy.pyx @@ -0,0 +1,506 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log + +from .core_cy cimport dtype_t, dtype_t_out, _core + + +cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, delta + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + delta = imax - imin + if delta > 0: + return (max_bin - 1) * (g - imin) / delta + else: + return 0 + else: + return 0 + + +cdef inline double _kernel_bottomhat(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin): + if histo[i]: + break + return g - i + else: + return 0 + + +cdef inline double _kernel_equalize(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + sum += histo[i] + if i >= g: + break + return ((max_bin - 1) * sum) / pop + else: + return 0 + + +cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + return imax - imin + else: + return 0 + + +cdef inline double _kernel_maximum(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + return i + else: + return 0 + + +cdef inline double _kernel_mean(Py_ssize_t* histo, double pop,dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return mean / pop + else: + return 0 + + +cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return (g - mean / pop) / 2. + 127 + else: + return 0 + + +cdef inline double _kernel_median(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef double sum = pop / 2.0 + + if pop: + for i in range(max_bin): + if histo[i]: + sum -= histo[i] + if sum < 0: + return i + else: + return 0 + + +cdef inline double _kernel_minimum(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin): + if histo[i]: + return i + else: + return 0 + + +cdef inline double _kernel_modal(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t hmax = 0, imax = 0 + + if pop: + for i in range(max_bin): + if histo[i] > hmax: + hmax = histo[i] + imax = i + return imax + else: + return 0 + + +cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + if imax - g < g - imin: + return imax + else: + return imin + else: + return 0 + + +cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + return pop + + +cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return g > (mean / pop) + else: + return 0 + + +cdef inline double _kernel_tophat(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + break + return i - g + else: + return 0 + + +cdef inline double _kernel_noise_filter(Py_ssize_t* histo, double pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double 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, max_bin): + if histo[i]: + break + if i - g < min_i: + return i - g + else: + return min_i + + +cdef inline double _kernel_entropy(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef double e, p + + if pop: + e = 0. + for i in range(max_bin): + p = histo[i] / pop + if p > 0: + e -= p * log(p) / 0.6931471805599453 + return e + else: + return 0 + + +cdef inline double _kernel_otsu(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef Py_ssize_t max_i + cdef double P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b + cdef double mu = 0. + + # compute local mean + if pop: + for i in range(max_bin): + mu += histo[i] * i + mu = mu / pop + else: + return 0 + + # maximizing the between class variance + max_i = 0 + q1 = histo[0] / pop + mu1 = 0. + max_sigma_b = 0. + + for i in range(1, max_bin): + 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 + + +def _autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_autolevel[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _bottomhat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_bottomhat[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _equalize(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_equalize[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_gradient[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _maximum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_maximum[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_mean[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _subtract_mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_subtract_mean[dtype_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _median(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_median[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _minimum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_minimum[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _enhance_contrast(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _modal(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_modal[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_pop[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_threshold[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _tophat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_tophat[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _noise_filter(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_noise_filter[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _entropy(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_entropy[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _otsu(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_otsu[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) diff --git a/skimage/filter/rank/percentile_rank.pyx b/skimage/filter/rank/percentile.py similarity index 50% rename from skimage/filter/rank/percentile_rank.pyx rename to skimage/filter/rank/percentile.py index 7deae623..ff3b1559 100644 --- a/skimage/filter/rank/percentile_rank.pyx +++ b/skimage/filter/rank/percentile.py @@ -1,17 +1,17 @@ """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] +``autolevel_percentile`` 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. +Input image can be 8-bit or 16-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. +Result image is 8-/16-bit or double with respect to the input image and the +rank filter operation. References ---------- @@ -23,55 +23,31 @@ References """ 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 + +from . import percentile_cy +from .generic import _handle_input -__all__ = ['percentile_autolevel', 'percentile_gradient', - 'percentile_mean', 'percentile_mean_substraction', - 'percentile_morph_contr_enh', 'percentile', 'percentile_pop', - 'percentile_threshold'] +__all__ = ['autolevel_percentile', 'gradient_percentile', + 'mean_percentile', 'subtract_mean_percentile', + 'enhance_contrast_percentile', 'percentile', 'pop_percentile', + 'threshold_percentile'] -def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, p0, p1): - selem = img_as_ubyte(selem) - image = np.ascontiguousarray(image) +def _apply(func, image, selem, out, mask, shift_x, shift_y, p0, p1, + out_dtype=None): - if mask is None: - mask = np.ones(image.shape, dtype=np.uint8) - else: - mask = np.ascontiguousarray(mask) - mask = img_as_ubyte(mask) + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype) - 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.") + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin, p0=p0, p1=p1) return out -def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): +def autolevel_percentile(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 @@ -79,15 +55,13 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, 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. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray 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 @@ -100,59 +74,56 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local autolevel : uint8 array or uint16 - The result of the local autolevel. + out : ndarray (same dtype as input image) + Output image. """ - 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, + return _apply(percentile_cy._autolevel, 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.): +def gradient_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0, p1=1): + """Return greyscale local gradient of an image. + + gradient is computed on the given structuring element. Only + levels between percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + 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 + ------- + out : ndarray (same dtype as input image) + Output image. + + """ + + return _apply(percentile_cy._gradient, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def mean_percentile(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 @@ -160,15 +131,13 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, 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. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray 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 @@ -181,34 +150,32 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local mean : uint8 array or uint16 - The result of the local mean. + out : ndarray (same dtype as input image) + Output image. """ - return _apply(_crank8_percentiles.mean, _crank16_percentiles.mean, + return _apply(percentile_cy._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. +def subtract_mean_percentile(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, p0=0, p1=1): + """Return greyscale local subtract_mean of an image. - mean_substraction is computed on the given structuring element. Only levels + subtract_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. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray 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 @@ -221,36 +188,32 @@ def percentile_mean_substraction(image, selem, out=None, mask=None, Returns ------- - local mean_substraction : uint8 array or uint16 - The result of the local mean_substraction. + out : ndarray (same dtype as input image) + Output image. """ - return _apply(_crank8_percentiles.mean_substraction, - _crank16_percentiles.mean_substraction, + return _apply(percentile_cy._subtract_mean, 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. +def enhance_contrast_percentile(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, p0=0, p1=1): + """Return greyscale local enhance_contrast of an image. - morph_contr_enh is computed on the given structuring element. Only levels + enhance_contrast 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. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray 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 @@ -263,60 +226,55 @@ def percentile_morph_contr_enh( Returns ------- - local morph_contr_enh : uint8 array or uint16 - The result of the local morph_contr_enh. + out : ndarray (same dtype as input image) + Output image. """ - return _apply(_crank8_percentiles.morph_contr_enh, - _crank16_percentiles.morph_contr_enh, + return _apply(percentile_cy._enhance_contrast, 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.): + p0=0): """Return greyscale local percentile of an image. - percentile is computed on the given structuring element. Only levels between - percentiles [p0, p1] are used. + percentile is computed on the given structuring element. Returns the value + of the p0 lower percentile of the neighborhood value distribution. 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. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray 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. + p0 : float in [0, ..., 1] + Set the percentile value. Returns ------- - local percentile : uint8 array or uint16 - The result of the local percentile. + out : ndarray (same dtype as input image) + Output image. """ - return _apply(_crank8_percentiles.percentile, - _crank16_percentiles.percentile, + return _apply(percentile_cy._percentile, image, selem, out=out, mask=mask, shift_x=shift_x, - shift_y=shift_y, p0=p0, p1=p1) + shift_y=shift_y, p0=p0, p1=0.) -def percentile_pop(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): +def pop_percentile(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 @@ -324,15 +282,13 @@ def percentile_pop(image, selem, out=None, mask=None, shift_x=False, 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. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray 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 @@ -345,52 +301,50 @@ def percentile_pop(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local pop : uint8 array or uint16 - The result of the local pop. + out : ndarray (same dtype as input image) + Output image. """ - return _apply(_crank8_percentiles.pop, _crank16_percentiles.pop, + return _apply(percentile_cy._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.): +def threshold_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0): """Return greyscale local threshold of an image. - threshold is computed on the given structuring element. Only levels between - percentiles [p0, p1] are used. + threshold is computed on the given structuring element. Returns + thresholded image such that pixels having a higher value than the the p0 + percentile of the neighborhood value distribution are set to 2^nbit-1 + (e.g. 255 for 8bit image). 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. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray 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. + p0 : float in [0, ..., 1] + Set the percentile value. - Returns - ------- - local threshold : uint8 array or uint16 + out : ndarray (same dtype as input image) + Output image. + local threshold : ndarray (same dtype as input) 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) + return _apply(percentile_cy._threshold, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=0) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx new file mode 100644 index 00000000..e951a76e --- /dev/null +++ b/skimage/filter/rank/percentile_cy.pyx @@ -0,0 +1,301 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from .core_cy cimport dtype_t, dtype_t_out, _core, _min, _max + + +cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range(max_bin - 1, -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + + delta = imax - imin + if delta > 0: + return (max_bin - 1) * (_min(_max(imin, g), imax) + - imin) / delta + else: + return imax - imin + else: + return 0 + + +cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum >= p0 * pop: + imin = i + break + sum = 0 + for i in range(max_bin - 1, -1, -1): + sum += histo[i] + if sum >= p1 * pop: + imax = i + break + + return imax - imin + else: + return 0 + + +cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + + if n > 0: + return mean / n + else: + return 0 + else: + return 0 + + +cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(max_bin): + 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 + mid_bin + else: + return 0 + else: + return 0 + + +cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range(max_bin - 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 double _kernel_percentile(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t sum = 0 + + if pop: + if p0 == 1: # make sure p0 = 1 returns the maximum filter + for i in range(max_bin - 1, -1, -1): + if histo[i]: + break + else: + for i in range(max_bin): + sum += histo[i] + if sum > p0 * pop: + break + return i + else: + return 0 + + +cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, sum, n + + if pop: + sum = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + return n + else: + return 0 + + +cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + sum += histo[i] + if sum >= p0 * pop: + break + + return (max_bin - 1) * (g >= i) + else: + return 0 + + +def _autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_autolevel[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_gradient[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_mean[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _subtract_mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_subtract_mean[dtype_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _enhance_contrast(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _percentile(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_percentile[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_pop[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_threshold[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) diff --git a/skimage/filter/rank/rank.pyx b/skimage/filter/rank/rank.pyx deleted file mode 100644 index e8a4c8f1..00000000 --- a/skimage/filter/rank/rank.pyx +++ /dev/null @@ -1,769 +0,0 @@ -"""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 index 43ff030f..1d573b81 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -1,7 +1,8 @@ import numpy as np from numpy.testing import run_module_suite, assert_array_equal, assert_raises -from skimage import data +from skimage import img_as_ubyte, img_as_uint, img_as_float +from skimage import data, util from skimage.morphology import cmorph, disk from skimage.filter import rank @@ -32,10 +33,10 @@ def test_random_sizes(): shift_x=+1, shift_y=+1) assert_array_equal(image16.shape, out16.shape) - rank.percentile_mean(image=image16, mask=mask, out=out16, + rank.mean_percentile(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, + rank.mean_percentile(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) @@ -50,7 +51,7 @@ def test_compare_with_cmorph_dilate(): 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) + cm = cmorph._dilate(image=image, selem=elem) assert_array_equal(out, cm) @@ -64,7 +65,7 @@ def test_compare_with_cmorph_erode(): 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) + cm = cmorph._erode(image=image, selem=elem) assert_array_equal(out, cm) @@ -77,7 +78,7 @@ def test_bitdepth(): 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, + r = rank.mean_percentile(image=image, selem=elem, mask=mask, out=out, shift_x=0, shift_y=0, p0=.1, p1=.9) @@ -129,17 +130,6 @@ def test_structuring_element8(): 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 @@ -162,41 +152,80 @@ 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() + image = util.img_as_ubyte(data.camera()) selem = disk(20) loc_autolevel = rank.autolevel(image, selem=selem) - loc_perc_autolevel = rank.percentile_autolevel(image, selem=selem, + loc_perc_autolevel = rank.autolevel_percentile(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 + # 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, + loc_perc_autolevel = rank.autolevel_percentile(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 +def test_compare_ubyte_vs_float(): - image8 = data.camera() + # Create signed int8 image that and convert it to uint8 + image_uint = img_as_ubyte(data.camera()[:50, :50]) + image_float = img_as_float(image_uint) + + methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'threshold', + 'subtract_mean', 'enhance_contrast', 'pop', 'tophat'] + + for method in methods: + func = getattr(rank, method) + out_u = func(image_uint, disk(3)) + out_f = func(image_float, disk(3)) + assert_array_equal(out_u, out_f) + + +def test_compare_8bit_unsigned_vs_signed(): + # filters applied on 8-bit image ore 16-bit image (having only real 8-bit + # of dynamic) should be identical + + # Create signed int8 image that and convert it to uint8 + image = img_as_ubyte(data.camera()) + image[image > 127] = 0 + image_s = image.astype(np.int8) + image_u = img_as_ubyte(image_s) + + assert_array_equal(image_u, img_as_ubyte(image_s)) + + methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', + 'mean', 'subtract_mean', 'median', 'minimum', 'modal', + 'enhance_contrast', 'pop', 'threshold', 'tophat'] + + for method in methods: + func = getattr(rank, method) + out_u = func(image_u, disk(3)) + out_s = func(image_s, disk(3)) + assert_array_equal(out_u, out_s) + + +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 = util.img_as_ubyte(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'] + 'mean', 'subtract_mean', 'median', 'minimum', 'modal', + 'enhance_contrast', 'pop', 'threshold', 'tophat'] for method in methods: func = getattr(rank, method) @@ -298,7 +327,8 @@ def test_smallest_selem16(): def test_empty_selem(): - # check that min, max and mean returns zeros if structuring element is empty + # 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) @@ -325,13 +355,11 @@ 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 = 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)) + 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) @@ -343,37 +371,132 @@ def test_entropy(): 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) + assert(np.max(rank.entropy(data, selem)) == 1) # 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) + assert(np.max(rank.entropy(data, selem)) == 2) # 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) + assert(np.max(rank.entropy(data, selem)) == 3) # 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) + assert(np.max(rank.entropy(data, selem)) == 4) # 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) + assert(np.max(rank.entropy(data, selem)) == 6) # 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) + assert(np.max(rank.entropy(data, selem)) == 8) # 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) + assert(np.max(rank.entropy(data, selem)) == 12) + + # make sure output is of dtype double + out = rank.entropy(data, np.ones((16, 16), dtype=np.uint8)) + assert out.dtype == np.double + + +def test_selem_dtypes(): + + 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 + + for dtype in (np.uint8, np.uint16, np.int32, np.int64, + np.float32, np.float64): + elem = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=dtype) + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.mean_percentile(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + + +def test_16bit(): + image = np.zeros((21, 21), dtype=np.uint16) + selem = np.ones((3, 3), dtype=np.uint8) + + for bitdepth in range(17): + value = 2 ** bitdepth - 1 + image[10, 10] = value + assert rank.minimum(image, selem)[10, 10] == 0 + assert rank.maximum(image, selem)[10, 10] == value + assert rank.mean(image, selem)[10, 10] == value / selem.size + + +def test_bilateral(): + image = np.zeros((21, 21), dtype=np.uint16) + selem = np.ones((3, 3), dtype=np.uint8) + + image[10, 10] = 1000 + image[10, 11] = 1010 + image[10, 9] = 900 + + assert rank.mean_bilateral(image, selem, s0=1, s1=1)[10, 10] == 1000 + assert rank.pop_bilateral(image, selem, s0=1, s1=1)[10, 10] == 1 + assert rank.mean_bilateral(image, selem, s0=11, s1=11)[10, 10] == 1005 + assert rank.pop_bilateral(image, selem, s0=11, s1=11)[10, 10] == 2 + + +def test_percentile_min(): + # check that percentile p0 = 0 is identical to local min + img = data.camera() + img16 = img.astype(np.uint16) + selem = disk(15) + # check for 8bit + img_p0 = rank.percentile(img, selem=selem, p0=0) + img_min = rank.minimum(img, selem=selem) + assert_array_equal(img_p0, img_min) + # check for 16bit + img_p0 = rank.percentile(img16, selem=selem, p0=0) + img_min = rank.minimum(img16, selem=selem) + assert_array_equal(img_p0, img_min) + + +def test_percentile_max(): + # check that percentile p0 = 1 is identical to local max + img = data.camera() + img16 = img.astype(np.uint16) + selem = disk(15) + # check for 8bit + img_p0 = rank.percentile(img, selem=selem, p0=1.) + img_max = rank.maximum(img, selem=selem) + assert_array_equal(img_p0, img_max) + # check for 16bit + img_p0 = rank.percentile(img16, selem=selem, p0=1.) + img_max = rank.maximum(img16, selem=selem) + assert_array_equal(img_p0, img_max) + + +def test_percentile_median(): + # check that percentile p0 = 0.5 is identical to local median + img = data.camera() + img16 = img.astype(np.uint16) + selem = disk(15) + # check for 8bit + img_p0 = rank.percentile(img, selem=selem, p0=.5) + img_max = rank.median(img, selem=selem) + assert_array_equal(img_p0, img_max) + # check for 16bit + img_p0 = rank.percentile(img16, selem=selem, p0=.5) + img_max = rank.median(img16, selem=selem) + assert_array_equal(img_p0, img_max) if __name__ == "__main__": diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index b1d070fc..33ad97df 100644 --- a/skimage/filter/setup.py +++ b/skimage/filter/setup.py @@ -14,50 +14,29 @@ def configuration(parent_package='', top_path=None): 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) + cython(['rank/core_cy.pyx'], working_path=base_path) + cython(['rank/generic_cy.pyx'], working_path=base_path) + cython(['rank/percentile_cy.pyx'], working_path=base_path) + cython(['rank/bilateral_cy.pyx'], working_path=base_path) config.add_extension('_ctmf', sources=['_ctmf.c'], 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'], + config.add_extension('rank.core_cy', sources=['rank/core_cy.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'], + config.add_extension('rank.generic_cy', sources=['rank/generic_cy.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'], + 'rank.percentile_cy', sources=['rank/percentile_cy.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'], + 'rank.bilateral_cy', sources=['rank/bilateral_cy.c'], include_dirs=[get_numpy_include_dirs()]) return config + if __name__ == '__main__': from numpy.distutils.core import setup setup(maintainer='scikit-image Developers', diff --git a/skimage/filter/tests/test_edges.py b/skimage/filter/tests/test_edges.py index 628de16d..523d6dd8 100644 --- a/skimage/filter/tests/test_edges.py +++ b/skimage/filter/tests/test_edges.py @@ -2,16 +2,43 @@ import numpy as np from numpy.testing import assert_array_almost_equal as assert_close import skimage.filter as F +from skimage.filter.edges import _mask_filter_result + + +def test_roberts_zeros(): + """Roberts' filter on an array of all zeros.""" + result = F.roberts(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_roberts_diagonal1(): + """Roberts' filter on a diagonal edge should be a diagonal line.""" + image = np.tri(10, 10, 0) + expected = ~(np.tri(10, 10, -1).astype(bool) | + np.tri(10, 10, -2).astype(bool).transpose()) + expected = _mask_filter_result(expected, None) + result = F.roberts(image).astype(bool) + assert_close(result, expected) + + +def test_roberts_diagonal2(): + """Roberts' filter on a diagonal edge should be a diagonal line.""" + image = np.rot90(np.tri(10, 10, 0), 3) + expected = ~np.rot90(np.tri(10, 10, -1).astype(bool) | + np.tri(10, 10, -2).astype(bool).transpose()) + expected = _mask_filter_result(expected, None) + result = F.roberts(image).astype(bool) + assert_close(result, expected) def test_sobel_zeros(): - """Sobel on an array of all 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_sobel_mask(): - """Sobel on a masked array should be zero""" + """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)) @@ -19,7 +46,7 @@ def test_sobel_mask(): def test_sobel_horizontal(): - """Sobel on an edge should be a horizontal line""" + """Sobel on a horizontal edge should be a horizontal line.""" i, j = np.mgrid[-5:6, -5:6] image = (i >= 0).astype(float) result = F.sobel(image) @@ -30,7 +57,7 @@ def test_sobel_horizontal(): def test_sobel_vertical(): - """Sobel on a vertical edge should be a vertical line""" + """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) @@ -40,13 +67,13 @@ def test_sobel_vertical(): def test_hsobel_zeros(): - """Horizontal sobel on an array of all 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_hsobel_mask(): - """Horizontal Sobel on a masked array should be zero""" + """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)) @@ -54,7 +81,7 @@ def test_hsobel_mask(): def test_hsobel_horizontal(): - """Horizontal Sobel on an edge should be a horizontal line""" + """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) @@ -65,7 +92,7 @@ def test_hsobel_horizontal(): def test_hsobel_vertical(): - """Horizontal Sobel on a vertical edge should be zero""" + """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) @@ -73,13 +100,13 @@ def test_hsobel_vertical(): def test_vsobel_zeros(): - """Vertical sobel on an array of all 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_vsobel_mask(): - """Vertical Sobel on a masked array should be zero""" + """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)) @@ -87,7 +114,7 @@ def test_vsobel_mask(): def test_vsobel_vertical(): - """Vertical Sobel on an edge should be a vertical line""" + """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) @@ -98,7 +125,7 @@ def test_vsobel_vertical(): def test_vsobel_horizontal(): - """vertical Sobel on a horizontal edge should be zero""" + """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) @@ -107,13 +134,13 @@ def test_vsobel_horizontal(): def test_scharr_zeros(): - """Scharr on an array of all 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""" + """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)) @@ -121,7 +148,7 @@ def test_scharr_mask(): def test_scharr_horizontal(): - """Scharr on an edge should be a horizontal line""" + """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) @@ -132,7 +159,7 @@ def test_scharr_horizontal(): def test_scharr_vertical(): - """Scharr on a vertical edge should be a vertical line""" + """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) @@ -142,13 +169,13 @@ def test_scharr_vertical(): def test_hscharr_zeros(): - """Horizontal Scharr on an array of all 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""" + """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)) @@ -156,7 +183,7 @@ def test_hscharr_mask(): def test_hscharr_horizontal(): - """Horizontal Scharr on an edge should be a horizontal line""" + """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) @@ -167,7 +194,7 @@ def test_hscharr_horizontal(): def test_hscharr_vertical(): - """Horizontal Scharr on a vertical edge should be zero""" + """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) @@ -175,13 +202,13 @@ def test_hscharr_vertical(): def test_vscharr_zeros(): - """Vertical Scharr on an array of all 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""" + """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)) @@ -189,7 +216,7 @@ def test_vscharr_mask(): def test_vscharr_vertical(): - """Vertical Scharr on an edge should be a vertical line""" + """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) @@ -200,7 +227,7 @@ def test_vscharr_vertical(): def test_vscharr_horizontal(): - """vertical Scharr on a horizontal edge should be zero""" + """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) @@ -209,13 +236,13 @@ def test_vscharr_horizontal(): def test_prewitt_zeros(): - """Prewitt on an array of all 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""" + """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)) @@ -224,7 +251,7 @@ def test_prewitt_mask(): def test_prewitt_horizontal(): - """Prewitt on an edge should be a horizontal line""" + """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) @@ -236,7 +263,7 @@ def test_prewitt_horizontal(): def test_prewitt_vertical(): - """Prewitt on a vertical edge should be a vertical line""" + """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) @@ -247,13 +274,13 @@ def test_prewitt_vertical(): def test_hprewitt_zeros(): - """Horizontal prewitt on an array of all 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""" + """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)) @@ -262,7 +289,7 @@ def test_hprewitt_mask(): def test_hprewitt_horizontal(): - """Horizontal prewitt on an edge should be a horizontal line""" + """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) @@ -274,7 +301,7 @@ def test_hprewitt_horizontal(): def test_hprewitt_vertical(): - """Horizontal prewitt on a vertical edge should be zero""" + """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) @@ -283,13 +310,13 @@ def test_hprewitt_vertical(): def test_vprewitt_zeros(): - """Vertical prewitt on an array of all 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""" + """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)) @@ -297,7 +324,7 @@ def test_vprewitt_mask(): def test_vprewitt_vertical(): - """Vertical prewitt on an edge should be a vertical line""" + """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) @@ -309,7 +336,7 @@ def test_vprewitt_vertical(): def test_vprewitt_horizontal(): - """Vertical prewitt on a horizontal edge should be zero""" + """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) diff --git a/skimage/filter/tests/test_gabor.py b/skimage/filter/tests/test_gabor.py new file mode 100644 index 00000000..7035d82a --- /dev/null +++ b/skimage/filter/tests/test_gabor.py @@ -0,0 +1,82 @@ +import numpy as np +from numpy.testing import (assert_equal, assert_almost_equal, + assert_array_almost_equal) + +from skimage.filter._gabor import gabor_kernel, gabor_filter, _sigma_prefactor + + +def test_gabor_kernel_size(): + sigma_x = 5 + sigma_y = 10 + # Sizes cut off at +/- three sigma + 1 for the center + size_x = sigma_x * 6 + 1 + size_y = sigma_y * 6 + 1 + + kernel = gabor_kernel(0, theta=0, sigma_x=sigma_x, sigma_y=sigma_y) + assert_equal(kernel.shape, (size_y, size_x)) + + kernel = gabor_kernel(0, theta=np.pi/2, sigma_x=sigma_x, sigma_y=sigma_y) + assert_equal(kernel.shape, (size_x, size_y)) + + +def test_gabor_kernel_bandwidth(): + kernel = gabor_kernel(1, bandwidth=1) + assert_equal(kernel.shape, (5, 5)) + + kernel = gabor_kernel(1, bandwidth=0.5) + assert_equal(kernel.shape, (9, 9)) + + kernel = gabor_kernel(0.5, bandwidth=1) + assert_equal(kernel.shape, (9, 9)) + + +def test_sigma_prefactor(): + assert_almost_equal(_sigma_prefactor(1), 0.56, 2) + assert_almost_equal(_sigma_prefactor(0.5), 1.09, 2) + + +def test_gabor_kernel_sum(): + for sigma_x in range(1, 10, 2): + for sigma_y in range(1, 10, 2): + for frequency in range(0, 10, 2): + kernel = gabor_kernel(frequency+0.1, theta=0, + sigma_x=sigma_x, sigma_y=sigma_y) + # make sure gaussian distribution is covered nearly 100% + assert_almost_equal(np.abs(kernel).sum(), 1, 2) + + +def test_gabor_kernel_theta(): + for sigma_x in range(1, 10, 2): + for sigma_y in range(1, 10, 2): + for frequency in range(0, 10, 2): + for theta in range(0, 10, 2): + kernel0 = gabor_kernel(frequency+0.1, theta=theta, + sigma_x=sigma_x, sigma_y=sigma_y) + kernel180 = gabor_kernel(frequency, theta=theta+np.pi, + sigma_x=sigma_x, sigma_y=sigma_y) + + assert_array_almost_equal(np.abs(kernel0), + np.abs(kernel180)) + + +def test_gabor_filter(): + Y, X = np.mgrid[:40, :40] + frequencies = (0.1, 0.3) + wave_images = [np.sin(2 * np.pi * X * f) for f in frequencies] + + def match_score(image, frequency): + gabor_responses = gabor_filter(image, frequency) + return np.mean(np.hypot(*gabor_responses)) + + # Gabor scores: diagonals are frequency-matched, off-diagonals are not. + responses = np.array([[match_score(image, f) for f in frequencies] + for image in wave_images]) + assert responses[0, 0] > responses[0, 1] + assert responses[1, 1] > responses[0, 1] + assert responses[0, 0] > responses[1, 0] + assert responses[1, 1] > responses[1, 0] + + +if __name__ == "__main__": + from numpy import testing + testing.run_module_suite() diff --git a/skimage/filter/tests/test_gaussian.py b/skimage/filter/tests/test_gaussian.py new file mode 100644 index 00000000..9118bfae --- /dev/null +++ b/skimage/filter/tests/test_gaussian.py @@ -0,0 +1,42 @@ +import numpy as np +from skimage.filter._gaussian import gaussian_filter + + +def test_null_sigma(): + a = np.zeros((3, 3)) + a[1, 1] = 1. + assert np.all(gaussian_filter(a, 0) == a) + + +def test_energy_decrease(): + a = np.zeros((3, 3)) + a[1, 1] = 1. + gaussian_a = gaussian_filter(a, sigma=1, mode='reflect') + assert gaussian_a.std() < a.std() + + +def test_multichannel(): + a = np.zeros((5, 5, 3)) + a[1, 1] = np.arange(1, 4) + gaussian_rgb_a = gaussian_filter(a, sigma=1, mode='reflect', + multichannel=True) + # Check that the mean value is conserved in each channel + # (color channels are not mixed together) + assert np.allclose([a[..., i].mean() for i in range(3)], + [gaussian_rgb_a[..., i].mean() for i in range(3)]) + # Test multichannel = None + gaussian_rgb_a = gaussian_filter(a, sigma=1, mode='reflect') + # Check that the mean value is conserved in each channel + # (color channels are not mixed together) + assert np.allclose([a[..., i].mean() for i in range(3)], + [gaussian_rgb_a[..., i].mean() for i in range(3)]) + # Iterable sigma + gaussian_rgb_a = gaussian_filter(a, sigma=[1, 2], mode='reflect', + multichannel=True) + assert np.allclose([a[..., i].mean() for i in range(3)], + [gaussian_rgb_a[..., i].mean() for i in range(3)]) + + +if __name__ == "__main__": + from numpy import testing + testing.run_module_suite() diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 97d3d9e3..0edfe4e7 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -3,7 +3,9 @@ from numpy.testing import assert_array_equal import skimage from skimage import data -from skimage.filter.thresholding import threshold_otsu, threshold_adaptive +from skimage.filter.thresholding import (threshold_adaptive, + threshold_otsu, + threshold_yen) class TestSimpleImage(): @@ -25,6 +27,26 @@ class TestSimpleImage(): image = np.float64(self.image) assert 2 <= threshold_otsu(image) < 3 + def test_yen(self): + assert threshold_yen(self.image) == 2 + + def test_yen_negative_int(self): + image = self.image - 2 + assert threshold_yen(image) == 0 + + def test_yen_float_image(self): + image = np.float64(self.image) + assert 2 <= threshold_yen(image) < 3 + + def test_yen_arange(self): + image = np.arange(256) + assert threshold_yen(image) == 127 + + def test_yen_binary(self): + image = np.zeros([2,256], dtype='uint8') + image[0] = 255 + assert threshold_yen(image) < 1 + def test_threshold_adaptive_generic(self): def func(arr): return arr.sum() / arr.shape[0] @@ -92,5 +114,15 @@ def test_otsu_lena_image(): assert 140 < threshold_otsu(lena) < 142 +def test_yen_coins_image(): + coins = skimage.img_as_ubyte(data.coins()) + assert 109 < threshold_yen(coins) < 111 + + +def test_yen_coins_image_as_float(): + coins = skimage.img_as_float(data.coins()) + assert 0.43 < threshold_yen(coins) < 0.44 + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 77f244fc..7f980387 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -1,4 +1,4 @@ -__all__ = ['threshold_otsu', 'threshold_adaptive'] +__all__ = ['threshold_adaptive', 'threshold_otsu', 'threshold_yen'] import numpy as np import scipy.ndimage @@ -9,10 +9,10 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, mode='reflect', param=None): """Applies an adaptive threshold to an array. - Also known as local or dynamic thresholding where the threshold value is the - weighted mean for the local neighborhood of a pixel subtracted by a - constant. Alternatively the threshold can be determined dynamically by a - a given function using the 'generic' method. + Also known as local or dynamic thresholding where the threshold value is + the weighted mean for the local neighborhood of a pixel subtracted by a + constant. Alternatively the threshold can be determined dynamically by a a + given function using the 'generic' method. Parameters ---------- @@ -26,10 +26,10 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, weighted mean image. * 'generic': use custom function (see `param` parameter) - * 'gaussian': apply gaussian filter (see `param` parameter for custom + * 'gaussian': apply gaussian filter (see `param` parameter for custom\ sigma value) * 'mean': apply arithmetic mean filter - * 'median' apply median rank filter + * 'median': apply median rank filter By default the 'gaussian' method is used. offset : float, optional @@ -42,8 +42,8 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, param : {int, function}, optional Either specify sigma for 'gaussian' method or function object for 'generic' method. This functions takes the flat array of local - neighbourhood as a single argument and returns the calculated threshold - for the centre pixel. + neighbourhood as a single argument and returns the calculated + threshold for the centre pixel. Returns ------- @@ -52,8 +52,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, References ---------- - http://docs.opencv.org/modules/imgproc/doc/miscellaneous_transformations - .html?highlight=threshold#adaptivethreshold + .. [1] http://docs.opencv.org/modules/imgproc/doc/miscellaneous_transformations.html?highlight=threshold#adaptivethreshold Examples -------- @@ -66,7 +65,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, thresh_image = np.zeros(image.shape, 'double') if method == 'generic': scipy.ndimage.generic_filter(image, param, block_size, - output=thresh_image, mode=mode) + output=thresh_image, mode=mode) elif method == 'gaussian': if param is None: # automatically determine sigma which covers > 99% of distribution @@ -74,17 +73,17 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, else: sigma = param scipy.ndimage.gaussian_filter(image, sigma, output=thresh_image, - mode=mode) + mode=mode) elif method == 'mean': mask = 1. / block_size * np.ones((block_size,)) # separation of filters to speedup convolution scipy.ndimage.convolve1d(image, mask, axis=0, output=thresh_image, - mode=mode) + mode=mode) scipy.ndimage.convolve1d(thresh_image, mask, axis=1, - output=thresh_image, mode=mode) + output=thresh_image, mode=mode) elif method == 'median': scipy.ndimage.median_filter(image, block_size, output=thresh_image, - mode=mode) + mode=mode) return image > (thresh_image - offset) @@ -96,14 +95,15 @@ def threshold_otsu(image, nbins=256): ---------- image : array Input image. - nbins : int + nbins : int, optional Number of bins used to calculate histogram. This value is ignored for integer arrays. Returns ------- threshold : float - Threshold value. + Upper threshold value. All pixels intensities that less or equal of + this value assumed as foreground. References ---------- @@ -114,7 +114,7 @@ def threshold_otsu(image, nbins=256): >>> from skimage.data import camera >>> image = camera() >>> thresh = threshold_otsu(image) - >>> binary = image > thresh + >>> binary = image <= thresh """ hist, bin_centers = histogram(image, nbins) hist = hist.astype(float) @@ -134,3 +134,53 @@ def threshold_otsu(image, nbins=256): idx = np.argmax(variance12) threshold = bin_centers[:-1][idx] return threshold + + +def threshold_yen(image, nbins=256): + """Return threshold value based on Yen's method. + + Parameters + ---------- + image : array + Input image. + nbins : int, optional + Number of bins used to calculate histogram. This value is ignored for + integer arrays. + + Returns + ------- + threshold : float + Upper threshold value. All pixels intensities that less or equal of + this value assumed as foreground. + + References + ---------- + .. [1] Yen J.C., Chang F.J., and Chang S. (1995) "A New Criterion + for Automatic Multilevel Thresholding" IEEE Trans. on Image + Processing, 4(3): 370-378 + .. [2] Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding + Techniques and Quantitative Performance Evaluation" Journal of + Electronic Imaging, 13(1): 146-165, + http://www.busim.ee.boun.edu.tr/~sankur/SankurFolder/Threshold_survey.pdf + .. [3] ImageJ AutoThresholder code, http://fiji.sc/wiki/index.php/Auto_Threshold + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera() + >>> thresh = threshold_yen(image) + >>> binary = image <= thresh + """ + hist, bin_centers = histogram(image, nbins) + norm_histo = hist.astype(float) / hist.sum() # Probability mass function + P1 = np.cumsum(norm_histo) # Cumulative normalized histogram + P1_sq = np.cumsum(norm_histo ** 2) + # Get cumsum calculated from end of squared array: + P2_sq = np.cumsum(norm_histo[::-1] ** 2)[::-1] + # P2_sq indexes is shifted +1. I assume, with P1[:-1] it's help avoid '-inf' + # in crit. ImageJ Yen implementation replaces those values by zero. + crit = np.log(((P1_sq[:-1] * P2_sq[1:]) ** -1) * \ + (P1[:-1] * (1.0 - P1[:-1])) ** 2) + max_crit = np.argmax(crit) + threshold = bin_centers[:-1][max_crit] + return threshold diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index 9e2ac4e0..eb817c77 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -1,10 +1,7 @@ -try: - from .spath import shortest_path - from .mcp import MCP, MCP_Geometric, route_through_array -except ImportError: - print """*** The cython extensions have not been compiled. Run +from .spath import shortest_path +from .mcp import MCP, MCP_Geometric, route_through_array -python setup.py build_ext -i - -in the source directory to build in-place. Please refer to INSTALL.txt -for further detail.""" +__all__ = ['shortest_path', + 'MCP', + 'MCP_Geometric', + 'route_through_array'] \ No newline at end of file diff --git a/skimage/graph/_mcp.pyx b/skimage/graph/_mcp.pyx index 4e5f6d52..34904dcc 100644 --- a/skimage/graph/_mcp.pyx +++ b/skimage/graph/_mcp.pyx @@ -1,7 +1,5 @@ #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. @@ -34,6 +32,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ +import cython import numpy as np import heap @@ -51,7 +50,8 @@ INDEX_D = np.intp FLOAT_D = np.float64 - +@cython.boundscheck(False) +@cython.wraparound(False) def _get_edge_map(shape): """Return an array with edge points/lines/planes/hyperplanes marked. @@ -75,6 +75,9 @@ def _get_edge_map(shape): edges[tuple(slices)] = 1 return edges + +@cython.boundscheck(False) +@cython.wraparound(False) def _offset_edge_map(shape, offsets): """Return an array with positions marked where offsets will step out of bounds. @@ -117,6 +120,9 @@ def _offset_edge_map(shape, offsets): neg[neg < mn] = 0 return pos_edges.astype(EDGE_D), neg_edges.astype(EDGE_D) + +@cython.boundscheck(False) +@cython.wraparound(False) def make_offsets(d, fully_connected): """Make a list of offsets from a center point defining a n-dim neighborhood. @@ -160,6 +166,9 @@ def make_offsets(d, fully_connected): offsets.append(indices) return offsets + +@cython.boundscheck(True) +@cython.wraparound(True) def _unravel_index_fortran(flat_indices, shape): """_unravel_index_fortran(flat_indices, shape) @@ -170,6 +179,9 @@ def _unravel_index_fortran(flat_indices, shape): indices = [tuple(idx/strides % shape) for idx in flat_indices] return indices + +@cython.boundscheck(True) +@cython.wraparound(True) def _ravel_index_fortran(indices, shape): """_ravel_index_fortran(flat_indices, shape) @@ -180,6 +192,9 @@ def _ravel_index_fortran(indices, shape): flat_indices = [np.sum(strides * idx) for idx in indices] return flat_indices + +@cython.boundscheck(False) +@cython.wraparound(False) def _normalize_indices(indices, shape): """_normalize_indices(indices, shape) @@ -201,6 +216,16 @@ def _normalize_indices(indices, shape): new_indices.append(new_index) return new_indices + +@cython.boundscheck(True) +@cython.wraparound(True) +def _reverse(arr): + """Reverse index an array safely, with bounds/wraparound checks on.""" + return arr[::-1] + + +@cython.boundscheck(False) +@cython.wraparound(False) cdef class MCP: """MCP(costs, offsets=None, fully_connected=True) @@ -574,8 +599,11 @@ cdef class MCP: for d in range(dim): position[d] -= offsets[offset, d] traceback.append(tuple(position)) - return traceback[::-1] + return _reverse(traceback) + +@cython.boundscheck(False) +@cython.wraparound(False) cdef class MCP_Geometric(MCP): """MCP_Geometric(costs, offsets=None, fully_connected=True) diff --git a/skimage/graph/tests/test_heap.py b/skimage/graph/tests/test_heap.py index 8322fd4e..cf7e3d0b 100644 --- a/skimage/graph/tests/test_heap.py +++ b/skimage/graph/tests/test_heap.py @@ -1,5 +1,3 @@ -from numpy.testing import * - import time import random import skimage.graph.heap as heap @@ -49,4 +47,5 @@ def _test_heap(n, fast_update): return t1 - t0 if __name__ == "__main__": + from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index e3fd45a0..560f19d0 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -1,5 +1,7 @@ import numpy as np -from numpy.testing import * +from numpy.testing import (assert_array_equal, + assert_almost_equal, + ) import skimage.graph.mcp as mcp @@ -7,15 +9,6 @@ a = np.ones((8, 8), dtype=np.float32) a[1:-1, 1] = 0 a[1, 1:-1] = 0 -## array([[ 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., 1., 1., 1., 1., 1.], -## [ 1., 0., 1., 1., 1., 1., 1., 1.], -## [ 1., 0., 1., 1., 1., 1., 1., 1.], -## [ 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) @@ -159,4 +152,4 @@ def _test_random(shape): if __name__ == "__main__": - run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/graph/tests/test_spath.py b/skimage/graph/tests/test_spath.py index 62f9f303..d018449a 100644 --- a/skimage/graph/tests/test_spath.py +++ b/skimage/graph/tests/test_spath.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import * +from numpy.testing import assert_equal, assert_array_equal import skimage.graph.spath as spath @@ -33,4 +33,4 @@ def test_non_square(): if __name__ == "__main__": - run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/io/_io.py b/skimage/io/_io.py index f8f395bd..a7df4694 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,9 +1,13 @@ __all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] +try: + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + import os import re -import urllib2 import tempfile from io import BytesIO @@ -11,6 +15,7 @@ import numpy as np from skimage.io._plugins import call as call_plugin from skimage.color import rgb2grey +from skimage._shared import six # Shared image queue @@ -21,7 +26,7 @@ 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 + return (isinstance(filename, six.string_types) and URL_REGEX.match(filename) is not None) @@ -30,28 +35,33 @@ class Image(np.ndarray): These objects have tags for image metadata and IPython display protocol methods for image display. - """ - tags = {'filename': '', - 'EXIF': {}, - 'info': {}} + Parameters + ---------- + arr : ndarray + Image data. + kwargs : Image tags as keywords + Specified in the form ``tag0=value``, ``tag1=value``. + + Attributes + ---------- + tags : dict + Meta-data. + + """ 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))) + x.tags = kwargs + return x + def __array_finalize__(self, obj): + self.tags = getattr(obj, 'tags', {}) + def _repr_png_(self): return self._repr_image_format('png') @@ -130,8 +140,9 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, as_grey = flatten if is_url(fname): - with tempfile.NamedTemporaryFile(delete=False) as f: - u = urllib2.urlopen(fname) + _, ext = os.path.splitext(fname) + with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as f: + u = urlopen(fname) f.write(u.read()) img = call_plugin('imread', f.name, plugin=plugin, **plugin_args) os.remove(f.name) @@ -141,7 +152,7 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, if as_grey and getattr(img, 'ndim', 0) >= 3: img = rgb2grey(img) - return Image(img) + return img def imread_collection(load_pattern, conserve_memory=True, @@ -215,7 +226,7 @@ def imshow(arr, plugin=None, **plugin_args): Passed to the given plugin. """ - if isinstance(arr, basestring): + if isinstance(arr, six.string_types): arr = call_plugin('imread', arr, plugin=plugin) return call_plugin('imshow', arr, plugin=plugin, **plugin_args) diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index d2a9d4f9..a26125fa 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -593,6 +593,7 @@ def write_multipage(arrays, filename, flags=0): multibitmap = _FI.FreeImage_OpenMultiBitmap(ftype, filename, create_new, read_only, keep_cache_in_memory, 0) + multibitmap = ctypes.c_void_p(multibitmap) if not multibitmap: raise ValueError('Could not open %s for writing multi-page image.' % filename) diff --git a/skimage/io/_plugins/gtk_plugin.py b/skimage/io/_plugins/gtk_plugin.py index 39cd9a27..6130289c 100644 --- a/skimage/io/_plugins/gtk_plugin.py +++ b/skimage/io/_plugins/gtk_plugin.py @@ -5,14 +5,14 @@ try: # or else the gui import might trample another # gui's pyos_inputhook. window_manager.acquire('gtk') -except GuiLockError, gle: - print gle +except GuiLockError as gle: + print(gle) else: try: import gtk except ImportError: - print 'pygtk libraries not installed.' - print 'plugin not loaded.' + print('pygtk libraries not installed.') + print('plugin not loaded.') window_manager._release('gtk') else: @@ -51,4 +51,4 @@ else: window_manager.register_callback(gtk.main_quit) gtk.main() else: - print 'no images to display' + print('no images to display') diff --git a/skimage/io/_plugins/imread_plugin.py b/skimage/io/_plugins/imread_plugin.py index 323fbec8..46382cdf 100644 --- a/skimage/io/_plugins/imread_plugin.py +++ b/skimage/io/_plugins/imread_plugin.py @@ -1,6 +1,5 @@ __all__ = ['imread', 'imsave'] -import numpy as np from skimage.utils.dtype import convert try: diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 2247f3a1..2ddbfe26 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -11,6 +11,8 @@ except ImportError: from skimage.util import img_as_ubyte +from skimage._shared import six + def imread(fname, dtype=None): """Load an image from file. @@ -71,9 +73,8 @@ def imsave(fname, arr, format_str=None): 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. + Format to save as, this is defaulted to PNG if using a file-like + object; this will be derived from the extension if fname is a string Notes ----- @@ -104,6 +105,10 @@ def imsave(fname, arr, format_str=None): # Force all integers to bytes arr = arr.astype(np.uint8) + # default to PNG if file-like object + if not isinstance(fname, six.string_types) and format_str is None: + format_str = "PNG" + img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) img.save(fname, format=format_str) diff --git a/skimage/io/_plugins/plugin.py b/skimage/io/_plugins/plugin.py index da69363d..5e04f320 100644 --- a/skimage/io/_plugins/plugin.py +++ b/skimage/io/_plugins/plugin.py @@ -4,10 +4,15 @@ __all__ = ['use', 'available', 'call', 'info', 'configuration', 'reset_plugins'] -from ConfigParser import ConfigParser +try: + from configparser import ConfigParser +except ImportError: + from ConfigParser import ConfigParser + import os.path from glob import glob + plugin_store = None plugin_provides = {} @@ -52,8 +57,8 @@ def _scan_plugins(): for p in provides: if not p in plugin_store: - print "Plugin `%s` wants to provide non-existent `%s`." \ - " Ignoring." % (name, p) + print("Plugin `%s` wants to provide non-existent `%s`." \ + " Ignoring." % (name, p)) plugin_provides[name] = valid_provides plugin_module_name[name] = os.path.basename(f)[:-4] @@ -170,7 +175,7 @@ def available(loaded=False): """ active_plugins = set() - for plugin_func in plugin_store.itervalues(): + for plugin_func in plugin_store.values(): for plugin, func in plugin_func: active_plugins.add(plugin) @@ -208,8 +213,8 @@ def _load(plugin): provides = plugin_provides[plugin] for p in provides: if not hasattr(plugin_module, p): - print "Plugin %s does not provide %s as advertised. Ignoring." % \ - (plugin, p) + print("Plugin %s does not provide %s as advertised. Ignoring." % \ + (plugin, p)) else: store = plugin_store[p] func = getattr(plugin_module, p) diff --git a/skimage/io/_plugins/qt_plugin.py b/skimage/io/_plugins/qt_plugin.py index 24cf472c..db4cb4cc 100644 --- a/skimage/io/_plugins/qt_plugin.py +++ b/skimage/io/_plugins/qt_plugin.py @@ -144,7 +144,7 @@ def _app_show(): if app and window_manager.has_windows(): app.exec_() else: - print 'No images to show. See `imshow`.' + print('No images to show. See `imshow`.') def imsave(filename, img, format_str=None): diff --git a/skimage/io/_plugins/util.py b/skimage/io/_plugins/util.py index ed547222..98dd4f87 100644 --- a/skimage/io/_plugins/util.py +++ b/skimage/io/_plugins/util.py @@ -76,8 +76,8 @@ class WindowManager(object): try: self._windows.remove(win) except ValueError: - print 'Unable to find referenced window in tracked windows.' - print 'Ignoring...' + print('Unable to find referenced window in tracked windows.') + print('Ignoring...') else: if len(self._windows) == 0: self._exec_callback() diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 5a5dfff6..96855dbd 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -10,6 +10,7 @@ from copy import copy import numpy as np from ._io import imread +from .._shared import six def concatenate_images(ic): @@ -98,13 +99,14 @@ class MultiImage(object): >>> len(img) 2 >>> for frame in img: - ... print frame.shape + ... print(frame.shape) (15, 10) (15, 10) The two frames in this image can be shown with matplotlib: .. plot:: show_collection.py + """ def __init__(self, filename, conserve_memory=True, dtype=None): """Load a multi-img.""" @@ -295,7 +297,7 @@ class ImageCollection(object): """ def __init__(self, load_pattern, conserve_memory=True, load_func=None): """Load and manage a collection of images.""" - if isinstance(load_pattern, basestring): + if isinstance(load_pattern, six.string_types): load_pattern = load_pattern.split(':') self._files = [] for pattern in load_pattern: diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 2bbfefa7..f56d753b 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -2,13 +2,17 @@ import sys import os.path import numpy as np -from numpy.testing import * +from numpy.testing import (assert_raises, + assert_equal, + assert_array_almost_equal, + ) 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 +from skimage._shared import six try: @@ -18,8 +22,6 @@ except ImportError: else: PIL_available = True -if sys.version_info[0] > 2: - basestring = str class TestAlphanumericKey(): def setUp(self): @@ -55,7 +57,7 @@ class TestImageCollection(): def test_getitem(self): num = len(self.collection) for i in range(-num, num): - assert type(self.collection[i]) is ioImage + assert type(self.collection[i]) is np.ndarray assert_array_almost_equal(self.collection[0], self.collection[-num]) @@ -126,7 +128,7 @@ class TestMultiImage(): @skipif(not PIL_available) def test_files_property(self): - assert isinstance(self.img.filename, basestring) + assert isinstance(self.img.filename, six.string_types) def set_filename(f): self.img.filename = f @@ -148,4 +150,5 @@ class TestMultiImage(): if __name__ == "__main__": + from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/io/tests/test_colormixer.py b/skimage/io/tests/test_colormixer.py index b9f362a2..392a7b5b 100644 --- a/skimage/io/tests/test_colormixer.py +++ b/skimage/io/tests/test_colormixer.py @@ -1,4 +1,8 @@ -from numpy.testing import * +from numpy.testing import (assert_array_equal, + assert_almost_equal, + assert_equal, + assert_array_almost_equal, + ) import numpy as np import skimage.io._plugins._colormixer as cm @@ -136,4 +140,5 @@ class TestColorMixer(object): if __name__ == "__main__": + from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index 7a294f9e..550b39a2 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -53,7 +53,18 @@ def test_imread_uint16_big_endian(): assert img.dtype == np.uint16 assert_array_almost_equal(img, expected) - +@skipif(not FI_available) +def test_write_multipage(): + shape = (64,64,64) + x = np.ones(shape, dtype=np.uint8) * np.random.random(shape) * 255 + x = x.astype(np.uint8) + f = NamedTemporaryFile(suffix='.tif') + fname = f.name + f.close() + fi.write_multipage(x, fname) + y = fi.read_multipage(fname) + assert_array_equal(x, y) + class TestSave: def roundtrip(self, dtype, x, suffix): f = NamedTemporaryFile(suffix='.' + suffix) diff --git a/skimage/io/tests/test_image.py b/skimage/io/tests/test_image.py new file mode 100644 index 00000000..6c54695b --- /dev/null +++ b/skimage/io/tests/test_image.py @@ -0,0 +1,17 @@ +from skimage.io import Image + +from numpy.testing import assert_equal, assert_array_equal + +def test_tags(): + f = Image([1, 2, 3], foo='bar', sigma='delta') + g = Image([3, 2, 1], sun='moon') + h = Image([1, 1, 1]) + + assert_equal(f.tags['foo'], 'bar') + assert_array_equal((g + 2).tags['sun'], 'moon') + assert_equal(h.tags, {}) + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() + diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index 484784ee..8049860a 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -1,6 +1,6 @@ import os -from numpy.testing import * +from numpy.testing import assert_array_equal, raises, run_module_suite import numpy as np import skimage.io as io diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index a9d986d6..aa582ebc 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -6,7 +6,10 @@ 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 +from skimage.io import (imread, imsave, use_plugin, reset_plugins, + Image as ioImage) +from skimage._shared.six import BytesIO + try: from PIL import Image @@ -55,7 +58,6 @@ def test_imread_palette(): @skipif(not PIL_available) def test_palette_is_gray(): - from PIL import Image gray = Image.open(os.path.join(data_dir, 'palette_gray.png')) assert _palette_is_grayscale(gray) color = Image.open(os.path.join(data_dir, 'palette_color.png')) @@ -82,7 +84,7 @@ def test_imread_uint16(): @skipif(not PIL_available) def test_repr_png(): img_path = os.path.join(data_dir, 'camera.png') - original_img = imread(img_path) + original_img = ioImage(imread(img_path)) original_img_str = original_img._repr_png_() with NamedTemporaryFile(suffix='.png') as temp_png: @@ -125,5 +127,22 @@ class TestSave: x = (x * 255).astype(dtype) yield self.roundtrip, dtype, x + +@skipif(not PIL_available) +def test_imsave_filelike(): + shape = (2, 2) + image = np.zeros(shape) + s = BytesIO() + + # save to file-like object + imsave(s, image) + + # read from file-like object + s.seek(0) + out = imread(s) + assert out.shape == shape + assert_allclose(out, image) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/test_simpleitk.py b/skimage/io/tests/test_simpleitk.py index 4bb2cc23..bf3d3614 100644 --- a/skimage/io/tests/test_simpleitk.py +++ b/skimage/io/tests/test_simpleitk.py @@ -1,6 +1,5 @@ import os.path import numpy as np -from numpy.testing import * from numpy.testing.decorators import skipif from tempfile import NamedTemporaryFile @@ -49,7 +48,7 @@ def test_bilevel(): expected[::2] = 255 img = imread(os.path.join(data_dir, 'checker_bilevel.png')) - assert_array_equal(img, expected) + np.testing.assert_array_equal(img, expected) @skipif(not sitk_available) @@ -57,14 +56,14 @@ 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) + np.testing.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) + np.testing.assert_array_almost_equal(img, expected) class TestSave: @@ -75,7 +74,7 @@ class TestSave: imsave(fname, x) y = imread(fname) - assert_array_almost_equal(x, y) + np.testing.assert_array_almost_equal(x, y) @skipif(not sitk_available) def test_imsave_roundtrip(self): @@ -90,4 +89,5 @@ class TestSave: yield self.roundtrip, dtype, x if __name__ == "__main__": + from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/io/video.py b/skimage/io/video.py index 6cb1a8e9..003a1f53 100644 --- a/skimage/io/video.py +++ b/skimage/io/video.py @@ -256,7 +256,7 @@ class Video(object): Backend to use. """ def __init__(self, source=None, size=None, sync=False, backend=None): - if backend == None: + if backend is None: # select backend that is available if gstreamer_available: self.video = GstVideo(source, size, sync) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 1c92d9ee..108cd7d9 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -1,4 +1,28 @@ from .find_contours import find_contours +from ._marching_cubes import marching_cubes, mesh_surface_area from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity -from ._polygon import approximate_polygon, subdivide_polygon \ No newline at end of file +from ._polygon import approximate_polygon, subdivide_polygon +from ._moments import moments, moments_central, moments_normalized, moments_hu +from .fit import LineModel, CircleModel, EllipseModel, ransac +from .block import block_reduce + + +__all__ = ['find_contours', + 'regionprops', + 'perimeter', + 'structural_similarity', + 'approximate_polygon', + 'subdivide_polygon', + 'LineModel', + 'CircleModel', + 'EllipseModel', + 'ransac', + 'block_reduce', + 'moments', + 'moments_central', + 'moments_normalized', + 'moments_hu', + 'sum_blocks', + 'marching_cubes', + 'mesh_surface_area'] diff --git a/skimage/measure/_find_contours.pyx b/skimage/measure/_find_contours.pyx index d05d9aa7..e2f5a49b 100644 --- a/skimage/measure/_find_contours.pyx +++ b/skimage/measure/_find_contours.pyx @@ -4,8 +4,6 @@ #cython: wraparound=False import numpy as np -cimport numpy as cnp - cdef inline double _get_fraction(double from_value, double to_value, double level): @@ -14,7 +12,7 @@ cdef inline double _get_fraction(double from_value, double to_value, return ((level - from_value) / (to_value - from_value)) -def iterate_and_store(cnp.ndarray[double, ndim=2] array, +def iterate_and_store(double[:, :] 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 @@ -46,7 +44,7 @@ def iterate_and_store(cnp.ndarray[double, ndim=2] array, # Calculate the number of iterations we'll need cdef Py_ssize_t num_square_steps = (array.shape[0] - 1) \ - * (array.shape[1] - 1) + * (array.shape[1] - 1) cdef unsigned char square_case = 0 cdef tuple top, bottom, left, right diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py new file mode 100644 index 00000000..c3ec9070 --- /dev/null +++ b/skimage/measure/_marching_cubes.py @@ -0,0 +1,157 @@ +import numpy as np +from . import _marching_cubes_cy + + +def marching_cubes(volume, level, spacing=(1., 1., 1.)): + """ + Marching cubes algorithm to find iso-valued surfaces in 3d volumetric data + + Parameters + ---------- + volume : (M, N, P) array of doubles + Input data volume to find isosurfaces. Will be cast to `np.float64`. + level : float + Contour value to search for isosurfaces in `volume`. + spacing : length-3 tuple of floats + Voxel spacing in spatial dimensions corresponding to numpy array + indexing dimensions (M, N, P) as in `volume`. + + Returns + ------- + verts : (V, 3) array + Spatial coordinates for V unique mesh vertices. Coordinate order + matches input `volume` (M, N, P). + faces : (F, 3) array + Define triangular faces via referencing vertex indices from ``verts``. + This algorithm specifically outputs triangles, so each face has + exactly three indices. + + Notes + ----- + The marching cubes algorithm is implemented as described in [1]_. + A simple explanation is available here:: + + http://www.essi.fr/~lingrand/MarchingCubes/algo.html + + There are several known ambiguous cases in the marching cubes algorithm. + Using point labeling as in [1]_, Figure 4, as shown:: + + v8 ------ v7 + / | / | y + / | / | ^ z + v4 ------ v3 | | / + | v5 ----|- v6 |/ (note: NOT right handed!) + | / | / ----> x + | / | / + v1 ------ v2 + + Most notably, if v4, v8, v2, and v6 are all >= `level` (or any + generalization of this case) two parallel planes are generated by this + algorithm, separating v4 and v8 from v2 and v6. An equally valid + interpretation would be a single connected thin surface enclosing all + four points. This is the best known ambiguity, though there are others. + + This algorithm does not attempt to resolve such ambiguities; it is a naive + implementation of marching cubes as in [1]_, but may be a good beginning + for work with more recent techniques (Dual Marching Cubes, Extended + Marching Cubes, Cubic Marching Squares, etc.). + + Because of interactions between neighboring cubes, the isosurface(s) + generated by this algorithm are NOT guaranteed to be closed, particularly + for complicated contours. Furthermore, this algorithm does not guarantee + a single contour will be returned. Indeed, ALL isosurfaces which cross + `level` will be found, regardless of connectivity. + + The output is a triangular mesh consisting of a set of unique vertices and + connecting triangles. The order of these vertices and triangles in the + output list is determined by the position of the smallest ``x,y,z`` (in + lexicographical order) coordinate in the contour. This is a side-effect + of how the input array is traversed, but can be relied upon. + + To quantify the area of an isosurface generated by this algorithm, pass + the outputs directly into `skimage.measure.mesh_surface_area`. + + Regarding visualization of algorithm output, the ``mayavi`` package + is recommended. To contour a volume named `myvolume` about the level 0.0:: + + >>> from mayavi import mlab + >>> verts, tris = marching_cubes(myvolume, 0.0, (1., 1., 2.)) + >>> mlab.triangular_mesh([vert[0] for vert in verts], + ... [vert[1] for vert in verts], + ... [vert[2] for vert in verts], + ... tris) + >>> mlab.show() + + References + ---------- + .. [1] Lorensen, William and Harvey E. Cline. Marching Cubes: A High + Resolution 3D Surface Construction Algorithm. Computer Graphics + (SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170). + + See Also + -------- + skimage.measure.mesh_surface_area + + """ + # Check inputs and ensure `volume` is C-contiguous for memoryviews + if volume.ndim != 3: + raise ValueError("Input volume must have 3 dimensions.") + if level < volume.min() or level > volume.max(): + raise ValueError("Contour level must be within volume data range.") + volume = np.array(volume, dtype=np.float64, order="C") + + # Extract raw triangles using marching cubes in Cython + # Returns a list of length-3 lists, each sub-list containing three + # tuples. The tuples hold (x, y, z) coordinates for triangle vertices. + # Note: this algorithm is fast, but returns degenerate "triangles" which + # have repeated vertices - and equivalent vertices are redundantly + # placed in every triangle they connect with. + raw_tris = _marching_cubes_cy.iterate_and_store_3d(volume, float(level), + spacing) + + # Find and collect unique vertices, storing triangle verts as indices. + # Returns a true mesh with no degenerate faces. + verts, faces = _marching_cubes_cy.unpack_unique_verts(raw_tris) + + return np.asarray(verts), np.asarray(faces) + + +def mesh_surface_area(verts, tris): + """ + Compute surface area, given vertices & triangular faces + + Parameters + ---------- + verts : (V, 3) array of floats + Array containing (x, y, z) coordinates for V unique mesh vertices. + faces : (F, 3) array of ints + List of length-3 lists of integers, referencing vertex coordinates as + provided in `verts` + + Returns + ------- + area : float + Surface area of mesh. Units now [coordinate units] ** 2. + + Notes + ----- + The arguments expected by this function are the exact outputs from + `skimage.measure.marching_cubes`. For unit correct output, ensure correct + `spacing` was passed to `skimage.measure.marching_cubes`. + + This algorithm works properly only if the ``faces`` provided are all + triangles. + + See Also + -------- + skimage.measure.marching_cubes + + """ + # Fancy indexing to define two vector arrays from triangle vertices + actual_verts = verts[tris] + a = actual_verts[:, 0, :] - actual_verts[:, 1, :] + b = actual_verts[:, 0, :] - actual_verts[:, 2, :] + del actual_verts + + # Area of triangle in 3D = 1/2 * Euclidean norm of cross product + return ((np.cross(a, b) ** 2).sum(axis=1) ** 0.5).sum() / 2. diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx new file mode 100644 index 00000000..085108ab --- /dev/null +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -0,0 +1,987 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +cimport numpy as cnp + + +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 unpack_unique_verts(list trilist): + """ + Convert a list of lists of tuples corresponding to triangle vertices + into a unique vertex list, and a list of triangle faces w/indices + corresponding to entries of the vertex list. + + """ + cdef Py_ssize_t idx = 0 + cdef Py_ssize_t n_tris = len(trilist) + cdef Py_ssize_t i, j + cdef dict vert_index = {} + cdef list vert_list = [] + cdef list face_list = [] + cdef list templist + + # Iterate over triangles + for i in range(n_tris): + templist = [] + + # Only parse vertices from non-degenerate triangles + if not ((trilist[i][0] == trilist[i][1]) or + (trilist[i][0] == trilist[i][2]) or + (trilist[i][1] == trilist[i][2])): + + # Iterate over vertices within each triangle + for j in range(3): + vert = trilist[i][j] + + # Check if a new unique vertex found + if vert not in vert_index: + vert_index[vert] = idx + templist.append(idx) + vert_list.append(vert) + idx += 1 + else: + templist.append(vert_index[vert]) + + face_list.append(templist) + + return vert_list, face_list + + +def iterate_and_store_3d(double[:, :, ::1] arr, double level, + tuple spacing=(1., 1., 1.)): + """Iterate across the given array in a marching-cubes fashion, + looking for volumes with edges that cross 'level'. If such a volume is + found, appropriate triangulations are added to a growing list of + faces to be returned by this function. + + If `spacing` is not provided, vertices are returned in the indexing + coordinate system (assuming all 3 spatial dimensions sampled equally). + If `spacing` is provided, vertices will be returned in volume coordinates + relative to the origin, regularly spaced as specified in each dimension. + + """ + if arr.shape[0] < 2 or arr.shape[1] < 2 or arr.shape[2] < 2: + raise ValueError("Input array must be at least 2x2x2.") + if len(spacing) != 3: + raise ValueError("`spacing` must be (double, double, double)") + + cdef list face_list = [] + cdef list norm_list = [] + cdef Py_ssize_t n + cdef bint odd_spacing, plus_z + plus_z = False + if [float(i) for i in spacing] == [1.0, 1.0, 1.0]: + odd_spacing = False + else: + odd_spacing = True + + # The plan is to iterate a 2x2x2 cube across the input array. This means + # the upper-left corner of the cube needs to iterate across a sub-array + # of size one-less-large in each direction (so we can get away with no + # bounds checking in Cython). The cube is represented by eight vertices: + # v1, v2, ..., v8, oriented thus (see Lorensen, Figure 4): + # + # v8 ------ v7 + # / | / | y + # / | / | ^ z + # v4 ------ v3 | | / + # | v5 ----|- v6 |/ (note: NOT right handed!) + # | / | / ----> x + # | / | / + # v1 ------ v2 + # + # We also maintain the current 2D coordinates for v1, and ensure the array + # is of type 'double' and is C-contiguous (last index varies fastest). + + # Coords start at (0, 0, 0). + cdef Py_ssize_t[3] coords + coords[0] = 0 + coords[1] = 0 + coords[2] = 0 + + # Extract doubles from `spacing` for speed + cdef double[3] spacing2 + spacing2[0] = spacing[0] + spacing2[1] = spacing[1] + spacing2[2] = spacing[2] + + # Calculate the number of iterations we'll need + cdef Py_ssize_t num_cube_steps = ((arr.shape[0] - 1) * + (arr.shape[1] - 1) * + (arr.shape[2] - 1)) + + cdef unsigned char cube_case = 0 + cdef tuple e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12 + cdef double v1, v2, v3, v4, v5, v6, v7, v8, r0, r1, c0, c1, d0, d1 + cdef Py_ssize_t x0, y0, z0, x1, y1, z1 + e5, e6, e7, e8 = (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0) + + for n in range(num_cube_steps): + # There are 255 unique values for `cube_case`. This algorithm follows + # the Lorensen paper in vertex and edge labeling, however, it should + # be noted that Lorensen used a left-handed coordinate system while + # NumPy uses a proper right handed system. Transforming between these + # coordinate systems was handled in the definitions of the cube + # vertices v1, v2, ..., v8. + # + # Refer to the paper, figure 4, for cube edge designations e1, ... e12 + + # Standard Py_ssize_t coordinates for indexing + x0, y0, z0 = coords[0], coords[1], coords[2] + x1, y1, z1 = x0 + 1, y0 + 1, z0 + 1 + + if odd_spacing: + # These doubles are the modified world coordinates; they are only + # calculated if non-default `spacing` provided. + r0 = coords[0] * spacing2[0] + c0 = coords[1] * spacing2[1] + d0 = coords[2] * spacing2[2] + r1 = r0 + spacing2[0] + c1 = c0 + spacing2[1] + d1 = d0 + spacing2[2] + else: + r0, c0, d0, r1, c1, d1 = x0, y0, z0, x1, y1, z1 + + # We use a right-handed coordinate system, UNlike the paper, but want + # to index in agreement - the coordinate adjustment takes place here. + v1 = arr[x0, y0, z0] + v2 = arr[x1, y0, z0] + v3 = arr[x1, y1, z0] + v4 = arr[x0, y1, z0] + v5 = arr[x0, y0, z1] + v6 = arr[x1, y0, z1] + v7 = arr[x1, y1, z1] + v8 = arr[x0, y1, z1] + + # Unique triangulation cases + cube_case = 0 + if (v1 > level): cube_case += 1 + if (v2 > level): cube_case += 2 + if (v3 > level): cube_case += 4 + if (v4 > level): cube_case += 8 + if (v5 > level): cube_case += 16 + if (v6 > level): cube_case += 32 + if (v7 > level): cube_case += 64 + if (v8 > level): cube_case += 128 + + if (cube_case != 0 and cube_case != 255): + # Only do anything if there's a plane intersecting the cube. + # Cases 0 and 255 are entirely below/above the contour. + + if cube_case > 127: + if ((cube_case != 150) and + (cube_case != 170) and + (cube_case != 195)): + cube_case = 255 - cube_case + + # Calculate cube edges, to become triangulation vertices. + # If we moved in a convenient direction, save 1/3 of the effort by + # re-assigning prior results. + if plus_z: + # Reassign prior calculated edges + e1 = e5 + e2 = e6 + e3 = e7 + e4 = e8 + else: + # Calculate edges normally + if odd_spacing: + e1 = r0 + _get_fraction(v1, v2, level) * spacing2[0], c0, d0 + e2 = r1, c0 + _get_fraction(v2, v3, level) * spacing2[1], d0 + e3 = r0 + _get_fraction(v4, v3, level) * spacing2[0], c1, d0 + e4 = r0, c0 + _get_fraction(v1, v4, level) * spacing2[1], d0 + else: + e1 = r0 + _get_fraction(v1, v2, level), c0, d0 + e2 = r1, c0 + _get_fraction(v2, v3, level), d0 + e3 = r0 + _get_fraction(v4, v3, level), c1, d0 + e4 = r0, c0 + _get_fraction(v1, v4, level), d0 + + # These must be calculated at each point unless we implemented a + # large, growing lookup table for all adjacent values; could save + # ~30% in terms of runtime at the expense of memory usage and + # much greater complexity. + if odd_spacing: + e5 = r0 + _get_fraction(v5, v6, level) * spacing2[0], c0, d1 + e6 = r1, c0 + _get_fraction(v6, v7, level) * spacing2[1], d1 + e7 = r0 + _get_fraction(v8, v7, level) * spacing2[0], c1, d1 + e8 = r0, c0 + _get_fraction(v5, v8, level) * spacing2[1], d1 + e9 = r0, c0, d0 + _get_fraction(v1, v5, level) * spacing2[2] + e10 = r1, c0, d0 + _get_fraction(v2, v6, level) * spacing2[2] + e11 = r0, c1, d0 + _get_fraction(v4, v8, level) * spacing2[2] + e12 = r1, c1, d0 + _get_fraction(v3, v7, level) * spacing2[2] + else: + e5 = r0 + _get_fraction(v5, v6, level), c0, d1 + e6 = r1, c0 + _get_fraction(v6, v7, level), d1 + e7 = r0 + _get_fraction(v8, v7, level), c1, d1 + e8 = r0, c0 + _get_fraction(v5, v8, level), d1 + e9 = r0, c0, d0 + _get_fraction(v1, v5, level) + e10 = r1, c0, d0 + _get_fraction(v2, v6, level) + e11 = r0, c1, d0 + _get_fraction(v4, v8, level) + e12 = r1, c1, d0 + _get_fraction(v3, v7, level) + + + # Append appropriate triangles to the growing output `face_list` + _append_tris(face_list, cube_case, e1, e2, e3, e4, e5, + e6, e7, e8, e9, e10, e11, e12) + + # Advance the coords indices + if coords[2] < arr.shape[2] - 2: + coords[2] += 1 + plus_z = True + elif coords[1] < arr.shape[1] - 2: + coords[1] += 1 + coords[2] = 0 + plus_z = False + else: + coords[0] += 1 + coords[1] = 0 + coords[2] = 0 + plus_z = False + + return face_list + + +def _append_tris(list face_list, unsigned char case, tuple e1, tuple e2, + tuple e3, tuple e4, tuple e5, tuple e6, tuple e7, tuple e8, + tuple e9, tuple e10, tuple e11, tuple e12): + # Permits recursive use for duplicated planes to conserve code - it's + # quite long enough as-is. + + if (case == 1): + # front lower left corner + face_list.append([e1, e4, e9]) + elif (case == 2): + # front lower right corner + face_list.append([e10, e2, e1]) + elif (case == 3): + # front lower plane + face_list.append([e2, e4, e9]) + face_list.append([e2, e9, e10]) + elif (case == 4): + # front upper right corner + face_list.append([e12, e3, e2]) + elif (case == 5): + # lower left, upper right corners + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 6): + # front right plane + face_list.append([e12, e3, e1]) + face_list.append([e12, e1, e10]) + elif (case == 7): + # Shelf including v1, v2, v3 + face_list.append([e3, e4, e12]) + face_list.append([e4, e9, e12]) + face_list.append([e12, e9, e10]) + elif (case == 8): + # front upper left corner + face_list.append([e3, e11, e4]) + elif (case == 9): + # front left plane + face_list.append([e3, e11, e9]) + face_list.append([e3, e9, e1]) + elif (case == 10): + # upper left, lower right corners + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 11): + # Shelf including v4, v1, v2 + face_list.append([e3, e11, e2]) + face_list.append([e11, e10, e2]) + face_list.append([e11, e9, e10]) + elif (case == 12): + # front upper plane + face_list.append([e11, e4, e12]) + face_list.append([e2, e4, e12]) + elif (case == 13): + # Shelf including v1, v4, v3 + face_list.append([e11, e9, e12]) + face_list.append([e12, e9, e1]) + face_list.append([e12, e1, e2]) + elif (case == 14): + # Shelf including v2, v3, v4 + face_list.append([e11, e10, e12]) + face_list.append([e11, e4, e10]) + face_list.append([e4, e1, e10]) + elif (case == 15): + # Plane parallel to x-axis through middle + face_list.append([e11, e9, e12]) + face_list.append([e12, e9, e10]) + elif (case == 16): + # back lower left corner + face_list.append([e8, e9, e5]) + elif (case == 17): + # lower left plane + face_list.append([e4, e1, e8]) + face_list.append([e8, e1, e5]) + elif (case == 18): + # lower left back, lower right front corners + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 19): + # Shelf including v1, v2, v5 + face_list.append([e8, e4, e2]) + face_list.append([e8, e2, e10]) + face_list.append([e8, e10, e5]) + elif (case == 20): + # lower left back, upper right front corners + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 21): + # lower left plane + upper right front corner, v1, v3, v5 + _append_tris(face_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 22): + # front right plane + lower left back corner, v2, v3, v5 + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 23): + # Rotated case 14 in the paper + face_list.append([e3, e10, e8]) + face_list.append([e3, e10, e12]) + face_list.append([e8, e10, e5]) + face_list.append([e3, e4, e8]) + elif (case == 24): + # upper front left, lower back left corners + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 25): + # Shelf including v1, v4, v5 + face_list.append([e1, e5, e3]) + face_list.append([e3, e8, e11]) + face_list.append([e3, e5, e8]) + elif (case == 26): + # Three isolated corners + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 27): + # Full corner v1, case 9 in paper: (v1, v2, v4, v5) + face_list.append([e11, e3, e2]) + face_list.append([e11, e2, e10]) + face_list.append([e10, e11, e8]) + face_list.append([e8, e5, e10]) + elif (case == 28): + # upper front plane + corner v5 + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 29): + # special case of 11 in the paper: (v1, v3, v4, v5) + face_list.append([e11, e5, e2]) + face_list.append([e11, e12, e2]) + face_list.append([e11, e5, e8]) + face_list.append([e2, e1, e5]) + elif (case == 30): + # Shelf (v2, v3, v4) and lower left back corner + _append_tris(face_list, 14, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 31): + # Shelf: (v6, v7, v8) by inversion + face_list.append([e11, e12, e10]) + face_list.append([e11, e8, e10]) + face_list.append([e8, e10, e5]) + elif (case == 32): + # lower right back corner + face_list.append([e6, e5, e10]) + elif (case == 33): + # lower right back, lower left front corners + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 34): + # lower right plane + face_list.append([e1, e2, e5]) + face_list.append([e2, e6, e5]) + elif (case == 35): + # Shelf: v1, v2, v6 + face_list.append([e4, e2, e6]) + face_list.append([e4, e9, e6]) + face_list.append([e6, e9, e5]) + elif (case == 36): + # upper right front, lower right back corners + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 37): + # lower left front, upper right front, lower right back corners + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 38): + # Shelf: v2, v3, v6 + face_list.append([e3, e1, e5]) + face_list.append([e3, e5, e12]) + face_list.append([e12, e5, e6]) + elif (case == 39): + # Full corner v2: (v1, v2, v3, v6) + face_list.append([e3, e4, e5]) + face_list.append([e4, e9, e5]) + face_list.append([e3, e5, e6]) + face_list.append([e3, e12, e6]) + elif (case == 40): + # upper left front, lower right back corners + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 41): + # front left plane, lower right back corner + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 42): + # lower right plane, upper front left corner + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 43): + # Rotated case 11 in paper + face_list.append([e11, e3, e9]) + face_list.append([e3, e9, e6]) + face_list.append([e3, e2, e6]) + face_list.append([e9, e5, e6]) + elif (case == 44): + # upper front plane, lower right back corner + _append_tris(face_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 45): + # Shelf: (v1, v3, v4) + lower right back corner + _append_tris(face_list, 13, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 46): + # Rotated case 14 in paper + face_list.append([e4, e11, e12]) + face_list.append([e4, e12, e5]) + face_list.append([e12, e5, e6]) + face_list.append([e4, e5, e1]) + elif (case == 47): + # Shelf: (v5, v8, v7) by inversion + face_list.append([e11, e9, e12]) + face_list.append([e12, e9, e5]) + face_list.append([e12, e5, e6]) + elif (case == 48): + # Back lower plane + face_list.append([e9, e10, e6]) + face_list.append([e9, e6, e8]) + elif (case == 49): + # Shelf: (v1, v5, v6) + face_list.append([e4, e8, e6]) + face_list.append([e4, e6, e1]) + face_list.append([e6, e1, e10]) + elif (case == 50): + # Shelf: (v2, v5, v6) + face_list.append([e8, e6, e2]) + face_list.append([e8, e2, e1]) + face_list.append([e8, e9, e1]) + elif (case == 51): + # Plane through middle of cube, parallel to x-z axis + face_list.append([e4, e8, e2]) + face_list.append([e8, e2, e6]) + elif (case == 52): + # Back lower plane, and front upper right corner + _append_tris(face_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 53): + # Shelf (v1, v5, v6) and front upper right corner + _append_tris(face_list, 49, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 54): + # Rotated case 11 from paper (v2, v3, v5, v6) + face_list.append([e1, e9, e3]) + face_list.append([e9, e3, e6]) + face_list.append([e9, e8, e6]) + face_list.append([e12, e3, e6]) + elif (case == 55): + # Shelf: (v4, v8, v7) by inversion + face_list.append([e4, e8, e6]) + face_list.append([e4, e6, e3]) + face_list.append([e6, e3, e12]) + elif (case == 56): + # Back lower plane + upper left front corner + _append_tris(face_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 57): + # Rotated case 14 from paper (v4, v1, v5, v6) + face_list.append([e3, e11, e8]) + face_list.append([e3, e8, e10]) + face_list.append([e10, e6, e8]) + face_list.append([e3, e1, e10]) + elif (case == 58): + # Shelf: (v2, v6, v5) + upper left front corner + _append_tris(face_list, 50, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 59): + # Shelf: (v3, v7, v8) by inversion + face_list.append([e2, e6, e8]) + face_list.append([e8, e2, e3]) + face_list.append([e8, e3, e11]) + elif (case == 60): + # AMBIGUOUS CASE: parallel planes (front upper, back lower) + _append_tris(face_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 61): + # Upper back plane + lower right front corner by inversion + _append_tris(face_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 62): + # Upper back plane + lower left front corner by inversion + _append_tris(face_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 63): + # Upper back plane + face_list.append([e11, e12, e6]) + face_list.append([e11, e8, e6]) + elif (case == 64): + # Upper right back corner + face_list.append([e12, e7, e6]) + elif (case == 65): + # upper right back, lower left front corners + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 66): + # upper right back, lower right front corners + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 67): + # lower front plane + upper right back corner + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 68): + # upper right plane + face_list.append([e3, e2, e6]) + face_list.append([e3, e7, e6]) + elif (case == 69): + # Upper right plane, lower left front corner + _append_tris(face_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 70): + # Shelf: (v2, v3, v7) + face_list.append([e1, e3, e7]) + face_list.append([e1, e10, e7]) + face_list.append([e7, e10, e6]) + elif (case == 71): + # Rotated version of case 11 in paper (v1, v2, v3, v7) + face_list.append([e10, e7, e4]) + face_list.append([e4, e3, e7]) + face_list.append([e10, e4, e9]) + face_list.append([e7, e10, e6]) + elif (case == 72): + # upper left front, upper right back corners + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 73): + # front left plane, upper right back corner + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 74): + # Three isolated corners, exactly case 7 in paper + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 75): + # Shelf: (v1, v2, v4) + upper right back corner + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 11, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 76): + # Shelf: (v4, v3, v7) + face_list.append([e4, e2, e6]) + face_list.append([e4, e11, e7]) + face_list.append([e4, e7, e6]) + elif (case == 77): + # Rotated case 14 in paper (v1, v4, v3, v7) + face_list.append([e11, e9, e1]) + face_list.append([e11, e1, e6]) + face_list.append([e1, e6, e2]) + face_list.append([e11, e6, e7]) + elif (case == 78): + # Full corner v3: (v2, v3, v4, v7) + face_list.append([e1, e4, e7]) + face_list.append([e1, e7, e6]) + face_list.append([e4, e11, e7]) + face_list.append([e1, e10, e6]) + elif (case == 79): + # Shelf: (v6, v5, v8) by inversion + face_list.append([e9, e11, e10]) + face_list.append([e11, e7, e10]) + face_list.append([e7, e10, e6]) + elif (case == 80): + # lower left back, upper right back corners (v5, v7) + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 81): + # lower left plane, upper right back corner + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 82): + # isolated corners (v2, v5, v7) + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 83): + # Shelf: (v1, v2, v5) + upper right back corner + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 19, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 84): + # upper right plane, lower left back corner + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 85): + # AMBIGUOUS CASE: upper right and lower left parallel planes + _append_tris(face_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 86): + # Shelf: (v2, v3, v7) + lower left back corner + _append_tris(face_list, 70, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 87): + # Upper left plane + lower right back corner, by inversion + _append_tris(face_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 88): + # Isolated corners v4, v5, v7 + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 89): + # Shelf: (v1, v4, v5) + isolated corner v7 + _append_tris(face_list, 25, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 90): + # Four isolated corners v2, v4, v5, v7 + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 91): + # Three isolated corners, v3, v6, v8 by inversion + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 92): + # Shelf (v4, v3, v7) + isolated corner v5 + _append_tris(face_list, 76, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 93): + # Lower right plane + isolated corner v8 by inversion + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 94): + # Isolated corners v1, v6, v8 by inversion + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 95): + # Isolated corners v6, v8 by inversion + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 96): + # back right plane + face_list.append([e7, e12, e5]) + face_list.append([e5, e10, e12]) + elif (case == 97): + # back right plane + isolated corner v1 + _append_tris(face_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 98): + # Shelf: (v2, v6, v7) + face_list.append([e1, e7, e5]) + face_list.append([e7, e1, e12]) + face_list.append([e1, e12, e2]) + elif (case == 99): + # Rotated case 14 in paper: (v1, v2, v6, v7) + face_list.append([e9, e2, e7]) + face_list.append([e9, e2, e4]) + face_list.append([e2, e7, e12]) + face_list.append([e7, e9, e5]) + elif (case == 100): + # Shelf: (v3, v6, v7) + face_list.append([e3, e7, e5]) + face_list.append([e3, e5, e2]) + face_list.append([e2, e5, e10]) + elif (case == 101): + # Shelf: (v3, v6, v7) + isolated corner v1 + _append_tris(face_list, 100, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 102): + # Plane bisecting left-right halves of cube + face_list.append([e1, e3, e7]) + face_list.append([e1, e7, e5]) + elif (case == 103): + # Shelf: (v4, v5, v8) by inversion + face_list.append([e3, e7, e5]) + face_list.append([e3, e5, e4]) + face_list.append([e4, e5, e9]) + elif (case == 104): + # Back right plane + isolated corner v4 + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 105): + # AMBIGUOUS CASE: back right and front left planes + _append_tris(face_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 106): + # Shelf: (v2, v6, v7) + isolated corner v4 + _append_tris(face_list, 98, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 107): + # Back left plane + isolated corner v3 by inversion + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 108): + # Rotated case 11 from paper: (v4, v3, v7, v6) + face_list.append([e4, e10, e7]) + face_list.append([e4, e10, e2]) + face_list.append([e4, e11, e7]) + face_list.append([e7, e10, e5]) + elif (case == 109): + # Back left plane + isolated corner v2 by inversion + _append_tris(face_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 110): + # Shelf: (v1, v5, v8) by inversion + face_list.append([e1, e5, e7]) + face_list.append([e1, e7, e11]) + face_list.append([e1, e11, e4]) + elif (case == 111): + # Back left plane + face_list.append([e11, e9, e7]) + face_list.append([e9, e7, e5]) + elif (case == 112): + # Shelf: (v5, v6, v7) + face_list.append([e9, e10, e12]) + face_list.append([e9, e12, e7]) + face_list.append([e9, e7, e8]) + elif (case == 113): + # Exactly case 11 from paper: (v1, v5, v6, v7) + face_list.append([e1, e8, e12]) + face_list.append([e1, e8, e4]) + face_list.append([e8, e7, e12]) + face_list.append([e12, e1, e10]) + elif (case == 114): + # Full corner v6: (v2, v6, v7, v5) + face_list.append([e1, e9, e7]) + face_list.append([e1, e7, e12]) + face_list.append([e1, e12, e2]) + face_list.append([e9, e8, e7]) + elif (case == 115): + # Shelf: (v3, v4, v8) + face_list.append([e2, e4, e8]) + face_list.append([e2, e12, e7]) + face_list.append([e2, e8, e7]) + elif (case == 116): + # Rotated case 14 in paper: (v5, v6, v7, v3) + face_list.append([e9, e2, e7]) + face_list.append([e9, e2, e10]) + face_list.append([e9, e8, e7]) + face_list.append([e2, e3, e7]) + elif (case == 117): + # upper left plane + isolated corner v2 by inversion + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 118): + # Shelf: (v1, v4, v8) + face_list.append([e1, e3, e7]) + face_list.append([e7, e1, e8]) + face_list.append([e1, e8, e9]) + elif (case == 119): + # Upper left plane + face_list.append([e4, e3, e7]) + face_list.append([e4, e8, e7]) + elif (case == 120): + # Shelf: (v5, v6, v7) + isolated corner v4 + _append_tris(face_list, 112, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 121): + # Front right plane + isolated corner v8 + _append_tris(face_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 122): + # Isolated corners v1, v3, v8 + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 123): + # Isolated corners v3, v8 + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 124): + # Front lower plane + isolated corner v8 + _append_tris(face_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 125): + # Isolated corners v2, v8 + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 126): + # Isolated corners v1, v8 + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 127): + # Isolated corner v8 + face_list.append([e11, e7, e8]) + elif (case == 150): + # AMBIGUOUS CASE: back right and front left planes + # In these cube_case > 127 cases, the vertices are identical BUT + # they are connected in the opposite fashion. + _append_tris(face_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 170): + # AMBIGUOUS CASE: upper left and lower right planes + # In these cube_case > 127 cases, the vertices are identical BUT + # they are connected in the opposite fashion. + _append_tris(face_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 195): + # AMBIGUOUS CASE: back upper and front lower planes + # In these cube_case > 127 cases, the vertices are identical BUT + # they are connected in the opposite fashion. + _append_tris(face_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + + return diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index 145f6052..6b7197d2 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -4,53 +4,178 @@ #cython: wraparound=False import numpy as np -cimport numpy as cnp + +def moments(double[:, :] image, Py_ssize_t order=3): + """Calculate all raw image moments up to a certain order. + + The following properties can be calculated from raw image moments: + * Area as ``m[0, 0]``. + * Centroid as {``m[0, 1] / m[0, 0]``, ``m[1, 0] / m[0, 0]``}. + + Note that raw moments are whether translation, scale nor rotation + invariant. + + Parameters + ---------- + image : 2D double array + Rasterized shape as image. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + m : (``order + 1``, ``order + 1``) array + Raw image moments. + + References + ---------- + .. [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 + + """ + return moments_central(image, 0, 0, order) -def central_moments(cnp.ndarray[cnp.double_t, ndim=2] array, double cr, - double cc, int order): +def moments_central(double[:, :] image, double cr, double cc, + Py_ssize_t order=3): + """Calculate all central image moments up to a certain order. + + Note that central moments are translation invariant but not scale and + rotation invariant. + + Parameters + ---------- + image : 2D double array + Rasterized shape as image. + cr : double + Center row coordinate. + cc : double + Center column coordinate. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + mu : (``order + 1``, ``order + 1``) array + Central image moments. + + References + ---------- + .. [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 + + """ 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') + cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) for p in range(order + 1): for q in range(order + 1): - for r in range(array.shape[0]): - for c in range(array.shape[1]): - mu[p,q] += array[r,c] * (r - cr) ** q * (c - cc) ** p - return mu + for r in range(image.shape[0]): + for c in range(image.shape[1]): + mu[p, q] += image[r, c] * (r - cr) ** q * (c - cc) ** p + return np.asarray(mu) -def normalized_moments(cnp.ndarray[cnp.double_t, ndim=2] mu, int order): +def moments_normalized(double[:, :] mu, Py_ssize_t order=3): + """Calculate all normalized central image moments up to a certain order. + + Note that normalized central moments are translation and scale invariant + but not rotation invariant. + + Parameters + ---------- + mu : (M, M) array + Central image moments, where M must be > ``order``. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + nu : (``order + 1``, ``order + 1``) array + Normalized central image moments. + + References + ---------- + .. [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 + + """ cdef Py_ssize_t p, q - cdef cnp.ndarray[cnp.double_t, ndim=2] nu - nu = np.zeros((order + 1, order + 1), 'double') + cdef double[:, ::1] nu = np.zeros((order + 1, order + 1), dtype=np.double) for p in range(order + 1): for q in range(order + 1): if p + q >= 2: - nu[p,q] = mu[p,q] / mu[0,0]**((p + q) / 2 + 1) + nu[p, q] = mu[p, q] / mu[0, 0] ** ((p + q) / 2 + 1) else: - nu[p,q] = np.nan - return nu + nu[p, q] = np.nan + return np.asarray(nu) -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] +def moments_hu(double[:, :] nu): + """Calculate Hu's set of image moments. + + Note that this set of moments is proofed to be translation, scale and + rotation invariant. + + Parameters + ---------- + nu : (M, M) array + Normalized central image moments, where M must be > 4. + + Returns + ------- + nu : (7, 1) array + Hu's set of image moments. + + References + ---------- + .. [1] M. K. Hu, "Visual Pattern Recognition by Moment Invariants", + IRE Trans. Info. Theory, vol. IT-8, pp. 179-187, 1962 + .. [2] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [3] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [4] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [5] http://en.wikipedia.org/wiki/Image_moment + + + """ + cdef double[::1] hu = np.zeros((7, ), dtype=np.double) + cdef double t0 = nu[3, 0] + nu[1, 2] + cdef double t1 = nu[2, 1] + nu[0, 3] cdef double q0 = t0 * t0 cdef double q1 = t1 * t1 - cdef double n4 = 4 * nu[1,1] - cdef double s = nu[2,0] + nu[0,2] - cdef double d = nu[2,0] - nu[0,2] + cdef double n4 = 4 * nu[1, 1] + cdef double s = nu[2, 0] + nu[0, 2] + cdef double d = nu[2, 0] - nu[0, 2] hu[0] = s - hu[1] = d * d + n4 * nu[1,1] + hu[1] = d * d + n4 * nu[1, 1] hu[3] = q0 + q1 hu[5] = d * (q0 - q1) + n4 * t0 * t1 t0 *= q0 - 3 * q1 t1 *= 3 * q0 - q1 - q0 = nu[3,0]- 3 * nu[1,2] - q1 = 3 * nu[2,1] - nu[0,3] + q0 = nu[3, 0]- 3 * nu[1, 2] + q1 = 3 * nu[2, 1] - nu[0, 3] hu[2] = q0 * q0 + q1 * q1 hu[4] = q0 * t0 + q1 * t1 hu[6] = q1 * t0 - q0 * t1 - return hu + return np.asarray(hu) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 72d1e2f4..798e04cc 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -1,60 +1,335 @@ # coding: utf-8 +import warnings from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage -from skimage.morphology import convex_hull_image -from . import _moments +from collections import MutableMapping + +from skimage.morphology import convex_hull_image, label +from skimage.measure import _moments -__all__ = ['regionprops'] +__all__ = ['regionprops', 'perimeter'] STREL_4 = np.array([[0, 1, 0], [1, 1, 1], - [0, 1, 0]]) -STREL_8 = np.ones((3, 3), 'int8') -PROPS = ( - 'Area', - 'BoundingBox', - 'CentralMoments', - 'Centroid', - 'ConvexArea', + [0, 1, 0]], dtype=np.uint8) +STREL_8 = np.ones((3, 3), dtype=np.uint8) +PROPS = { + 'Area': 'area', + 'BoundingBox': 'bbox', + 'CentralMoments': 'moments_central', + 'Centroid': 'centroid', + 'ConvexArea': 'convex_area', # 'ConvexHull', - 'ConvexImage', - 'Coordinates', - 'Eccentricity', - 'EquivDiameter', - 'EulerNumber', - 'Extent', + 'ConvexImage': 'convex_image', + 'Coordinates': 'coords', + 'Eccentricity': 'eccentricity', + 'EquivDiameter': 'equivalent_diameter', + 'EulerNumber': 'euler_number', + 'Extent': 'extent', # 'Extrema', - 'FilledArea', - 'FilledImage', - 'HuMoments', - 'Image', - 'MajorAxisLength', - 'MaxIntensity', - 'MeanIntensity', - 'MinIntensity', - 'MinorAxisLength', - 'Moments', - 'NormalizedMoments', - 'Orientation', - 'Perimeter', + 'FilledArea': 'filled_area', + 'FilledImage': 'filled_image', + 'HuMoments': 'moments_hu', + 'Image': 'image', + 'Label': 'label', + 'MajorAxisLength': 'major_axis_length', + 'MaxIntensity': 'max_intensity', + 'MeanIntensity': 'mean_intensity', + 'MinIntensity': 'min_intensity', + 'MinorAxisLength': 'minor_axis_length', + 'Moments': 'moments', + 'NormalizedMoments': 'moments_normalized', + 'Orientation': 'orientation', + 'Perimeter': 'perimeter', # 'PixelIdxList', # 'PixelList', - 'Solidity', + 'Solidity': 'solidity', # 'SubarrayIdx' - 'WeightedCentralMoments', - 'WeightedCentroid', - 'WeightedHuMoments', - 'WeightedMoments', - 'WeightedNormalizedMoments' -) + 'WeightedCentralMoments': 'weighted_moments_central', + 'WeightedCentroid': 'weighted_centroid', + 'WeightedHuMoments': 'weighted_moments_hu', + 'WeightedMoments': 'weighted_moments', + 'WeightedNormalizedMoments': 'weighted_moments_normalized' +} -def regionprops(label_image, properties=['Area', 'Centroid'], - intensity_image=None): +class _cached_property(object): + """Decorator to use a function as a cached property. + + The function is only called the first time and each successive call returns + the cached result of the first call. + + class Foo(object): + + @_cached_property + def foo(self): + return "Cached" + + class Foo(object): + + def __init__(self): + self._cache_active = False + + @_cached_property + def foo(self): + return "Not cached" + + Adapted from . + + """ + + def __init__(self, func, name=None, doc=None): + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func + + def __get__(self, obj, type=None): + if obj is None: + return self + + # call every time, if cache is not active + if not obj.__dict__.get('_cache_active', True): + return self.func(obj) + + # try to retrieve from cache or call and store result in cache + try: + value = obj.__dict__[self.__name__] + except KeyError: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + +class _RegionProperties(MutableMapping): + + def __init__(self, slice, label, label_image, intensity_image, + cache_active, properties=None): + self.label = label + self._slice = slice + self._label_image = label_image + self._intensity_image = intensity_image + self._cache_active = cache_active + self._properties = properties + + @_cached_property + def area(self): + return self.moments[0, 0] + + @_cached_property + def bbox(self): + return (self._slice[0].start, self._slice[1].start, + self._slice[0].stop, self._slice[1].stop) + + @_cached_property + def centroid(self): + row, col = self.local_centroid + return row + self._slice[0].start, col + self._slice[1].start + + @_cached_property + def convex_area(self): + return np.sum(self.convex_image) + + @_cached_property + def convex_image(self): + return convex_hull_image(self.image) + + @_cached_property + def coords(self): + rr, cc = np.nonzero(self.image) + return np.vstack((rr + self._slice[0].start, + cc + self._slice[1].start)).T + + @_cached_property + def eccentricity(self): + l1, l2 = self.inertia_tensor_eigvals + if l1 == 0: + return 0 + return sqrt(1 - l2 / l1) + + @_cached_property + def equivalent_diameter(self): + return sqrt(4 * self.moments[0, 0] / PI) + + @_cached_property + def euler_number(self): + euler_array = self.filled_image != self.image + _, num = label(euler_array, neighbors=8, return_num=True) + return -num + 1 + + @_cached_property + def extent(self): + rows, cols = self.image.shape + return self.moments[0, 0] / (rows * cols) + + @_cached_property + def filled_area(self): + return np.sum(self.filled_image) + + @_cached_property + def filled_image(self): + return ndimage.binary_fill_holes(self.image, STREL_8) + + @_cached_property + def image(self): + return self._label_image[self._slice] == self.label + + @_cached_property + def _image_double(self): + return self.image.astype(np.double) + + @_cached_property + def inertia_tensor(self): + mu = self.moments_central + a = mu[2, 0] / mu[0, 0] + b = -mu[1, 1] / mu[0, 0] + c = mu[0, 2] / mu[0, 0] + return np.array([[a, b], [b, c]]) + + @_cached_property + def inertia_tensor_eigvals(self): + a, b, b, c = self.inertia_tensor.flat + # 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 + return l1, l2 + + @_cached_property + def intensity_image(self): + if self._intensity_image is None: + raise AttributeError('No intensity image specified.') + return self._intensity_image[self._slice] * self.image + + @_cached_property + def _intensity_image_double(self): + return self.intensity_image.astype(np.double) + + @_cached_property + def local_centroid(self): + m = self.moments + row = m[0, 1] / m[0, 0] + col = m[1, 0] / m[0, 0] + return row, col + + @_cached_property + def max_intensity(self): + return np.max(self.intensity_image[self.image]) + + @_cached_property + def mean_intensity(self): + return np.mean(self.intensity_image[self.image]) + + @_cached_property + def min_intensity(self): + return np.min(self.intensity_image[self.image]) + + @_cached_property + def major_axis_length(self): + l1, _ = self.inertia_tensor_eigvals + return 4 * sqrt(l1) + + @_cached_property + def minor_axis_length(self): + _, l2 = self.inertia_tensor_eigvals + return 4 * sqrt(l2) + + @_cached_property + def moments(self): + return _moments.moments(self._image_double, 3) + + @_cached_property + def moments_central(self): + row, col = self.local_centroid + return _moments.moments_central(self._image_double, row, col, 3) + + @_cached_property + def moments_hu(self): + return _moments.moments_hu(self.moments_normalized) + + @_cached_property + def moments_normalized(self): + return _moments.moments_normalized(self.moments_central, 3) + + @_cached_property + def orientation(self): + a, b, b, c = self.inertia_tensor.flat + b = -b + if a - c == 0: + if b > 0: + return -PI / 4. + else: + return PI / 4. + else: + return - 0.5 * atan2(2 * b, (a - c)) + + @_cached_property + def perimeter(self): + return perimeter(self.image, 4) + + @_cached_property + def solidity(self): + return self.moments[0, 0] / np.sum(self.convex_image) + + @_cached_property + def weighted_centroid(self): + row, col = self.weighted_local_centroid + return row + self._slice[0].start, col + self._slice[1].start + + @_cached_property + def weighted_local_centroid(self): + m = self.weighted_moments + row = m[0, 1] / m[0, 0] + col = m[1, 0] / m[0, 0] + return row, col + + @_cached_property + def weighted_moments(self): + return _moments.moments_central(self._intensity_image_double, 0, 0, 3) + + @_cached_property + def weighted_moments_central(self): + row, col = self.weighted_local_centroid + return _moments.moments_central(self._intensity_image_double, + row, col, 3) + + @_cached_property + def weighted_moments_hu(self): + return _moments.moments_hu(self.weighted_moments_normalized) + + @_cached_property + def weighted_moments_normalized(self): + return _moments.moments_normalized(self.weighted_moments_central, 3) + + + # Preserve dictionary interface + def __delitem__(self, key): + pass + + def __len__(self): + return len(self._properties or PROPS.values()) + + def __setitem__(self, key, value): + raise RuntimeError("Cannot assign region properties.") + + def __iter__(self): + return iter(self._properties or PROPS.values()) + + def __getitem__(self, key): + value = getattr(self, key, None) + if value is not None: + return value + else: # backwards compatability + warnings.warn('Usage of deprecated property name.', + category=DeprecationWarning) + return getattr(self, PROPS[key]) + + +def regionprops(label_image, properties=None, + intensity_image=None, cache=True): """Measure properties of labelled image regions. Parameters @@ -62,150 +337,134 @@ def regionprops(label_image, properties=['Area', 'Centroid'], 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: + **Deprecated parameter** - * Area : int - Number of pixels of region. - - * BoundingBox : tuple - Bounding box `(min_row, min_col, max_row, max_col)` - - * 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, 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, 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, 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, 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, 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. - - * 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, 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, 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, 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). + This parameter is not needed any more since all properties are + determined dynamically. intensity_image : (N, M) ndarray, optional Intensity image with same size as labelled image. Default is None. + cache : bool, optional + Determine whether to cache calculated properties. The computation is + much faster for cached properties, whereas the memory consumption + increases. Returns ------- - properties : list of dicts - List containing a property dict for each region. The property dicts - contain all the specified properties plus a 'Label' field. + properties : list + List containing a properties for each region. The properties of each + region can be accessed as attributes and keys. + + Notes + ----- + The following properties can be accessed as attributes or keys: + + **area** : int + Number of pixels of region. + **bbox** : tuple + Bounding box ``(min_row, min_col, max_row, max_col)`` + **centroid** : array + Centroid coordinate tuple ``(row, col)``. + **convex_area** : int + Number of pixels of convex hull image. + **convex_image** : (H, J) ndarray + Binary convex hull image which has the same size as bounding box. + **coords** : (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. + **equivalent_diameter** : float + The diameter of a circle with the same area as the region. + **euler_number** : 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)`` + **filled_area** : int + Number of pixels of filled region. + **filled_image** : (H, J) ndarray + Binary region image with filled holes which has the same size as + bounding box. + **image** : (H, J) ndarray + Sliced binary region image which has the same size as bounding box. + **inertia_tensor** : (2, 2) ndarray + Inertia tensor of the region for the rotation around its mass. + **inertia_tensor_eigvals** : tuple + The two eigen values of the inertia tensor in decreasing order. + **label** : int + The label in the labeled input image. + **major_axis_length** : float + The length of the major axis of the ellipse that has the same + normalized second central moments as the region. + **min_intensity** : float + Value with the greatest intensity in the region. + **mean_intensity** : float + Value with the mean intensity in the region. + **min_intensity** : float + Value with the least intensity in the region. + **minor_axis_length** : float + The length of the minor axis of the ellipse that has the same + normalized second central moments as the region. + **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. + **moments_central** : (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. + **moments_hu** : tuple + Hu moments (translation, scale and rotation invariant). + **moments_normalized** : (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. + **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. + **weighted_centroid** : array + Centroid coordinate tuple ``(row, col)`` weighted with intensity + image. + **weighted_moments** : (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. + **weighted_moments_central** : (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. + **weighted_moments_hu** : tuple + Hu moments (translation, scale and rotation invariant) of intensity + image. + **weighted_moments_normalized** : (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). References ---------- @@ -225,194 +484,32 @@ def regionprops(label_image, properties=['Area', 'Centroid'], >>> img = coins() > 110 >>> label_img = label(img) >>> props = regionprops(label_img) - >>> props[0]['Centroid'] # centroid of first labelled object + >>> props[0].centroid # centroid of first labelled object + >>> props[0]['centroid'] # centroid of first labelled object """ - if not np.issubdtype(label_image.dtype, 'int'): - raise TypeError('labelled image must be of integer dtype') - # determine all properties if nothing specified - if properties == 'all': - properties = PROPS + label_image = np.squeeze(label_image) - props = [] + if label_image.ndim != 2: + raise TypeError('Only 2-D images supported.') + + if properties is not None: + warnings.warn('The ``properties`` argument is deprecated and is ' + 'not needed any more as properties are ' + 'determined dynamically.', + category=DeprecationWarning) + + regions = [] objects = ndimage.find_objects(label_image) for i, sl in enumerate(objects): label = i + 1 - # create property dict for current label - obj_props = {} - props.append(obj_props) + props = _RegionProperties(sl, label, label_image, + intensity_image, cache, properties=properties) + regions.append(props) - obj_props['Label'] = label - - array = (label_image[sl] == label).astype('double') - - # upper left corner of object bbox - r0 = sl[0].start - c0 = sl[1].start - - m = _moments.central_moments(array, 0, 0, 3) - # centroid - 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 - l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 - l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 - - # cached results which are used by several properties - _filled_image = None - _convex_image = None - _nu = None - - if 'Area' in properties: - obj_props['Area'] = m[0, 0] - - if 'BoundingBox' in properties: - obj_props['BoundingBox'] = (r0, c0, sl[0].stop, sl[1].stop) - - if 'Centroid' in properties: - obj_props['Centroid'] = cr + r0, cc + c0 - - if 'CentralMoments' in properties: - obj_props['CentralMoments'] = mu - - if 'ConvexArea' in properties: - if _convex_image is None: - _convex_image = convex_hull_image(array) - obj_props['ConvexArea'] = np.sum(_convex_image) - - if 'ConvexImage' in properties: - if _convex_image is None: - _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 - else: - obj_props['Eccentricity'] = sqrt(1 - l2 / l1) - - if 'EquivDiameter' in properties: - 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 - - if 'Extent' in properties: - obj_props['Extent'] = m[0, 0] / (array.shape[0] * array.shape[1]) - - if 'HuMoments' in properties: - if _nu is None: - _nu = _moments.normalized_moments(mu, 3) - obj_props['HuMoments'] = _moments.hu_moments(_nu) - - if 'Image' in properties: - obj_props['Image'] = array - - if 'FilledArea' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - obj_props['FilledArea'] = np.sum(_filled_image) - - if 'FilledImage' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - obj_props['FilledImage'] = _filled_image - - if 'MajorAxisLength' in properties: - obj_props['MajorAxisLength'] = 4 * sqrt(l1) - - if 'MinorAxisLength' in properties: - obj_props['MinorAxisLength'] = 4 * sqrt(l2) - - if 'Moments' in properties: - obj_props['Moments'] = m - - if 'NormalizedMoments' in properties: - if _nu is None: - _nu = _moments.normalized_moments(mu, 3) - obj_props['NormalizedMoments'] = _nu - - if 'Orientation' in properties: - if a - c == 0: - if b > 0: - obj_props['Orientation'] = -PI / 4. - else: - obj_props['Orientation'] = PI / 4. - else: - 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) - - 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] - wmu = _moments.central_moments(weighted_array, wcr, wcc, 3) - - # cached results which are used by several properties - _wnu = None - _vals = None - - if 'MaxIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MaxIntensity'] = np.max(_vals) - - if 'MeanIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MeanIntensity'] = np.mean(_vals) - - if 'MinIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MinIntensity'] = np.min(_vals) - - if 'WeightedCentralMoments' in properties: - obj_props['WeightedCentralMoments'] = wmu - - if 'WeightedCentroid' in properties: - obj_props['WeightedCentroid'] = wcr + r0, wcc + c0 - - if 'WeightedHuMoments' in properties: - if _wnu is None: - _wnu = _moments.normalized_moments(wmu, 3) - obj_props['WeightedHuMoments'] = _moments.hu_moments(_wnu) - - if 'WeightedMoments' in properties: - obj_props['WeightedMoments'] = wm - - if 'WeightedNormalizedMoments' in properties: - if _wnu is None: - _wnu = _moments.normalized_moments(wmu, 3) - obj_props['WeightedNormalizedMoments'] = _wnu - - return props + return regions def perimeter(image, neighbourhood=4): @@ -421,14 +518,14 @@ def perimeter(image, neighbourhood=4): Parameters ---------- image : array - binary image + Binary image. neighbourhood : 4 or 8, optional - neighbourhood connectivity for border pixel determination, default 4 + Neighborhood connectivity for border pixel determination. Returns ------- perimeter : float - total perimeter of all objects in binary image + Total perimeter of all objects in binary image. References ---------- @@ -440,24 +537,25 @@ def perimeter(image, neighbourhood=4): strel = STREL_4 else: strel = STREL_8 + image = image.astype(np.uint8) 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_weights = np.zeros(50, dtype=np.double) + perimeter_weights[[5, 7, 15, 17, 25, 27]] = 1 + perimeter_weights[[21, 33]] = sqrt(2) + perimeter_weights[[13, 23]] = (1 + sqrt(2)) / 2 + + 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 + # You can also write + # return perimeter_weights[perimeter_image].sum() + # but that was measured as taking much longer than bincount + np.dot (5x + # as much time) + perimeter_histogram = np.bincount(perimeter_image.ravel(), minlength=50) + total_perimeter = np.dot(perimeter_histogram, perimeter_weights) return total_perimeter diff --git a/skimage/measure/block.py b/skimage/measure/block.py new file mode 100644 index 00000000..fad5668c --- /dev/null +++ b/skimage/measure/block.py @@ -0,0 +1,77 @@ +import numpy as np +from skimage.util import view_as_blocks, pad + + +def block_reduce(image, block_size, func=np.sum, cval=0): + """Down-sample image by applying function to local blocks. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + block_size : array_like + Array containing down-sampling integer factor along each axis. + func : callable + Function object which is used to calculate the return value for each + local block. This function must implement an ``axis`` parameter such as + ``numpy.sum`` or ``numpy.min``. + cval : float + Constant padding value if image is not perfectly divisible by the + block size. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Examples + -------- + >>> from skimage.measure import block_reduce + >>> image = np.arange(3*3*4).reshape(3, 3, 4) + >>> 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]]]) + >>> block_reduce(image, block_size=(3, 3, 1), func=np.mean) + array([[[ 16., 17., 18., 19.]]]) + >>> block_reduce(image, block_size=(1, 3, 4), func=np.max) + array([[[11]], + + [[23]], + + [[35]]]) + >>> block_reduce(image, block_size=(3, 1, 4), func=np.max) + array([[[27], + [31], + [35]]]) + """ + + if len(block_size) != image.ndim: + raise ValueError("`block_size` must have the same length " + "as `image.shape`.") + + pad_width = [] + for i in range(len(block_size)): + if image.shape[i] % block_size[i] != 0: + after_width = block_size[i] - (image.shape[i] % block_size[i]) + else: + after_width = 0 + pad_width.append((0, after_width)) + + image = pad(image, pad_width=pad_width, mode='constant', + constant_values=cval) + + out = view_as_blocks(image, block_size) + + for i in range(len(out.shape) // 2): + out = func(out, axis=-1) + + return out diff --git a/skimage/measure/find_contours.py b/skimage/measure/find_contours.py index 3a546ccf..d36c2110 100755 --- a/skimage/measure/find_contours.py +++ b/skimage/measure/find_contours.py @@ -95,6 +95,17 @@ def find_contours(array, level, Resolution 3D Surface Construction Algorithm. Computer Graphics (SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170). + Examples + -------- + >>> a = np.zeros((3, 3)) + >>> a[0, 0] = 1 + >>> a + array([[ 1., 0., 0.], + [ 0., 0., 0.], + [ 0., 0., 0.]]) + >>> find_contours(a, 0.5) + [array([[ 0. , 0.5], + [ 0.5, 0. ]])] """ array = np.asarray(array, dtype=np.double) if array.ndim != 2: @@ -105,7 +116,7 @@ def find_contours(array, level, 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') + fully_connected == 'high') contours = _assemble_contours(_take_2(point_list)) if positive_orientation == 'high': contours = [c[::-1] for c in contours] @@ -115,8 +126,8 @@ def find_contours(array, level, def _take_2(seq): iterator = iter(seq) while(True): - n1 = iterator.next() - n2 = iterator.next() + n1 = next(iterator) + n2 = next(iterator) yield (n1, n2) diff --git a/skimage/measure/fit.py b/skimage/measure/fit.py new file mode 100644 index 00000000..66502ffa --- /dev/null +++ b/skimage/measure/fit.py @@ -0,0 +1,653 @@ +import math +import numpy as np +from scipy import optimize + + +def _check_data_dim(data, dim): + if data.ndim != 2 or data.shape[1] != dim: + raise ValueError('Input data must have shape (N, %d).' % dim) + + +class BaseModel(object): + + def __init__(self): + self._params = None + + +class LineModel(BaseModel): + + """Total least squares estimator for 2D lines. + + Lines are parameterized using polar coordinates as functional model:: + + dist = x * cos(theta) + y * sin(theta) + + This parameterization is able to model vertical lines in contrast to the + standard line model ``y = a*x + b``. + + This estimator minimizes the squared distances from all points to the + line:: + + min{ sum((dist - x_i * cos(theta) + y_i * sin(theta))**2) } + + The ``_params`` attribute contains the parameters in the following order:: + + dist, theta + + A minimum number of 2 points is required to solve for the parameters. + + """ + + def estimate(self, data): + """Estimate line model from data using total least squares. + + Parameters + ---------- + data : (N, 2) array + N points with ``(x, y)`` coordinates, respectively. + + """ + + _check_data_dim(data, dim=2) + + X0 = data.mean(axis=0) + + if data.shape[0] == 2: # well determined + theta = np.arctan2(data[1, 1] - data[0, 1], + data[1, 0] - data[0, 0]) + elif data.shape[0] > 2: # over-determined + data = data - X0 + # first principal component + _, _, v = np.linalg.svd(data) + theta = np.arctan2(v[0, 1], v[0, 0]) + else: # under-determined + raise ValueError('At least 2 input points needed.') + + # angle perpendicular to line angle + theta = (theta + np.pi / 2) % np.pi + # line always passes through mean + dist = X0[0] * math.cos(theta) + X0[1] * math.sin(theta) + + self._params = (dist, theta) + + def residuals(self, data): + """Determine residuals of data to model. + + For each point the shortest distance to the line is returned. + + Parameters + ---------- + data : (N, 2) array + N points with ``(x, y)`` coordinates, respectively. + + Returns + ------- + residuals : (N, ) array + Residual for each data point. + + """ + + _check_data_dim(data, dim=2) + + dist, theta = self._params + + x = data[:, 0] + y = data[:, 1] + + return dist - (x * math.cos(theta) + y * math.sin(theta)) + + def predict_x(self, y, params=None): + """Predict x-coordinates using the estimated model. + + Parameters + ---------- + y : array + y-coordinates. + params : (2, ) array, optional + Optional custom parameter set. + + Returns + ------- + x : array + Predicted x-coordinates. + + """ + + if params is None: + params = self._params + dist, theta = params + return (dist - y * math.sin(theta)) / math.cos(theta) + + def predict_y(self, x, params=None): + """Predict y-coordinates using the estimated model. + + Parameters + ---------- + x : array + x-coordinates. + params : (2, ) array, optional + Optional custom parameter set. + + Returns + ------- + y : array + Predicted y-coordinates. + + """ + + if params is None: + params = self._params + dist, theta = params + return (dist - x * math.cos(theta)) / math.sin(theta) + + +class CircleModel(BaseModel): + + """Total least squares estimator for 2D circles. + + The functional model of the circle is:: + + r**2 = (x - xc)**2 + (y - yc)**2 + + This estimator minimizes the squared distances from all points to the + circle:: + + min{ sum((r - sqrt((x_i - xc)**2 + (y_i - yc)**2))**2) } + + The ``_params`` attribute contains the parameters in the following order:: + + xc, yc, r + + A minimum number of 3 points is required to solve for the parameters. + + """ + + def estimate(self, data): + """Estimate circle model from data using total least squares. + + Parameters + ---------- + data : (N, 2) array + N points with ``(x, y)`` coordinates, respectively. + + """ + + _check_data_dim(data, dim=2) + + x = data[:, 0] + y = data[:, 1] + # pre-allocate jacobian for all iterations + A = np.zeros((3, data.shape[0]), dtype=np.double) + # same for all iterations: r + A[2, :] = -1 + + def dist(xc, yc): + return np.sqrt((x - xc)**2 + (y - yc)**2) + + def fun(params): + xc, yc, r = params + return dist(xc, yc) - r + + def Dfun(params): + xc, yc, r = params + d = dist(xc, yc) + A[0, :] = -(x - xc) / d + A[1, :] = -(y - yc) / d + # same for all iterations, so not changed in each iteration + #A[2, :] = -1 + return A + + xc0 = x.mean() + yc0 = y.mean() + r0 = dist(xc0, yc0).mean() + params0 = (xc0, yc0, r0) + params, _ = optimize.leastsq(fun, params0, Dfun=Dfun, col_deriv=True) + + self._params = params + + def residuals(self, data): + """Determine residuals of data to model. + + For each point the shortest distance to the circle is returned. + + Parameters + ---------- + data : (N, 2) array + N points with ``(x, y)`` coordinates, respectively. + + Returns + ------- + residuals : (N, ) array + Residual for each data point. + + """ + + _check_data_dim(data, dim=2) + + xc, yc, r = self._params + + x = data[:, 0] + y = data[:, 1] + + return r - np.sqrt((x - xc)**2 + (y - yc)**2) + + def predict_xy(self, t, params=None): + """Predict x- and y-coordinates using the estimated model. + + Parameters + ---------- + t : array + Angles in circle in radians. Angles start to count from positive + x-axis to positive y-axis in a right-handed system. + params : (3, ) array, optional + Optional custom parameter set. + + Returns + ------- + xy : (..., 2) array + Predicted x- and y-coordinates. + + """ + if params is None: + params = self._params + xc, yc, r = params + + x = xc + r * np.cos(t) + y = yc + r * np.sin(t) + + return np.concatenate((x[..., None], y[..., None]), axis=t.ndim) + + +class EllipseModel(BaseModel): + + """Total least squares estimator for 2D ellipses. + + The functional model of the ellipse is:: + + xt = xc + a*cos(theta)*cos(t) - b*sin(theta)*sin(t) + yt = yc + a*sin(theta)*cos(t) + b*cos(theta)*sin(t) + d = sqrt((x - xt)**2 + (y - yt)**2) + + where ``(xt, yt)`` is the closest point on the ellipse to ``(x, y)``. Thus + d is the shortest distance from the point to the ellipse. + + This estimator minimizes the squared distances from all points to the + ellipse:: + + min{ sum(d_i**2) } = min{ sum((x_i - xt)**2 + (y_i - yt)**2) } + + Thus you have ``2 * N`` equations (x_i, y_i) for ``N + 5`` unknowns (t_i, + xc, yc, a, b, theta), which gives you an effective redundancy of ``N - 5``. + + The ``_params`` attribute contains the parameters in the following order:: + + xc, yc, a, b, theta + + A minimum number of 5 points is required to solve for the parameters. + + """ + + def estimate(self, data): + """Estimate circle model from data using total least squares. + + Parameters + ---------- + data : (N, 2) array + N points with ``(x, y)`` coordinates, respectively. + + """ + + _check_data_dim(data, dim=2) + + x = data[:, 0] + y = data[:, 1] + + N = data.shape[0] + + # pre-allocate jacobian for all iterations + A = np.zeros((N + 5, 2 * N), dtype=np.double) + # same for all iterations: xc, yc + A[0, :N] = -1 + A[1, N:] = -1 + + diag_idxs = np.diag_indices(N) + + def fun(params): + xyt = self.predict_xy(params[5:], params[:5]) + fx = x - xyt[:, 0] + fy = y - xyt[:, 1] + return np.append(fx, fy) + + def Dfun(params): + xc, yc, a, b, theta = params[:5] + t = params[5:] + + ct = np.cos(t) + st = np.sin(t) + ctheta = math.cos(theta) + stheta = math.sin(theta) + + # derivatives for fx, fy in the following order: + # xc, yc, a, b, theta, t_i + + # fx + A[2, :N] = - ctheta * ct + A[3, :N] = stheta * st + A[4, :N] = a * stheta * ct + b * ctheta * st + A[5:, :N][diag_idxs] = a * ctheta * st + b * stheta * ct + # fy + A[2, N:] = - stheta * ct + A[3, N:] = - ctheta * st + A[4, N:] = - a * ctheta * ct + b * stheta * st + A[5:, N:][diag_idxs] = a * stheta * st - b * ctheta * ct + + return A + + # initial guess of parameters using a circle model + params0 = np.empty((N + 5, ), dtype=np.double) + xc0 = x.mean() + yc0 = y.mean() + r0 = np.sqrt((x - xc0)**2 + (y - yc0)**2).mean() + params0[:5] = (xc0, yc0, r0, 0, 0) + params0[5:] = np.arctan2(y - yc0, x - xc0) + + params, _ = optimize.leastsq(fun, params0, Dfun=Dfun, col_deriv=True) + + self._params = params[:5] + + def residuals(self, data): + """Determine residuals of data to model. + + For each point the shortest distance to the ellipse is returned. + + Parameters + ---------- + data : (N, 2) array + N points with ``(x, y)`` coordinates, respectively. + + Returns + ------- + residuals : (N, ) array + Residual for each data point. + + """ + + _check_data_dim(data, dim=2) + + xc, yc, a, b, theta = self._params + + ctheta = math.cos(theta) + stheta = math.sin(theta) + + x = data[:, 0] + y = data[:, 1] + + N = data.shape[0] + + def fun(t, xi, yi): + ct = math.cos(t) + st = math.sin(t) + xt = xc + a * ctheta * ct - b * stheta * st + yt = yc + a * stheta * ct + b * ctheta * st + return (xi - xt)**2 + (yi - yt)**2 + + # def Dfun(t, xi, yi): + # ct = math.cos(t) + # st = math.sin(t) + # xt = xc + a * ctheta * ct - b * stheta * st + # yt = yc + a * stheta * ct + b * ctheta * st + # dfx_t = - 2 * (xi - xt) * (- a * ctheta * st + # - b * stheta * ct) + # dfy_t = - 2 * (yi - yt) * (- a * stheta * st + # + b * ctheta * ct) + # return [dfx_t + dfy_t] + + residuals = np.empty((N, ), dtype=np.double) + + # initial guess for parameter t of closest point on ellipse + t0 = np.arctan2(y - yc, x - xc) - theta + + # determine shortest distance to ellipse for each point + for i in range(N): + xi = x[i] + yi = y[i] + # faster without Dfun, because of the python overhead + t, _ = optimize.leastsq(fun, t0[i], args=(xi, yi)) + residuals[i] = np.sqrt(fun(t, xi, yi)) + + return residuals + + def predict_xy(self, t, params=None): + """Predict x- and y-coordinates using the estimated model. + + Parameters + ---------- + t : array + Angles in circle in radians. Angles start to count from positive + x-axis to positive y-axis in a right-handed system. + params : (5, ) array, optional + Optional custom parameter set. + + Returns + ------- + xy : (..., 2) array + Predicted x- and y-coordinates. + + """ + + if params is None: + params = self._params + xc, yc, a, b, theta = params + + ct = np.cos(t) + st = np.sin(t) + ctheta = math.cos(theta) + stheta = math.sin(theta) + + x = xc + a * ctheta * ct - b * stheta * st + y = yc + a * stheta * ct + b * ctheta * st + + return np.concatenate((x[..., None], y[..., None]), axis=t.ndim) + + +def ransac(data, model_class, min_samples, residual_threshold, + is_data_valid=None, is_model_valid=None, + max_trials=100, stop_sample_num=np.inf, stop_residuals_sum=0): + """Fit a model to data with the RANSAC (random sample consensus) algorithm. + + RANSAC is an iterative algorithm for the robust estimation of parameters + from a subset of inliers from the complete data set. Each iteration + performs the following tasks: + + 1. Select `min_samples` random samples from the original data and check + whether the set of data is valid (see `is_data_valid`). + 2. Estimate a model to the random subset + (`model_cls.estimate(*data[random_subset]`) and check whether the + estimated model is valid (see `is_model_valid`). + 3. Classify all data as inliers or outliers by calculating the residuals + to the estimated model (`model_cls.residuals(*data)`) - all data samples + with residuals smaller than the `residual_threshold` are considered as + inliers. + 4. Save estimated model as best model if number of inlier samples is + maximal. In case the current estimated model has the same number of + inliers, it is only considered as the best model if it has less sum of + residuals. + + These steps are performed either a maximum number of times or until one of + the special stop criteria are met. The final model is estimated using all + inlier samples of the previously determined best model. + + Parameters + ---------- + data : [list, tuple of] (N, D) array + Data set to which the model is fitted, where N is the number of data + points and D the dimensionality of the data. + If the model class requires multiple input data arrays (e.g. source and + destination coordinates of ``skimage.transform.AffineTransform``), + they can be optionally passed as tuple or list. Note, that in this case + the functions ``estimate(*data)``, ``residuals(*data)``, + ``is_model_valid(model, *random_data)`` and + ``is_data_valid(*random_data)`` must all take each data array as + separate arguments. + model_class : object + Object with the following object methods: + + * ``estimate(*data)`` + * ``residuals(*data)`` + + min_samples : int + The minimum number of data points to fit a model to. + residual_threshold : float + Maximum distance for a data point to be classified as an inlier. + is_data_valid : function, optional + This function is called with the randomly selected data before the + model is fitted to it: `is_data_valid(*random_data)`. + is_model_valid : function, optional + This function is called with the estimated model and the randomly + selected data: `is_model_valid(model, *random_data)`, . + max_trials : int, optional + Maximum number of iterations for random sample selection. + stop_sample_num : int, optional + Stop iteration if at least this number of inliers are found. + stop_residuals_sum : float, optional + Stop iteration if sum of residuals is less equal than this threshold. + + Returns + ------- + model : object + Best model with largest consensus set. + inliers : (N, ) array + Boolean mask of inliers classified as ``True``. + + References + ---------- + .. [1] "RANSAC", Wikipedia, http://en.wikipedia.org/wiki/RANSAC + + Examples + -------- + + Generate ellipse data without tilt and add noise: + + >>> t = np.linspace(0, 2 * np.pi, 50) + >>> a = 5 + >>> b = 10 + >>> xc = 20 + >>> yc = 30 + >>> x = xc + a * np.cos(t) + >>> y = yc + b * np.sin(t) + >>> data = np.column_stack([x, y]) + >>> np.random.seed(seed=1234) + >>> data += np.random.normal(size=data.shape) + + Add some faulty data: + + >>> data[0] = (100, 100) + >>> data[1] = (110, 120) + >>> data[2] = (120, 130) + >>> data[3] = (140, 130) + + Estimate ellipse model using all available data: + + >>> model = EllipseModel() + >>> model.estimate(data) + >>> model._params + array([ 4.85808595e+02, 4.51492793e+02, 1.15018491e+03, + 5.52428289e+00, 7.32420126e-01]) + + Estimate ellipse model using RANSAC: + + >>> ransac_model, inliers = ransac(data, EllipseModel, 5, 3, max_trials=50) + >>> # ransac_model._params, inliers + + Should give the correct result estimated without the faulty data:: + + [ 20.12762373, 29.73563061, 4.81499637, 10.4743584, 0.05217117] + [ 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, 36, 37, + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49] + + Robustly estimate geometric transformation: + + >>> from skimage.transform import SimilarityTransform + >>> src = 100 * np.random.random((50, 2)) + >>> model0 = SimilarityTransform(scale=0.5, rotation=1, + ... translation=(10, 20)) + >>> dst = model0(src) + >>> dst[0] = (10000, 10000) + >>> dst[1] = (-100, 100) + >>> dst[2] = (50, 50) + >>> model, inliers = ransac((src, dst), SimilarityTransform, 2, 10) + >>> inliers + array([ 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, 36, + 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]) + + + """ + + best_model = None + best_inlier_num = 0 + best_inlier_residuals_sum = np.inf + best_inliers = None + + if not isinstance(data, list) and not isinstance(data, tuple): + data = [data] + + # make sure data is list and not tuple, so it can be modified below + data = list(data) + # number of samples + N = data[0].shape[0] + + for _ in range(max_trials): + + # choose random sample set + samples = [] + random_idxs = np.random.randint(0, N, min_samples) + for d in data: + samples.append(d[random_idxs]) + + # check if random sample set is valid + if is_data_valid is not None and not is_data_valid(*samples): + continue + + # estimate model for current random sample set + sample_model = model_class() + sample_model.estimate(*samples) + + # check if estimated model is valid + if is_model_valid is not None and not is_model_valid(sample_model, + *samples): + continue + + sample_model_residuals = np.abs(sample_model.residuals(*data)) + # consensus set / inliers + sample_model_inliers = sample_model_residuals < residual_threshold + sample_model_residuals_sum = np.sum(sample_model_residuals**2) + + # choose as new best model if number of inliers is maximal + sample_inlier_num = np.sum(sample_model_inliers) + if ( + # more inliers + sample_inlier_num > best_inlier_num + # same number of inliers but less "error" in terms of residuals + or (sample_inlier_num == best_inlier_num + and sample_model_residuals_sum < best_inlier_residuals_sum) + ): + best_model = sample_model + best_inlier_num = sample_inlier_num + best_inlier_residuals_sum = sample_model_residuals_sum + best_inliers = sample_model_inliers + if ( + best_inlier_num >= stop_sample_num + or best_inlier_residuals_sum <= stop_residuals_sum + ): + break + + # estimate final model using all inliers + if best_inliers is not None: + # select inliers for each data array + for i in range(len(data)): + data[i] = data[i][best_inliers] + best_model.estimate(*data) + + return best_model, best_inliers diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index 21d9964e..be57ca7b 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -14,11 +14,15 @@ def configuration(parent_package='', top_path=None): cython(['_find_contours.pyx'], working_path=base_path) cython(['_moments.pyx'], working_path=base_path) + cython(['_marching_cubes_cy.pyx'], working_path=base_path) config.add_extension('_find_contours', sources=['_find_contours.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_moments', sources=['_moments.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_marching_cubes_cy', + sources=['_marching_cubes_cy.c'], + include_dirs=[get_numpy_include_dirs()]) return config diff --git a/skimage/measure/tests/test_block.py b/skimage/measure/tests/test_block.py new file mode 100644 index 00000000..a8bc62a9 --- /dev/null +++ b/skimage/measure/tests/test_block.py @@ -0,0 +1,81 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.measure import block_reduce + + +def test_block_reduce_sum(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_reduce(image1, (2, 3)) + expected1 = np.array([[ 24, 42], + [ 96, 114]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_reduce(image2, (3, 3)) + expected2 = np.array([[ 81, 108, 87], + [174, 192, 138]]) + assert_array_equal(expected2, out2) + + +def test_block_reduce_mean(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_reduce(image1, (2, 3), func=np.mean) + expected1 = np.array([[ 4., 7.], + [ 16., 19.]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_reduce(image2, (4, 5), func=np.mean) + expected2 = np.array([[14. , 10.8], + [ 8.5, 5.7]]) + assert_array_equal(expected2, out2) + + +def test_block_reduce_median(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_reduce(image1, (2, 3), func=np.median) + expected1 = np.array([[ 4., 7.], + [ 16., 19.]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_reduce(image2, (4, 5), func=np.median) + expected2 = np.array([[ 14., 17.], + [ 0., 0.]]) + assert_array_equal(expected2, out2) + + image3 = np.array([[1, 5, 5, 5], [5, 5, 5, 1000]]) + out3 = block_reduce(image3, (2, 4), func=np.median) + assert_array_equal(5, out3) + + +def test_block_reduce_min(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_reduce(image1, (2, 3), func=np.min) + expected1 = np.array([[ 0, 3], + [12, 15]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_reduce(image2, (4, 5), func=np.min) + expected2 = np.array([[0, 0], + [0, 0]]) + assert_array_equal(expected2, out2) + + +def test_block_reduce_max(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_reduce(image1, (2, 3), func=np.max) + expected1 = np.array([[ 8, 11], + [20, 23]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_reduce(image2, (4, 5), func=np.max) + expected2 = np.array([[28, 31], + [36, 39]]) + assert_array_equal(expected2, out2) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_find_contours.py b/skimage/measure/tests/test_find_contours.py index 11b9b443..da2497eb 100644 --- a/skimage/measure/tests/test_find_contours.py +++ b/skimage/measure/tests/test_find_contours.py @@ -1,5 +1,7 @@ import numpy as np -from numpy.testing import * +from numpy.testing import (assert_raises, + assert_array_equal, + ) from skimage.measure import find_contours @@ -7,15 +9,6 @@ a = np.ones((8, 8), dtype=np.float32) a[1:-1, 1] = 0 a[1, 1:-1] = 0 -## array([[ 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., 1., 1., 1., 1., 1.], -## [ 1., 0., 1., 1., 1., 1., 1., 1.], -## [ 1., 0., 1., 1., 1., 1., 1., 1.], -## [ 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] r = np.sqrt(x**2 + y**2) diff --git a/skimage/measure/tests/test_fit.py b/skimage/measure/tests/test_fit.py new file mode 100644 index 00000000..bde1fcc7 --- /dev/null +++ b/skimage/measure/tests/test_fit.py @@ -0,0 +1,208 @@ +import numpy as np +from numpy.testing import assert_equal, assert_raises, assert_almost_equal +from skimage.measure import LineModel, CircleModel, EllipseModel, ransac +from skimage.transform import AffineTransform + + +def test_line_model_invalid_input(): + assert_raises(ValueError, LineModel().estimate, np.empty((5, 3))) + + +def test_line_model_predict(): + model = LineModel() + model._params = (10, 1) + x = np.arange(-10, 10) + y = model.predict_y(x) + assert_almost_equal(x, model.predict_x(y)) + + +def test_line_model_estimate(): + # generate original data without noise + model0 = LineModel() + model0._params = (10, 1) + x0 = np.arange(-100, 100) + y0 = model0.predict_y(x0) + data0 = np.column_stack([x0, y0]) + + # add gaussian noise to data + np.random.seed(1234) + data = data0 + np.random.normal(size=data0.shape) + + # estimate parameters of noisy data + model_est = LineModel() + model_est.estimate(data) + + # test whether estimated parameters almost equal original parameters + assert_almost_equal(model0._params, model_est._params, 1) + + +def test_line_model_residuals(): + model = LineModel() + model._params = (0, 0) + assert_equal(abs(model.residuals(np.array([[0, 0]]))), 0) + assert_equal(abs(model.residuals(np.array([[0, 10]]))), 0) + assert_equal(abs(model.residuals(np.array([[10, 0]]))), 10) + model._params = (5, np.pi / 4) + assert_equal(abs(model.residuals(np.array([[0, 0]]))), 5) + assert_equal(abs(model.residuals(np.array([[np.sqrt(50), 0]]))), 5) + + +def test_line_model_under_determined(): + data = np.empty((1, 2)) + assert_raises(ValueError, LineModel().estimate, data) + + +def test_circle_model_invalid_input(): + assert_raises(ValueError, CircleModel().estimate, np.empty((5, 3))) + + +def test_circle_model_predict(): + model = CircleModel() + r = 5 + model._params = (0, 0, r) + t = np.arange(0, 2 * np.pi, np.pi / 2) + + xy = np.array(((5, 0), (0, 5), (-5, 0), (0, -5))) + assert_almost_equal(xy, model.predict_xy(t)) + + +def test_circle_model_estimate(): + # generate original data without noise + model0 = CircleModel() + model0._params = (10, 12, 3) + t = np.linspace(0, 2 * np.pi, 1000) + data0 = model0.predict_xy(t) + + # add gaussian noise to data + np.random.seed(1234) + data = data0 + np.random.normal(size=data0.shape) + + # estimate parameters of noisy data + model_est = CircleModel() + model_est.estimate(data) + + # test whether estimated parameters almost equal original parameters + assert_almost_equal(model0._params, model_est._params, 1) + + +def test_circle_model_residuals(): + model = CircleModel() + model._params = (0, 0, 5) + assert_almost_equal(abs(model.residuals(np.array([[5, 0]]))), 0) + assert_almost_equal(abs(model.residuals(np.array([[6, 6]]))), + np.sqrt(2 * 6**2) - 5) + assert_almost_equal(abs(model.residuals(np.array([[10, 0]]))), 5) + + +def test_ellipse_model_invalid_input(): + assert_raises(ValueError, EllipseModel().estimate, np.empty((5, 3))) + + +def test_ellipse_model_predict(): + model = EllipseModel() + r = 5 + model._params = (0, 0, 5, 10, 0) + t = np.arange(0, 2 * np.pi, np.pi / 2) + + xy = np.array(((5, 0), (0, 10), (-5, 0), (0, -10))) + assert_almost_equal(xy, model.predict_xy(t)) + + +def test_ellipse_model_estimate(): + # generate original data without noise + model0 = EllipseModel() + model0._params = (10, 20, 15, 25, 0) + t = np.linspace(0, 2 * np.pi, 100) + data0 = model0.predict_xy(t) + + # add gaussian noise to data + np.random.seed(1234) + data = data0 + np.random.normal(size=data0.shape) + + # estimate parameters of noisy data + model_est = EllipseModel() + model_est.estimate(data) + + # test whether estimated parameters almost equal original parameters + assert_almost_equal(model0._params, model_est._params, 0) + + +def test_line_model_residuals(): + model = EllipseModel() + # vertical line through origin + model._params = (0, 0, 10, 5, 0) + assert_almost_equal(abs(model.residuals(np.array([[10, 0]]))), 0) + assert_almost_equal(abs(model.residuals(np.array([[0, 5]]))), 0) + assert_almost_equal(abs(model.residuals(np.array([[0, 10]]))), 5) + + +def test_ransac_shape(): + np.random.seed(1) + + # generate original data without noise + model0 = CircleModel() + model0._params = (10, 12, 3) + t = np.linspace(0, 2 * np.pi, 1000) + data0 = model0.predict_xy(t) + + # add some faulty data + outliers = (10, 30, 200) + data0[outliers[0], :] = (1000, 1000) + data0[outliers[1], :] = (-50, 50) + data0[outliers[2], :] = (-100, -10) + + # estimate parameters of corrupted data + model_est, inliers = ransac(data0, CircleModel, 3, 5) + + # test whether estimated parameters equal original parameters + assert_equal(model0._params, model_est._params) + for outlier in outliers: + assert outlier not in inliers + + +def test_ransac_geometric(): + np.random.seed(1) + + # generate original data without noise + src = 100 * np.random.random((50, 2)) + model0 = AffineTransform(scale=(0.5, 0.3), rotation=1, + translation=(10, 20)) + dst = model0(src) + + # add some faulty data + outliers = (0, 5, 20) + dst[outliers[0]] = (10000, 10000) + dst[outliers[1]] = (-100, 100) + dst[outliers[2]] = (50, 50) + + # estimate parameters of corrupted data + model_est, inliers = ransac((src, dst), AffineTransform, 2, 20) + + # test whether estimated parameters equal original parameters + assert_almost_equal(model0._matrix, model_est._matrix) + assert np.all(np.nonzero(inliers == False)[0] == outliers) + + +def test_ransac_is_data_valid(): + np.random.seed(1) + + is_data_valid = lambda data: data.shape[0] > 2 + model, inliers = ransac(np.empty((10, 2)), LineModel, 2, np.inf, + is_data_valid=is_data_valid) + assert_equal(model, None) + assert_equal(inliers, None) + + +def test_ransac_is_model_valid(): + np.random.seed(1) + + def is_model_valid(model, data): + return False + model, inliers = ransac(np.empty((10, 2)), LineModel, 2, np.inf, + is_model_valid=is_model_valid) + assert_equal(model, None) + assert_equal(inliers, None) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_marching_cubes.py b/skimage/measure/tests/test_marching_cubes.py new file mode 100644 index 00000000..b3c2ddc1 --- /dev/null +++ b/skimage/measure/tests/test_marching_cubes.py @@ -0,0 +1,40 @@ +import numpy as np +from numpy.testing import assert_raises + +from skimage.draw import ellipsoid, ellipsoid_stats +from skimage.measure import marching_cubes, mesh_surface_area + + +def test_marching_cubes_isotropic(): + ellipsoid_isotropic = ellipsoid(6, 10, 16, levelset=True) + _, surf = ellipsoid_stats(6, 10, 16) + verts, faces = marching_cubes(ellipsoid_isotropic, 0.) + surf_calc = mesh_surface_area(verts, faces) + + # Test within 1% tolerance for isotropic. Will always underestimate. + assert surf > surf_calc and surf_calc > surf * 0.99 + + +def test_marching_cubes_anisotropic(): + spacing = (1., 10 / 6., 16 / 6.) + ellipsoid_anisotropic = ellipsoid(6, 10, 16, spacing=spacing, + levelset=True) + _, surf = ellipsoid_stats(6, 10, 16) + verts, faces = marching_cubes(ellipsoid_anisotropic, 0., + spacing=spacing) + surf_calc = mesh_surface_area(verts, faces) + + # Test within 1.5% tolerance for anisotropic. Will always underestimate. + assert surf > surf_calc and surf_calc > surf * 0.985 + + +def test_invalid_input(): + assert_raises(ValueError, marching_cubes, np.zeros((2, 2, 1)), 0) + assert_raises(ValueError, marching_cubes, np.zeros((2, 2, 1)), 1) + assert_raises(ValueError, marching_cubes, np.ones((3, 3, 3)), 1, + spacing=(1, 2)) + assert_raises(ValueError, marching_cubes, np.zeros((20, 20)), 0) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_moments.py b/skimage/measure/tests/test_moments.py new file mode 100644 index 00000000..0667f688 --- /dev/null +++ b/skimage/measure/tests/test_moments.py @@ -0,0 +1,72 @@ +from numpy.testing import assert_equal, assert_almost_equal +import numpy as np + +from skimage.measure import (moments, moments_central, moments_normalized, + moments_hu) + + +def test_moments(): + image = np.zeros((20, 20), dtype=np.double) + image[14, 14] = 1 + image[15, 15] = 1 + image[14, 15] = 0.5 + image[15, 14] = 0.5 + m = moments(image) + assert_equal(m[0, 0], 3) + assert_almost_equal(m[0, 1] / m[0, 0], 14.5) + assert_almost_equal(m[1, 0] / m[0, 0], 14.5) + + +def test_moments_central(): + image = np.zeros((20, 20), dtype=np.double) + image[14, 14] = 1 + image[15, 15] = 1 + image[14, 15] = 0.5 + image[15, 14] = 0.5 + mu = moments_central(image, 14.5, 14.5) + + # shift image by dx=2, dy=2 + image2 = np.zeros((20, 20), dtype=np.double) + image2[16, 16] = 1 + image2[17, 17] = 1 + image2[16, 17] = 0.5 + image2[17, 16] = 0.5 + mu2 = moments_central(image2, 14.5 + 2, 14.5 + 2) + # central moments must be translation invariant + assert_equal(mu, mu2) + + +def test_moments_normalized(): + image = np.zeros((20, 20), dtype=np.double) + image[13:17, 13:17] = 1 + mu = moments_central(image, 14.5, 14.5) + nu = moments_normalized(mu) + # shift image by dx=-3, dy=-3 and scale by 0.5 + image2 = np.zeros((20, 20), dtype=np.double) + image2[11:13, 11:13] = 1 + mu2 = moments_central(image2, 11.5, 11.5) + nu2 = moments_normalized(mu2) + # central moments must be translation and scale invariant + assert_almost_equal(nu, nu2, decimal=1) + + +def test_moments_hu(): + image = np.zeros((20, 20), dtype=np.double) + image[13:15, 13:17] = 1 + mu = moments_central(image, 13.5, 14.5) + nu = moments_normalized(mu) + hu = moments_hu(nu) + # shift image by dx=2, dy=3, scale by 0.5 and rotate by 90deg + image2 = np.zeros((20, 20), dtype=np.double) + image2[11, 11:13] = 1 + image2 = image2.T + mu2 = moments_central(image2, 11.5, 11) + nu2 = moments_normalized(mu2) + hu2 = moments_hu(nu2) + # central moments must be translation and scale invariant + assert_almost_equal(hu, hu2, decimal=1) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index c1396670..c0c6443d 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -1,5 +1,5 @@ from numpy.testing import assert_array_equal, assert_almost_equal, \ - assert_array_almost_equal, assert_raises + assert_array_almost_equal, assert_raises, assert_equal import numpy as np import math @@ -22,33 +22,43 @@ INTENSITY_SAMPLE = SAMPLE.copy() 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] + regions = regionprops(SAMPLE, 'all', INTENSITY_SAMPLE)[0] for prop in PROPS: - assert prop in props + regions[prop] + + +def test_dtype(): + regionprops(np.zeros((10, 10), dtype=np.int)) + regionprops(np.zeros((10, 10), dtype=np.uint)) + assert_raises((TypeError, RuntimeError), regionprops, + np.zeros((10, 10), dtype=np.double)) + + +def test_ndim(): + regionprops(np.zeros((10, 10), dtype=np.int)) + regionprops(np.zeros((10, 10, 1), dtype=np.int)) + regionprops(np.zeros((10, 10, 1, 1), dtype=np.int)) + assert_raises(TypeError, regionprops, np.zeros((10, 10, 2), dtype=np.int)) def test_area(): - area = regionprops(SAMPLE, ['Area'])[0]['Area'] + area = regionprops(SAMPLE)[0].area assert area == np.sum(SAMPLE) def test_bbox(): - bbox = regionprops(SAMPLE, ['BoundingBox'])[0]['BoundingBox'] + bbox = regionprops(SAMPLE)[0].bbox assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1])) SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[:, -1] = 0 - bbox = regionprops(SAMPLE_mod, ['BoundingBox'])[0]['BoundingBox'] + bbox = regionprops(SAMPLE_mod)[0].bbox assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]-1)) -def test_central_moments(): - mu = regionprops(SAMPLE, ['CentralMoments'])[0]['CentralMoments'] +def test_moments_central(): + mu = regionprops(SAMPLE)[0].moments_central # determined with OpenCV assert_almost_equal(mu[0,2], 436.00000000000045) # different from OpenCV results, bug in OpenCV @@ -61,19 +71,19 @@ def test_central_moments(): def test_centroid(): - centroid = regionprops(SAMPLE, ['Centroid'])[0]['Centroid'] + centroid = regionprops(SAMPLE)[0].centroid # determined with MATLAB assert_array_almost_equal(centroid, (5.66666666666666, 9.444444444444444)) def test_convex_area(): - area = regionprops(SAMPLE, ['ConvexArea'])[0]['ConvexArea'] + area = regionprops(SAMPLE)[0].convex_area # determined with MATLAB assert area == 124 def test_convex_image(): - img = regionprops(SAMPLE, ['ConvexImage'])[0]['ConvexImage'] + img = regionprops(SAMPLE)[0].convex_image # determined with MATLAB ref = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0], @@ -94,43 +104,43 @@ 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'] + prop_coords = regionprops(sample)[0].coords assert_array_equal(prop_coords, coords) def test_eccentricity(): - eps = regionprops(SAMPLE, ['Eccentricity'])[0]['Eccentricity'] + eps = regionprops(SAMPLE)[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'] + eps = regionprops(img)[0].eccentricity assert_almost_equal(eps, 0) def test_equiv_diameter(): - diameter = regionprops(SAMPLE, ['EquivDiameter'])[0]['EquivDiameter'] + diameter = regionprops(SAMPLE)[0].equivalent_diameter # determined with MATLAB assert_almost_equal(diameter, 9.57461472963) def test_euler_number(): - en = regionprops(SAMPLE, ['EulerNumber'])[0]['EulerNumber'] + en = regionprops(SAMPLE)[0].euler_number assert en == 0 SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[7, -3] = 0 - en = regionprops(SAMPLE_mod, ['EulerNumber'])[0]['EulerNumber'] + en = regionprops(SAMPLE_mod)[0].euler_number assert en == -1 def test_extent(): - extent = regionprops(SAMPLE, ['Extent'])[0]['Extent'] + extent = regionprops(SAMPLE)[0].extent assert_almost_equal(extent, 0.4) -def test_hu_moments(): - hu = regionprops(SAMPLE, ['HuMoments'])[0]['HuMoments'] +def test_moments_hu(): + hu = regionprops(SAMPLE)[0].moments_hu ref = np.array([ 3.27117627e-01, 2.63869194e-02, @@ -145,59 +155,64 @@ def test_hu_moments(): def test_image(): - img = regionprops(SAMPLE, ['Image'])[0]['Image'] + img = regionprops(SAMPLE)[0].image assert_array_equal(img, SAMPLE) +def test_label(): + label = regionprops(SAMPLE)[0].label + assert_array_equal(label, 1) + + def test_filled_area(): - area = regionprops(SAMPLE, ['FilledArea'])[0]['FilledArea'] + area = regionprops(SAMPLE)[0].filled_area assert area == np.sum(SAMPLE) SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[7, -3] = 0 - area = regionprops(SAMPLE_mod, ['FilledArea'])[0]['FilledArea'] + area = regionprops(SAMPLE_mod)[0].filled_area assert area == np.sum(SAMPLE) def test_filled_image(): - img = regionprops(SAMPLE, ['FilledImage'])[0]['FilledImage'] + img = regionprops(SAMPLE)[0].filled_image assert_array_equal(img, SAMPLE) def test_major_axis_length(): - length = regionprops(SAMPLE, ['MajorAxisLength'])[0]['MajorAxisLength'] + length = regionprops(SAMPLE)[0].major_axis_length # 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'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].max_intensity assert_almost_equal(intensity, 2) def test_mean_intensity(): - intensity = regionprops(SAMPLE, ['MeanIntensity'], INTENSITY_SAMPLE - )[0]['MeanIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].mean_intensity assert_almost_equal(intensity, 1.02777777777777) def test_min_intensity(): - intensity = regionprops(SAMPLE, ['MinIntensity'], INTENSITY_SAMPLE - )[0]['MinIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].min_intensity assert_almost_equal(intensity, 1) def test_minor_axis_length(): - length = regionprops(SAMPLE, ['MinorAxisLength'])[0]['MinorAxisLength'] + length = regionprops(SAMPLE)[0].minor_axis_length # 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'] + m = regionprops(SAMPLE)[0].moments # determined with OpenCV assert_almost_equal(m[0,0], 72.0) assert_almost_equal(m[0,1], 408.0) @@ -211,8 +226,8 @@ def test_moments(): assert_almost_equal(m[3,0], 95588.0) -def test_normalized_moments(): - nu = regionprops(SAMPLE, ['NormalizedMoments'])[0]['NormalizedMoments'] +def test_moments_normalized(): + nu = regionprops(SAMPLE)[0].moments_normalized # determined with OpenCV assert_almost_equal(nu[0,2], 0.08410493827160502) assert_almost_equal(nu[1,1], -0.016846707818929982) @@ -223,29 +238,26 @@ def test_normalized_moments(): def test_orientation(): - orientation = regionprops(SAMPLE, ['Orientation'])[0]['Orientation'] + orientation = regionprops(SAMPLE)[0].orientation # determined with MATLAB assert_almost_equal(orientation, 0.10446844651921) # test correct quadrant determination - orientation2 = regionprops(SAMPLE.T, ['Orientation'])[0]['Orientation'] + orientation2 = regionprops(SAMPLE.T)[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'] + orientation_diag = regionprops(diag)[0].orientation assert_almost_equal(orientation_diag, -math.pi / 4) - orientation_diag = regionprops(np.flipud(diag), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.flipud(diag))[0].orientation assert_almost_equal(orientation_diag, math.pi / 4) - orientation_diag = regionprops(np.fliplr(diag), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.fliplr(diag))[0].orientation assert_almost_equal(orientation_diag, math.pi / 4) - orientation_diag = regionprops(np.fliplr(np.flipud(diag)), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.fliplr(np.flipud(diag)))[0].orientation assert_almost_equal(orientation_diag, -math.pi / 4) def test_perimeter(): - per = regionprops(SAMPLE, ['Perimeter'])[0]['Perimeter'] + per = regionprops(SAMPLE)[0].perimeter assert_almost_equal(per, 55.2487373415) per = perimeter(SAMPLE.astype('double'), neighbourhood=8) @@ -253,14 +265,14 @@ def test_perimeter(): def test_solidity(): - solidity = regionprops(SAMPLE, ['Solidity'])[0]['Solidity'] + solidity = regionprops(SAMPLE)[0].solidity # determined with MATLAB assert_almost_equal(solidity, 0.580645161290323) -def test_weighted_central_moments(): - wmu = regionprops(SAMPLE, ['WeightedCentralMoments'], INTENSITY_SAMPLE - )[0]['WeightedCentralMoments'] +def test_weighted_moments_central(): + wmu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_moments_central ref = np.array( [[ 7.4000000000e+01, -2.1316282073e-13, 4.7837837838e+02, -7.5943608473e+02], @@ -276,14 +288,14 @@ def test_weighted_central_moments(): def test_weighted_centroid(): - centroid = regionprops(SAMPLE, ['WeightedCentroid'], INTENSITY_SAMPLE - )[0]['WeightedCentroid'] + centroid = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_centroid assert_array_almost_equal(centroid, (5.540540540540, 9.445945945945)) -def test_weighted_hu_moments(): - whu = regionprops(SAMPLE, ['WeightedHuMoments'], INTENSITY_SAMPLE - )[0]['WeightedHuMoments'] +def test_weighted_moments_hu(): + whu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_moments_hu ref = np.array([ 3.1750587329e-01, 2.1417517159e-02, @@ -297,8 +309,8 @@ def test_weighted_hu_moments(): def test_weighted_moments(): - wm = regionprops(SAMPLE, ['WeightedMoments'], INTENSITY_SAMPLE - )[0]['WeightedMoments'] + wm = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_moments ref = np.array( [[ 7.4000000000e+01, 4.1000000000e+02, 2.7500000000e+03, 1.9778000000e+04], @@ -312,9 +324,9 @@ def test_weighted_moments(): assert_array_almost_equal(wm, ref) -def test_weighted_normalized_moments(): - wnu = regionprops(SAMPLE, ['WeightedNormalizedMoments'], INTENSITY_SAMPLE - )[0]['WeightedNormalizedMoments'] +def test_weighted_moments_normalized(): + wnu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_moments_normalized ref = np.array( [[ np.nan, np.nan, 0.0873590903, -0.0161217406], [ np.nan, -0.0160405109, -0.0031421072, -0.0031376984], @@ -324,6 +336,17 @@ def test_weighted_normalized_moments(): assert_array_almost_equal(wnu, ref) +def test_old_dict_interface(): + feats = regionprops(SAMPLE, + ['Area', 'Eccentricity', 'EulerNumber', + 'Extent', 'MinIntensity', 'MeanIntensity', + 'MaxIntensity', 'Solidity'], + intensity_image=INTENSITY_SAMPLE) + + np.array([list(props.values()) for props in feats], np.float) + assert_equal(len(feats[0]), 8) + + 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 ec5ce7ef..e08f2c31 100644 --- a/skimage/measure/tests/test_structural_similarity.py +++ b/skimage/measure/tests/test_structural_similarity.py @@ -68,5 +68,6 @@ def test_invalid_input(): 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 044fcf89..4788c308 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -1,10 +1,48 @@ from .binary import (binary_erosion, binary_dilation, binary_opening, binary_closing) -from .grey import * -from .selem import * +from .grey import (erosion, dilation, opening, closing, white_tophat, + black_tophat, greyscale_erode, greyscale_dilate, + greyscale_open, greyscale_close, greyscale_white_top_hat, + greyscale_black_top_hat) +from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball, + octagon, star) from .ccomp import label -from .watershed import watershed, is_local_maximum +from .watershed import watershed from ._skeletonize import skeletonize, medial_axis -from .convex_hull import convex_hull_image +from .convex_hull import convex_hull_image, convex_hull_object from .greyreconstruct import reconstruction from .misc import remove_small_objects + + +__all__ = ['binary_erosion', + 'binary_dilation', + 'binary_opening', + 'binary_closing', + 'erosion', + 'dilation', + 'opening', + 'closing', + 'white_tophat', + 'black_tophat', + 'greyscale_erode', + 'greyscale_dilate', + 'greyscale_open', + 'greyscale_close', + 'greyscale_white_top_hat', + 'greyscale_black_top_hat', + 'square', + 'rectangle', + 'diamond', + 'disk', + 'cube', + 'octahedron', + 'ball', + 'octagon', + 'label', + 'watershed', + 'skeletonize', + 'medial_axis', + 'convex_hull_image', + 'convex_hull_object', + 'reconstruction', + 'remove_small_objects'] diff --git a/skimage/morphology/_convex_hull.pyx b/skimage/morphology/_convex_hull.pyx index 7298e1ed..cd9270cc 100644 --- a/skimage/morphology/_convex_hull.pyx +++ b/skimage/morphology/_convex_hull.pyx @@ -7,7 +7,7 @@ import numpy as np cimport numpy as cnp -def possible_hull(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] img): +def possible_hull(cnp.uint8_t[:, ::1] img): """Return positions of pixels that possibly belong to the convex hull. Parameters @@ -30,31 +30,43 @@ def possible_hull(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] img): # 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.intp) - nonzero *= -1 + coords = np.ones((2 * (rows + cols), 2), dtype=np.intp) + coords *= -1 + + cdef Py_ssize_t[:, ::1] nonzero = coords + cdef Py_ssize_t rows_cols = rows + cols + cdef Py_ssize_t rows_2_cols = 2 * rows + cols + cdef Py_ssize_t rows_cols_r, rows_c for r in range(rows): + + rows_cols_r = rows_cols + r + for c in range(cols): + if img[r, c] != 0: + + rows_c = rows + c + rows_2_cols_c = rows_2_cols + c + # Left check if nonzero[r, 1] == -1: nonzero[r, 0] = r nonzero[r, 1] = c # Right check - elif nonzero[rows + cols + r, 1] < c: - nonzero[rows + cols + r, 0] = r - nonzero[rows + cols + r, 1] = c + elif nonzero[rows_cols_r, 1] < c: + nonzero[rows_cols_r, 0] = r + nonzero[rows_cols_r, 1] = c # Top check - if nonzero[rows + c, 1] == -1: - nonzero[rows + c, 0] = r - nonzero[rows + c, 1] = c + if nonzero[rows_c, 1] == -1: + nonzero[rows_c, 0] = r + nonzero[rows_c, 1] = c # Bottom check - elif nonzero[2 * rows + cols + c, 0] < r: - nonzero[2 * rows + cols + c, 0] = r - nonzero[2 * rows + cols + c, 1] = c + elif nonzero[rows_2_cols_c, 0] < r: + nonzero[rows_2_cols_c, 0] = r + nonzero[rows_2_cols_c, 1] = c - return nonzero[nonzero[:, 0] != -1] + return coords[coords[:, 0] != -1] diff --git a/skimage/morphology/_greyreconstruct.pyx b/skimage/morphology/_greyreconstruct.pyx index e8a84f3b..fa92ecec 100644 --- a/skimage/morphology/_greyreconstruct.pyx +++ b/skimage/morphology/_greyreconstruct.pyx @@ -21,8 +21,8 @@ def reconstruction_loop(cnp.ndarray[dtype=cnp.uint32_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): + Py_ssize_t current_idx, + Py_ssize_t image_stride): """The inner loop for reconstruction. This algorithm uses the rank-order of pixels. If low intensity pixels have diff --git a/skimage/morphology/_pnpoly.pyx b/skimage/morphology/_pnpoly.pyx index f32778cc..12b48e5d 100644 --- a/skimage/morphology/_pnpoly.pyx +++ b/skimage/morphology/_pnpoly.pyx @@ -29,7 +29,7 @@ def grid_points_inside_poly(shape, verts): True where the grid falls inside the polygon. """ - cdef cnp.ndarray[cnp.double_t, ndim=1, mode="c"] vx, vy + cdef double[:] vx, vy verts = np.asarray(verts) vx = verts[:, 0].astype(np.double) @@ -45,8 +45,7 @@ def grid_points_inside_poly(shape, verts): for m in range(M): for n in range(N): - out[m, n] = point_in_polygon(V, vx.data, vy.data, - m, n) + out[m, n] = point_in_polygon(V, &vx[0], &vy[0], m, n) return out.view(bool) @@ -57,18 +56,18 @@ def points_inside_poly(points, verts): Parameters ---------- points : (N, 2) array - Input points, ``(x, y)``. + 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. + 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. + True if corresponding point is inside the polygon. """ - cdef cnp.ndarray[cnp.double_t, ndim=1, mode="c"] x, y, vx, vy + cdef double[:] x, y, vx, vy points = np.asarray(points) verts = np.asarray(verts) @@ -82,8 +81,8 @@ def points_inside_poly(points, verts): cdef cnp.ndarray[cnp.uint8_t, ndim=1] out = \ np.zeros(x.shape[0], dtype=np.uint8) - points_in_polygon(vx.shape[0], vx.data, vy.data, - x.shape[0], x.data, y.data, + points_in_polygon(vx.shape[0], &vx[0], &vy[0], + x.shape[0], &x[0], &y[0], out.data) return out.astype(bool) diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py index b48beb86..11181a7b 100644 --- a/skimage/morphology/_skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -84,17 +84,21 @@ def skeletonize(image): # 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,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,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] + 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, 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, 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] # convert to unsigned int (this should work for boolean values) - skeleton = np.array(image).astype(np.uint8) + skeleton = image.astype(np.uint8) # check some properties of the input image: # - 2D @@ -106,13 +110,13 @@ def skeletonize(image): # 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 + pixel_removed = True + while pixel_removed: + pixel_removed = False # assign each pixel a unique value based on its foreground neighbours neighbours = ndimage.correlate(skeleton, mask, mode='constant') @@ -126,11 +130,11 @@ def skeletonize(image): # pass 1 - remove the 1's and 3's code_mask = (codes == 1) if np.any(code_mask): - pixelRemoved = True + pixel_removed = True skeleton[code_mask] = 0 code_mask = (codes == 3) if np.any(code_mask): - pixelRemoved = True + pixel_removed = True skeleton[code_mask] = 0 # pass 2 - remove the 2's and 3's @@ -139,14 +143,14 @@ def skeletonize(image): codes = np.take(lut, neighbours) code_mask = (codes == 2) if np.any(code_mask): - pixelRemoved = True + pixel_removed = True skeleton[code_mask] = 0 code_mask = (codes == 3) if np.any(code_mask): - pixelRemoved = True + pixel_removed = True skeleton[code_mask] = 0 - return skeleton + return skeleton.astype(bool) # --------- Skeletonization by medial axis transform -------- @@ -208,6 +212,7 @@ def medial_axis(image, mask=None, return_distance=False): Examples -------- + >>> from skimage import morphology >>> square = np.zeros((7, 7), dtype=np.uint8) >>> square[1:-1, 2:-2] = 1 >>> square @@ -277,8 +282,8 @@ def medial_axis(image, mask=None, return_distance=False): 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.intp) - j = np.ascontiguousarray(j[result], np.intp) + i = np.ascontiguousarray(i[result], dtype=np.intp) + j = np.ascontiguousarray(j[result], dtype=np.intp) result = np.ascontiguousarray(result, np.uint8) # Determine the order in which pixels are processed. @@ -291,9 +296,9 @@ def medial_axis(image, mask=None, return_distance=False): order = np.lexsort((tiebreaker, corner_score[masked_image], distance)) - order = np.ascontiguousarray(order, np.int32) + order = np.ascontiguousarray(order, dtype=np.int32) - table = np.ascontiguousarray(table, np.uint8) + table = np.ascontiguousarray(table, dtype=np.uint8) # Remove pixels not belonging to the medial axis _skeletonize_loop(result, i, j, order, table) diff --git a/skimage/morphology/_skeletonize_cy.pyx b/skimage/morphology/_skeletonize_cy.pyx index 13e303d4..c4471a11 100644 --- a/skimage/morphology/_skeletonize_cy.pyx +++ b/skimage/morphology/_skeletonize_cy.pyx @@ -19,16 +19,9 @@ import numpy as np cimport numpy as cnp -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): +def _skeletonize_loop(cnp.uint8_t[:, ::1] result, + Py_ssize_t[::1] i, Py_ssize_t[::1] j, + cnp.int32_t[::1] order, cnp.uint8_t[::1] table): """ Inner loop of skeletonize function @@ -65,9 +58,11 @@ def _skeletonize_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, pixels. """ cdef: - cnp.int32_t accumulator + Py_ssize_t accumulator Py_ssize_t index, order_index Py_ssize_t ii, jj + Py_ssize_t rows = result.shape[0] + Py_ssize_t cols = result.shape[1] for index in range(order.shape[0]): accumulator = 16 @@ -80,26 +75,25 @@ def _skeletonize_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, accumulator += 1 if result[ii - 1, jj]: accumulator += 2 - if jj < result.shape[1] - 1 and result[ii - 1, jj + 1]: + if jj < cols - 1 and result[ii - 1, jj + 1]: accumulator += 4 if jj > 0 and result[ii, jj - 1]: accumulator += 8 - if jj < result.shape[1] - 1 and result[ii, jj + 1]: + if jj < cols - 1 and result[ii, jj + 1]: accumulator += 32 - if ii < result.shape[0]-1: + if ii < rows - 1: if jj > 0 and result[ii + 1, jj - 1]: accumulator += 64 if result[ii + 1, jj]: accumulator += 128 - if jj < result.shape[1] - 1 and result[ii + 1, jj + 1]: + if jj < cols - 1 and result[ii + 1, jj + 1]: accumulator += 256 # Assign the value of table corresponding to the configuration result[ii, jj] = table[accumulator] -def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] image): +def _table_lookup_index(cnp.uint8_t[:, ::1] image): """ Return an index into a table per pixel of a binary image @@ -120,9 +114,8 @@ def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, hardwired kernel. """ cdef: - cnp.ndarray[dtype=cnp.int32_t, ndim=2, - negative_indices=False, mode='c'] indexer - cnp.int32_t *p_indexer + Py_ssize_t[:, ::1] indexer + Py_ssize_t *p_indexer cnp.uint8_t *p_image Py_ssize_t i_stride Py_ssize_t i_shape @@ -133,9 +126,9 @@ def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, 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 + indexer = np.zeros((i_shape, j_shape), dtype=np.intp) + p_indexer = &indexer[0, 0] + p_image = &image[0, 0] i_stride = image.strides[0] assert i_shape >= 3 and j_shape >= 3, \ "Please use the slow method for arrays < 3x3" @@ -214,4 +207,4 @@ def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, indexer[i - 1, j_shape - 1] += 128 indexer[i, j_shape - 1] += 16 indexer[i + 1, j_shape - 1] += 2 - return indexer + return np.asarray(indexer) diff --git a/skimage/morphology/_watershed.pyx b/skimage/morphology/_watershed.pyx index 122f0262..a0857707 100644 --- a/skimage/morphology/_watershed.pyx +++ b/skimage/morphology/_watershed.pyx @@ -23,17 +23,12 @@ 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, +def watershed(DTYPE_INT32_t[::1] image, + DTYPE_INT32_t[:, ::1] 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): + DTYPE_INT32_t[:, ::1] structure, + DTYPE_BOOL_t[::1] mask, + DTYPE_INT32_t[::1] output): """Do heavy lifting of watershed algorithm Parameters diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index e2e0f20b..24c5c0ea 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -1,3 +1,4 @@ +import warnings import numpy as np from scipy import ndimage @@ -8,32 +9,40 @@ def binary_erosion(image, selem, out=None): 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. + 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. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool 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. + eroded : ndarray of bool or uint + The result of the morphological erosion with values in ``[0, 1]``. """ + selem = (selem != 0) + selem_sum = np.sum(selem) - conv = ndimage.convolve(image > 0, selem, output=out, - mode='constant', cval=1) - if conv is not None: + if selem_sum <= 255: + conv = np.empty_like(image, dtype=np.uint8) + else: + conv = np.empty_like(image, dtype=np.uint) + + binary = (image > 0).view(np.uint8) + ndimage.convolve(binary, selem, mode='constant', cval=1, output=conv) + + if out is None: out = conv - return np.equal(out, np.sum(selem), out=out) + return np.equal(conv, selem_sum, out=out) def binary_dilation(image, selem, out=None): @@ -42,33 +51,40 @@ def binary_dilation(image, selem, out=None): 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. + 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. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool 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. + dilated : ndarray of bool or uint + The result of the morphological dilation with values in ``[0, 1]``. """ + selem = (selem != 0) - conv = ndimage.convolve(image > 0, selem, output=out, - mode='constant', cval=0) - if conv is not None: + if np.sum(selem) <= 255: + conv = np.empty_like(image, dtype=np.uint8) + else: + conv = np.empty_like(image, dtype=np.uint) + + binary = (image > 0).view(np.uint8) + ndimage.convolve(binary, selem, mode='constant', cval=0, output=conv) + + if out is None: out = conv - return np.not_equal(out, 0, out=out) + return np.not_equal(conv, 0, out=out) def binary_opening(image, selem, out=None): @@ -85,20 +101,19 @@ def binary_opening(image, selem, out=None): Parameters ---------- image : ndarray - Image array. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool The array to store the result of the morphology. If None is passed, a new array will be allocated. Returns ------- - opening : bool array + opening : ndarray of bool The result of the morphological opening. """ - eroded = binary_erosion(image, selem) out = binary_dilation(eroded, selem, out=out) return out @@ -118,16 +133,16 @@ def binary_closing(image, selem, out=None): Parameters ---------- image : ndarray - Image array. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool The array to store the result of the morphology. If None, is passed, a new array will be allocated. Returns ------- - closing : bool array + closing : ndarray of bool The result of the morphological closing. """ diff --git a/skimage/morphology/ccomp.pxd b/skimage/morphology/ccomp.pxd index 569921fa..62d21fec 100644 --- a/skimage/morphology/ccomp.pxd +++ b/skimage/morphology/ccomp.pxd @@ -5,6 +5,6 @@ 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) +cdef void set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root) +cdef void join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m) +cdef void 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 1db78a6d..91e2611c 100644 --- a/skimage/morphology/ccomp.pyx +++ b/skimage/morphology/ccomp.pyx @@ -39,7 +39,7 @@ cdef DTYPE_t find_root(DTYPE_t *forest, DTYPE_t n): return root -cdef set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root): +cdef inline void set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root): """ Set all nodes on a path to point to new_root. @@ -53,7 +53,7 @@ cdef set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root): forest[n] = root -cdef join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m): +cdef inline void join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m): """Join two trees containing nodes n and m. """ @@ -70,7 +70,7 @@ cdef join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m): set_root(forest, m, root) -cdef link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node): +cdef inline void link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node): """ Link a node to the background node. @@ -80,9 +80,9 @@ cdef link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node): join_trees(forest, n, background_node[0]) -# Connected components search as described in Fiorio et al. -def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1): +# Connected components search as described in Fiorio et al. +def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): """Label connected regions of an integer array. Two pixels are connected when they are neighbors and have the same value. @@ -93,7 +93,7 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1): [ ] [ ] [ ] [ ] | \ | / [ ]--[ ]--[ ] [ ]--[ ]--[ ] - | / | \ + | / | \\ [ ] [ ] [ ] [ ] Parameters @@ -111,21 +111,24 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1): labels : ndarray of dtype int Labeled array, where all connected regions are assigned the same integer value. + num : int, optional + Number of labels, which equals the maximum label index and is only + returned if return_num is `True`. Examples -------- >>> x = np.eye(3).astype(int) - >>> print x + >>> print(x) [[1 0 0] [0 1 0] [0 0 1]] - >>> print m.label(x, neighbors=4) + >>> print(m.label(x, neighbors=4)) [[0 1 1] [2 3 1] [2 2 4]] - >>> print m.label(x, neighbors=8) + >>> print(m.label(x, neighbors=8)) [[0 1 1] [1 0 1] [1 1 0]] @@ -134,7 +137,7 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1): ... [1, 1, 5], ... [0, 0, 0]]) - >>> print m.label(x, background=0) + >>> print(m.label(x, background=0)) [[ 0 -1 -1] [ 0 0 1] [-1 -1 -1]] @@ -202,7 +205,6 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1): join_trees(forest_p, i*cols + j, i*cols + j - 1) # Label output - cdef DTYPE_t ctr = 0 for i in range(rows): for j in range(cols): @@ -216,6 +218,9 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1): # Work around a bug in ndimage's type checking on 32-bit platforms if data.dtype == np.int32: - return data.view(np.int32) + data = data.view(np.int32) + + if return_num: + return data, ctr else: return data diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index a09a39a3..4d491c3b 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -8,10 +8,35 @@ cimport numpy as np from libc.stdlib cimport malloc, free -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): +def _dilate(np.uint8_t[:, :] image, + np.uint8_t[:, :] selem, + np.uint8_t[:, :] out=None, + char shift_x=0, char shift_y=0): + """Return greyscale morphological dilation of an image. + + 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. + shift_x, shift_y : bool + shift structuring element about center point. This only affects + eccentric structuring elements (i.e. selem with even numbered sides). + + Returns + ------- + dilated : uint8 array + The result of the morphological dilation. + """ cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] @@ -27,12 +52,9 @@ def dilate(np.ndarray[np.uint8_t, ndim=2] image, else: out = np.ascontiguousarray(out) - cdef np.uint8_t* out_data = out.data - cdef np.uint8_t* image_data = image.data - cdef Py_ssize_t r, c, rr, cc, s, value, local_max - cdef Py_ssize_t selem_num = np.sum(selem != 0) + cdef Py_ssize_t selem_num = np.sum(np.asarray(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)) @@ -51,22 +73,46 @@ def dilate(np.ndarray[np.uint8_t, ndim=2] image, rr = r + sr[s] cc = c + sc[s] if 0 <= rr < rows and 0 <= cc < cols: - value = image_data[rr * cols + cc] + value = image[rr, cc] if value > local_max: local_max = value - out_data[r * cols + c] = local_max + out[r, c] = local_max free(sr) free(sc) - return out + return np.asarray(out) -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): +def _erode(np.uint8_t[:, :] image, + np.uint8_t[:, :] selem, + np.uint8_t[:, :] out=None, + char shift_x=0, char shift_y=0): + """Return greyscale morphological erosion of an image. + + 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. + shift_x, shift_y : bool + shift structuring element about center point. This only affects + eccentric structuring elements (i.e. selem with even numbered sides). + + Returns + ------- + eroded : uint8 array + The result of the morphological erosion. + """ cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] @@ -82,12 +128,9 @@ def erode(np.ndarray[np.uint8_t, ndim=2] image, else: out = np.ascontiguousarray(out) - cdef np.uint8_t* out_data = out.data - cdef np.uint8_t* image_data = image.data - cdef int r, c, rr, cc, s, value, local_min - cdef Py_ssize_t selem_num = np.sum(selem != 0) + cdef Py_ssize_t selem_num = np.sum(np.asarray(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)) @@ -106,13 +149,13 @@ def erode(np.ndarray[np.uint8_t, ndim=2] image, rr = r + sr[s] cc = c + sc[s] if 0 <= rr < rows and 0 <= cc < cols: - value = image_data[rr * cols + cc] + value = image[rr, cc] if value < local_min: local_min = value - out_data[r * cols + c] = local_min + out[r, c] = local_min free(sr) free(sc) - return out + return np.asarray(out) diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index 08ff0e04..adc63607 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -1,8 +1,10 @@ -__all__ = ['convex_hull_image'] +__all__ = ['convex_hull_image', 'convex_hull_object'] import numpy as np from ._pnpoly import grid_points_inside_poly from ._convex_hull import possible_hull +from skimage.morphology import label +from skimage.util import unique_rows def convex_hull_image(image): @@ -14,12 +16,12 @@ def convex_hull_image(image): Parameters ---------- image : ndarray - Binary input image. This array is cast to bool before processing. + Binary input image. This array is cast to bool before processing. Returns ------- - hull : ndarray of uint8 - Binary image with pixels in convex hull set to 255. + hull : ndarray of bool + Binary image with pixels in convex hull set to True. References ---------- @@ -42,7 +44,9 @@ def convex_hull_image(image): (-0.5, 0.5, 0, 0))): coords_corners[i * N:(i + 1) * N] = coords + [x_offset, y_offset] - coords = coords_corners + # repeated coordinates can *sometimes* cause problems in + # scipy.spatial.Delaunay, so we remove them. + coords = unique_rows(coords_corners) try: from scipy.spatial import Delaunay @@ -64,3 +68,45 @@ def convex_hull_image(image): mask = grid_points_inside_poly(image.shape[:2], v) return mask + + +def convex_hull_object(image, neighbors=8): + """Compute the convex hull image of individual objects in a binary image. + + The convex hull is the set of pixels included in the smallest convex + polygon that surround all white pixels in the input image. + + Parameters + ---------- + image : ndarray + Binary input image. + neighbors : {4, 8}, int + Whether to use 4- or 8-connectivity. + + Returns + ------- + hull : ndarray of bool + Binary image with pixels in convex hull set to True. + + Note + ---- + This function uses skimage.morphology.label to define unique objects, + finds the convex hull of each using convex_hull_image, and combines + these regions with logical OR. Be aware the convex hulls of unconnected + objects may overlap in the result. If this is suspected, consider using + convex_hull_image separately on each object. + + """ + + if neighbors != 4 and neighbors != 8: + raise ValueError('Neighbors must be either 4 or 8.') + + labeled_im = label(image, neighbors, background=0) + convex_obj = np.zeros(image.shape, dtype=bool) + convex_img = np.zeros(image.shape, dtype=bool) + + for i in range(0, labeled_im.max() + 1): + convex_obj = convex_hull_image(labeled_im == i) + convex_img = np.logical_or(convex_img, convex_obj) + + return convex_img diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 2cca69b6..ef7158b2 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -58,8 +58,8 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): raise NotImplementedError("In-place erosion not supported!") 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) + 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): @@ -111,8 +111,8 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): raise NotImplementedError("In-place dilation not supported!") 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) + return cmorph._dilate(image, selem, out=out, + shift_x=shift_x, shift_y=shift_y) def opening(image, selem, out=None): diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 09d3c9e6..3fffd28e 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -60,13 +60,15 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): >>> import numpy as np >>> from skimage.morphology import reconstruction - First, we create a sinusoidal mask image w/ peaks at middle and ends. + First, we create a sinusoidal mask image with 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 @@ -131,7 +133,7 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): else: selem = selem.copy() - if offset == None: + if offset is 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]) @@ -157,7 +159,7 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): # 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 + 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() diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 6274df53..5c157e7f 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -21,8 +21,10 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): Raises ------ - ValueError + TypeError If the input array is of an invalid type, such as float or string. + ValueError + If the input array contains negative values. Returns ------- @@ -32,7 +34,6 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): 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) @@ -50,10 +51,10 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): >>> 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) + # Should use `issubdtype` for bool below, but there's a bug in numpy 1.7 + if not (ar.dtype == bool or np.issubdtype(ar.dtype, np.integer)): + raise TypeError("Only bool or integer image types are supported. " + "Got %s." % ar.dtype) if in_place: out = ar @@ -65,7 +66,8 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): if out.dtype == bool: selem = nd.generate_binary_structure(ar.ndim, connectivity) - ccs = nd.label(ar, selem)[0] + ccs = np.zeros_like(ar, dtype=np.int32) + nd.label(ar, selem, output=ccs) else: ccs = out diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 41be0737..7e566773 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -15,18 +15,18 @@ def square(width, dtype=np.uint8): Parameters ---------- width : int - The width and height of the square + The width and height of the square Other Parameters ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - A structuring element consisting only of ones, i.e. every - pixel belongs to the neighborhood. + A structuring element consisting only of ones, i.e. every + pixel belongs to the neighborhood. """ return np.ones((width, width), dtype=dtype) @@ -41,21 +41,20 @@ def rectangle(width, height, dtype=np.uint8): Parameters ---------- width : int - The width of the rectangle - + The width of the rectangle height : int - The height of the rectangle + The height of the rectangle Other Parameters ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - A structuring element consisting only of ones, i.e. every - pixel belongs to the neighborhood. + A structuring element consisting only of ones, i.e. every + pixel belongs to the neighborhood. """ return np.ones((width, height), dtype=dtype) @@ -71,17 +70,19 @@ def diamond(radius, dtype=np.uint8): Parameters ---------- radius : int - The radius of the diamond-shaped structuring element. + The radius of the diamond-shaped structuring element. + Other Parameters + ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. """ half = radius (I, J) = np.meshgrid(range(0, radius * 2 + 1), range(0, radius * 2 + 1)) @@ -93,24 +94,199 @@ 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. + it and the origin is no greater than radius. Parameters ---------- radius : int - The radius of the disk-shaped structuring element. + The radius of the disk-shaped structuring element. + Other Parameters + ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. """ L = np.linspace(-radius, radius, 2 * radius + 1) (X, Y) = np.meshgrid(L, L) s = X**2 s += Y**2 return np.array(s <= radius * radius, dtype=dtype) + + +def cube(width, dtype=np.uint8): + """ + Generates a cube-shaped structuring element (the 3D equivalent of + a square). Every pixel along the perimeter has a chessboard distance + no greater than radius (radius=floor(width/2)) pixels. + + Parameters + ---------- + width : int + The width, height and depth of the cube + + Other Parameters + ---------------- + dtype : data-type + The data type of the structuring element. + + Returns + ------- + selem : ndarray + A structuring element consisting only of ones, i.e. every + pixel belongs to the neighborhood. + + """ + return np.ones((width, width, width), dtype=dtype) + + +def octahedron(radius, dtype=np.uint8): + """ + Generates a octahedron-shaped structuring element of a given radius + (the 3D equivalent of a diamond). A pixel is part of the + neighborhood (i.e. labeled 1) if the city block/manhattan distance + between it and the center of the neighborhood is no greater than + radius. + + Parameters + ---------- + radius : int + The radius of the octahedron-shaped structuring element. + + Other Parameters + ---------------- + dtype : data-type + The data type of the structuring element. + + Returns + ------- + + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + """ + # note that in contrast to diamond(), this method allows non-integer radii + n = 2 * radius + 1 + Z, Y, X = np.mgrid[-radius:radius:n*1j, + -radius:radius:n*1j, + -radius:radius:n*1j] + s = np.abs(X) + np.abs(Y) + np.abs(Z) + return np.array(s <= radius, dtype=dtype) + + +def ball(radius, dtype=np.uint8): + """ + Generates a ball-shaped structuring element of a given radius (the + 3D equivalent of a disk). A pixel is within the neighborhood if the + euclidean distance between it and the origin is no greater than + radius. + + Parameters + ---------- + radius : int + The radius of the ball-shaped structuring element. + + Other Parameters + ---------------- + dtype : data-type + The data type of the structuring element. + + Returns + ------- + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + """ + n = 2 * radius + 1 + Z, Y, X = np.mgrid[-radius:radius:n*1j, + -radius:radius:n*1j, + -radius:radius:n*1j] + s = X**2 + Y**2 + Z**2 + return np.array(s <= radius * radius, dtype=dtype) + + +def octagon(m, n, dtype=np.uint8): + """ + Generates an octagon shaped structuring element with a given size of + horizontal and vertical sides and a given height or width of slanted + sides. The slanted sides are 45 or 135 degrees to the horizontal axis + and hence the widths and heights are equal. + + Parameters + ---------- + m : int + The size of the horizontal and vertical sides. + n : int + The height or width of the slanted sides. + + Other Parameters + ---------------- + dtype : data-type + The data type of the structuring element. + + Returns + ------- + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + + """ + from . import convex_hull_image + selem = np.zeros((m + 2*n, m + 2*n)) + selem[0, n] = 1 + selem[n, 0] = 1 + selem[0, m + n - 1] = 1 + selem[m + n - 1, 0] = 1 + selem[-1, n] = 1 + selem[n, -1] = 1 + selem[-1, m + n - 1] = 1 + selem[m + n - 1, -1] = 1 + selem = convex_hull_image(selem).astype(dtype) + return selem + + +def star(a, dtype=np.uint8): + """ + Generates a star shaped structuring element that has 8 vertices and is an + overlap of square of size `2*a + 1` with its 45 degree rotated version. + The slanted sides are 45 or 135 degrees to the horizontal axis. + + Parameters + ---------- + a : int + Parameter deciding the size of the star structural element. The side + of the square array returned is `2*a + 1 + 2*floor(a / 2)`. + + Other Parameters + ---------------- + dtype : data-type + The data type of the structuring element. + + Returns + ------- + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + + """ + from . import convex_hull_image + if a == 1: + bfilter = np.zeros((3, 3), dtype) + bfilter[:] = 1 + return bfilter + m = 2 * a + 1 + n = a // 2 + selem_square = np.zeros((m + 2 * n, m + 2 * n)) + selem_square[n: m + n, n: m + n] = 1 + c = (m + 2 * n - 1) // 2 + selem_rotated = np.zeros((m + 2 * n, m + 2 * n)) + selem_rotated[0, c] = selem_rotated[-1, c] = selem_rotated[c, 0] = selem_rotated[c, -1] = 1 + selem_rotated = convex_hull_image(selem_rotated).astype(int) + selem = selem_square + selem_rotated + selem[selem > 0] = 1 + return selem.astype(dtype) diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py new file mode 100644 index 00000000..deab3d82 --- /dev/null +++ b/skimage/morphology/tests/test_binary.py @@ -0,0 +1,68 @@ +import numpy as np +from numpy import testing + +from skimage import data, color +from skimage.util import img_as_bool +from skimage.morphology import binary, grey, selem + + +lena = color.rgb2gray(data.lena()) +bw_lena = lena > 100 + + +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) + + +def test_selem_overflow(): + strel = np.ones((17, 17), dtype=np.uint8) + img = np.zeros((20, 20)) + img[2:19, 2:19] = 1 + binary_res = binary.binary_erosion(img, strel) + grey_res = img_as_bool(grey.erosion(img, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_out_argument(): + for func in (binary.binary_erosion, binary.binary_dilation): + strel = np.ones((3, 3), dtype=np.uint8) + img = np.ones((10, 10)) + out = np.zeros_like(img) + out_saved = out.copy() + func(img, strel, out=out) + testing.assert_(np.any(out != out_saved)) + testing.assert_array_equal(out, func(img, strel)) + +if __name__ == '__main__': + testing.run_module_suite() diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index 1e0829ca..1169be85 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -82,6 +82,14 @@ class TestConnectedComponents: [-1, 0, -1], [-1, -1, -1]]) + def test_return_num(self): + x = np.array([[1, 0, 6], + [0, 0, 6], + [5, 5, 5]]) + + assert_array_equal(label(x, return_num=True)[1], 4) + assert_array_equal(label(x, background=0, return_num=True)[1], 3) + if __name__ == "__main__": run_module_suite() diff --git a/skimage/morphology/tests/test_convex_hull.py b/skimage/morphology/tests/test_convex_hull.py index 9ab514d0..ee3b6bfa 100644 --- a/skimage/morphology/tests/test_convex_hull.py +++ b/skimage/morphology/tests/test_convex_hull.py @@ -1,7 +1,7 @@ import numpy as np -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_equal, assert_raises from numpy.testing.decorators import skipif -from skimage.morphology import convex_hull_image +from skimage.morphology import convex_hull_image, convex_hull_object from skimage.morphology._convex_hull import possible_hull try: @@ -32,6 +32,19 @@ def test_basic(): assert_array_equal(convex_hull_image(image), expected) +@skipif(not scipy_spatial) +def test_pathological_qhull_example(): + image = np.array( + [[0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1], + [1, 1, 1, 0, 0, 0, 0]], dtype=bool) + expected = np.array( + [[0, 0, 0, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 0, 0, 0]], dtype=bool) + assert_array_equal(convex_hull_image(image), expected) + + @skipif(not scipy_spatial) def test_possible_hull(): image = np.array( @@ -65,5 +78,47 @@ def test_possible_hull(): ph = possible_hull(image) assert_array_equal(ph, expected) + +@skipif(not scipy_spatial) +def test_object(): + image = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 0, 0, 1, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 1, 0], + [1, 0, 0, 0, 0, 0, 1, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool) + + expected4 = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 0, 0, 1, 0, 1], + [1, 1, 1, 0, 0, 0, 0, 1, 0], + [1, 1, 0, 0, 0, 0, 1, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool) + + assert_array_equal(convex_hull_object(image, 4), expected4) + + expected8 = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 0, 0, 1, 1, 1], + [1, 1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 0, 0, 0, 0, 1, 1, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool) + + assert_array_equal(convex_hull_object(image, 8), expected8) + + assert_raises(ValueError, convex_hull_object, image, 7) + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 244ec566..e2a3928d 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -6,7 +6,7 @@ from numpy import testing import skimage from skimage import data_dir from skimage.util import img_as_bool -from skimage.morphology import binary, grey, selem +from skimage.morphology import grey, selem lena = np.load(os.path.join(data_dir, 'lena_GRAY_U8.npy')) @@ -155,40 +155,5 @@ 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 index abc718c2..94d27f64 100644 --- a/skimage/morphology/tests/test_misc.py +++ b/skimage/morphology/tests/test_misc.py @@ -42,9 +42,22 @@ def test_labeled_image(): assert_array_equal(observed, expected) +def test_uint_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=np.uint8) + expected = np.array([[2, 2, 2, 0, 0], + [2, 2, 2, 0, 0], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=np.uint8) + 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) + assert_raises(TypeError, remove_small_objects, float_test) def test_negative_input(): diff --git a/skimage/morphology/tests/test_selem.py b/skimage/morphology/tests/test_selem.py index eec6cd5b..7895fa58 100644 --- a/skimage/morphology/tests/test_selem.py +++ b/skimage/morphology/tests/test_selem.py @@ -8,7 +8,7 @@ from numpy.testing import * from skimage import data_dir from skimage.io import * from skimage import data_dir -from skimage.morphology import * +from skimage.morphology import selem class TestSElem(): @@ -37,8 +37,77 @@ class TestSElem(): assert_equal(expected_mask, actual_mask) k = k + 1 + def strel_worker_3d(self, fn, func): + matlab_masks = np.load(os.path.join(data_dir, fn)) + k = 0 + for arrname in sorted(matlab_masks): + expected_mask = matlab_masks[arrname] + actual_mask = func(k) + if (expected_mask.shape == (1,)): + expected_mask = expected_mask[:, np.newaxis] + # Test center slice for each dimension. This gives a good + # indication of validity without the need for a 3D reference + # mask. + c = int(expected_mask.shape[0]/2) + assert_equal(expected_mask, actual_mask[c,:,:]) + assert_equal(expected_mask, actual_mask[:,c,:]) + assert_equal(expected_mask, actual_mask[:,:,c]) + 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) + + def test_selem_ball(self): + self.strel_worker_3d("disk-matlab-output.npz", selem.ball) + + def test_selem_octahedron(self): + self.strel_worker_3d("diamond-matlab-output.npz", selem.octahedron) + + def test_selem_octagon(self): + expected_mask1 = np.array([[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [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], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0]], dtype=np.uint8) + actual_mask1 = selem.octagon(5, 3) + expected_mask2 = np.array([[0, 1, 0], + [1, 1, 1], + [0, 1, 0]], dtype=np.uint8) + actual_mask2 = selem.octagon(1, 1) + assert_equal(expected_mask1, actual_mask1) + assert_equal(expected_mask2, actual_mask2) + + def test_selem_star(self): + expected_mask1 = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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, 0, 0, 0]], dtype=np.uint8) + actual_mask1 = selem.star(4) + expected_mask2 = np.array([[1, 1, 1], + [1, 1, 1], + [1, 1, 1]], dtype=np.uint8) + actual_mask2 = selem.star(1) + assert_equal(expected_mask1, actual_mask1) + assert_equal(expected_mask2, actual_mask2) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/morphology/tests/test_skeletonize.py b/skimage/morphology/tests/test_skeletonize.py index 9c8fe249..4f7c943a 100644 --- a/skimage/morphology/tests/test_skeletonize.py +++ b/skimage/morphology/tests/test_skeletonize.py @@ -71,10 +71,10 @@ class TestSkeletonize(): image[10:-10, -100:-10] = 1 # foreground object 2 - rs, cs = draw.bresenham(250, 150, 10, 280) + rs, cs = draw.line(250, 150, 10, 280) for i in range(10): image[rs + i, cs] = 1 - rs, cs = draw.bresenham(10, 150, 250, 280) + rs, cs = draw.line(10, 150, 250, 280) for i in range(20): image[rs + i, cs] = 1 diff --git a/skimage/morphology/tests/test_watershed.py b/skimage/morphology/tests/test_watershed.py index 46298ae5..5dc0f07c 100644 --- a/skimage/morphology/tests/test_watershed.py +++ b/skimage/morphology/tests/test_watershed.py @@ -48,8 +48,7 @@ import unittest import numpy as np import scipy.ndimage -from skimage.morphology.watershed import watershed, \ - _slow_watershed, is_local_maximum +from skimage.morphology.watershed import watershed, _slow_watershed eps = 1e-12 @@ -296,27 +295,27 @@ 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]]) + 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) markers[6, 7] = 1 @@ -332,27 +331,27 @@ class TestWatershed(unittest.TestCase): 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]]) + 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]]) mask = (data != 255) markers = np.zeros(data.shape, int) markers[6, 7] = 1 @@ -387,101 +386,5 @@ class TestWatershed(unittest.TestCase): self.eight) -class TestIsLocalMaximum(unittest.TestCase): - def test_00_00_empty(self): - image = np.zeros((10, 20)) - 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) - image[5, 5] = 1 - 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) - image[5, 5:6] = 1 - 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) - image[5, 5] = 1 - image[5, 6] = .5 - labels[5, 5:6] = 1 - expected = (image == 1) - result = is_local_maximum(image, labels, np.ones((3, 3), bool)) - 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 - labels[image > 0] = 1 - expected = (labels == 1) - 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 - expected = (labels > 0) - 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 - expected = (labels > 0) - 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] - 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 = 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)) - footprint = np.array([[1]]) - 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 fbd63281..2bdb57b2 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -116,7 +116,9 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): >>> # to the background >>> from scipy import ndimage >>> distance = ndimage.distance_transform_edt(image) - >>> local_maxi = is_local_maximum(distance, image, np.ones((3, 3))) + >>> from skimage.feature import peak_local_max + >>> local_maxi = peak_local_max(distance, labels=image, + ... footprint=np.ones((3, 3))) >>> markers = ndimage.label(local_maxi)[0] >>> labels = watershed(-distance, markers, mask=image) @@ -124,13 +126,13 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): separate overlapping spheres. """ - if connectivity == None: + if connectivity is 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") - if offset == None: + if offset is None: if any([x % 2 == 0 for x in c_connectivity.shape]): raise ValueError("Connectivity array must have an unambiguous " "center") @@ -162,7 +164,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): "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: + if mask is not 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") @@ -200,7 +202,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): stride = np.dot(image_stride, np.array(offs)) offs.insert(0, stride) c.append(offs) - c = np.array(c, np.int32) + c = np.array(c, dtype=np.int32) pq, age = __heapify_markers(c_markers, c_image) pq = np.ascontiguousarray(pq, dtype=np.int32) @@ -224,79 +226,6 @@ 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 - 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 - to be the point in question. - - Returns - ------- - 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)) - >>> image[1, 2] = 2 - >>> image[3, 3] = 1 - >>> image - array([[ 0., 0., 0., 0.], - [ 0., 0., 2., 0.], - [ 0., 0., 0., 0.], - [ 0., 0., 0., 1.]]) - >>> is_local_maximum(image) - array([[ True, False, False, False], - [ True, False, True, False], - [ True, False, False, False], - [ 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) - >>> labels - array([[1, 1, 2, 2], - [1, 1, 2, 2], - [3, 3, 4, 4], - [3, 3, 4, 4]]) - >>> image - array([[ 0, 1, 2, 3], - [ 4, 5, 6, 7], - [ 8, 9, 10, 11], - [12, 13, 14, 15]]) - >>> is_local_maximum(image, labels=labels) - array([[False, False, False, False], - [False, True, False, True], - [False, False, False, False], - [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 # pedagogical purposes @@ -398,7 +327,7 @@ def _slow_watershed(image, markers, connectivity=8, mask=None): continue if labels[x, y]: continue - if mask != None and not mask[x, y]: + if mask is not None and not mask[x, y]: continue # label the pixel labels[x, y] = pix_label diff --git a/skimage/scripts/skivi.py b/skimage/scripts/skivi.py index 2e907bbe..7b7b5aeb 100644 --- a/skimage/scripts/skivi.py +++ b/skimage/scripts/skivi.py @@ -6,7 +6,7 @@ def main(): import sys if len(sys.argv) != 2: - print "Usage: skivi " + print("Usage: skivi ") sys.exit(-1) io.use_plugin('qt') diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index a0fab77a..aea6c70f 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,7 +1,20 @@ from .random_walker_segmentation import random_walker from ._felzenszwalb import felzenszwalb -from ._slic import slic +from .slic_superpixels 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 +from ._join import join_segmentations, relabel_from_one, relabel_sequential + + +__all__ = ['random_walker', + 'felzenszwalb', + 'slic', + 'quickshift', + 'find_boundaries', + 'visualize_boundaries', + 'mark_boundaries', + 'clear_border', + 'join_segmentations', + 'relabel_from_one', + 'relabel_sequential'] diff --git a/skimage/segmentation/_felzenszwalb_cy.pyx b/skimage/segmentation/_felzenszwalb_cy.pyx index b525a0d9..8590e17d 100644 --- a/skimage/segmentation/_felzenszwalb_cy.pyx +++ b/skimage/segmentation/_felzenszwalb_cy.pyx @@ -12,7 +12,8 @@ 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): +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 @@ -25,12 +26,12 @@ def _felzenszwalb_grey(image, double scale=1, sigma=0.8, Py_ssize_t min_size=20) ---------- image: ndarray Input image. - scale: float + scale: float, optional (default 1) Sets the obervation level. Higher means larger clusters. - sigma: float + sigma: float, optional (default 0.8) Width of Gaussian smoothing kernel used in preprocessing. Larger sigma gives smother segment boundaries. - min_size: int + min_size: int, optional (default 20) Minimum component size. Enforced using postprocessing. Returns @@ -76,7 +77,7 @@ def _felzenszwalb_grey(image, double scale=1, sigma=0.8, Py_ssize_t min_size=20) = np.ones(width * height, dtype=np.intp) # 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 cnp.intp_t 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. diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index 454da71e..6095382d 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -1,11 +1,13 @@ import numpy as np +from skimage._shared.utils import deprecated + 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. + The join J of S1 and S2 is defined as the segmentation in which two + voxels are in the same segment if and only if they are in the same + segment in *both* S1 and S2. Parameters ---------- @@ -19,7 +21,6 @@ def join_segmentations(s1, s2): Examples -------- - >>> import numpy as np >>> from skimage.segmentation import join_segmentations >>> s1 = np.array([[0, 0, 1, 1], ... [0, 2, 1, 1], @@ -35,36 +36,66 @@ def join_segmentations(s1, s2): 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] + s1 = relabel_sequential(s1)[0] + s2 = relabel_sequential(s2)[0] j = (s2.max() + 1) * s1 + s2 - j = relabel_from_one(j)[0] + j = relabel_sequential(j)[0] return j + +@deprecated('relabel_sequential') def relabel_from_one(label_field): """Convert labels in an arbitrary label field to {1, ... number_of_labels}. + This function is deprecated, see ``relabel_sequential`` for more. + """ + return relabel_sequential(label_field, offset=1) + + +def relabel_sequential(label_field, offset=1): + """Relabel arbitrary labels to {`offset`, ... `offset` + 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) + label_field : numpy array of int, arbitrary shape + An array of labels. + offset : int, optional + The return labels will start at `offset`, which should be + strictly positive. 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) + relabeled : numpy array of int, same shape as `label_field` + The input label field with labels mapped to + {1, ..., number_of_labels}. + forward_map : numpy array of int, shape ``(label_field.max() + 1,)`` + The map from the original label space to the returned label + space. Can be used to re-apply the same mapping. See examples + for usage. + inverse_map : 1D numpy array of int, of length offset + number of labels + The map from the new label space to the original space. This + can be used to reconstruct the original label field from the + relabeled one. + + Notes + ----- + The label 0 is assumed to denote the background and is never remapped. + + The forward map can be extremely big for some inputs, since its + length is given by the maximum of the label field. However, in most + situations, ``label_field.max()`` is much smaller than + ``label_field.size``, and in these cases the forward map is + guaranteed to be smaller than either the input or output images. 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) + >>> from skimage.segmentation import relabel_sequential + >>> label_field = np.array([1, 1, 5, 5, 8, 99, 42]) + >>> relab, fw, inv = relabel_sequential(label_field) >>> relab array([1, 1, 2, 2, 3, 5, 4]) >>> fw @@ -79,15 +110,20 @@ def relabel_from_one(label_field): True >>> (inv[relab] == label_field).all() True + >>> relab, fw, inv = relabel_sequential(label_field, offset=5) + >>> relab + array([5, 5, 6, 6, 7, 9, 8]) """ labels = np.unique(label_field) labels0 = labels[labels != 0] m = labels.max() - if m == len(labels0): # nothing to do, already 1...n labels + 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) + forward_map[labels0] = np.arange(offset, offset + 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 + inverse_map = np.zeros(offset - 1 + len(labels), dtype=np.intp) + inverse_map[(offset - 1):] = labels + relabeled = forward_map[label_field] + return relabeled, forward_map, inverse_map diff --git a/skimage/segmentation/_quickshift.pyx b/skimage/segmentation/_quickshift.pyx index cc649e8f..b43775b6 100644 --- a/skimage/segmentation/_quickshift.pyx +++ b/skimage/segmentation/_quickshift.pyx @@ -24,23 +24,23 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, ---------- image : (width, height, channels) ndarray Input image. - ratio : float, between 0 and 1. + ratio : float, optional, between 0 and 1 (default 1). Balances color-space proximity and image-space proximity. Higher values give more weight to color-space. - kernel_size : float + kernel_size : float, optional (default 5) Width of Gaussian kernel used in smoothing the sample density. Higher means fewer clusters. - max_dist : float + max_dist : float, optional (default 10) Cut-off point for data distances. Higher means fewer clusters. - return_tree : bool + return_tree : bool, optional (default False) Whether to return the full segmentation hierarchy tree and distances. - sigma : float + sigma : float, optional (default 0) Width for Gaussian smoothing as preprocessing. Zero means no smoothing. - convert2lab : bool + convert2lab : bool, optional (default True) 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 : None (default) or int, optional Random seed used for breaking ties. Returns diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 7db072c0..c3d95ee0 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -2,140 +2,147 @@ #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False -import numpy as np -from time import time -from scipy import ndimage +from libc.float cimport DBL_MAX +import numpy as np cimport numpy as cnp -from ..util import img_as_float -from ..color import rgb2lab, gray2rgb +from skimage.util import regular_grid -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. +def _slic_cython(double[:, :, :, ::1] image_zyx, + double[:, ::1] segments, + Py_ssize_t max_iter, + double[::1] spacing): + """Helper function for SLIC segmentation. 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. + image_zyx : 4D array of double, shape (Z, Y, X, C) + The input image. + segments : 2D array of double, shape (N, 3 + C) + The initial centroids obtained by SLIC as [Z, Y, X, C...]. 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. + The maximum number of k-means iterations. + spacing : 1D array of double, shape (3,) + The voxel spacing along each image dimension. This parameter + controls the weights of the distances along z, y, and x during + k-means clustering. Returns ------- - segment_mask : (width, height) ndarray - Integer mask indicating segment labels. + nearest_segments : 3D array of int, shape (Z, Y, X) + The label field/superpixels found by SLIC. Notes ----- - The image is smoothed using a Gaussian kernel prior to segmentation. + The image is considered to be in (z, y, x) order, which can be + surprising. More commonly, the order (x, y, z) is used. However, + in 3D image analysis, 'z' is usually the "special" dimension, with, + for example, a different effective resolution than the other two + axes. Therefore, x and y are often processed together, or viewed as + a cut-plane through the volume. So, if the order was (x, y, z) and + we wanted to look at the 5th cut plane, we would write:: - 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. + my_z_plane = img3d[:, :, 5] - 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) + but, assuming a C-contiguous array, this would grab a discontiguous + slice of memory, which is bad for performance. In contrast, if we + see the image as (z, y, x) ordered, we would do:: + + my_z_plane = img3d[5] + + and get back a contiguous block of memory. This is better both for + performance and for readability. """ - 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] + # initialize on grid + cdef Py_ssize_t depth, height, width + depth = image_zyx.shape[0] + height = image_zyx.shape[1] + width = image_zyx.shape[2] + + cdef Py_ssize_t n_segments = segments.shape[0] + # number of features [X, Y, Z, ...] + cdef Py_ssize_t n_features = segments.shape[1] + # 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] + cdef Py_ssize_t step_z, step_y, step_x + slices = regular_grid((depth, height, width), n_segments) + step_z, step_y, step_x = [int(s.step) for s in slices] - 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 Py_ssize_t[:, :, ::1] nearest_segments \ + = np.empty((depth, height, width), dtype=np.intp) + cdef double[:, :, ::1] distance \ + = np.empty((depth, height, width), dtype=np.double) + cdef Py_ssize_t[::1] n_segment_elems = np.zeros(n_segments, dtype=np.intp) + + cdef Py_ssize_t i, c, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max + cdef char change + cdef double dist_center, cx, cy, cz, dy, dz + + cdef double sz, sy, sx + sz = spacing[0] + sy = spacing[1] + sx = spacing[2] - 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: + change = 0 + distance[:, :, :] = DBL_MAX + + # assign pixels to segments + for k in range(n_segments): + + # segment coordinate centers + cz = segments[k, 0] + cy = segments[k, 1] + cx = segments[k, 2] + + # compute windows + z_min = max(cz - 2 * step_z, 0) + z_max = min(cz + 2 * step_z + 1, depth) + y_min = max(cy - 2 * step_y, 0) + y_max = min(cy + 2 * step_y + 1, height) + x_min = max(cx - 2 * step_x, 0) + x_max = min(cx + 2 * step_x + 1, width) + + for z in range(z_min, z_max): + dz = (sz * (cz - z)) ** 2 + for y in range(y_min, y_max): + dy = (sy * (cy - y)) ** 2 + for x in range(x_min, x_max): + dist_center = dz + dy + (sx * (cx - x)) ** 2 + for c in range(3, n_features): + dist_center += (image_zyx[z, y, x, c - 3] + - segments[k, c]) ** 2 + if distance[z, y, x] > dist_center: + nearest_segments[z, y, x] = k + distance[z, y, x] = dist_center + change = 1 + + # stop if no pixel changed its segment + if change == 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 + + # recompute segment centers + + # sum features for all segments + n_segment_elems[:] = 0 + segments[:, :] = 0 + for z in range(depth): + for y in range(height): + for x in range(width): + k = nearest_segments[z, y, x] + n_segment_elems[k] += 1 + segments[k, 0] += z + segments[k, 1] += y + segments[k, 2] += x + for c in range(3, n_features): + segments[k, c] += image_zyx[z, y, x, c - 3] + + # divide by number of elements per segment to obtain mean + for k in range(n_segments): + for c in range(n_features): + segments[k, c] /= n_segment_elems[k] + + return np.asarray(nearest_segments) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 6df714d2..17c4c98b 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -12,11 +12,26 @@ import warnings import numpy as np from scipy import sparse, ndimage + +# executive summary for next code block: try to import umfpack from +# scipy, but make sure not to raise a fuss if it fails since it's only +# needed to speed up a few cases. +# See discussions at: +# https://groups.google.com/d/msg/scikit-image/FrM5IGP6wh4/1hp-FtVZmfcJ +# http://stackoverflow.com/questions/13977970/ignore-exceptions-printed-to-stderr-in-del/13977992?noredirect=1#comment28386412_13977992 try: from scipy.sparse.linalg.dsolve import umfpack + old_del = umfpack.UmfpackContext.__del__ + def new_del(self): + try: + old_del(self) + except AttributeError: + pass + umfpack.UmfpackContext.__del__ = new_del UmfpackContext = umfpack.UmfpackContext() except: UmfpackContext = None + try: from pyamg import ruge_stuben_solver amg_loaded = True @@ -62,14 +77,14 @@ def _make_graph_edges_3d(n_x, n_y, n_z): return edges -def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1., +def _compute_weights_3d(data, spacing, beta=130, eps=1.e-6, 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 + spacing) ** 2 # All channels considered together in this standard deviation beta /= 10 * data.std() if multichannel: @@ -82,10 +97,10 @@ def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1., return weights -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() +def _compute_gradients_3d(data, spacing): + gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel() / spacing[2] + gr_right = np.abs(data[:, :-1] - data[:, 1:]).ravel() / spacing[1] + gr_down = np.abs(data[:-1] - data[1:]).ravel() / spacing[0] return np.r_[gr_deep, gr_right, gr_down] @@ -101,9 +116,10 @@ def _make_laplacian_sparse(edges, weights): lap = sparse.coo_matrix((data, (i_indices, j_indices)), shape=(pixel_nb, pixel_nb)) connect = - np.ravel(lap.sum(axis=1)) - lap = sparse.coo_matrix((np.hstack((data, connect)), - (np.hstack((i_indices, diag)), np.hstack((j_indices, diag)))), - shape=(pixel_nb, pixel_nb)) + lap = sparse.coo_matrix( + (np.hstack((data, connect)), (np.hstack((i_indices, diag)), + np.hstack((j_indices, diag)))), + shape=(pixel_nb, pixel_nb)) return lap.tocsr() @@ -153,14 +169,15 @@ def _mask_edges_weights(edges, weights, mask): # Reassign edges labels to 0, 1, ... edges_number - 1 order = np.searchsorted(np.unique(edges.ravel()), np.arange(max_node_index + 1)) - edges = order[edges] + edges = order[edges.astype(np.int64)] return edges, weights -def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): - l_x, l_y, l_z = data.shape[:3] +def _build_laplacian(data, spacing, mask=None, beta=50, + multichannel=False): + l_x, l_y, l_z = tuple(data.shape[i] for i in range(3)) edges = _make_graph_edges_3d(l_x, l_y, l_z) - weights = _compute_weights_3d(data, beta=beta, eps=1.e-10, depth=depth, + weights = _compute_weights_3d(data, spacing, beta=beta, eps=1.e-10, multichannel=multichannel) if mask is not None: edges, weights = _mask_edges_weights(edges, weights, mask) @@ -173,7 +190,8 @@ def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, - multichannel=False, return_full_prob=False, depth=1.): + multichannel=False, return_full_prob=False, depth=1., + spacing=None): """Random walker algorithm for segmentation from markers. Random walker algorithm is implemented for gray-level or multichannel @@ -214,7 +232,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, - '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 - requires that the pyamg module (http://code.google.com/p/pyamg/) is + requires that the pyamg module (http://pyamg.org/) is installed. For images of size > 512x512, this is the recommended (fastest) mode. @@ -231,12 +249,16 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, 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. + depth : float, default 1. [DEPRECATED] 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. + `depth` is deprecated as of 0.9, in favor of `spacing`. + spacing : iterable of floats + Spacing between voxels in each spatial dimension. If `None`, then + the spacing between pixels/voxels in each dimension is assumed 1. Returns ------- @@ -259,12 +281,9 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, 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 `spacing` argument is specifically for anisotropic datasets, where + data points are spaced differently in one or more spatial dimensions. + Anisotropic data is commonly encountered in medical imaging. The algorithm was first proposed in *Random walks for image segmentation*, Leo Grady, IEEE Trans Pattern Anal Mach Intell. @@ -324,27 +343,47 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, """ - if UmfpackContext is None: + if mode is None: + mode = 'bf' + warnings.warn("Default mode will change in the next release from 'bf' " + "to 'cg_mg' if pyamg is installed, else to 'cg' if " + "SciPy was built with UMFPACK, or to 'bf' otherwise.") + + if UmfpackContext is None and mode == 'cg': 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)') + if depth != 1.: + warnings.warn('`depth` kwarg is deprecated, and will be removed in the' + ' next major release. Use `spacing` instead.') + + # Spacing kwarg checks + if spacing is None: + spacing = (1., 1.) + (depth, ) + elif len(spacing) == 2: + spacing = tuple(spacing) + (depth, ) + elif len(spacing) == 3: + pass + else: + raise ValueError('Input argument `spacing` incorrect, see docstring.') # Parse input data if not multichannel: # We work with 4-D arrays of floats + assert data.ndim > 1 and data.ndim < 4, 'For non-multichannel input, \ + data must be of dimension 2 \ + or 3.' dims = data.shape - data = np.atleast_3d(img_as_float(data)) - data.shape += (1,) + data = np.atleast_3d(img_as_float(data))[..., np.newaxis] 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)) + data = data[..., np.newaxis].transpose((0, 1, 3, 2)) if copy: labels = np.copy(labels) @@ -362,10 +401,10 @@ 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, - depth=depth, multichannel=multichannel) + lap_sparse = _build_laplacian(data, spacing, mask=labels >= 0, + beta=beta, multichannel=multichannel) else: - lap_sparse = _build_laplacian(data, beta=beta, depth=depth, + lap_sparse = _build_laplacian(data, spacing, beta=beta, multichannel=multichannel) lap_sparse, B = _buildAB(lap_sparse, labels) # We solve the linear system @@ -378,9 +417,9 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, 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.""") + """pyamg (http://pyamg.org/)) 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, return_full_prob=return_full_prob) else: @@ -411,7 +450,7 @@ def _solve_bf(lap_sparse, B, return_full_prob=False): """ lap_sparse = lap_sparse.tocsc() solver = sparse.linalg.factorized(lap_sparse.astype(np.double)) - X = np.array([solver(np.array((-B[i]).todense()).ravel())\ + X = np.array([solver(np.array((-B[i]).todense()).ravel()) for i in range(len(B))]) if not return_full_prob: X = np.argmax(X, axis=0) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py new file mode 100644 index 00000000..8276b1a8 --- /dev/null +++ b/skimage/segmentation/slic_superpixels.py @@ -0,0 +1,166 @@ +# coding=utf-8 + +import collections as coll +import numpy as np +from scipy import ndimage +import warnings + +from skimage.util import img_as_float, regular_grid +from skimage.segmentation._slic import _slic_cython +from skimage.color import rgb2lab + + +def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, + spacing=None, multichannel=True, convert2lab=True, ratio=None): + """Segments image using k-means clustering in Color-(x,y,z) space. + + Parameters + ---------- + image : 2D, 3D or 4D ndarray + Input image, which can be 2D or 3D, and grayscale or multichannel + (see `multichannel` parameter). + n_segments : int, optional + The (approximate) number of labels in the segmented output image. + compactness : float, optional + Balances color-space proximity and image-space proximity. Higher + values give more weight to image-space. As `compactness` tends to + infinity, superpixel shapes become square/cubic. + max_iter : int, optional + Maximum number of iterations of k-means. + sigma : float or (3,) array-like of floats, optional + Width of Gaussian smoothing kernel for pre-processing for each + dimension of the image. The same sigma is applied to each dimension in + case of a scalar value. Zero means no smoothing. + Note, that `sigma` is automatically scaled if it is scalar and a + manual voxel spacing is provided (see Notes section). + spacing : (3,) array-like of floats, optional + The voxel spacing along each image dimension. By default, `slic` + assumes uniform spacing (same voxel resolution along z, y and x). + This parameter controls the weights of the distances along z, y, + and x during k-means clustering. + multichannel : bool, optional + Whether the last axis of the image is to be interpreted as multiple + channels or another spatial dimension. + convert2lab : bool, optional + Whether the input should be converted to Lab colorspace prior to + segmentation. For this purpose, the input is assumed to be RGB. Highly + recommended. + ratio : float, optional + Synonym for `compactness`. This keyword is deprecated. + + Returns + ------- + labels : 2D or 3D array + Integer mask indicating segment labels. + + Raises + ------ + ValueError + If: + - the image dimension is not 2 or 3 and `multichannel == False`, OR + - the image dimension is not 3 or 4 and `multichannel == True` + + Notes + ----- + * If `sigma > 0`, the image is smoothed using a Gaussian kernel prior to + segmentation. + + * If `sigma` is scalar and `spacing` is provided, the kernel width is + divided along each dimension by the spacing. For example, if ``sigma=1`` + and ``spacing=[5, 1, 1]``, the effective `sigma` is ``[0.2, 1, 1]``. This + ensures sensible smoothing for anisotropic images. + + * The image is rescaled to be in [0, 1] prior to processing. + + * Images of shape (M, N, 3) are interpreted as 2D RGB images by default. To + interpret them as 3D with the last dimension having length 3, use + `multichannel=False`. + + 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, compactness=10) + >>> # Increasing the compactness parameter yields more square regions + >>> segments = slic(img, n_segments=100, compactness=20) + """ + + if sigma is None: + warnings.warn('Default value of keyword `sigma` changed from ``1`` ' + 'to ``0``.') + sigma = 0 + if ratio is not None: + warnings.warn('Keyword `ratio` is deprecated. Use `compactness` ' + 'instead.') + compactness = ratio + + image = img_as_float(image) + is_2d = False + if image.ndim == 2: + # 2D grayscale image + image = image[np.newaxis, ..., np.newaxis] + is_2d = True + elif image.ndim == 3 and multichannel: + # Make 2D multichannel image 3D with depth = 1 + image = image[np.newaxis, ...] + is_2d = True + elif image.ndim == 3 and not multichannel: + # Add channel as single last dimension + image = image[..., np.newaxis] + + if spacing is None: + spacing = np.ones(3) + elif isinstance(spacing, (list, tuple)): + spacing = np.array(spacing, dtype=np.double) + + if not isinstance(sigma, coll.Iterable): + sigma = np.array([sigma, sigma, sigma], dtype=np.double) + sigma /= spacing.astype(np.double) + elif isinstance(sigma, (list, tuple)): + sigma = np.array(sigma, dtype=np.double) + if (sigma > 0).any(): + # add zero smoothing for multichannel dimension + sigma = list(sigma) + [0] + image = ndimage.gaussian_filter(image, sigma) + + if convert2lab and multichannel: + if image.shape[3] != 3: + raise ValueError("Lab colorspace conversion requires a RGB image.") + image = rgb2lab(image) + + depth, height, width = image.shape[:3] + + # initialize cluster centroids for desired number of segments + grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] + slices = regular_grid(image.shape[:3], n_segments) + step_z, step_y, step_x = [int(s.step) for s in slices] + segments_z = grid_z[slices] + segments_y = grid_y[slices] + segments_x = grid_x[slices] + + segments_color = np.zeros(segments_z.shape + (image.shape[3],)) + segments = np.concatenate([segments_z[..., np.newaxis], + segments_y[..., np.newaxis], + segments_x[..., np.newaxis], + segments_color + ], axis=-1).reshape(-1, 3 + image.shape[3]) + segments = np.ascontiguousarray(segments) + + # we do the scaling of ratio in the same way as in the SLIC paper + # so the values have the same meaning + ratio = float(max((step_z, step_y, step_x))) / compactness + image = np.ascontiguousarray(image * ratio) + + labels = _slic_cython(image, segments, max_iter, spacing) + + if is_2d: + labels = labels[0] + + return labels diff --git a/skimage/segmentation/tests/test_join.py b/skimage/segmentation/tests/test_join.py index f03244e9..548fcc8d 100644 --- a/skimage/segmentation/tests/test_join.py +++ b/skimage/segmentation/tests/test_join.py @@ -1,6 +1,6 @@ import numpy as np from numpy.testing import assert_array_equal, assert_raises -from skimage.segmentation import join_segmentations, relabel_from_one +from skimage.segmentation import join_segmentations, relabel_sequential def test_join_segmentations(): s1 = np.array([[0, 0, 1, 1], @@ -24,9 +24,10 @@ def test_join_segmentations(): s3 = np.array([[0, 0, 1, 1], [0, 2, 2, 1]]) assert_raises(ValueError, join_segmentations, s1, s3) -def test_relabel_from_one(): + +def test_relabel_sequential_offset1(): ar = np.array([1, 1, 5, 5, 8, 99, 42]) - ar_relab, fw, inv = relabel_from_one(ar) + ar_relab, fw, inv = relabel_sequential(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) @@ -36,5 +37,29 @@ def test_relabel_from_one(): assert_array_equal(inv, inv_ref) +def test_relabel_sequential_offset5(): + ar = np.array([1, 1, 5, 5, 8, 99, 42]) + ar_relab, fw, inv = relabel_sequential(ar, offset=5) + ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 5; fw_ref[5] = 6; fw_ref[8] = 7; fw_ref[42] = 8; fw_ref[99] = 9 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + +def test_relabel_sequential_offset5_with0(): + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0]) + ar_relab, fw, inv = relabel_sequential(ar, offset=5) + ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8, 0]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 5; fw_ref[5] = 6; fw_ref[8] = 7; fw_ref[42] = 8; fw_ref[99] = 9 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 0, 0, 0, 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_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index 7df0241c..46a82d8e 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -1,26 +1,23 @@ import numpy as np from skimage.segmentation import random_walker -try: - import pyamg - amg_loaded = True -except ImportError: - amg_loaded = False +from skimage.transform import resize def make_2d_syntheticdata(lx, ly=None): if ly is None: ly = lx + np.random.seed(1234) data = np.zeros((lx, ly)) + 0.1 * np.random.randn(lx, ly) - small_l = int(lx / 5) - data[lx / 2 - small_l:lx / 2 + small_l, - ly / 2 - small_l:ly / 2 + small_l] = 1 - data[lx / 2 - small_l + 1:lx / 2 + small_l - 1, - ly / 2 - small_l + 1:ly / 2 + small_l - 1] = \ - 0.1 * np.random.randn(2 * small_l - 2, 2 * small_l - 2) - data[lx / 2 - small_l, ly / 2 - small_l / 8:ly / 2 + small_l / 8] = 0 + small_l = int(lx // 5) + data[lx // 2 - small_l:lx // 2 + small_l, + ly // 2 - small_l:ly // 2 + small_l] = 1 + data[lx // 2 - small_l + 1:lx // 2 + small_l - 1, + ly // 2 - small_l + 1:ly // 2 + small_l - 1] = ( + 0.1 * np.random.randn(2 * small_l - 2, 2 * small_l - 2)) + data[lx // 2 - small_l, ly // 2 - small_l // 8:ly // 2 + small_l // 8] = 0 seeds = np.zeros_like(data) - seeds[lx / 5, ly / 5] = 1 - seeds[lx / 2 + small_l / 4, ly / 2 - small_l / 4] = 2 + seeds[lx // 5, ly // 5] = 1 + seeds[lx // 2 + small_l // 4, ly // 2 - small_l // 4] = 2 return data, seeds @@ -29,22 +26,25 @@ def make_3d_syntheticdata(lx, ly=None, lz=None): ly = lx if lz is None: lz = lx + np.random.seed(1234) data = np.zeros((lx, ly, lz)) + 0.1 * np.random.randn(lx, ly, lz) - small_l = int(lx / 5) - data[lx / 2 - small_l:lx / 2 + small_l, - ly / 2 - small_l:ly / 2 + small_l, - lz / 2 - small_l:lz / 2 + small_l] = 1 - data[lx / 2 - small_l + 1:lx / 2 + small_l - 1, - ly / 2 - small_l + 1:ly / 2 + small_l - 1, - lz / 2 - small_l + 1:lz / 2 + small_l - 1] = 0 + small_l = int(lx // 5) + data[lx // 2 - small_l:lx // 2 + small_l, + ly // 2 - small_l:ly // 2 + small_l, + lz // 2 - small_l:lz // 2 + small_l] = 1 + data[lx // 2 - small_l + 1:lx // 2 + small_l - 1, + ly // 2 - small_l + 1:ly // 2 + small_l - 1, + lz // 2 - small_l + 1:lz // 2 + small_l - 1] = 0 # make a hole - hole_size = np.max([1, small_l / 8]) - data[lx / 2 - small_l, - ly / 2 - hole_size:ly / 2 + hole_size,\ - lz / 2 - hole_size:lz / 2 + hole_size] = 0 + hole_size = np.max([1, small_l // 8]) + data[lx // 2 - small_l, + ly // 2 - hole_size:ly // 2 + hole_size, + lz // 2 - hole_size:lz // 2 + hole_size] = 0 seeds = np.zeros_like(data) - seeds[lx / 5, ly / 5, lz / 5] = 1 - seeds[lx / 2 + small_l / 4, ly / 2 - small_l / 4, lz / 2 - small_l / 4] = 2 + seeds[lx // 5, ly // 5, lz // 5] = 1 + seeds[lx // 2 + small_l // 4, + ly // 2 - small_l // 4, + lz // 2 - small_l // 4] = 2 return data, seeds @@ -54,17 +54,21 @@ 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() + assert data.shape == labels.shape full_prob_bf = random_walker(data, labels, beta=90, mode='bf', - return_full_prob=True) + return_full_prob=True) assert (full_prob_bf[1, 25:45, 40:60] >= - full_prob_bf[0, 25:45, 40:60]).all() + full_prob_bf[0, 25:45, 40:60]).all() + assert data.shape == labels.shape # 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) + return_full_prob=True) assert (full_prob_bf[1, 25:45, 40:60] >= - full_prob_bf[0, 25:45, 40:60]).all() + full_prob_bf[0, 25:45, 40:60]).all() assert len(full_prob_bf) == 3 + assert data.shape == labels.shape + def test_2d_cg(): lx = 70 @@ -72,10 +76,12 @@ 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() + assert data.shape == labels.shape full_prob = random_walker(data, labels, beta=90, mode='cg', - return_full_prob=True) + return_full_prob=True) assert (full_prob[1, 25:45, 40:60] >= - full_prob[0, 25:45, 40:60]).all() + full_prob[0, 25:45, 40:60]).all() + assert data.shape == labels.shape return data, labels_cg @@ -85,10 +91,12 @@ 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() + assert data.shape == labels.shape full_prob = random_walker(data, labels, beta=90, mode='cg_mg', - return_full_prob=True) + return_full_prob=True) assert (full_prob[1, 25:45, 40:60] >= - full_prob[0, 25:45, 40:60]).all() + full_prob[0, 25:45, 40:60]).all() + assert data.shape == labels.shape return data, labels_cg_mg @@ -96,10 +104,11 @@ def test_types(): lx = 70 ly = 100 data, labels = make_2d_syntheticdata(lx, ly) - data = 255 * (data - data.min()) / (data.max() - data.min()) + 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() + assert data.shape == labels.shape return data, labels_cg_mg @@ -110,6 +119,7 @@ def test_reorder_labels(): labels[labels == 2] = 4 labels_bf = random_walker(data, labels, beta=90, mode='bf') assert (labels_bf[25:45, 40:60] == 2).all() + assert data.shape == labels.shape return data, labels_bf @@ -121,6 +131,7 @@ def test_2d_inactive(): labels[46:50, 33:38] = -2 labels = random_walker(data, labels, beta=90) assert (labels.reshape((lx, ly))[25:45, 40:60] == 2).all() + assert data.shape == labels.shape return data, labels @@ -130,6 +141,7 @@ def test_3d(): data, labels = make_3d_syntheticdata(lx, ly, lz) labels = random_walker(data, labels, mode='cg') assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all() + assert data.shape == labels.shape return data, labels @@ -142,18 +154,19 @@ def test_3d_inactive(): after_labels = np.copy(labels) labels = random_walker(data, labels, mode='cg') assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all() + assert data.shape == labels.shape 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 + data = data[..., np.newaxis].repeat(2, axis=-1) # Expect identical output multi_labels = random_walker(data, labels, mode='cg', multichannel=True) - single_labels = random_walker(data2, labels, mode='cg') + assert data[..., 0].shape == labels.shape + single_labels = random_walker(data[..., 0], labels, mode='cg') assert (multi_labels.reshape(labels.shape)[25:45, 40:60] == 2).all() + assert data[..., 0].shape == labels.shape return data, multi_labels, single_labels, labels @@ -161,14 +174,87 @@ 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 + data = data[..., np.newaxis].repeat(2, axis=-1) # Expect identical output multi_labels = random_walker(data, labels, mode='cg', multichannel=True) + assert data[..., 0].shape == labels.shape 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() + assert data[..., 0].shape == labels.shape return data, multi_labels, single_labels, labels + +def test_depth(): + n = 30 + lx, ly, lz = n, n, n + data, _ = make_3d_syntheticdata(lx, ly, lz) + + # Rescale `data` along Z axis + data_aniso = np.zeros((n, n, n // 2)) + for i, yz in enumerate(data): + data_aniso[i, :, :] = resize(yz, (n, n // 2)) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso = np.zeros_like(data_aniso) + labels_aniso[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso[lx // 2 + small_l // 4, + ly // 2 - small_l // 4, + lz // 4 - small_l // 8] = 2 + + # Test with `depth` kwarg + labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', + depth=0.5) + + assert (labels_aniso[13:17, 13:17, 7:9] == 2).all() + + +def test_spacing(): + n = 30 + lx, ly, lz = n, n, n + data, _ = make_3d_syntheticdata(lx, ly, lz) + + # Rescale `data` along Y axis + # `resize` is not yet 3D capable, so this must be done by looping in 2D. + data_aniso = np.zeros((n, n * 2, n)) + for i, yz in enumerate(data): + data_aniso[i, :, :] = resize(yz, (n * 2, n)) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso = np.zeros_like(data_aniso) + labels_aniso[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso[lx // 2 + small_l // 4, + ly - small_l // 2, + lz // 2 - small_l // 4] = 2 + + # Test with `spacing` kwarg + # First, anisotropic along Y + labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', + spacing=(1., 2., 1.)) + assert (labels_aniso[13:17, 26:34, 13:17] == 2).all() + + # Rescale `data` along X axis + # `resize` is not yet 3D capable, so this must be done by looping in 2D. + data_aniso = np.zeros((n, n * 2, n)) + for i in range(data.shape[1]): + data_aniso[i, :, :] = resize(data[:, 1, :], (n * 2, n)) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso2 = np.zeros_like(data_aniso) + labels_aniso2[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso2[lx - small_l // 2, + ly // 2 + small_l // 4, + lz // 2 - small_l // 4] = 2 + + # Anisotropic along X + labels_aniso2 = random_walker(data_aniso, + labels_aniso2, + mode='cg', spacing=(2., 1., 1.)) + assert (labels_aniso2[26:34, 13:17, 13:17] == 2).all() + + 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 index 89dee59b..a4657785 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -1,9 +1,11 @@ +import itertools as it +import warnings import numpy as np from numpy.testing import assert_equal, assert_array_equal from skimage.segmentation import slic -def test_color(): +def test_color_2d(): rnd = np.random.RandomState(0) img = np.zeros((20, 21, 3)) img[:10, :10, 0] = 1 @@ -12,16 +14,20 @@ def test_color(): img += 0.01 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + seg = slic(img, n_segments=4, sigma=0) # we expect 4 segments assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape[:-1]) 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(): + +def test_gray_2d(): rnd = np.random.RandomState(0) img = np.zeros((20, 21)) img[:10, :10] = 0.33 @@ -30,14 +36,89 @@ def test_gray(): 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) + seg = slic(img, sigma=0, n_segments=4, compactness=1, + multichannel=False, convert2lab=False) assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape) 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_color_3d(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 22, 3)) + slices = [] + for dim_size in img.shape[:-1]: + midpoint = dim_size // 2 + slices.append((slice(None, midpoint), slice(midpoint, None))) + slices = list(it.product(*slices)) + colors = list(it.product(*(([0, 1],) * 3))) + for s, c in zip(slices, colors): + img[s] = c + img += 0.01 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, sigma=0, n_segments=8) + + assert_equal(len(np.unique(seg)), 8) + for s, c in zip(slices, range(8)): + assert_array_equal(seg[s], c) + + +def test_gray_3d(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 22)) + slices = [] + for dim_size in img.shape: + midpoint = dim_size // 2 + slices.append((slice(None, midpoint), slice(midpoint, None))) + slices = list(it.product(*slices)) + shades = np.arange(0, 1.000001, 1.0/7) + for s, sh in zip(slices, shades): + img[s] = sh + img += 0.001 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, sigma=0, n_segments=8, compactness=1, + multichannel=False, convert2lab=False) + + assert_equal(len(np.unique(seg)), 8) + for s, c in zip(slices, range(8)): + assert_array_equal(seg[s], c) + + +def test_list_sigma(): + rnd = np.random.RandomState(0) + img = np.array([[1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1]], np.float) + img += 0.1 * rnd.normal(size=img.shape) + result_sigma = np.array([[0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1]], np.int) + seg_sigma = slic(img, n_segments=2, sigma=[1, 50, 1], multichannel=False) + assert_equal(seg_sigma, result_sigma) + + +def test_spacing(): + rnd = np.random.RandomState(0) + img = np.array([[1, 1, 1, 0, 0], + [1, 1, 0, 0, 0]], np.float) + result_non_spaced = np.array([[0, 0, 0, 1, 1], + [0, 0, 1, 1, 1]], np.int) + result_spaced = np.array([[0, 0, 0, 0, 0], + [1, 1, 1, 1, 1]], np.int) + img += 0.1 * rnd.normal(size=img.shape) + seg_non_spaced = slic(img, n_segments=2, sigma=0, multichannel=False, + compactness=1.0) + seg_spaced = slic(img, n_segments=2, sigma=0, spacing=[1, 500, 1], + compactness=1.0, multichannel=False) + assert_equal(seg_non_spaced, result_non_spaced) + assert_equal(seg_spaced, result_spaced) + + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 089487ee..4f00a076 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -1,11 +1,46 @@ -from .hough_transform import * -from .radon_transform import * -from .finite_radon_transform import * -from .integral import * +from ._hough_transform import (hough_circle, hough_ellipse, hough_line, + probabilistic_hough_line) +from .hough_transform import hough_line_peaks +from .radon_transform import radon, iradon, iradon_sart +from .finite_radon_transform import frt2, ifrt2 +from .integral import integral_image, integrate from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) -from ._warps import swirl, resize, rotate, rescale +from ._warps import swirl, resize, rotate, rescale, downscale_local_mean from .pyramids import (pyramid_reduce, pyramid_expand, pyramid_gaussian, pyramid_laplacian) + + +__all__ = ['hough_circle', + 'hough_ellipse', + 'hough_line', + 'probabilistic_hough_line', + 'probabilistic_hough', + 'hough_peaks', + 'hough_line_peaks', + 'radon', + 'iradon', + 'iradon_sart', + 'frt2', + 'ifrt2', + 'integral_image', + 'integrate', + 'warp', + 'warp_coords', + 'estimate_transform', + 'SimilarityTransform', + 'AffineTransform', + 'ProjectiveTransform', + 'PolynomialTransform', + 'PiecewiseAffineTransform', + 'swirl', + 'resize', + 'rotate', + 'rescale', + 'downscale_local_mean', + 'pyramid_reduce', + 'pyramid_expand', + 'pyramid_gaussian', + 'pyramid_laplacian'] diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 448829de..25a13765 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -4,6 +4,9 @@ from scipy import ndimage, spatial from skimage.util import img_as_float from ._warps_cy import _warp_fast +from skimage._shared.utils import get_bound_method_class +from skimage._shared import six + class GeometricTransform(object): """Perform geometric transformations on a set of coordinates. @@ -41,6 +44,28 @@ class GeometricTransform(object): """ raise NotImplementedError() + def residuals(self, src, dst): + """Determine residuals of transformed destination coordinates. + + For each transformed source coordinate the euclidean distance to the + respective destination coordinate is determined. + + Parameters + ---------- + src : (N, 2) array + Source coordinates. + dst : (N, 2) array + Destination coordinates. + + Returns + ------- + residuals : (N, ) array + Residual for coordinate. + + """ + + return np.sqrt(np.sum((self(src) - dst)**2, axis=1)) + def __add__(self, other): """Combine this transformation with another. @@ -200,14 +225,14 @@ class ProjectiveTransform(GeometricTransform): A[rows:, 8] = yd # Select relevant columns, depending on params - A = A[:, self._coeffs + [8]] + A = A[:, list(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.flat[list(self._coeffs) + [8]] = - V[-1, :-1] / V[-1, -1] H[2, 2] = 1 self._matrix = H @@ -471,8 +496,8 @@ class SimilarityTransform(ProjectiveTransform): 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.") + 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.") @@ -490,7 +515,7 @@ class SimilarityTransform(ProjectiveTransform): [math.sin(rotation), math.cos(rotation), 0], [ 0, 0, 1] ]) - self._matrix *= scale + self._matrix[0:2, 0:2] *= scale self._matrix[0:2, 2] = translation else: # default to an identity transform @@ -924,40 +949,73 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Parameters ---------- - image : 2-D array + image : 2-D or 3-D array Input image. - inverse_map : transformation object, callable ``xy = f(xy, **kwargs)`` + inverse_map : transformation object, callable ``xy = f(xy, **kwargs)``, (3, 3) array 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). + inverse). See example section for usage. 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 + output_shape : tuple (rows, cols), optional + Shape of the output image generated. By default the shape of the input + image is preserved. + order : int, optional + The order of interpolation. The order has to be in the range 0-5: + * 0: Nearest-neighbor + * 1: Bi-linear (default) + * 2: Bi-quadratic + * 3: Bi-cubic + * 4: Bi-quartic + * 5: Bi-quintic + mode : string, optional + Points outside the boundaries of the input are filled according + to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). + cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + Notes + ----- + In case of a `SimilarityTransform`, `AffineTransform` and + `ProjectiveTransform` and `order` in [0, 3] this function uses the + underlying transformation matrix to warp the image with a much faster + routine. + Examples -------- - Shift an image to the right: - + >>> from skimage.transform import warp >>> from skimage import data >>> image = data.camera() - >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 + + The following image warps are all equal but differ substantially in + execution time. + + Use a geometric transform to warp an image (fast): + + >>> from skimage.transform import SimilarityTransform + >>> tform = SimilarityTransform(translation=(0, -10)) + >>> warp(image, tform) + + Shift an image to the right with a callable (slow): + + >>> def shift(xy): + ... xy[:, 1] -= 10 ... return xy - >>> >>> warp(image, shift_right) + Use a transformation matrix to warp an image (fast): + + >>> matrix = np.array([[1, 0, 0], [0, 1, -10], [0, 0, 1]]) + >>> warp(image, matrix) + >>> from skimage.transform import ProjectiveTransform + >>> warp(image, ProjectiveTransform(matrix=matrix)) + + You can also use the inverse of a geometric transformation (fast): + + >>> warp(image, tform.inverse) + """ # Backward API compatibility if reverse_map is not None: @@ -976,13 +1034,22 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # 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: + + if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + matrix = inverse_map + + elif 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) + + elif (hasattr(inverse_map, '__name__') + and inverse_map.__name__ == 'inverse' + and get_bound_method_class(inverse_map) + in HOMOGRAPHY_TRANSFORMS): + + matrix = np.linalg.inv(six.get_method_self(inverse_map)._matrix) + if matrix is not None: + matrix = matrix.astype(np.double) # transform all bands dims = [] for dim in range(image.shape[2]): @@ -1000,25 +1067,30 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, rows, cols = output_shape[:2] + if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + inverse_map = ProjectiveTransform(matrix=inverse_map) + 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 not necessary for order 0, 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) + # 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 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] + out = clipped + + if out.ndim == 3 and orig_ndim == 2: + # remove singleton dimension introduced by atleast_3d + return out[..., 0] else: - return clipped + return out diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 2b1acc7c..29344fa8 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -7,7 +7,7 @@ import numpy as np cimport numpy as cnp cimport cython -from libc.math cimport abs, fabs, sqrt, ceil +from libc.math cimport abs, fabs, sqrt, ceil, atan2, M_PI from libc.stdlib cimport rand from skimage.draw import circle_perimeter @@ -20,9 +20,9 @@ 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): +def hough_circle(cnp.ndarray img, + cnp.ndarray[ndim=1, dtype=cnp.intp_t] radius, + char normalize=True, char full_output=False): """Perform a circular Hough transform. Parameters @@ -31,29 +31,39 @@ def _hough_circle(cnp.ndarray img, Input image with nonzero values representing edges. radius : ndarray Radii at which to compute the Hough transform. - normalize : boolean, optional + normalize : boolean, optional (default True) Normalize the accumulator with the number - of pixels used to draw the radius + of pixels used to draw the radius. + full_output : boolean, optional (default False) + Extend the output size by twice the largest + radius in order to detect centers outside the + input picture. Returns ------- - H : 3D ndarray (radius index, (M, N) ndarray) - Hough transform accumulator for each radius - + H : 3D ndarray (radius index, (M + 2R, N + 2R) ndarray) + Hough transform accumulator for each radius. + R designates the larger radius if full_output is True. + Otherwise, R = 0. """ if img.ndim != 2: raise ValueError('The input image must be 2D.') + cdef Py_ssize_t xmax = img.shape[0] + cdef Py_ssize_t ymax = img.shape[1] + # 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 offset = 0 + if full_output: + # Offset the image + offset = radius.max() + x = x + offset + y = y + offset cdef Py_ssize_t i, p, c, num_circle_pixels, tx, ty cdef double incr @@ -61,8 +71,8 @@ def _hough_circle(cnp.ndarray img, 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) + img.shape[0] + 2 * offset, + img.shape[1] + 2 * offset), dtype=np.double) for i, rad in enumerate(radius): # Store in memory the circle of given radius @@ -83,13 +93,191 @@ def _hough_circle(cnp.ndarray img, for c in range(num_circle_pixels): tx = circle_x[c] + x[p] ty = circle_y[c] + y[p] - acc[i, tx, ty] += incr + if offset: + acc[i, tx, ty] += incr + elif 0 <= tx < xmax and 0 <= ty < ymax: + acc[i, tx, ty] += incr return acc -def _hough(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): +def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, + int min_size=4, max_size=None): + """Perform an elliptical Hough transform. + Parameters + ---------- + img : (M, N) ndarray + Input image with nonzero values representing edges. + threshold: int, optional (default 4) + Accumulator threshold value. + accuracy : double, optional (default 1) + Bin size on the minor axis used in the accumulator. + min_size : int, optional (default 4) + Minimal major axis length. + max_size : int, optional + Maximal minor axis length. (default None) + If None, the value is set to the half of the smaller + image dimension. + + Returns + ------- + result : ndarray with fields [(accumulator, y0, x0, a, b, orientation)] + Where ``(yc, xc)`` is the center, ``(a, b)`` the major and minor + axes, respectively. The `orientation` value follows + `skimage.draw.ellipse_perimeter` convention. + + Examples + -------- + >>> img = np.zeros((25, 25), dtype=np.uint8) + >>> rr, cc = ellipse_perimeter(10, 10, 6, 8) + >>> img[cc, rr] = 1 + >>> result = hough_ellipse(img, threshold=8) + [(10, 10.0, 8.0, 6.0, 0.0, 10.0)] + + Notes + ----- + The accuracy must be chosen to produce a peak in the accumulator + distribution. In other words, a flat accumulator distribution with low + values may be caused by a too low bin size. + + References + ---------- + .. [1] Xie, Yonghong, and Qiang Ji. "A new efficient ellipse detection + method." Pattern Recognition, 2002. Proceedings. 16th International + Conference on. Vol. 2. IEEE, 2002 + """ + if img.ndim != 2: + raise ValueError('The input image must be 2D.') + + cdef Py_ssize_t[:, ::1] pixels = np.row_stack(np.nonzero(img)) + cdef Py_ssize_t num_pixels = pixels.shape[1] + cdef list acc = list() + cdef list results = list() + cdef double bin_size = accuracy ** 2 + + cdef int max_b_squared + if max_size is None: + if img.shape[0] < img.shape[1]: + max_b_squared = np.round(0.5 * img.shape[0]) ** 2 + else: + max_b_squared = np.round(0.5 * img.shape[1]) ** 2 + else: + max_b_squared = max_size**2 + + cdef Py_ssize_t p1, p2, p3, p1x, p1y, p2x, p2y, p3x, p3y + cdef double xc, yc, a, b, d, k + cdef double cos_tau_squared, b_squared, f_squared, orientation + + for p1 in range(num_pixels): + p1x = pixels[1, p1] + p1y = pixels[0, p1] + + for p2 in range(p1): + p2x = pixels[1, p2] + p2y = pixels[0, p2] + + # Candidate: center (xc, yc) and main axis a + a = 0.5 * sqrt((p1x - p2x)**2 + (p1y - p2y)**2) + if a > 0.5 * min_size: + xc = 0.5 * (p1x + p2x) + yc = 0.5 * (p1y + p2y) + + for p3 in range(num_pixels): + p3x = pixels[1, p3] + p3y = pixels[0, p3] + + d = sqrt((p3x - xc)**2 + (p3y - yc)**2) + if d > min_size: + f_squared = (p3x - p1x)**2 + (p3y - p1y)**2 + cos_tau_squared = ((a**2 + d**2 - f_squared) + / (2 * a * d))**2 + # Consider b2 > 0 and avoid division by zero + k = a**2 - d**2 * cos_tau_squared + if k > 0 and cos_tau_squared < 1: + b_squared = a**2 * d**2 * (1 - cos_tau_squared) / k + # b2 range is limited to avoid histogram memory + # overflow + if b_squared <= max_b_squared: + acc.append(b_squared) + + if len(acc) > 0: + bins = np.arange(0, np.max(acc) + bin_size, bin_size) + hist, bin_edges = np.histogram(acc, bins=bins) + hist_max = np.max(hist) + if hist_max > threshold: + orientation = atan2(p1x - p2x, p1y - p2y) + b = sqrt(bin_edges[hist.argmax()]) + # to keep ellipse_perimeter() convention + if orientation != 0: + orientation = M_PI - orientation + # When orientation is not in [-pi:pi] + # it would mean in ellipse_perimeter() + # that a < b. But we keep a > b. + if orientation > M_PI: + orientation = orientation - M_PI / 2. + a, b = b, a + results.append((hist_max, # Accumulator + yc, xc, + a, b, + orientation)) + acc = [] + + return np.array(results, dtype=[('accumulator', np.intp), + ('yc', np.double), + ('xc', np.double), + ('a', np.double), + ('b', np.double), + ('orientation', np.double)]) + + +def hough_line(cnp.ndarray img, + cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): + """Perform a straight line Hough transform. + + Parameters + ---------- + img : (M, N) ndarray + Input image with nonzero values representing edges. + theta : 1D ndarray of double + Angles at which to compute the transform, in radians. + Defaults to -pi/2 .. pi/2 + + Returns + ------- + H : 2-D ndarray of uint64 + Hough transform accumulator. + theta : ndarray + Angles at which the transform was computed, in radians. + distances : ndarray + Distance values. + + Notes + ----- + The origin is the top left corner of the original image. + X and Y axis are horizontal and vertical edges respectively. + The distance is the minimal algebraic distance from the origin + to the detected line. + + Examples + -------- + Generate a test image: + + >>> img = np.zeros((100, 150), dtype=bool) + >>> img[30, :] = 1 + >>> img[:, 65] = 1 + >>> img[35:45, 35:50] = 1 + >>> for i in range(90): + ... img[i, i] = 1 + >>> img += np.random.random(img.shape) > 0.95 + + Apply the Hough transform: + + >>> out, angles, d = hough_line(img) + + .. plot:: hough_tf.py + + """ if img.ndim != 2: raise ValueError('The input image must be 2D.') @@ -98,7 +286,7 @@ def _hough(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): 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(NEG_PI_2, PI_2, 180) ctheta = np.cos(theta) stheta = np.sin(theta) @@ -120,7 +308,7 @@ def _hough(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): # finally, run the transform cdef Py_ssize_t nidxs, nthetas, i, j, x, y, accum_idx - nidxs = y_idxs.shape[0] # x and y are the same shape + 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] @@ -131,10 +319,38 @@ def _hough(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): return accum, theta, bins -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): +def probabilistic_hough_line(cnp.ndarray img, int threshold=10, + int line_length=50, int line_gap=10, + cnp.ndarray[ndim=1, dtype=cnp.double_t] 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, optional (default 10) + 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. + 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 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. + """ if img.ndim != 2: raise ValueError('The input image must be 2D.') @@ -196,7 +412,7 @@ def _probabilistic_hough(cnp.ndarray img, int value_threshold, continue value = 0 - max_value = value_threshold - 1 + max_value = threshold - 1 max_theta = -1 # apply hough transform on point @@ -207,7 +423,7 @@ def _probabilistic_hough(cnp.ndarray img, int value_threshold, if value > max_value: max_value = value max_theta = j - if max_value < value_threshold: + if max_value < threshold: continue # from the random point walk in opposite directions and find line @@ -249,14 +465,14 @@ def _probabilistic_hough(cnp.ndarray img, int value_threshold, y1 = py >> shift else: x1 = px >> shift - y1 = py; + y1 = py # check when line exits image boundary if x1 < 0 or x1 >= width or y1 < 0 or y1 >= height: break gap += 1 # if non-zero point found, continue the line if mask[y1, x1]: - gap = 0; + gap = 0 line_end[k, 1] = y1 line_end[k, 0] = x1 # if gap to this point was too large, end the line diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx new file mode 100644 index 00000000..91b943b9 --- /dev/null +++ b/skimage/transform/_radon_transform.pyx @@ -0,0 +1,202 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np + +cimport numpy as cnp +cimport cython +from libc.math cimport cos, sin, floor, ceil, sqrt, abs, M_PI + + +cpdef bilinear_ray_sum(cnp.double_t[:, :] image, cnp.double_t theta, + cnp.double_t ray_position): + """ + Compute the projection of an image along a ray. + + Parameters + ---------- + image : 2D array, dtype=float + Image to project. + theta : float + Angle of the projection + ray_position : float + Position of the ray within the projection + + Returns + ------- + projected_value : float + Ray sum along the projection + norm_of_weights : + A measure of how long the ray's path through the reconstruction + circle was + """ + theta = theta / 180. * M_PI + cdef cnp.double_t radius = image.shape[0] // 2 - 1 + cdef cnp.double_t projection_center = image.shape[0] // 2 + cdef cnp.double_t rotation_center = image.shape[0] // 2 + # (s, t) is the (x, y) system rotated by theta + cdef cnp.double_t t = ray_position - projection_center + # s0 is the half-length of the ray's path in the reconstruction circle + cdef cnp.double_t s0 + s0 = sqrt(radius**2 - t**2) if radius**2 >= t**2 else 0. + cdef Py_ssize_t Ns = 2 * ( ceil(2 * s0)) # number of steps + # along the ray + cdef cnp.double_t ray_sum = 0. + cdef cnp.double_t weight_norm = 0. + cdef cnp.double_t ds, dx, dy, x0, y0, x, y, di, dj, + cdef cnp.double_t index_i, index_j, weight + cdef Py_ssize_t k, i, j + if Ns > 0: + # step length between samples + ds = 2 * s0 / Ns + dx = -ds * cos(theta) + dy = -ds * sin(theta) + # point of entry of the ray into the reconstruction circle + x0 = s0 * cos(theta) - t * sin(theta) + y0 = s0 * sin(theta) + t * cos(theta) + for k in range(Ns+1): + x = x0 + k * dx + y = y0 + k * dy + index_i = x + rotation_center + index_j = y + rotation_center + i = floor(index_i) + j = floor(index_j) + di = index_i - floor(index_i) + dj = index_j - floor(index_j) + # Use linear interpolation between values + # Where values fall outside the array, assume zero + if i > 0 and j > 0: + weight = (1. - di) * (1. - dj) * ds + ray_sum += weight * image[i, j] + weight_norm += weight**2 + if i > 0 and j < image.shape[1] - 1: + weight = (1. - di) * dj * ds + ray_sum += weight * image[i, j+1] + weight_norm += weight**2 + if i < image.shape[0] - 1 and j > 0: + weight = di * (1 - dj) * ds + ray_sum += weight * image[i+1, j] + weight_norm += weight**2 + if i < image.shape[0] - 1 and j < image.shape[1] - 1: + weight = di * dj * ds + ray_sum += weight * image[i+1, j+1] + weight_norm += weight**2 + return ray_sum, weight_norm + + +cpdef bilinear_ray_update(cnp.double_t[:, :] image, + cnp.double_t[:, :] image_update, + cnp.double_t theta, cnp.double_t ray_position, + cnp.double_t projected_value): + """ + Compute the update along a ray using bilinear interpolation. + + Parameters + ---------- + image : 2D array, dtype=float + Current reconstruction estimate + image_update : 2D array, dtype=float + Array of same shape as ``image``. Updates will be added to this array. + theta : float + Angle of the projection + ray_position : float + Position of the ray within the projection + projected_value : float + Projected value (from the sinogram) + + Returns + ------- + deviation : + Deviation before updating the image + """ + cdef cnp.double_t ray_sum, weight_norm, deviation + ray_sum, weight_norm = bilinear_ray_sum(image, theta, ray_position) + if weight_norm > 0.: + deviation = -(ray_sum - projected_value) / weight_norm + else: + deviation = 0. + theta = theta / 180. * M_PI + cdef cnp.double_t radius = image.shape[0] // 2 - 1 + cdef cnp.double_t projection_center = image.shape[0] // 2 + cdef cnp.double_t rotation_center = image.shape[0] // 2 + # (s, t) is the (x, y) system rotated by theta + cdef cnp.double_t t = ray_position - projection_center + # s0 is the half-length of the ray's path in the reconstruction circle + cdef cnp.double_t s0 + s0 = sqrt(radius*radius - t*t) if radius**2 >= t**2 else 0. + cdef Py_ssize_t Ns = 2 * ( ceil(2 * s0)) + cdef cnp.double_t hamming_beta = 0.46164 # beta for equiripple Hamming window + + cdef cnp.double_t ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j + cdef cnp.double_t hamming_window + cdef Py_ssize_t k, i, j + if Ns > 0: + # Step length between samples + ds = 2 * s0 / Ns + dx = -ds * cos(theta) + dy = -ds * sin(theta) + # Point of entry of the ray into the reconstruction circle + x0 = s0 * cos(theta) - t * sin(theta) + y0 = s0 * sin(theta) + t * cos(theta) + for k in range(Ns+1): + x = x0 + k * dx + y = y0 + k * dy + index_i = x + rotation_center + index_j = y + rotation_center + i = floor(index_i) + j = floor(index_j) + di = index_i - floor(index_i) + dj = index_j - floor(index_j) + hamming_window = ((1 - hamming_beta) + - hamming_beta * cos(2 * M_PI * k / (Ns - 1))) + if i > 0 and j > 0: + image_update[i, j] += (deviation * (1. - di) * (1. - dj) + * ds * hamming_window) + if i > 0 and j < image.shape[1] - 1: + image_update[i, j+1] += (deviation * (1. - di) * dj + * ds * hamming_window) + if i < image.shape[0] - 1 and j > 0: + image_update[i+1, j] += (deviation * di * (1 - dj) + * ds * hamming_window) + if i < image.shape[0] - 1 and j < image.shape[1] - 1: + image_update[i+1, j+1] += (deviation * di * dj + * ds * hamming_window) + return deviation + + +@cython.boundscheck(True) +def sart_projection_update(cnp.double_t[:, :] image not None, + cnp.double_t theta, + cnp.double_t[:] projection not None, + cnp.double_t projection_shift=0.): + """ + Compute update to a reconstruction estimate from a single projection + using bilinear interpolation. + + Parameters + ---------- + image : 2D array, dtype=float + Current reconstruction estimate + theta : float + Angle of the projection + projection : 1D array, dtype=float + Projected values, taken from the sinogram + projection_shift : float + Shift the position of the projection by this many pixels before + using it to compute an update to the reconstruction estimate + + Returns + ------- + image_update : 2D array, dtype=float + Array of same shape as ``image`` containing updates that should be + added to ``image`` to improve the reconstruction estimate + """ + cdef cnp.ndarray[cnp.double_t, ndim=2] image_update = np.zeros_like(image) + cdef cnp.double_t ray_position + cdef Py_ssize_t i + for i in range(projection.shape[0]): + ray_position = i + projection_shift + bilinear_ray_update(image, image_update, theta, ray_position, + projection[i]) + return image_update diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 7ac3e63f..64b129dd 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,19 +1,26 @@ import numpy as np from scipy import ndimage -from ._geometric import (warp, SimilarityTransform, AffineTransform, - ProjectiveTransform) + +from skimage.transform._geometric import (warp, SimilarityTransform, + AffineTransform) +from skimage.measure import block_reduce def resize(image, output_shape, order=1, mode='constant', cval=0.): """Resize image to match a certain size. + Performs interpolation to up-size or down-size images. For down-sampling + N-dimensional images by applying the arithmetic sum or mean, see + `skimage.measure.local_sum` and `skimage.transform.downscale_local_mean`, + respectively. + 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 + not provided, the number of channels is preserved. In case the number of input channels does not equal the number of output channels a 3-dimensional interpolation is applied. @@ -24,16 +31,24 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): 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 + order : int, optional + The order of the spline interpolation, default is 1. The order has to + be in the range 0-5. See `skimage.transform.warp` for detail. + mode : string, optional + Points outside the boundaries of the input are filled according + to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). + cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + Examples + -------- + >>> from skimage import data + >>> from skimage.transform import resize + >>> image = data.camera() + >>> resize(image, (100, 100)).shape + (100, 100) + """ rows, cols = output_shape[0], output_shape[1] @@ -59,7 +74,7 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): out = ndimage.map_coordinates(image, coord_map, order=order, mode=mode, cval=cval) - else: # 2-dimensional interpolation + else: # 2-dimensional interpolation # 3 control points necessary to estimate exact AffineTransform src_corners = np.array([[1, 1], [1, rows], [cols, rows]]) - 1 @@ -80,6 +95,11 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): def rescale(image, scale, order=1, mode='constant', cval=0.): """Scale image by a certain factor. + Performs interpolation to upscale or down-scale images. For down-sampling + N-dimensional images with integer factors by applying the arithmetic sum or + mean, see `skimage.measure.local_sum` and + `skimage.transform.downscale_local_mean`, respectively. + Parameters ---------- image : ndarray @@ -95,16 +115,26 @@ def rescale(image, scale, order=1, mode='constant', cval=0.): 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 + order : int, optional + The order of the spline interpolation, default is 1. The order has to + be in the range 0-5. See `skimage.transform.warp` for detail. + mode : string, optional + Points outside the boundaries of the input are filled according + to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). + cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + Examples + -------- + >>> from skimage import data + >>> from skimage.transform import rescale + >>> image = data.camera() + >>> rescale(image, 0.1).shape + (51, 51) + >>> rescale(image, 0.5).shape + (256, 256) + """ try: @@ -129,7 +159,7 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): Input image. angle : float Rotation angle in degrees in counter-clockwise direction. - resize: bool, optional + 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. @@ -141,16 +171,28 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): 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 + order : int, optional + The order of the spline interpolation, default is 1. The order has to + be in the range 0-5. See `skimage.transform.warp` for detail. + mode : string, optional + Points outside the boundaries of the input are filled according + to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). + cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + Examples + -------- + >>> from skimage import data + >>> from skimage.transform import rotate + >>> image = data.camera() + >>> rotate(image, 2).shape + (512, 512) + >>> rotate(image, 2, resize=True).shape + (530, 530) + >>> rotate(image, 90, resize=True).shape + (512, 512) + """ rows, cols = image.shape[0], image.shape[1] @@ -184,6 +226,47 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): mode=mode, cval=cval) +def downscale_local_mean(image, factors, cval=0): + """Down-sample N-dimensional image by local averaging. + + The image is padded with `cval` if it is not perfectly divisible by the + integer factors. + + In contrast to the 2-D interpolation in `skimage.transform.resize` and + `skimage.transform.rescale` this function may be applied to N-dimensional + images and calculates the local mean of elements in each block of size + `factors` in the input image. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + factors : array_like + Array containing down-sampling integer factor along each axis. + cval : float, optional + Constant padding value if image is not perfectly divisible by the + integer factors. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + array([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> downscale_local_mean(a, (2, 3)) + array([[3.5, 4.], + [5.5, 4.5]]) + + """ + return block_reduce(image, factors, np.mean, cval) + + def _swirl_mapping(xy, center, rotation, strength, radius): x, y = xy.T x0, y0 = center @@ -211,14 +294,14 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, ---------- image : ndarray Input image. - center : (x,y) tuple or (2,) ndarray + center : (x,y) tuple or (2,) ndarray, optional Center coordinate of transformation. - strength : float + strength : float, optional The amount of swirling applied. - radius : float + radius : float, optional The extent of the swirl in pixels. The effect dies out rapidly beyond `radius`. - rotation : float + rotation : float, optional Additional rotation applied to the image. Returns @@ -228,15 +311,16 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, 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 + output_shape : tuple (rows, cols), optional + Shape of the output image generated. By default the shape of the input + image is preserved. + order : int, optional + The order of the spline interpolation, default is 1. The order has to + be in the range 0-5. See `skimage.transform.warp` for detail. + mode : string, optional + Points outside the boundaries of the input are filled according + to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). + cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 968f643a..b3136c22 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -35,27 +35,23 @@ cdef inline void _matrix_transform(double x, double y, double* H, double *x_, y_[0] = yy / zz -def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, int order=1, - mode='constant', double cval=0): +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. + floating point image, using 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 - - :: + 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, - - :: + or, to translate x by 10 and y by 20:: [[1 0 10] [0 1 20] @@ -67,26 +63,24 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, int order=1, 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} + output_shape : tuple (rows, cols), optional + Shape of the output image generated (default None). + order : {0, 1, 2, 3}, optional 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 + * 0: Nearest-neighbor + * 1: Bi-linear (default) + * 2: Bi-quadratic + * 3: Bi-cubic + mode : {'constant', 'reflect', 'wrap', 'nearest'}, optional + How to handle values outside the image borders (default is constant). + cval : string, optional (default 0) 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) + cdef double[:, ::1] img = np.ascontiguousarray(image, dtype=np.double) + cdef double[:, ::1] M = np.ascontiguousarray(H) if mode not in ('constant', 'wrap', 'reflect', 'nearest'): raise ValueError("Invalid mode specified. Please use " @@ -101,8 +95,7 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, int order=1, 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 double[:, ::1] out = np.zeros((out_r, out_c), dtype=np.double) cdef Py_ssize_t tfr, tfc cdef double r, c @@ -122,8 +115,8 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, int order=1, 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, + _matrix_transform(tfc, tfr, &M[0, 0], &c, &r) + out[tfr, tfc] = interp_func(&img[0, 0], rows, cols, r, c, mode_c, cval) - return out + return np.asarray(out) diff --git a/skimage/transform/finite_radon_transform.py b/skimage/transform/finite_radon_transform.py index c107546f..dd3bad33 100644 --- a/skimage/transform/finite_radon_transform.py +++ b/skimage/transform/finite_radon_transform.py @@ -4,7 +4,6 @@ """ __all__ = ["frt2", "ifrt2"] -__docformat__ = "restructuredtext en" import numpy as np from numpy import roll, newaxis diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index e83cecd5..0a7d35a2 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -1,189 +1,26 @@ -__all__ = ['hough', 'hough_line', 'hough_circle', 'hough_peaks', 'probabilistic_hough'] - -from itertools import izip as zip - import numpy as np from scipy import ndimage -from ._hough_transform import _probabilistic_hough from skimage import measure, morphology -def _hough(img, theta=None): - if img.ndim != 2: - raise ValueError('The input image must be 2-D') - - if theta is None: - theta = np.linspace(-np.pi / 2, np.pi / 2, 180) - - # compute the vertical bins (the distances) - d = np.ceil(np.hypot(*img.shape)) - nr_bins = 2 * d - bins = np.linspace(-d, d, nr_bins) - - # allocate the output image - out = np.zeros((nr_bins, len(theta)), dtype=np.uint64) - - # precompute the sin and cos of the angles - cos_theta = np.cos(theta) - sin_theta = np.sin(theta) - - # find the indices of the non-zero values in - # the input image - y, x = np.nonzero(img) - - # x and y can be large, so we can't just broadcast to 2D - # arrays as we may run out of memory. Instead we process - # one vertical slice at a time. - for i, (cT, sT) in enumerate(zip(cos_theta, sin_theta)): - - # compute the base distances - distances = x * cT + y * sT - - # round the distances to the nearest integer - # and shift them to a nonzero bin - shifted = np.round(distances) - bins[0] - - # cast the shifted values to ints to use as indices - indices = shifted.astype(np.int) - - # use bin count to accumulate the coefficients - bincount = np.bincount(indices) - - # finally assign the proper values to the out array - out[:len(bincount), i] = bincount - - return out, theta, bins - -_py_hough = _hough - -# try to import and use the faster Cython version if it exists -try: - from ._hough_transform import _hough -except ImportError: - pass - - -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 - 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. - 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 - 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. - """ - 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 - ---------- - img : (M, N) ndarray - Input image with nonzero values representing edges. - theta : 1D ndarray of double - Angles at which to compute the transform, in radians. - Defaults to -pi/2 .. pi/2 - - Returns - ------- - H : 2-D ndarray of uint64 - Hough transform accumulator. - theta : ndarray - Angles at which the transform was computed. - distances : ndarray - Distance values. - - Examples - -------- - Generate a test image: - - >>> img = np.zeros((100, 150), dtype=bool) - >>> img[30, :] = 1 - >>> img[:, 65] = 1 - >>> img[35:45, 35:50] = 1 - >>> for i in range(90): - ... img[i, i] = 1 - >>> img += np.random.random(img.shape) > 0.95 - - Apply the Hough transform: - - >>> out, angles, d = hough(img) - - .. 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.astype(np.intp), normalize) - -def hough_peaks(hspace, angles, dists, min_distance=10, min_angle=10, - threshold=None, num_peaks=np.inf): +def hough_line_peaks(hspace, angles, dists, min_distance=9, 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. + 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. + Hough space returned by the `hough_line` function. angles : (M,) array - Angles returned by the `hough` function. Assumed to be continuous + Angles returned by the `hough_line` function. Assumed to be continuous. (`angles[-1] - angles[0] == PI`). dists : (N, ) array - Distances returned by the `hough` function. + Distances returned by the `hough_line` function. min_distance : int Minimum distance separating lines (maximum filter size for first dimension of hough space). @@ -204,14 +41,14 @@ def hough_peaks(hspace, angles, dists, min_distance=10, min_angle=10, Examples -------- >>> import numpy as np - >>> from skimage.transform import hough, hough_peaks + >>> from skimage.transform import hough_line, 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_line(img) >>> hspace, angles, dists = hough_peaks(hspace, angles, dists) >>> angles array([ 0.74590887, -0.79856126]) diff --git a/skimage/transform/integral.py b/skimage/transform/integral.py index c34b6a19..3503a652 100644 --- a/skimage/transform/integral.py +++ b/skimage/transform/integral.py @@ -1,3 +1,6 @@ +import numpy as np + + def integral_image(x): """Integral image / summed area table. @@ -34,28 +37,34 @@ def integrate(ii, r0, c0, r1, c1): ---------- ii : ndarray Integral image. - r0, c0 : int - Top-left corner of block to be summed. - r1, c1 : int - Bottom-right corner of block to be summed. + r0, c0 : int or ndarray + Top-left corner(s) of block to be summed. + r1, c1 : int or ndarray + Bottom-right corner(s) of block to be summed. Returns ------- - S : int - Integral (sum) over the given window. + S : scalar or ndarray + Integral (sum) over the given window(s). """ - S = 0 + if np.isscalar(r0): + r0, c0, r1, c1 = [np.asarray([x]) for x in (r0, c0, r1, c1)] + + S = np.zeros(r0.shape, ii.dtype) S += ii[r1, c1] - if (r0 - 1 >= 0) and (c0 - 1 >= 0): - S += ii[r0 - 1, c0 - 1] + good = (r0 >= 1) & (c0 >= 1) + S[good] += ii[r0[good] - 1, c0[good] - 1] - if (r0 - 1 >= 0): - S -= ii[r0 - 1, c1] + good = r0 >= 1 + S[good] -= ii[r0[good] - 1, c1[good]] - if (c0 - 1 >= 0): - S -= ii[r1, c0 - 1] + good = c0 >= 1 + S[good] -= ii[r1[good], c0[good] - 1] + + if S.size == 1: + return np.asscalar(S) return S diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 19001de8..a68b7a70 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -6,11 +6,11 @@ from skimage.util import img_as_float def _smooth(image, sigma, mode, cval): - """Return image with each channel smoothed by the gaussian filter.""" + """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 + 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], @@ -38,13 +38,13 @@ def pyramid_reduce(image, downscale=2, sigma=None, order=1, downscale : float, optional Downscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + 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. + 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 + `skimage.transform.warp` 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 @@ -92,13 +92,13 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, upscale : float, optional Upscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * upscale / 6.0` which + 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. + 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 + `skimage.transform.warp` 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 @@ -137,7 +137,7 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, 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. + """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. @@ -157,13 +157,13 @@ def pyramid_gaussian(image, max_layer=-1, downscale=2, sigma=None, order=1, downscale : float, optional Downscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + 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. + 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 + `skimage.transform.warp` 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 @@ -238,13 +238,13 @@ def pyramid_laplacian(image, max_layer=-1, downscale=2, sigma=None, order=1, downscale : float, optional Downscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + 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. + 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 + `skimage.transform.warp` 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 diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 0213b2bc..101d09c6 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ radon.py - Radon and inverse radon transforms @@ -14,13 +15,17 @@ References: """ from __future__ import division import numpy as np -from scipy.fftpack import fftshift, fft, ifft +from scipy.fftpack import fft, ifft, fftfreq +from scipy.interpolate import interp1d from ._warps_cy import _warp_fast - -__all__ = ["radon", "iradon"] +from ._radon_transform import sart_projection_update +from .. import util -def radon(image, theta=None): +__all__ = ["radon", "iradon", "iradon_sart"] + + +def radon(image, theta=None, circle=False): """ Calculates the radon transform of an image given specified projection angles. @@ -28,65 +33,99 @@ def radon(image, theta=None): Parameters ---------- image : array_like, dtype=float - Input image. + Input image. The rotation axis will be located in the pixel with + indices ``(image.shape[0] // 2, image.shape[1] // 2)``. theta : array_like, dtype=float, optional (default np.arange(180)) Projection angles (in degrees). + circle : boolean, optional + Assume image is zero outside the inscribed circle, making the + width of each projection (the first dimension of the sinogram) + equal to ``min(image.shape)``. Returns ------- - output : ndarray - Radon transform (sinogram). + radon_image : ndarray + Radon transform (sinogram). The tomography rotation axis will lie + at the pixel index ``radon_image.shape[0] // 2`` along the 0th + dimension of ``radon_image``. + Raises + ------ + ValueError + If called with ``circle=True`` and ``image != 0`` outside the inscribed + circle """ if image.ndim != 2: raise ValueError('The input image must be 2-D') if theta is None: theta = np.arange(180) - height, width = image.shape - diagonal = np.sqrt(height**2 + width**2) - heightpad = np.ceil(diagonal - height) - widthpad = np.ceil(diagonal - width) - padded_image = np.zeros((int(height + heightpad), - int(width + widthpad))) - y0, y1 = int(np.ceil(heightpad / 2)), \ - int((np.ceil(heightpad / 2) + height)) - x0, x1 = int((np.ceil(widthpad / 2))), \ - int((np.ceil(widthpad / 2) + width)) + if circle: + radius = min(image.shape) // 2 + c0, c1 = np.ogrid[0:image.shape[0], 0:image.shape[1]] + reconstruction_circle = ((c0 - image.shape[0] // 2)**2 + + (c1 - image.shape[1] // 2)**2) <= radius**2 + if not np.all(reconstruction_circle | (image == 0)): + raise ValueError('Image must be zero outside the reconstruction' + ' circle') + # Crop image to make it square + slices = [] + for d in (0, 1): + if image.shape[d] > min(image.shape): + excess = image.shape[d] - min(image.shape) + slices.append(slice(int(np.ceil(excess / 2)), + int(np.ceil(excess / 2) + + min(image.shape)))) + else: + slices.append(slice(None)) + slices = tuple(slices) + padded_image = image[slices] + else: + diagonal = np.sqrt(2) * max(image.shape) + pad = [int(np.ceil(diagonal - s)) for s in image.shape] + new_center = [(s + p) // 2 for s, p in zip(image.shape, pad)] + old_center = [s // 2 for s in image.shape] + pad_before = [nc - oc for oc, nc in zip(old_center, new_center)] + pad_width = [(pb, p - pb) for pb, p in zip(pad_before, pad)] + padded_image = util.pad(image, pad_width, mode='constant', + constant_values=0) + # padded_image is always square + assert padded_image.shape[0] == padded_image.shape[1] + radon_image = np.zeros((padded_image.shape[0], len(theta))) + center = padded_image.shape[0] // 2 - padded_image[y0:y1, x0:x1] = image - out = np.zeros((max(padded_image.shape), len(theta))) - - h, w = padded_image.shape - dh, dw = h // 2, w // 2 - shift0 = np.array([[1, 0, -dw], - [0, 1, -dh], + shift0 = np.array([[1, 0, -center], + [0, 1, -center], [0, 0, 1]]) - - shift1 = np.array([[1, 0, dw], - [0, 1, dh], + shift1 = np.array([[1, 0, center], + [0, 1, center], [0, 0, 1]]) def build_rotation(theta): - T = -np.deg2rad(theta) - - R = np.array([[np.cos(T), -np.sin(T), 0], - [np.sin(T), np.cos(T), 0], + T = np.deg2rad(theta) + R = np.array([[np.cos(T), np.sin(T), 0], + [-np.sin(T), np.cos(T), 0], [0, 0, 1]]) - return shift1.dot(R).dot(shift0) for i in range(len(theta)): - rotated = _warp_fast(padded_image, - np.linalg.inv(build_rotation(-theta[i]))) + rotated = _warp_fast(padded_image, build_rotation(theta[i])) + radon_image[:, i] = rotated.sum(0) + return radon_image - out[:, i] = rotated.sum(0)[::-1] - return out +def _sinogram_circle_to_square(sinogram): + diagonal = int(np.ceil(np.sqrt(2) * sinogram.shape[0])) + pad = diagonal - sinogram.shape[0] + old_center = sinogram.shape[0] // 2 + new_center = diagonal // 2 + pad_before = new_center - old_center + pad_width = ((pad_before, pad - pad_before), (0, 0)) + return util.pad(sinogram, pad_width, mode='constant', constant_values=0) def iradon(radon_image, theta=None, output_size=None, - filter="ramp", interpolation="linear"): + filter="ramp", interpolation="linear", circle=False): """ Inverse radon transform. @@ -97,7 +136,10 @@ def iradon(radon_image, theta=None, output_size=None, ---------- radon_image : array_like, dtype=float Image containing radon transform (sinogram). Each column of - the image corresponds to a projection along a different angle. + the image corresponds to a projection along a different angle. The + tomography rotation axis should lie at the pixel index + ``radon_image.shape[0] // 2`` along the 0th dimension of + ``radon_image``. 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 (N, M)). @@ -105,104 +147,277 @@ def iradon(radon_image, theta=None, output_size=None, Number of rows and columns in the reconstruction. filter : str, optional (default ramp) Filter used in frequency domain filtering. Ramp filter used by default. - Filters available: ramp, shepp-logan, cosine, hamming, hann + Filters available: ramp, shepp-logan, cosine, hamming, hann. Assign None to use no filter. - interpolation : str, optional (default linear) - Interpolation method used in reconstruction. - Methods available: nearest, linear. + interpolation : str, optional (default 'linear') + Interpolation method used in reconstruction. Methods available: + 'linear', 'nearest', and 'cubic' ('cubic' is slow). + circle : boolean, optional + Assume the reconstructed image is zero outside the inscribed circle. + Also changes the default output_size to match the behaviour of + ``radon`` called with ``circle=True``. Returns ------- - output : ndarray - Reconstructed image. + reconstructed : ndarray + Reconstructed image. The rotation axis will be located in the pixel + with indices + ``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``. Notes ----- - It applies the fourier slice theorem to reconstruct an image by + It applies the Fourier slice theorem to reconstruct an image by multiplying the frequency domain of the filter with the FFT of the projection data. This algorithm is called filtered back projection. """ if radon_image.ndim != 2: raise ValueError('The input image must be 2-D') - 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``.") + interpolation_types = ('linear', 'nearest', 'cubic') + if not interpolation in interpolation_types: + raise ValueError("Unknown interpolation: %s" % interpolation) + if not output_size: + # If output size not specified, estimate from input radon image + if circle: + output_size = radon_image.shape[0] + else: + output_size = int(np.floor(np.sqrt((radon_image.shape[0])**2 + / 2.0))) + if circle: + radon_image = _sinogram_circle_to_square(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))) - n = radon_image.shape[0] + # resize image to next power of two (but no less than 64) for + # Fourier analysis; speeds up Fourier and lessens artifacts + projection_size_padded = \ + max(64, int(2**np.ceil(np.log2(2 * radon_image.shape[0])))) + pad_width = ((0, projection_size_padded - radon_image.shape[0]), (0, 0)) + img = util.pad(radon_image, pad_width, mode='constant', constant_values=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))) - # zero pad input image - img.resize((order, img.shape[1])) - # construct the fourier filter - - f = fftshift(abs(np.mgrid[-1:1:2 / order])).reshape(-1, 1) - w = 2 * np.pi * f - # start from first element to avoid divide by zero + # Construct the Fourier filter + f = fftfreq(projection_size_padded).reshape(-1, 1) # digital frequency + omega = 2 * np.pi * f # angular frequency + fourier_filter = 2 * np.abs(f) # ramp filter if filter == "ramp": pass elif filter == "shepp-logan": - f[1:] = f[1:] * np.sin(w[1:] / 2) / (w[1:] / 2) + # Start from first element to avoid divide by zero + fourier_filter[1:] = fourier_filter[1:] * np.sin(omega[1:]) / omega[1:] elif filter == "cosine": - f[1:] = f[1:] * np.cos(w[1:] / 2) + fourier_filter *= np.cos(omega) elif filter == "hamming": - f[1:] = f[1:] * (0.54 + 0.46 * np.cos(w[1:])) + fourier_filter *= (0.54 + 0.46 * np.cos(omega / 2)) elif filter == "hann": - f[1:] = f[1:] * (1 + np.cos(w[1:])) / 2 - elif filter == None: - f[1:] = 1 + fourier_filter *= (1 + np.cos(omega / 2)) / 2 + elif filter is None: + fourier_filter[:] = 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 + # Apply filter in Fourier domain + projection = fft(img, axis=0) * fourier_filter radon_filtered = np.real(ifft(projection, axis=0)) - # resize filtered image back to original size + # Resize filtered image back to original size radon_filtered = radon_filtered[:radon_image.shape[0], :] reconstructed = np.zeros((output_size, output_size)) - mid_index = np.ceil(n / 2.0) + # Determine the center of the projections (= center of sinogram) + mid_index = radon_image.shape[0] // 2 - x = output_size - y = output_size - [X, Y] = np.mgrid[0.0:x, 0.0:y] + [X, Y] = np.mgrid[0:output_size, 0:output_size] xpr = X - int(output_size) // 2 ypr = Y - int(output_size) // 2 - # reconstruct image by interpolation - if interpolation == "nearest": - for i in range(len(theta)): - 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] - - else: - raise ValueError("Unknown interpolation: %s" % interpolation) + # Reconstruct image by interpolation + for i in range(len(theta)): + t = ypr * np.cos(th[i]) - xpr * np.sin(th[i]) + x = np.arange(radon_filtered.shape[0]) - mid_index + if interpolation == 'linear': + backprojected = np.interp(t, x, radon_filtered[:, i], + left=0, right=0) + else: + interpolant = interp1d(x, radon_filtered[:, i], kind=interpolation, + bounds_error=False, fill_value=0) + backprojected = interpolant(t) + reconstructed += backprojected + if circle: + radius = output_size // 2 + reconstruction_circle = (xpr**2 + ypr**2) <= radius**2 + reconstructed[~reconstruction_circle] = 0. return reconstructed * np.pi / (2 * len(th)) + + +def order_angles_golden_ratio(theta): + """ + Order angles to reduce the amount of correlated information + in subsequent projections. + + Parameters + ---------- + theta : 1D array of floats + Projection angles in degrees. Duplicate angles are not allowed. + + Returns + ------- + indices_generator : generator yielding unsigned integers + The returned generator yields indices into ``theta`` such that + ``theta[indices]`` gives the approximate golden ratio ordering + of the projections. In total, ``len(theta)`` indices are yielded. + All non-negative integers < ``len(theta)`` are yielded exactly once. + + Notes + ----- + The method used here is that of the golden ratio introduced + by T. Kohler. + + References + ---------- + .. [1] Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + .. [2] Winkelmann, Stefanie, et al. "An optimal radial profile order + based on the Golden Ratio for time-resolved MRI." + Medical Imaging, IEEE Transactions on 26.1 (2007): 68-76. + """ + interval = 180 + + def angle_distance(a, b): + difference = a - b + return min(abs(difference % interval), abs(difference % -interval)) + + remaining = list(np.argsort(theta)) # indices into theta + # yield an arbitrary angle to start things off + index = remaining.pop(0) + angle = theta[index] + yield index + # determine subsequent angles using the golden ratio method + angle_increment = interval * (1 - (np.sqrt(5) - 1) / 2) + while remaining: + angle = (angle + angle_increment) % interval + insert_point = np.searchsorted(theta[remaining], angle) + index_below = insert_point - 1 + index_above = 0 if insert_point == len(remaining) else insert_point + distance_below = angle_distance(angle, theta[remaining[index_below]]) + distance_above = angle_distance(angle, theta[remaining[index_above]]) + if distance_below < distance_above: + yield remaining.pop(index_below) + else: + yield remaining.pop(index_above) + + +def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, + clip=None, relaxation=0.15): + """ + Inverse radon transform + + Reconstruct an image from the radon transform, using a single iteration of + the Simultaneous Algebraic Reconstruction Technique (SART) algorithm. + + Parameters + ---------- + radon_image : 2D array, dtype=float + Image containing radon transform (sinogram). Each column of + the image corresponds to a projection along a different angle. The + tomography rotation axis should lie at the pixel index + ``radon_image.shape[0] // 2`` along the 0th dimension of + ``radon_image``. + theta : 1D array, dtype=float, optional + Reconstruction angles (in degrees). Default: m angles evenly spaced + between 0 and 180 (if the shape of `radon_image` is (N, M)). + image : 2D array, dtype=float, optional + Image containing an initial reconstruction estimate. Shape of this + array should be ``(radon_image.shape[0], radon_image.shape[0])``. The + default is an array of zeros. + projection_shifts : 1D array, dtype=float + Shift the projections contained in ``radon_image`` (the sinogram) by + this many pixels before reconstructing the image. The i'th value + defines the shift of the i'th column of ``radon_image``. + clip : length-2 sequence of floats + Force all values in the reconstructed tomogram to lie in the range + ``[clip[0], clip[1]]`` + relaxation : float + Relaxation parameter for the update step. A higher value can + improve the convergence rate, but one runs the risk of instabilities. + Values close to or higher than 1 are not recommended. + + Returns + ------- + reconstructed : ndarray + Reconstructed image. The rotation axis will be located in the pixel + with indices + ``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``. + + Notes + ----- + Algebraic Reconstruction Techniques are based on formulating the tomography + reconstruction problem as a set of linear equations. Along each ray, + the projected value is the sum of all the values of the cross section along + the ray. A typical feature of SART (and a few other variants of algebraic + techniques) is that it samples the cross section at equidistant points + along the ray, using linear interpolation between the pixel values of the + cross section. The resulting set of linear equations are then solved using + a slightly modified Kaczmarz method. + + When using SART, a single iteration is usually sufficient to obtain a good + reconstruction. Further iterations will tend to enhance high-frequency + information, but will also often increase the noise. + + References + ---------- + .. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic + Imaging", IEEE Press 1988. + .. [2] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction + technique (SART): a superior implementation of the ART algorithm", + Ultrasonic Imaging 6 pp 81--94 (1984) + .. [3] S Kaczmarz, "Angenäherte auflösung von systemen linearer + gleichungen", Bulletin International de l’Academie Polonaise des + Sciences et des Lettres 35 pp 355--357 (1937) + .. [4] Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + .. [5] Kaczmarz' method, Wikipedia, + http://en.wikipedia.org/wiki/Kaczmarz_method + """ + if radon_image.ndim != 2: + raise ValueError('radon_image must be two dimensional') + reconstructed_shape = (radon_image.shape[0], radon_image.shape[0]) + if theta is None: + theta = np.linspace(0, 180, radon_image.shape[1], endpoint=False) + elif theta.shape != (radon_image.shape[1],): + raise ValueError('Shape of theta (%s) does not match the ' + 'number of projections (%d)' + % (projection_shifts.shape, radon_image.shape[1])) + if image is None: + image = np.zeros(reconstructed_shape, dtype=np.float) + elif image.shape != reconstructed_shape: + raise ValueError('Shape of image (%s) does not match first dimension ' + 'of radon_image (%s)' + % (image.shape, reconstructed_shape)) + if projection_shifts is None: + projection_shifts = np.zeros((radon_image.shape[1],), dtype=np.float) + elif projection_shifts.shape != (radon_image.shape[1],): + raise ValueError('Shape of projection_shifts (%s) does not match the ' + 'number of projections (%d)' + % (projection_shifts.shape, radon_image.shape[1])) + if not clip is None: + if len(clip) != 2: + raise ValueError('clip must be a length-2 sequence') + clip = (float(clip[0]), float(clip[1])) + relaxation = float(relaxation) + + for angle_index in order_angles_golden_ratio(theta): + image_update = sart_projection_update(image, theta[angle_index], + radon_image[:, angle_index], + projection_shifts[angle_index]) + image += relaxation * image_update + if not clip is None: + image = np.clip(image, clip[0], clip[1]) + return image diff --git a/skimage/transform/setup.py b/skimage/transform/setup.py index b0093d87..22f31696 100644 --- a/skimage/transform/setup.py +++ b/skimage/transform/setup.py @@ -15,6 +15,7 @@ def configuration(parent_package='', top_path=None): cython(['_hough_transform.pyx'], working_path=base_path) cython(['_warps_cy.pyx'], working_path=base_path) + cython(['_radon_transform.pyx'], working_path=base_path) config.add_extension('_hough_transform', sources=['_hough_transform.c'], include_dirs=[get_numpy_include_dirs()]) @@ -22,6 +23,10 @@ def configuration(parent_package='', top_path=None): config.add_extension('_warps_cy', sources=['_warps_cy.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) + config.add_extension('_radon_transform', + sources=['_radon_transform.c'], + include_dirs=[get_numpy_include_dirs()]) + return config if __name__ == '__main__': diff --git a/skimage/transform/tests/test_finite_radon_transform.py b/skimage/transform/tests/test_finite_radon_transform.py index b5fcf43a..c4c4de7e 100644 --- a/skimage/transform/tests/test_finite_radon_transform.py +++ b/skimage/transform/tests/test_finite_radon_transform.py @@ -1,7 +1,6 @@ import numpy as np -from numpy.testing import * -from skimage.transform import * +from skimage.transform import frt2, ifrt2 def test_frt(): @@ -17,3 +16,8 @@ def test_frt(): f = frt2(L) fi = ifrt2(f) assert len(np.nonzero(L - fi)[0]) == 0 + +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 00427332..fb19d8c1 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -1,10 +1,8 @@ import numpy as np -from numpy.testing import * +from numpy.testing import assert_almost_equal, assert_equal import skimage.transform as tf -import skimage.transform.hough_transform as ht -from skimage.transform import probabilistic_hough -from skimage.draw import circle_perimeter +from skimage.draw import line, circle_perimeter, ellipse_perimeter def append_desc(func, description): @@ -16,11 +14,11 @@ def append_desc(func, description): return func -def test_hough(): +def test_hough_line(): # Generate a test image - img = np.zeros((100, 100), dtype=int) - for i in range(25, 75): - img[100 - i, i] = 1 + img = np.zeros((100, 150), dtype=int) + rr, cc = line(60, 130, 80, 10) + img[rr, cc] = 1 out, angles, d = tf.hough_line(img) @@ -28,11 +26,11 @@ def test_hough(): dist = d[y[0]] theta = angles[x[0]] - assert_equal(dist > 70, dist < 72) - assert_equal(theta > 0.78, theta < 0.79) + assert_almost_equal(dist, 80.723, 1) + assert_almost_equal(theta, 1.41, 1) -def test_hough_angles(): +def test_hough_line_angles(): img = np.zeros((10, 10)) img[0, 0] = 1 @@ -41,15 +39,6 @@ def test_hough_angles(): assert_equal(len(angles), 10) -def test_py_hough(): - ht._hough, fast_hough = ht._py_hough, ht._hough - - yield append_desc(test_hough, '_python') - yield append_desc(test_hough_angles, '_python') - - tf._hough = fast_hough - - def test_probabilistic_hough(): # Generate a test image img = np.zeros((100, 100), dtype=int) @@ -59,8 +48,8 @@ def test_probabilistic_hough(): # 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) + lines = tf.probabilistic_hough_line(img, threshold=10, line_length=10, + line_gap=1, theta=theta) # sort the lines according to the x-axis sorted_lines = [] for line in lines: @@ -71,58 +60,300 @@ def test_probabilistic_hough(): assert([(25, 25), (74, 74)] in sorted_lines) -def test_hough_peaks_dist(): +def test_hough_line_peaks(): + img = np.zeros((100, 150), dtype=int) + rr, cc = line(60, 130, 80, 10) + img[rr, cc] = 1 + + out, angles, d = tf.hough_line(img) + + out, theta, dist = tf.hough_line_peaks(out, angles, d) + + assert_equal(len(dist), 1) + assert_almost_equal(dist[0], 80.723, 1) + assert_almost_equal(theta[0], 1.41, 1) + + +def test_hough_line_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 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_distance=5)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_distance=15)[0]) == 1 -def test_hough_peaks_angle(): +def test_hough_line_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 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=45)[0]) == 2 + assert len(tf.hough_line_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 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=45)[0]) == 2 + assert len(tf.hough_line_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 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=45)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=90)[0]) == 1 -def test_hough_peaks_num(): +def test_hough_line_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 + assert len(tf.hough_line_peaks(hspace, angles, dists, min_distance=0, + min_angle=0, num_peaks=1)[0]) == 1 -def test_houghcircle(): +def test_hough_circle(): # 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 + y, x = circle_perimeter(y_0, x_0, radius) + img[x, y] = 1 - out = tf.hough_circle(img, np.array([radius])) + out = tf.hough_circle(img, np.array([radius], dtype=np.intp)) + + x, y = np.where(out[0] == out[0].max()) + assert_equal(x[0], x_0) + assert_equal(y[0], y_0) + + +def test_hough_circle_extended(): + # Prepare picture + # The circle center is outside the image + img = np.zeros((100, 100), dtype=int) + radius = 20 + x_0, y_0 = (-5, 50) + y, x = circle_perimeter(y_0, x_0, radius) + img[x[np.where(x > 0)], y[np.where(x > 0)]] = 1 + + out = tf.hough_circle(img, np.array([radius], dtype=np.intp), + full_output=True) 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) + +def test_hough_ellipse_zero_angle(): + img = np.zeros((25, 25), dtype=int) + rx = 6 + ry = 8 + x0 = 12 + y0 = 15 + angle = 0 + rr, cc = ellipse_perimeter(y0, x0, ry, rx) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=9) + best = result[-1] + assert_equal(best[1], y0) + assert_equal(best[2], x0) + assert_almost_equal(best[3], ry, decimal=1) + assert_almost_equal(best[4], rx, decimal=1) + assert_equal(best[5], angle) + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_posangle1(): + # ry > rx, angle in [0:pi/2] + img = np.zeros((30, 24), dtype=int) + rx = 6 + ry = 12 + x0 = 10 + y0 = 15 + angle = np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + assert_almost_equal(best[1] / 100., y0 / 100., decimal=1) + assert_almost_equal(best[2] / 100., x0 / 100., decimal=1) + assert_almost_equal(best[3] / 10., ry / 10., decimal=1) + assert_almost_equal(best[4] / 100., rx / 100., decimal=1) + assert_almost_equal(best[5], angle, decimal=1) + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_posangle2(): + # ry < rx, angle in [0:pi/2] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + assert_almost_equal(best[1] / 100., y0 / 100., decimal=1) + assert_almost_equal(best[2] / 100., x0 / 100., decimal=1) + assert_almost_equal(best[3] / 10., ry / 10., decimal=1) + assert_almost_equal(best[4] / 100., rx / 100., decimal=1) + assert_almost_equal(best[5], angle, decimal=1) + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_posangle3(): + # ry < rx, angle in [pi/2:pi] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = np.pi / 1.35 + np.pi / 2. + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_posangle4(): + # ry < rx, angle in [pi:3pi/4] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = np.pi / 1.35 + np.pi + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle1(): + # ry > rx, angle in [0:-pi/2] + img = np.zeros((30, 24), dtype=int) + rx = 6 + ry = 12 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle2(): + # ry < rx, angle in [0:-pi/2] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle3(): + # ry < rx, angle in [-pi/2:-pi] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 - np.pi / 2. + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle4(): + # ry < rx, angle in [-pi:-3pi/4] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 - np.pi + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + if __name__ == "__main__": - run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/transform/tests/test_integral.py b/skimage/transform/tests/test_integral.py index d443189c..4a641e71 100644 --- a/skimage/transform/tests/test_integral.py +++ b/skimage/transform/tests/test_integral.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import * +from numpy.testing import assert_equal from skimage.transform import integral_image, integrate @@ -26,6 +26,22 @@ 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)) +def test_vectorized_integrate(): + r0 = np.array([12, 0, 0, 10, 0, 10, 30]) + c0 = np.array([10, 0, 10, 0, 0, 10, 31]) + r1 = np.array([23, 19, 19, 19, 0, 10, 49]) + c1 = np.array([19, 19, 19, 19, 0, 10, 49]) + + expected = np.array([x[12:24, 10:20].sum(), + x[:20, :20].sum(), + x[:20, 10:20].sum(), + x[10:20, :20].sum(), + x[0,0], + x[10, 10], + x[30:, 31:].sum()]) + assert_equal(expected, integrate(s, r0, c0, r1, c1)) + if __name__ == '__main__': + from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 3b2f19dc..6fbc3f62 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -1,8 +1,42 @@ -from __future__ import print_function +from __future__ import print_function, division import numpy as np -from numpy.testing import * -from skimage.transform import * +from numpy.testing import assert_raises +import itertools +import os.path + +from skimage.transform import radon, iradon +from skimage.io import imread +from skimage import data_dir + + +__PHANTOM = imread(os.path.join(data_dir, "phantom.png"), + as_grey=True)[::2, ::2] + + +def _get_phantom(): + return __PHANTOM + + +def _debug_plot(original, result, sinogram=None): + from matplotlib import pyplot as plt + imkwargs = dict(cmap='gray', interpolation='nearest') + if sinogram is None: + plt.figure(figsize=(15, 6)) + sp = 130 + else: + plt.figure(figsize=(11, 11)) + sp = 221 + plt.subplot(sp + 0) + plt.imshow(sinogram, aspect='auto', **imkwargs) + plt.subplot(sp + 1) + plt.imshow(original, **imkwargs) + plt.subplot(sp + 2) + plt.imshow(result, vmin=original.min(), vmax=original.max(), **imkwargs) + plt.subplot(sp + 3) + plt.imshow(result - original, **imkwargs) + plt.colorbar() + plt.show() def rescale(x): @@ -12,33 +46,102 @@ def rescale(x): return x -def test_radon_iradon(): - size = 100 +def check_radon_center(shape, circle): + # Create a test image with only a single non-zero pixel at the origin + image = np.zeros(shape, dtype=np.float) + image[(shape[0] // 2, shape[1] // 2)] = 1. + # Calculate the sinogram + theta = np.linspace(0., 180., max(shape), endpoint=False) + sinogram = radon(image, theta=theta, circle=circle) + # The sinogram should be a straight, horizontal line + sinogram_max = np.argmax(sinogram, axis=0) + print(sinogram_max) + assert np.std(sinogram_max) < 1e-6 + + +def test_radon_center(): + shapes = [(16, 16), (17, 17)] + circles = [False, True] + for shape, circle in itertools.product(shapes, circles): + yield check_radon_center, shape, circle + rectangular_shapes = [(32, 16), (33, 17)] + for shape in rectangular_shapes: + yield check_radon_center, shape, False + + +def check_iradon_center(size, theta, circle): debug = False - image = np.tri(size) + np.tri(size)[::-1] - for filter_type in ["ramp", "shepp-logan", "cosine", "hamming", "hann"]: - reconstructed = iradon(radon(image), filter=filter_type) + # Create a test sinogram corresponding to a single projection + # with a single non-zero pixel at the rotation center + if circle: + sinogram = np.zeros((size, 1), dtype=np.float) + sinogram[size // 2, 0] = 1. + else: + diagonal = int(np.ceil(np.sqrt(2) * size)) + sinogram = np.zeros((diagonal, 1), dtype=np.float) + sinogram[sinogram.shape[0] // 2, 0] = 1. + maxpoint = np.unravel_index(np.argmax(sinogram), sinogram.shape) + print('shape of generated sinogram', sinogram.shape) + print('maximum in generated sinogram', maxpoint) + # Compare reconstructions for theta=angle and theta=angle + 180; + # these should be exactly equal + reconstruction = iradon(sinogram, theta=[theta], circle=circle) + reconstruction_opposite = iradon(sinogram, theta=[theta + 180], + circle=circle) + print('rms deviance:', + np.sqrt(np.mean((reconstruction_opposite - reconstruction)**2))) + if debug: + import matplotlib.pyplot as plt + imkwargs = dict(cmap='gray', interpolation='nearest') + plt.figure() + plt.subplot(221) + plt.imshow(sinogram, **imkwargs) + plt.subplot(222) + plt.imshow(reconstruction_opposite - reconstruction, **imkwargs) + plt.subplot(223) + plt.imshow(reconstruction, **imkwargs) + plt.subplot(224) + plt.imshow(reconstruction_opposite, **imkwargs) + plt.show() - image = rescale(image) - reconstructed = rescale(reconstructed) - delta = np.mean(np.abs(image - reconstructed)) + assert np.allclose(reconstruction, reconstruction_opposite) - if debug: - print(delta) - import matplotlib.pyplot as plt - f, (ax1, ax2) = plt.subplots(1, 2) - ax1.imshow(image, cmap=plt.cm.gray) - ax2.imshow(reconstructed, cmap=plt.cm.gray) - plt.show() - assert delta < 0.05 +def test_iradon_center(): + sizes = [16, 17] + thetas = [0, 90] + circles = [False, True] + for size, theta, circle in itertools.product(sizes, thetas, circles): + yield check_iradon_center, size, theta, circle - reconstructed = iradon(radon(image), filter="ramp", interpolation="nearest") - delta = np.mean(abs(image - reconstructed)) - assert delta < 0.05 - size = 20 - image = np.tri(size) + np.tri(size)[::-1] - reconstructed = iradon(radon(image), filter="ramp", interpolation="nearest") + +def check_radon_iradon(interpolation_type, filter_type): + debug = False + image = _get_phantom() + reconstructed = iradon(radon(image), filter=filter_type, + interpolation=interpolation_type) + delta = np.mean(np.abs(image - reconstructed)) + print('\n\tmean error:', delta) + if debug: + _debug_plot(image, reconstructed) + if filter_type in ('ramp', 'shepp-logan'): + if interpolation_type == 'nearest': + allowed_delta = 0.03 + else: + allowed_delta = 0.02 + else: + allowed_delta = 0.05 + assert delta < allowed_delta + + +def test_radon_iradon(): + filter_types = ["ramp", "shepp-logan", "cosine", "hamming", "hann"] + interpolation_types = ['linear', 'nearest'] + for interpolation_type, filter_type in \ + itertools.product(interpolation_types, filter_types): + yield check_radon_iradon, interpolation_type, filter_type + # cubic interpolation is slow; only run one test for it + yield check_radon_iradon, 'cubic', 'shepp-logan' def test_iradon_angles(): @@ -51,14 +154,14 @@ def test_iradon_angles(): # Large number of projections: a good quality is expected nb_angles = 200 radon_image_200 = radon(image, theta=np.linspace(0, 180, nb_angles, - endpoint=False)) + endpoint=False)) reconstructed = iradon(radon_image_200) delta_200 = np.mean(abs(rescale(image) - rescale(reconstructed))) assert delta_200 < 0.03 # Lower number of projections nb_angles = 80 radon_image_80 = radon(image, theta=np.linspace(0, 180, nb_angles, - endpoint=False)) + endpoint=False)) # Test whether the sum of all projections is approximately the same s = radon_image_80.sum(axis=0) assert np.allclose(s, s[0], rtol=0.01) @@ -69,32 +172,29 @@ def test_iradon_angles(): assert delta_80 > delta_200 -def test_radon_minimal(): - """ - Test for small images for various angles - """ - thetas = [np.arange(180)] - for theta in thetas: - a = np.zeros((3, 3)) - a[1, 1] = 1 - p = radon(a, theta) - reconstructed = iradon(p, theta) - reconstructed /= np.max(reconstructed) - assert np.all(abs(a - reconstructed) < 0.4) +def check_radon_iradon_minimal(shape, slices): + debug = False + theta = np.arange(180) + image = np.zeros(shape, dtype=np.float) + image[slices] = 1. + sinogram = radon(image, theta) + reconstructed = iradon(sinogram, theta) + print('\n\tMaximum deviation:', np.max(np.abs(image - reconstructed))) + if debug: + _debug_plot(image, reconstructed, sinogram) + if image.sum() == 1: + assert (np.unravel_index(np.argmax(reconstructed), image.shape) + == np.unravel_index(np.argmax(image), image.shape)) - b = np.zeros((4, 4)) - b[1:3, 1:3] = 1 - p = radon(b, theta) - reconstructed = iradon(p, theta) - reconstructed /= np.max(reconstructed) - assert np.all(abs(b - reconstructed) < 0.4) - c = np.zeros((5, 5)) - c[1:3, 1:3] = 1 - p = radon(c, theta) - reconstructed = iradon(p, theta) - reconstructed /= np.max(reconstructed) - assert np.all(abs(c - reconstructed) < 0.4) +def test_radon_iradon_minimal(): + shapes = [(3, 3), (4, 4), (5, 5)] + for shape in shapes: + c0, c1 = shape[0] // 2, shape[1] // 2 + coordinates = itertools.product((c0 - 1, c0, c0 + 1), + (c1 - 1, c1, c1 + 1)) + for coordinate in coordinates: + yield check_radon_iradon_minimal, shape, coordinate def test_reconstruct_with_wrong_angles(): @@ -104,5 +204,181 @@ def test_reconstruct_with_wrong_angles(): assert_raises(ValueError, iradon, p, theta=[0, 1, 2, 3]) +def _random_circle(shape): + # Synthetic random data, zero outside reconstruction circle + np.random.seed(98312871) + image = np.random.rand(*shape) + c0, c1 = np.ogrid[0:shape[0], 0:shape[1]] + r = np.sqrt((c0 - shape[0] // 2)**2 + (c1 - shape[1] // 2)**2) + radius = min(shape) // 2 + image[r > radius] = 0. + return image + + +def test_radon_circle(): + a = np.ones((10, 10)) + assert_raises(ValueError, radon, a, circle=True) + + # Synthetic data, circular symmetry + shape = (61, 79) + c0, c1 = np.ogrid[0:shape[0], 0:shape[1]] + r = np.sqrt((c0 - shape[0] // 2)**2 + (c1 - shape[1] // 2)**2) + radius = min(shape) // 2 + image = np.clip(radius - r, 0, np.inf) + image = rescale(image) + angles = np.linspace(0, 180, min(shape), endpoint=False) + sinogram = radon(image, theta=angles, circle=True) + assert np.all(sinogram.std(axis=1) < 1e-2) + + # Synthetic data, random + image = _random_circle(shape) + sinogram = radon(image, theta=angles, circle=True) + mass = sinogram.sum(axis=0) + average_mass = mass.mean() + relative_error = np.abs(mass - average_mass) / average_mass + print(relative_error.max(), relative_error.mean()) + assert np.all(relative_error < 3.2e-3) + + +def check_sinogram_circle_to_square(size): + from skimage.transform.radon_transform import _sinogram_circle_to_square + image = _random_circle((size, size)) + theta = np.linspace(0., 180., size, False) + sinogram_circle = radon(image, theta, circle=True) + argmax_shape = lambda a: np.unravel_index(np.argmax(a), a.shape) + print('\n\targmax of circle:', argmax_shape(sinogram_circle)) + sinogram_square = radon(image, theta, circle=False) + print('\targmax of square:', argmax_shape(sinogram_square)) + sinogram_circle_to_square = _sinogram_circle_to_square(sinogram_circle) + print('\targmax of circle to square:', + argmax_shape(sinogram_circle_to_square)) + error = abs(sinogram_square - sinogram_circle_to_square) + print(np.mean(error), np.max(error)) + assert (argmax_shape(sinogram_square) + == argmax_shape(sinogram_circle_to_square)) + + +def test_sinogram_circle_to_square(): + for size in (50, 51): + yield check_sinogram_circle_to_square, size + + +def check_radon_iradon_circle(interpolation, shape, output_size): + # Forward and inverse radon on synthetic data + image = _random_circle(shape) + radius = min(shape) // 2 + sinogram_rectangle = radon(image, circle=False) + reconstruction_rectangle = iradon(sinogram_rectangle, + output_size=output_size, + interpolation=interpolation, + circle=False) + sinogram_circle = radon(image, circle=True) + reconstruction_circle = iradon(sinogram_circle, + output_size=output_size, + interpolation=interpolation, + circle=True) + # Crop rectangular reconstruction to match circle=True reconstruction + width = reconstruction_circle.shape[0] + excess = int(np.ceil((reconstruction_rectangle.shape[0] - width) / 2)) + s = np.s_[excess:width + excess, excess:width + excess] + reconstruction_rectangle = reconstruction_rectangle[s] + # Find the reconstruction circle, set reconstruction to zero outside + c0, c1 = np.ogrid[0:width, 0:width] + r = np.sqrt((c0 - width // 2)**2 + (c1 - width // 2)**2) + reconstruction_rectangle[r > radius] = 0. + print(reconstruction_circle.shape) + print(reconstruction_rectangle.shape) + np.allclose(reconstruction_rectangle, reconstruction_circle) + + +def test_radon_iradon_circle(): + shape = (61, 79) + interpolations = ('nearest', 'linear') + output_sizes = (None, min(shape), max(shape), 97) + for interpolation, output_size in itertools.product(interpolations, + output_sizes): + yield check_radon_iradon_circle, interpolation, shape, output_size + + +def test_order_angles_golden_ratio(): + from skimage.transform.radon_transform import order_angles_golden_ratio + np.random.seed(1231) + lengths = [1, 4, 10, 180] + for l in lengths: + theta_ordered = np.linspace(0, 180, l, endpoint=False) + theta_random = np.random.uniform(0, 180, l) + for theta in (theta_random, theta_ordered): + indices = [x for x in order_angles_golden_ratio(theta)] + # no duplicate indices allowed + assert len(indices) == len(set(indices)) + + +def test_iradon_sart(): + from skimage.io import imread + from skimage import data_dir + from skimage.transform import rescale, radon, iradon_sart + + debug = False + + shepp_logan = imread(os.path.join(data_dir, "phantom.png"), as_grey=True) + image = rescale(shepp_logan, scale=0.4) + theta_ordered = np.linspace(0., 180., image.shape[0], endpoint=False) + theta_missing_wedge = np.linspace(0., 150., image.shape[0], endpoint=True) + for theta, error_factor in ((theta_ordered, 1.), + (theta_missing_wedge, 2.)): + sinogram = radon(image, theta, circle=True) + reconstructed = iradon_sart(sinogram, theta) + + if debug: + from matplotlib import pyplot as plt + plt.figure() + plt.subplot(221) + plt.imshow(image, interpolation='nearest') + plt.subplot(222) + plt.imshow(sinogram, interpolation='nearest') + plt.subplot(223) + plt.imshow(reconstructed, interpolation='nearest') + plt.subplot(224) + plt.imshow(reconstructed - image, interpolation='nearest') + plt.show() + + delta = np.mean(np.abs(reconstructed - image)) + print('delta (1 iteration) =', delta) + assert delta < 0.016 * error_factor + reconstructed = iradon_sart(sinogram, theta, reconstructed) + delta = np.mean(np.abs(reconstructed - image)) + print('delta (2 iterations) =', delta) + assert delta < 0.013 * error_factor + reconstructed = iradon_sart(sinogram, theta, clip=(0, 1)) + delta = np.mean(np.abs(reconstructed - image)) + print('delta (1 iteration, clip) =', delta) + assert delta < 0.015 * error_factor + + np.random.seed(1239867) + shifts = np.random.uniform(-3, 3, sinogram.shape[1]) + x = np.arange(sinogram.shape[0]) + sinogram_shifted = np.vstack(np.interp(x + shifts[i], x, + sinogram[:, i]) + for i in range(sinogram.shape[1])).T + reconstructed = iradon_sart(sinogram_shifted, theta, + projection_shifts=shifts) + if debug: + from matplotlib import pyplot as plt + plt.figure() + plt.subplot(221) + plt.imshow(image, interpolation='nearest') + plt.subplot(222) + plt.imshow(sinogram_shifted, interpolation='nearest') + plt.subplot(223) + plt.imshow(reconstructed, interpolation='nearest') + plt.subplot(224) + plt.imshow(reconstructed - image, interpolation='nearest') + plt.show() + + delta = np.mean(np.abs(reconstructed - image)) + print('delta (1 iteration, shifted sinogram) =', delta) + assert delta < 0.018 * error_factor + if __name__ == "__main__": + from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 93f87320..7f7ef47d 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -1,19 +1,19 @@ -from numpy.testing import assert_array_almost_equal, run_module_suite +from numpy.testing import assert_array_almost_equal, run_module_suite, assert_array_equal import numpy as np from scipy.ndimage import map_coordinates from skimage.transform import (warp, warp_coords, rotate, resize, rescale, AffineTransform, ProjectiveTransform, - SimilarityTransform) + SimilarityTransform, + downscale_local_mean) 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) +def test_warp_tform(): + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 theta = - np.pi / 2 tform = SimilarityTransform(scale=1, rotation=theta, translation=(0, 4)) @@ -24,10 +24,36 @@ def test_warp(): assert_array_almost_equal(x90, np.rot90(x)) +def test_warp_callable(): + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 + refx = np.zeros((5, 5), dtype=np.double) + refx[1, 1] = 1 + + shift = lambda xy: xy + 1 + + outx = warp(x, shift, order=1) + assert_array_almost_equal(outx, refx) + + +def test_warp_matrix(): + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 + refx = np.zeros((5, 5), dtype=np.double) + refx[1, 1] = 1 + + matrix = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 1]]) + + # _warp_fast + outx = warp(x, matrix, order=1) + assert_array_almost_equal(outx, refx) + # check for ndimage.map_coordinates + outx = warp(x, matrix, order=5) + + def test_homography(): - x = np.zeros((5, 5), dtype=np.uint8) - x[1, 1] = 255 - x = img_as_float(x) + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 theta = -np.pi / 2 M = np.array([[np.cos(theta), - np.sin(theta), 0], [np.sin(theta), np.cos(theta), 4], @@ -194,5 +220,19 @@ def test_warp_coords_example(): map_coordinates(image[:, :, 0], coords[:2]) +def test_downscale_local_mean(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = downscale_local_mean(image1, (2, 3)) + expected1 = np.array([[ 4., 7.], + [ 16., 19.]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = downscale_local_mean(image2, (4, 5)) + expected2 = np.array([[ 14. , 10.8], + [ 8.5, 5.7]]) + assert_array_equal(expected2, out2) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index 8daaa60d..9cd2bc50 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -1,2 +1,29 @@ -from .dtype import * -from .shape import * +from .dtype import (img_as_float, img_as_int, img_as_uint, img_as_ubyte, + img_as_bool, dtype_limits) +from .shape import view_as_blocks, view_as_windows +from .noise import random_noise + +import numpy +ver = numpy.__version__.split('.') +chk = int(ver[0] + ver[1]) +if chk < 18: # Use internal version for numpy versions < 1.8.x + from .arraypad import pad +else: + from numpy import pad +del numpy, ver, chk +from ._regular_grid import regular_grid +from .unique import unique_rows + + +__all__ = ['img_as_float', + 'img_as_int', + 'img_as_uint', + 'img_as_ubyte', + 'img_as_bool', + 'dtype_limits', + 'view_as_blocks', + 'view_as_windows', + 'pad', + 'random_noise', + 'regular_grid', + 'unique_rows'] diff --git a/skimage/util/_regular_grid.py b/skimage/util/_regular_grid.py new file mode 100644 index 00000000..898a4aed --- /dev/null +++ b/skimage/util/_regular_grid.py @@ -0,0 +1,72 @@ +import numpy as np + + +def regular_grid(ar_shape, n_points): + """Find `n_points` regularly spaced along `ar_shape`. + + The returned points (as slices) should be as close to cubically-spaced as + possible. Essentially, the points are spaced by the Nth root of the input + array size, where N is the number of dimensions. However, if an array + dimension cannot fit a full step size, it is "discarded", and the + computation is done for only the remaining dimensions. + + Parameters + ---------- + ar_shape : array-like of ints + The shape of the space embedding the grid. ``len(ar_shape)`` is the + number of dimensions. + n_points : int + The (approximate) number of points to embed in the space. + + Returns + ------- + slices : list of slice objects + A slice along each dimension of `ar_shape`, such that the intersection + of all the slices give the coordinates of regularly spaced points. + + Examples + -------- + >>> ar = np.zeros((20, 40)) + >>> g = regular_grid(ar.shape, 8) + >>> g + [slice(5.0, None, 10.0), slice(5.0, None, 10.0)] + >>> ar[g] = 1 + >>> ar.sum() + 8.0 + >>> ar = np.zeros((20, 40)) + >>> g = regular_grid(ar.shape, 32) + >>> g + [slice(2.0, None, 5.0), slice(2.0, None, 5.0)] + >>> ar[g] = 1 + >>> ar.sum() + 32.0 + >>> ar = np.zeros((3, 20, 40)) + >>> g = regular_grid(ar.shape, 8) + >>> g + [slice(1.0, None, 3.0), slice(5.0, None, 10.0), slice(5.0, None, 10.0)] + >>> ar[g] = 1 + >>> ar.sum() + 8.0 + """ + ar_shape = np.asanyarray(ar_shape) + ndim = len(ar_shape) + unsort_dim_idxs = np.argsort(np.argsort(ar_shape)) + sorted_dims = np.sort(ar_shape) + space_size = float(np.prod(ar_shape)) + if space_size <= n_points: + return [slice(None)] * ndim + stepsizes = (space_size / n_points) ** (1.0 / ndim) * np.ones(ndim) + if (sorted_dims < stepsizes).any(): + for dim in range(ndim): + stepsizes[dim] = sorted_dims[dim] + space_size = float(np.prod(sorted_dims[dim+1:])) + stepsizes[dim+1:] = ((space_size / n_points) ** + (1.0 / (ndim - dim - 1))) + if (sorted_dims >= stepsizes).all(): + break + starts = stepsizes // 2 + stepsizes = np.round(stepsizes) + slices = [slice(start, None, step) for + start, step in zip(starts, stepsizes)] + slices = [slices[i] for i in unsort_dim_idxs] + return slices diff --git a/skimage/util/arraypad.py b/skimage/util/arraypad.py new file mode 100644 index 00000000..93f66bb8 --- /dev/null +++ b/skimage/util/arraypad.py @@ -0,0 +1,1467 @@ +""" +The arraypad module contains a group of functions to pad values onto the edges +of an n-dimensional array. + +""" +from __future__ import division, absolute_import, print_function +from skimage._shared.six import integer_types + +import numpy as np + +try: + # Available on 2.x at base, Py3 requires this compatibility import. + # Later versions of NumPy have this for 2.x as well. + from numpy.compat import long +except: + pass + +__all__ = ['pad'] + + +############################################################################### +# Private utility functions. + + +def _arange_ndarray(arr, shape, axis, reverse=False): + """ + Create an ndarray of `shape` with increments along specified `axis` + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + shape : tuple of ints + Shape of desired array. Should be equivalent to `arr.shape` except + `shape[axis]` which may have any positive value. + axis : int + Axis to increment along. + reverse : bool + If False, increment in a positive fashion from 1 to `shape[axis]`, + inclusive. If True, the bounds are the same but the order reversed. + + Returns + ------- + padarr : ndarray + Output array sized to pad `arr` along `axis`, with linear range from + 1 to `shape[axis]` along specified `axis`. + + Notes + ----- + The range is deliberately 1-indexed for this specific use case. Think of + this algorithm as broadcasting `np.arange` to a single `axis` of an + arbitrarily shaped ndarray. + + """ + initshape = tuple(1 if i != axis else shape[axis] + for (i, x) in enumerate(arr.shape)) + if not reverse: + padarr = np.arange(1, shape[axis] + 1) + else: + padarr = np.arange(shape[axis], 0, -1) + padarr = padarr.reshape(initshape) + for i, dim in enumerate(shape): + if padarr.shape[i] != dim: + padarr = padarr.repeat(dim, axis=i) + return padarr + + +def _round_ifneeded(arr, dtype): + """ + Rounds arr inplace if destination dtype is integer. + + Parameters + ---------- + arr : ndarray + Input array. + dtype : dtype + The dtype of the destination array. + + """ + if np.issubdtype(dtype, np.integer): + arr.round(out=arr) + + +def _prepend_const(arr, pad_amt, val, axis=-1): + """ + Prepend constant `val` along `axis` of `arr`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to prepend. + val : scalar + Constant value to use. For best results should be of type `arr.dtype`; + if not `arr.dtype` will be cast to `arr.dtype`. + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` constant `val` prepended along `axis`. + + """ + if pad_amt == 0: + return arr + padshape = tuple(x if i != axis else pad_amt + for (i, x) in enumerate(arr.shape)) + if val == 0: + return np.concatenate((np.zeros(padshape, dtype=arr.dtype), arr), + axis=axis) + else: + return np.concatenate(((np.zeros(padshape) + val).astype(arr.dtype), + arr), axis=axis) + + +def _append_const(arr, pad_amt, val, axis=-1): + """ + Append constant `val` along `axis` of `arr`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to append. + val : scalar + Constant value to use. For best results should be of type `arr.dtype`; + if not `arr.dtype` will be cast to `arr.dtype`. + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` constant `val` appended along `axis`. + + """ + if pad_amt == 0: + return arr + padshape = tuple(x if i != axis else pad_amt + for (i, x) in enumerate(arr.shape)) + if val == 0: + return np.concatenate((arr, np.zeros(padshape, dtype=arr.dtype)), + axis=axis) + else: + return np.concatenate( + (arr, (np.zeros(padshape) + val).astype(arr.dtype)), axis=axis) + + +def _prepend_edge(arr, pad_amt, axis=-1): + """ + Prepend `pad_amt` to `arr` along `axis` by extending edge values. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to prepend. + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, extended by `pad_amt` edge values appended along `axis`. + + """ + if pad_amt == 0: + return arr + + edge_slice = tuple(slice(None) if i != axis else 0 + for (i, x) in enumerate(arr.shape)) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + edge_arr = arr[edge_slice].reshape(pad_singleton) + return np.concatenate((edge_arr.repeat(pad_amt, axis=axis), arr), + axis=axis) + + +def _append_edge(arr, pad_amt, axis=-1): + """ + Append `pad_amt` to `arr` along `axis` by extending edge values. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to append. + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, extended by `pad_amt` edge values prepended along + `axis`. + + """ + if pad_amt == 0: + return arr + + edge_slice = tuple(slice(None) if i != axis else arr.shape[axis] - 1 + for (i, x) in enumerate(arr.shape)) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + edge_arr = arr[edge_slice].reshape(pad_singleton) + return np.concatenate((arr, edge_arr.repeat(pad_amt, axis=axis)), + axis=axis) + + +def _prepend_ramp(arr, pad_amt, end, axis=-1): + """ + Prepend linear ramp along `axis`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to prepend. + end : scalar + Constal value to use. For best results should be of type `arr.dtype`; + if not `arr.dtype` will be cast to `arr.dtype`. + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` values prepended along `axis`. The + prepended region ramps linearly from the edge value to `end`. + + """ + if pad_amt == 0: + return arr + + # Generate shape for final concatenated array + padshape = tuple(x if i != axis else pad_amt + for (i, x) in enumerate(arr.shape)) + + # Generate an n-dimensional array incrementing along `axis` + ramp_arr = _arange_ndarray(arr, padshape, axis, + reverse=True).astype(np.float64) + + # Appropriate slicing to extract n-dimensional edge along `axis` + edge_slice = tuple(slice(None) if i != axis else 0 + for (i, x) in enumerate(arr.shape)) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + + # Extract edge, reshape to original rank, and extend along `axis` + edge_pad = arr[edge_slice].reshape(pad_singleton).repeat(pad_amt, axis) + + # Linear ramp + slope = (end - edge_pad) / float(pad_amt) + ramp_arr = ramp_arr * slope + ramp_arr += edge_pad + _round_ifneeded(ramp_arr, arr.dtype) + + # Ramp values will most likely be float, cast them to the same type as arr + return np.concatenate((ramp_arr.astype(arr.dtype), arr), axis=axis) + + +def _append_ramp(arr, pad_amt, end, axis=-1): + """ + Append linear ramp along `axis`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to append. + end : scalar + Constal value to use. For best results should be of type `arr.dtype`; + if not `arr.dtype` will be cast to `arr.dtype`. + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` values appended along `axis`. The + appended region ramps linearly from the edge value to `end`. + + """ + if pad_amt == 0: + return arr + + # Generate shape for final concatenated array + padshape = tuple(x if i != axis else pad_amt + for (i, x) in enumerate(arr.shape)) + + # Generate an n-dimensional array incrementing along `axis` + ramp_arr = _arange_ndarray(arr, padshape, axis, + reverse=False).astype(np.float64) + + # Slice a chunk from the edge to calculate stats on + edge_slice = tuple(slice(None) if i != axis else -1 + for (i, x) in enumerate(arr.shape)) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + + # Extract edge, reshape to original rank, and extend along `axis` + edge_pad = arr[edge_slice].reshape(pad_singleton).repeat(pad_amt, axis) + + # Linear ramp + slope = (end - edge_pad) / float(pad_amt) + ramp_arr = ramp_arr * slope + ramp_arr += edge_pad + _round_ifneeded(ramp_arr, arr.dtype) + + # Ramp values will most likely be float, cast them to the same type as arr + return np.concatenate((arr, ramp_arr.astype(arr.dtype)), axis=axis) + + +def _prepend_max(arr, pad_amt, num, axis=-1): + """ + Prepend `pad_amt` maximum values along `axis`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to prepend. + num : int + Depth into `arr` along `axis` to calculate maximum. + Range: [1, `arr.shape[axis]`] or None (entire axis) + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` values appended along `axis`. The + prepended region is the maximum of the first `num` values along + `axis`. + + """ + if pad_amt == 0: + return arr + + # Equivalent to edge padding for single value, so do that instead + if num == 1: + return _prepend_edge(arr, pad_amt, axis) + + # Use entire array if `num` is too large + if num is not None: + if num >= arr.shape[axis]: + num = None + + # Slice a chunk from the edge to calculate stats on + max_slice = tuple(slice(None) if i != axis else slice(num) + for (i, x) in enumerate(arr.shape)) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + + # Extract slice, calculate max, reshape to add singleton dimension back + max_chunk = arr[max_slice].max(axis=axis).reshape(pad_singleton) + + # Concatenate `arr` with `max_chunk`, extended along `axis` by `pad_amt` + return np.concatenate((max_chunk.repeat(pad_amt, axis=axis), arr), + axis=axis) + + +def _append_max(arr, pad_amt, num, axis=-1): + """ + Pad one `axis` of `arr` with the maximum of the last `num` elements. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to append. + num : int + Depth into `arr` along `axis` to calculate maximum. + Range: [1, `arr.shape[axis]`] or None (entire axis) + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` values appended along `axis`. The + appended region is the maximum of the final `num` values along `axis`. + + """ + if pad_amt == 0: + return arr + + # Equivalent to edge padding for single value, so do that instead + if num == 1: + return _append_edge(arr, pad_amt, axis) + + # Use entire array if `num` is too large + if num is not None: + if num >= arr.shape[axis]: + num = None + + # Slice a chunk from the edge to calculate stats on + end = arr.shape[axis] - 1 + if num is not None: + max_slice = tuple( + slice(None) if i != axis else slice(end, end - num, -1) + for (i, x) in enumerate(arr.shape)) + else: + max_slice = tuple(slice(None) for x in arr.shape) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + + # Extract slice, calculate max, reshape to add singleton dimension back + max_chunk = arr[max_slice].max(axis=axis).reshape(pad_singleton) + + # Concatenate `arr` with `max_chunk`, extended along `axis` by `pad_amt` + return np.concatenate((arr, max_chunk.repeat(pad_amt, axis=axis)), + axis=axis) + + +def _prepend_mean(arr, pad_amt, num, axis=-1): + """ + Prepend `pad_amt` mean values along `axis`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to prepend. + num : int + Depth into `arr` along `axis` to calculate mean. + Range: [1, `arr.shape[axis]`] or None (entire axis) + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` values prepended along `axis`. The + prepended region is the mean of the first `num` values along `axis`. + + """ + if pad_amt == 0: + return arr + + # Equivalent to edge padding for single value, so do that instead + if num == 1: + return _prepend_edge(arr, pad_amt, axis) + + # Use entire array if `num` is too large + if num is not None: + if num >= arr.shape[axis]: + num = None + + # Slice a chunk from the edge to calculate stats on + mean_slice = tuple(slice(None) if i != axis else slice(num) + for (i, x) in enumerate(arr.shape)) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + + # Extract slice, calculate mean, reshape to add singleton dimension back + mean_chunk = arr[mean_slice].mean(axis).reshape(pad_singleton) + _round_ifneeded(mean_chunk, arr.dtype) + + # Concatenate `arr` with `mean_chunk`, extended along `axis` by `pad_amt` + return np.concatenate((mean_chunk.repeat(pad_amt, axis).astype(arr.dtype), + arr), axis=axis) + + +def _append_mean(arr, pad_amt, num, axis=-1): + """ + Append `pad_amt` mean values along `axis`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to append. + num : int + Depth into `arr` along `axis` to calculate mean. + Range: [1, `arr.shape[axis]`] or None (entire axis) + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` values appended along `axis`. The + appended region is the maximum of the final `num` values along `axis`. + + """ + if pad_amt == 0: + return arr + + # Equivalent to edge padding for single value, so do that instead + if num == 1: + return _append_edge(arr, pad_amt, axis) + + # Use entire array if `num` is too large + if num is not None: + if num >= arr.shape[axis]: + num = None + + # Slice a chunk from the edge to calculate stats on + end = arr.shape[axis] - 1 + if num is not None: + mean_slice = tuple( + slice(None) if i != axis else slice(end, end - num, -1) + for (i, x) in enumerate(arr.shape)) + else: + mean_slice = tuple(slice(None) for x in arr.shape) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + + # Extract slice, calculate mean, reshape to add singleton dimension back + mean_chunk = arr[mean_slice].mean(axis=axis).reshape(pad_singleton) + _round_ifneeded(mean_chunk, arr.dtype) + + # Concatenate `arr` with `mean_chunk`, extended along `axis` by `pad_amt` + return np.concatenate( + (arr, mean_chunk.repeat(pad_amt, axis).astype(arr.dtype)), axis=axis) + + +def _prepend_med(arr, pad_amt, num, axis=-1): + """ + Prepend `pad_amt` median values along `axis`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to prepend. + num : int + Depth into `arr` along `axis` to calculate median. + Range: [1, `arr.shape[axis]`] or None (entire axis) + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` values prepended along `axis`. The + prepended region is the median of the first `num` values along `axis`. + + """ + if pad_amt == 0: + return arr + + # Equivalent to edge padding for single value, so do that instead + if num == 1: + return _prepend_edge(arr, pad_amt, axis) + + # Use entire array if `num` is too large + if num is not None: + if num >= arr.shape[axis]: + num = None + + # Slice a chunk from the edge to calculate stats on + med_slice = tuple(slice(None) if i != axis else slice(num) + for (i, x) in enumerate(arr.shape)) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + + # Extract slice, calculate median, reshape to add singleton dimension back + med_chunk = np.median(arr[med_slice], axis=axis).reshape(pad_singleton) + _round_ifneeded(med_chunk, arr.dtype) + + # Concatenate `arr` with `med_chunk`, extended along `axis` by `pad_amt` + return np.concatenate( + (med_chunk.repeat(pad_amt, axis).astype(arr.dtype), arr), axis=axis) + + +def _append_med(arr, pad_amt, num, axis=-1): + """ + Append `pad_amt` median values along `axis`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to append. + num : int + Depth into `arr` along `axis` to calculate median. + Range: [1, `arr.shape[axis]`] or None (entire axis) + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` values appended along `axis`. The + appended region is the median of the final `num` values along `axis`. + + """ + if pad_amt == 0: + return arr + + # Equivalent to edge padding for single value, so do that instead + if num == 1: + return _append_edge(arr, pad_amt, axis) + + # Use entire array if `num` is too large + if num is not None: + if num >= arr.shape[axis]: + num = None + + # Slice a chunk from the edge to calculate stats on + end = arr.shape[axis] - 1 + if num is not None: + med_slice = tuple( + slice(None) if i != axis else slice(end, end - num, -1) + for (i, x) in enumerate(arr.shape)) + else: + med_slice = tuple(slice(None) for x in arr.shape) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + + # Extract slice, calculate median, reshape to add singleton dimension back + med_chunk = np.median(arr[med_slice], axis=axis).reshape(pad_singleton) + _round_ifneeded(med_chunk, arr.dtype) + + # Concatenate `arr` with `med_chunk`, extended along `axis` by `pad_amt` + return np.concatenate( + (arr, med_chunk.repeat(pad_amt, axis).astype(arr.dtype)), axis=axis) + + +def _prepend_min(arr, pad_amt, num, axis=-1): + """ + Prepend `pad_amt` minimum values along `axis`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to prepend. + num : int + Depth into `arr` along `axis` to calculate minimum. + Range: [1, `arr.shape[axis]`] or None (entire axis) + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` values prepended along `axis`. The + prepended region is the minimum of the first `num` values along + `axis`. + + """ + if pad_amt == 0: + return arr + + # Equivalent to edge padding for single value, so do that instead + if num == 1: + return _prepend_edge(arr, pad_amt, axis) + + # Use entire array if `num` is too large + if num is not None: + if num >= arr.shape[axis]: + num = None + + # Slice a chunk from the edge to calculate stats on + min_slice = tuple(slice(None) if i != axis else slice(num) + for (i, x) in enumerate(arr.shape)) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + + # Extract slice, calculate min, reshape to add singleton dimension back + min_chunk = arr[min_slice].min(axis=axis).reshape(pad_singleton) + + # Concatenate `arr` with `min_chunk`, extended along `axis` by `pad_amt` + return np.concatenate((min_chunk.repeat(pad_amt, axis=axis), arr), + axis=axis) + + +def _append_min(arr, pad_amt, num, axis=-1): + """ + Append `pad_amt` median values along `axis`. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : int + Amount of padding to append. + num : int + Depth into `arr` along `axis` to calculate minimum. + Range: [1, `arr.shape[axis]`] or None (entire axis) + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt` values appended along `axis`. The + appended region is the minimum of the final `num` values along `axis`. + + """ + if pad_amt == 0: + return arr + + # Equivalent to edge padding for single value, so do that instead + if num == 1: + return _append_edge(arr, pad_amt, axis) + + # Use entire array if `num` is too large + if num is not None: + if num >= arr.shape[axis]: + num = None + + # Slice a chunk from the edge to calculate stats on + end = arr.shape[axis] - 1 + if num is not None: + min_slice = tuple( + slice(None) if i != axis else slice(end, end - num, -1) + for (i, x) in enumerate(arr.shape)) + else: + min_slice = tuple(slice(None) for x in arr.shape) + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + + # Extract slice, calculate min, reshape to add singleton dimension back + min_chunk = arr[min_slice].min(axis=axis).reshape(pad_singleton) + + # Concatenate `arr` with `min_chunk`, extended along `axis` by `pad_amt` + return np.concatenate((arr, min_chunk.repeat(pad_amt, axis=axis)), + axis=axis) + + +def _pad_ref(arr, pad_amt, method, axis=-1): + """ + Pad `axis` of `arr` by reflection. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : tuple of ints, length 2 + Padding to (prepend, append) along `axis`. + method : str + Controls method of reflection; options are 'even' or 'odd'. + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt[0]` values prepended and `pad_amt[1]` + values appended along `axis`. Both regions are padded with reflected + values from the original array. + + Notes + ----- + This algorithm does not pad with repetition, i.e. the edges are not + repeated in the reflection. For that behavior, use `method='symmetric'`. + + The modes 'reflect', 'symmetric', and 'wrap' must be padded with a + single function, lest the indexing tricks in non-integer multiples of the + original shape would violate repetition in the final iteration. + + """ + # Implicit booleanness to test for zero (or None) in any scalar type + if pad_amt[0] == 0 and pad_amt[1] == 0: + return arr + + ########################################################################## + # Prepended region + + # Slice off a reverse indexed chunk from near edge to pad `arr` before + ref_slice = tuple(slice(None) if i != axis else slice(pad_amt[0], 0, -1) + for (i, x) in enumerate(arr.shape)) + + ref_chunk1 = arr[ref_slice] + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + if pad_amt[0] == 1: + ref_chunk1 = ref_chunk1.reshape(pad_singleton) + + # Memory/computationally more expensive, only do this if `method='odd'` + if 'odd' in method and pad_amt[0] > 0: + edge_slice1 = tuple(slice(None) if i != axis else 0 + for (i, x) in enumerate(arr.shape)) + edge_chunk = arr[edge_slice1].reshape(pad_singleton) + ref_chunk1 = 2 * edge_chunk - ref_chunk1 + del edge_chunk + + ########################################################################## + # Appended region + + # Slice off a reverse indexed chunk from far edge to pad `arr` after + start = arr.shape[axis] - pad_amt[1] - 1 + end = arr.shape[axis] - 1 + ref_slice = tuple(slice(None) if i != axis else slice(start, end) + for (i, x) in enumerate(arr.shape)) + rev_idx = tuple(slice(None) if i != axis else slice(None, None, -1) + for (i, x) in enumerate(arr.shape)) + ref_chunk2 = arr[ref_slice][rev_idx] + + if pad_amt[1] == 1: + ref_chunk2 = ref_chunk2.reshape(pad_singleton) + + if 'odd' in method: + edge_slice2 = tuple(slice(None) if i != axis else -1 + for (i, x) in enumerate(arr.shape)) + edge_chunk = arr[edge_slice2].reshape(pad_singleton) + ref_chunk2 = 2 * edge_chunk - ref_chunk2 + del edge_chunk + + # Concatenate `arr` with both chunks, extending along `axis` + return np.concatenate((ref_chunk1, arr, ref_chunk2), axis=axis) + + +def _pad_sym(arr, pad_amt, method, axis=-1): + """ + Pad `axis` of `arr` by symmetry. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : tuple of ints, length 2 + Padding to (prepend, append) along `axis`. + method : str + Controls method of symmetry; options are 'even' or 'odd'. + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt[0]` values prepended and `pad_amt[1]` + values appended along `axis`. Both regions are padded with symmetric + values from the original array. + + Notes + ----- + This algorithm DOES pad with repetition, i.e. the edges are repeated. + For a method that does not repeat edges, use `method='reflect'`. + + The modes 'reflect', 'symmetric', and 'wrap' must be padded with a + single function, lest the indexing tricks in non-integer multiples of the + original shape would violate repetition in the final iteration. + + """ + # Implicit booleanness to test for zero (or None) in any scalar type + if pad_amt[0] == 0 and pad_amt[1] == 0: + return arr + + ########################################################################## + # Prepended region + + # Slice off a reverse indexed chunk from near edge to pad `arr` before + sym_slice = tuple(slice(None) if i != axis else slice(0, pad_amt[0]) + for (i, x) in enumerate(arr.shape)) + rev_idx = tuple(slice(None) if i != axis else slice(None, None, -1) + for (i, x) in enumerate(arr.shape)) + sym_chunk1 = arr[sym_slice][rev_idx] + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + if pad_amt[0] == 1: + sym_chunk1 = sym_chunk1.reshape(pad_singleton) + + # Memory/computationally more expensive, only do this if `method='odd'` + if 'odd' in method and pad_amt[0] > 0: + edge_slice1 = tuple(slice(None) if i != axis else 0 + for (i, x) in enumerate(arr.shape)) + edge_chunk = arr[edge_slice1].reshape(pad_singleton) + sym_chunk1 = 2 * edge_chunk - sym_chunk1 + del edge_chunk + + ########################################################################## + # Appended region + + # Slice off a reverse indexed chunk from far edge to pad `arr` after + start = arr.shape[axis] - pad_amt[1] + end = arr.shape[axis] + sym_slice = tuple(slice(None) if i != axis else slice(start, end) + for (i, x) in enumerate(arr.shape)) + sym_chunk2 = arr[sym_slice][rev_idx] + + if pad_amt[1] == 1: + sym_chunk2 = sym_chunk2.reshape(pad_singleton) + + if 'odd' in method: + edge_slice2 = tuple(slice(None) if i != axis else -1 + for (i, x) in enumerate(arr.shape)) + edge_chunk = arr[edge_slice2].reshape(pad_singleton) + sym_chunk2 = 2 * edge_chunk - sym_chunk2 + del edge_chunk + + # Concatenate `arr` with both chunks, extending along `axis` + return np.concatenate((sym_chunk1, arr, sym_chunk2), axis=axis) + + +def _pad_wrap(arr, pad_amt, axis=-1): + """ + Pad `axis` of `arr` via wrapping. + + Parameters + ---------- + arr : ndarray + Input array of arbitrary shape. + pad_amt : tuple of ints, length 2 + Padding to (prepend, append) along `axis`. + axis : int + Axis along which to pad `arr`. + + Returns + ------- + padarr : ndarray + Output array, with `pad_amt[0]` values prepended and `pad_amt[1]` + values appended along `axis`. Both regions are padded wrapped values + from the opposite end of `axis`. + + Notes + ----- + This method of padding is also known as 'tile' or 'tiling'. + + The modes 'reflect', 'symmetric', and 'wrap' must be padded with a + single function, lest the indexing tricks in non-integer multiples of the + original shape would violate repetition in the final iteration. + + """ + # Implicit booleanness to test for zero (or None) in any scalar type + if pad_amt[0] == 0 and pad_amt[1] == 0: + return arr + + ########################################################################## + # Prepended region + + # Slice off a reverse indexed chunk from near edge to pad `arr` before + start = arr.shape[axis] - pad_amt[0] + end = arr.shape[axis] + wrap_slice = tuple(slice(None) if i != axis else slice(start, end) + for (i, x) in enumerate(arr.shape)) + wrap_chunk1 = arr[wrap_slice] + + # Shape to restore singleton dimension after slicing + pad_singleton = tuple(x if i != axis else 1 + for (i, x) in enumerate(arr.shape)) + if pad_amt[0] == 1: + wrap_chunk1 = wrap_chunk1.reshape(pad_singleton) + + ########################################################################## + # Appended region + + # Slice off a reverse indexed chunk from far edge to pad `arr` after + wrap_slice = tuple(slice(None) if i != axis else slice(0, pad_amt[1]) + for (i, x) in enumerate(arr.shape)) + wrap_chunk2 = arr[wrap_slice] + + if pad_amt[1] == 1: + wrap_chunk2 = wrap_chunk2.reshape(pad_singleton) + + # Concatenate `arr` with both chunks, extending along `axis` + return np.concatenate((wrap_chunk1, arr, wrap_chunk2), axis=axis) + + +def _normalize_shape(narray, shape): + """ + Private function which does some checks and normalizes the possibly + much simpler representations of 'pad_width', 'stat_length', + 'constant_values', 'end_values'. + + Parameters + ---------- + narray : ndarray + Input ndarray + shape : {sequence, int}, optional + The width of padding (pad_width) or the number of elements on the + edge of the narray used for statistics (stat_length). + ((before_1, after_1), ... (before_N, after_N)) unique number of + elements for each axis where `N` is rank of `narray`. + ((before, after),) yields same before and after constants for each + axis. + (constant,) or int is a shortcut for before = after = constant for + all axes. + + Returns + ------- + _normalize_shape : tuple of tuples + int => ((int, int), (int, int), ...) + [[int1, int2], [int3, int4], ...] => ((int1, int2), (int3, int4), ...) + ((int1, int2), (int3, int4), ...) => no change + [[int1, int2], ] => ((int1, int2), (int1, int2), ...) + ((int1, int2), ) => ((int1, int2), (int1, int2), ...) + [[int , ], ] => ((int, int), (int, int), ...) + ((int , ), ) => ((int, int), (int, int), ...) + + """ + normshp = None + shapelen = len(np.shape(narray)) + if (isinstance(shape, int)) or shape is None: + normshp = ((shape, shape), ) * shapelen + elif (isinstance(shape, (tuple, list)) + and isinstance(shape[0], (tuple, list)) + and len(shape) == shapelen): + normshp = shape + for i in normshp: + if len(i) != 2: + fmt = "Unable to create correctly shaped tuple from %s" + raise ValueError(fmt % (normshp,)) + elif (isinstance(shape, (tuple, list)) + and isinstance(shape[0], integer_types + (float,)) + and len(shape) == 1): + normshp = ((shape[0], shape[0]), ) * shapelen + elif (isinstance(shape, (tuple, list)) + and isinstance(shape[0], integer_types + (float,)) + and len(shape) == 2): + normshp = (shape, ) * shapelen + if normshp is None: + fmt = "Unable to create correctly shaped tuple from %s" + raise ValueError(fmt % (shape,)) + return normshp + + +def _validate_lengths(narray, number_elements): + """ + Private function which does some checks and reformats pad_width and + stat_length using _normalize_shape. + + Parameters + ---------- + narray : ndarray + Input ndarray + number_elements : {sequence, int}, optional + The width of padding (pad_width) or the number of elements on the edge + of the narray used for statistics (stat_length). + ((before_1, after_1), ... (before_N, after_N)) unique number of + elements for each axis. + ((before, after),) yields same before and after constants for each + axis. + (constant,) or int is a shortcut for before = after = constant for all + axes. + + Returns + ------- + _validate_lengths : tuple of tuples + int => ((int, int), (int, int), ...) + [[int1, int2], [int3, int4], ...] => ((int1, int2), (int3, int4), ...) + ((int1, int2), (int3, int4), ...) => no change + [[int1, int2], ] => ((int1, int2), (int1, int2), ...) + ((int1, int2), ) => ((int1, int2), (int1, int2), ...) + [[int , ], ] => ((int, int), (int, int), ...) + ((int , ), ) => ((int, int), (int, int), ...) + + """ + normshp = _normalize_shape(narray, number_elements) + for i in normshp: + chk = [1 if x is None else x for x in i] + chk = [1 if x >= 0 else -1 for x in chk] + if (chk[0] < 0) or (chk[1] < 0): + fmt = "%s cannot contain negative values." + raise ValueError(fmt % (number_elements,)) + return normshp + + +############################################################################### +# Public functions + + +def pad(array, pad_width, mode=None, **kwargs): + """ + Pads an array. + + Parameters + ---------- + array : array_like of rank N + Input array + pad_width : {sequence, int} + Number of values padded to the edges of each axis. + ((before_1, after_1), ... (before_N, after_N)) unique pad widths + for each axis. + ((before, after),) yields same before and after pad for each axis. + (pad,) or int is a shortcut for before = after = pad width for all + axes. + mode : {str, function} + One of the following string values or a user supplied function. + + 'constant' Pads with a constant value. + 'edge' Pads with the edge values of array. + 'linear_ramp' Pads with the linear ramp between end_value and the + array edge value. + 'maximum' Pads with the maximum value of all or part of the + vector along each axis. + 'mean' Pads with the mean value of all or part of the + vector along each axis. + 'median' Pads with the median value of all or part of the + vector along each axis. + 'minimum' Pads with the minimum value of all or part of the + vector along each axis. + 'reflect' Pads with the reflection of the vector mirrored on + the first and last values of the vector along each + axis. + 'symmetric' Pads with the reflection of the vector mirrored + along the edge of the array. + 'wrap' Pads with the wrap of the vector along the axis. + The first values are used to pad the end and the + end values are used to pad the beginning. + Padding function, see Notes. + stat_length : {sequence, int}, optional + Used in 'maximum', 'mean', 'median', and 'minimum'. Number of + values at edge of each axis used to calculate the statistic value. + + ((before_1, after_1), ... (before_N, after_N)) unique statistic + lengths for each axis. + + ((before, after),) yields same before and after statistic lengths + for each axis. + + (stat_length,) or int is a shortcut for before = after = statistic + length for all axes. + + Default is ``None``, to use the entire axis. + constant_values : {sequence, int}, optional + Used in 'constant'. The values to set the padded values for each + axis. + + ((before_1, after_1), ... (before_N, after_N)) unique pad constants + for each axis. + + ((before, after),) yields same before and after constants for each + axis. + + (constant,) or int is a shortcut for before = after = constant for + all axes. + + Default is 0. + end_values : {sequence, int}, optional + Used in 'linear_ramp'. The values used for the ending value of the + linear_ramp and that will form the edge of the padded array. + + ((before_1, after_1), ... (before_N, after_N)) unique end values + for each axis. + + ((before, after),) yields same before and after end values for each + axis. + + (constant,) or int is a shortcut for before = after = end value for + all axes. + + Default is 0. + reflect_type : str {'even', 'odd'}, optional + Used in 'reflect', and 'symmetric'. The 'even' style is the + default with an unaltered reflection around the edge value. For + the 'odd' style, the extented part of the array is created by + subtracting the reflected values from two times the edge value. + + Returns + ------- + pad : ndarray + Padded array of rank equal to `array` with shape increased + according to `pad_width`. + + Notes + ----- + For an array with rank greater than 1, some of the padding of later + axes is calculated from padding of previous axes. This is easiest to + think about with a rank 2 array where the corners of the padded array + are calculated by using padded values from the first axis. + + The padding function, if used, should return a rank 1 array equal in + length to the vector argument with padded values replaced. It has the + following signature: + + padding_func(vector, iaxis_pad_width, iaxis, **kwargs) + + where + + vector : ndarray + A rank 1 array already padded with zeros. Padded values are + vector[:pad_tuple[0]] and vector[-pad_tuple[1]:]. + iaxis_pad_width : tuple + A 2-tuple of ints, iaxis_pad_width[0] represents the number of + values padded at the beginning of vector where + iaxis_pad_width[1] represents the number of values padded at + the end of vector. + iaxis : int + The axis currently being calculated. + kwargs : misc + Any keyword arguments the function requires. + + Examples + -------- + >>> a = [1, 2, 3, 4, 5] + >>> np.lib.pad(a, (2,3), 'constant', constant_values=(4,6)) + array([4, 4, 1, 2, 3, 4, 5, 6, 6, 6]) + + >>> np.lib.pad(a, (2,3), 'edge') + array([1, 1, 1, 2, 3, 4, 5, 5, 5, 5]) + + >>> np.lib.pad(a, (2,3), 'linear_ramp', end_values=(5,-4)) + array([ 5, 3, 1, 2, 3, 4, 5, 2, -1, -4]) + + >>> np.lib.pad(a, (2,), 'maximum') + array([5, 5, 1, 2, 3, 4, 5, 5, 5]) + + >>> np.lib.pad(a, (2,), 'mean') + array([3, 3, 1, 2, 3, 4, 5, 3, 3]) + + >>> np.lib.pad(a, (2,), 'median') + array([3, 3, 1, 2, 3, 4, 5, 3, 3]) + + >>> a = [[1,2], [3,4]] + >>> np.lib.pad(a, ((3, 2), (2, 3)), 'minimum') + array([[1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1], + [3, 3, 3, 4, 3, 3, 3], + [1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1]]) + + >>> a = [1, 2, 3, 4, 5] + >>> np.lib.pad(a, (2,3), 'reflect') + array([3, 2, 1, 2, 3, 4, 5, 4, 3, 2]) + + >>> np.lib.pad(a, (2,3), 'reflect', reflect_type='odd') + array([-1, 0, 1, 2, 3, 4, 5, 6, 7, 8]) + + >>> np.lib.pad(a, (2,3), 'symmetric') + array([2, 1, 1, 2, 3, 4, 5, 5, 4, 3]) + + >>> np.lib.pad(a, (2,3), 'symmetric', reflect_type='odd') + array([0, 1, 1, 2, 3, 4, 5, 5, 6, 7]) + + >>> np.lib.pad(a, (2,3), 'wrap') + array([4, 5, 1, 2, 3, 4, 5, 1, 2, 3]) + + >>> def padwithtens(vector, pad_width, iaxis, kwargs): + ... vector[:pad_width[0]] = 10 + ... vector[-pad_width[1]:] = 10 + ... return vector + + >>> a = np.arange(6) + >>> a = a.reshape((2,3)) + + >>> np.lib.pad(a, 2, padwithtens) + array([[10, 10, 10, 10, 10, 10, 10], + [10, 10, 10, 10, 10, 10, 10], + [10, 10, 0, 1, 2, 10, 10], + [10, 10, 3, 4, 5, 10, 10], + [10, 10, 10, 10, 10, 10, 10], + [10, 10, 10, 10, 10, 10, 10]]) + + """ + + narray = np.array(array) + pad_width = _validate_lengths(narray, pad_width) + + allowedkwargs = { + 'constant': ['constant_values'], + 'edge': [], + 'linear_ramp': ['end_values'], + 'maximum': ['stat_length'], + 'mean': ['stat_length'], + 'median': ['stat_length'], + 'minimum': ['stat_length'], + 'reflect': ['reflect_type'], + 'symmetric': ['reflect_type'], + 'wrap': []} + + kwdefaults = { + 'stat_length': None, + 'constant_values': 0, + 'end_values': 0, + 'reflect_type': 'even'} + + if isinstance(mode, str): + # Make sure have allowed kwargs appropriate for mode + for key in kwargs: + if key not in allowedkwargs[mode]: + raise ValueError('%s keyword not in allowed keywords %s' % + (key, allowedkwargs[mode])) + + # Set kwarg defaults + for kw in allowedkwargs[mode]: + kwargs.setdefault(kw, kwdefaults[kw]) + + # Need to only normalize particular keywords. + for i in kwargs: + if i == 'stat_length': + kwargs[i] = _validate_lengths(narray, kwargs[i]) + if i in ['end_values', 'constant_values']: + kwargs[i] = _normalize_shape(narray, kwargs[i]) + elif mode is None: + raise ValueError('Keyword "mode" must be a function or one of %s.' % + (list(allowedkwargs.keys()),)) + else: + # Drop back to old, slower np.apply_along_axis mode for user-supplied + # vector function + function = mode + + # Create a new padded array + rank = list(range(len(narray.shape))) + total_dim_increase = [np.sum(pad_width[i]) for i in rank] + offset_slices = [slice(pad_width[i][0], + pad_width[i][0] + narray.shape[i]) + for i in rank] + new_shape = np.array(narray.shape) + total_dim_increase + newmat = np.zeros(new_shape).astype(narray.dtype) + + # Insert the original array into the padded array + newmat[offset_slices] = narray + + # This is the core of pad ... + for iaxis in rank: + np.apply_along_axis(function, + iaxis, + newmat, + pad_width[iaxis], + iaxis, + kwargs) + return newmat + + # If we get here, use new padding method + newmat = narray.copy() + + # API preserved, but completely new algorithm which pads by building the + # entire block to pad before/after `arr` with in one step, for each axis. + if mode == 'constant': + for axis, ((pad_before, pad_after), (before_val, after_val)) \ + in enumerate(zip(pad_width, kwargs['constant_values'])): + newmat = _prepend_const(newmat, pad_before, before_val, axis) + newmat = _append_const(newmat, pad_after, after_val, axis) + + elif mode == 'edge': + for axis, (pad_before, pad_after) in enumerate(pad_width): + newmat = _prepend_edge(newmat, pad_before, axis) + newmat = _append_edge(newmat, pad_after, axis) + + elif mode == 'linear_ramp': + for axis, ((pad_before, pad_after), (before_val, after_val)) \ + in enumerate(zip(pad_width, kwargs['end_values'])): + newmat = _prepend_ramp(newmat, pad_before, before_val, axis) + newmat = _append_ramp(newmat, pad_after, after_val, axis) + + elif mode == 'maximum': + for axis, ((pad_before, pad_after), (chunk_before, chunk_after)) \ + in enumerate(zip(pad_width, kwargs['stat_length'])): + newmat = _prepend_max(newmat, pad_before, chunk_before, axis) + newmat = _append_max(newmat, pad_after, chunk_after, axis) + + elif mode == 'mean': + for axis, ((pad_before, pad_after), (chunk_before, chunk_after)) \ + in enumerate(zip(pad_width, kwargs['stat_length'])): + newmat = _prepend_mean(newmat, pad_before, chunk_before, axis) + newmat = _append_mean(newmat, pad_after, chunk_after, axis) + + elif mode == 'median': + for axis, ((pad_before, pad_after), (chunk_before, chunk_after)) \ + in enumerate(zip(pad_width, kwargs['stat_length'])): + newmat = _prepend_med(newmat, pad_before, chunk_before, axis) + newmat = _append_med(newmat, pad_after, chunk_after, axis) + + elif mode == 'minimum': + for axis, ((pad_before, pad_after), (chunk_before, chunk_after)) \ + in enumerate(zip(pad_width, kwargs['stat_length'])): + newmat = _prepend_min(newmat, pad_before, chunk_before, axis) + newmat = _append_min(newmat, pad_after, chunk_after, axis) + + elif mode == 'reflect': + for axis, (pad_before, pad_after) in enumerate(pad_width): + # Recursive padding along any axis where `pad_amt` is too large + # for indexing tricks. We can only safely pad the original axis + # length, to keep the period of the reflections consistent. + if ((pad_before > 0) or + (pad_after > 0)) and newmat.shape[axis] == 1: + # Extending singleton dimension for 'reflect' is legacy + # behavior; it really should raise an error. + newmat = _prepend_edge(newmat, pad_before, axis) + newmat = _append_edge(newmat, pad_after, axis) + continue + + method = kwargs['reflect_type'] + safe_pad = newmat.shape[axis] - 1 + while ((pad_before > safe_pad) or (pad_after > safe_pad)): + offset = 0 + pad_iter_b = min(safe_pad, + safe_pad * (pad_before // safe_pad)) + pad_iter_a = min(safe_pad, safe_pad * (pad_after // safe_pad)) + newmat = _pad_ref(newmat, (pad_iter_b, + pad_iter_a), method, axis) + pad_before -= pad_iter_b + pad_after -= pad_iter_a + if pad_iter_b > 0: + offset += 1 + if pad_iter_a > 0: + offset += 1 + safe_pad += pad_iter_b + pad_iter_a + newmat = _pad_ref(newmat, (pad_before, pad_after), method, axis) + + elif mode == 'symmetric': + for axis, (pad_before, pad_after) in enumerate(pad_width): + # Recursive padding along any axis where `pad_amt` is too large + # for indexing tricks. We can only safely pad the original axis + # length, to keep the period of the reflections consistent. + method = kwargs['reflect_type'] + safe_pad = newmat.shape[axis] + while ((pad_before > safe_pad) or + (pad_after > safe_pad)): + pad_iter_b = min(safe_pad, + safe_pad * (pad_before // safe_pad)) + pad_iter_a = min(safe_pad, safe_pad * (pad_after // safe_pad)) + newmat = _pad_sym(newmat, (pad_iter_b, + pad_iter_a), method, axis) + pad_before -= pad_iter_b + pad_after -= pad_iter_a + safe_pad += pad_iter_b + pad_iter_a + newmat = _pad_sym(newmat, (pad_before, pad_after), method, axis) + + elif mode == 'wrap': + for axis, (pad_before, pad_after) in enumerate(pad_width): + # Recursive padding along any axis where `pad_amt` is too large + # for indexing tricks. We can only safely pad the original axis + # length, to keep the period of the reflections consistent. + safe_pad = newmat.shape[axis] + while ((pad_before > safe_pad) or + (pad_after > safe_pad)): + pad_iter_b = min(safe_pad, + safe_pad * (pad_before // safe_pad)) + pad_iter_a = min(safe_pad, safe_pad * (pad_after // safe_pad)) + newmat = _pad_wrap(newmat, (pad_iter_b, pad_iter_a), axis) + + pad_before -= pad_iter_b + pad_after -= pad_iter_a + safe_pad += pad_iter_b + pad_iter_a + newmat = _pad_wrap(newmat, (pad_before, pad_after), axis) + + return newmat diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 9f804406..3f85cc2b 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -1,11 +1,9 @@ from __future__ import division import numpy as np +from warnings import warn __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') + 'img_as_bool', 'dtype_limits'] dtype_range = {np.bool_: (False, True), np.bool8: (False, True), @@ -28,6 +26,23 @@ if np.__version__ >= "1.6.0": _supported_types += (np.float16, ) +def dtype_limits(image, clip_negative=True): + """Return intensity limits, i.e. (min, max) tuple, of the image's dtype. + + Parameters + ---------- + image : ndarray + Input image. + clip_negative : bool + If True, clip the negative range (i.e. return 0 for min intensity) + even if the image dtype allows negative values. + """ + imin, imax = dtype_range[image.dtype.type] + if clip_negative: + imin = 0 + return imin, imax + + def convert(image, dtype, force_copy=False, uniform=False): """ Convert an image to the requested data-type. @@ -84,12 +99,12 @@ def convert(image, dtype, force_copy=False, uniform=False): raise ValueError("can not convert %s to %s." % (dtypeobj_in, dtypeobj)) def sign_loss(): - log.warn("Possible sign loss when converting negative image of type " - "%s to positive image of type %s." % (dtypeobj_in, dtypeobj)) + warn("Possible sign loss when converting negative image of type " + "%s to positive image of type %s." % (dtypeobj_in, dtypeobj)) def prec_loss(): - log.warn("Possible precision loss when converting from " - "%s to %s" % (dtypeobj_in, dtypeobj)) + warn("Possible precision loss when converting from " + "%s to %s" % (dtypeobj_in, dtypeobj)) def _dtype(itemsize, *dtypes): # Return first of `dtypes` with itemsize greater than `itemsize` @@ -159,7 +174,7 @@ def convert(image, dtype, force_copy=False, uniform=False): if kind_in == 'b': # from binary image, to float and to integer - result = dtype(image) + result = image.astype(dtype) if kind != 'f': result *= dtype(dtype_range[dtype][1]) return result @@ -178,7 +193,7 @@ def convert(image, dtype, force_copy=False, uniform=False): # floating point -> floating point if itemsize_in > itemsize: prec_loss() - return dtype(image) + return image.astype(dtype) # floating point -> integer prec_loss() @@ -201,7 +216,7 @@ def convert(image, dtype, force_copy=False, uniform=False): image *= (imax - imin + 1.0) / 2.0 np.floor(image, out=image) np.clip(image, imin, imax, out=image) - return dtype(image) + return image.astype(dtype) if kind == 'f': # integer -> floating point @@ -219,7 +234,7 @@ def convert(image, dtype, force_copy=False, uniform=False): image *= 2.0 image += 1.0 image /= imax_in - imin_in - return dtype(image) + return image.astype(dtype) if kind_in == 'u': if kind == 'i': @@ -245,7 +260,7 @@ def convert(image, dtype, force_copy=False, uniform=False): image -= imin_in image = _scale(image, 8 * itemsize_in, 8 * itemsize, copy=False) image += imin - return dtype(image) + return image.astype(dtype) def img_as_float(image, force_copy=False): diff --git a/skimage/util/montage.py b/skimage/util/montage.py index 587c0939..805b78a8 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -6,11 +6,11 @@ from .. import exposure EPSILON = 1e-6 -def montage2d(arr_in, fill='mean', rescale_intensity=False): +def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=None): """Create a 2-dimensional 'montage' from a 3-dimensional input array representing an ensemble of equally shaped 2-dimensional images. - For example, montage2d(arr_in, fill) with the following `arr_in` + For example, ``montage2d(arr_in, fill)`` with the following `arr_in` +---+---+---+ | 1 | 2 | 3 | @@ -31,13 +31,14 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): arr_in: ndarray, shape=[n_images, height, width] 3-dimensional input array representing an ensemble of n_images of equal shape (i.e. [height, width]). - fill: float or 'mean', optional How to fill the 2-dimensional output array when sqrt(n_images) is not an integer. If 'mean' is chosen, then fill = arr_in.mean(). - rescale_intensity: bool, optional Whether to rescale the intensity of each image to [0, 1]. + grid_shape: tuple, optional + The desired grid shape for the montage (tiles_y, tiles_x). + The default aspect ratio is square. Returns ------- @@ -50,7 +51,7 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): >>> import numpy as np >>> from skimage.util.montage import montage2d >>> arr_in = np.arange(3 * 2 * 2).reshape(3, 2, 2) - >>> print arr_in # doctest: +NORMALIZE_WHITESPACE + >>> print(arr_in) # doctest: +NORMALIZE_WHITESPACE [[[ 0 1] [ 2 3]] [[ 4 5] @@ -58,20 +59,27 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): [[ 8 9] [10 11]]] >>> arr_out = montage2d(arr_in) - >>> print arr_out.shape + >>> print(arr_out.shape) (4, 4) - >>> print arr_out + >>> print(arr_out) [[ 0. 1. 4. 5. ] [ 2. 3. 6. 7. ] [ 8. 9. 5.5 5.5] [ 10. 11. 5.5 5.5]] - >>> print arr_in.mean() + >>> print(arr_in.mean()) 5.5 + >>> arr_out_nonsquare = montage2d(arr_in, grid_shape=(3, 4)) + >>> print(arr_out_nonsquare) + [[ 0. 1. 4. 5. ] + [ 2. 3. 6. 7. ] + [ 8. 9. 10. 11. ]] + >>> print(arr_out_nonsquare.shape) + (3, 4) """ assert arr_in.ndim == 3 n_images, height, width = arr_in.shape - + arr_in = arr_in.copy() # -- rescale intensity if necessary @@ -80,19 +88,22 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): arr_in[i] = exposure.rescale_intensity(arr_in[i]) # -- determine alpha - alpha = int(np.ceil(np.sqrt(n_images))) + if grid_shape: + alpha_y, alpha_x = grid_shape + else: + alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) # -- fill missing patches if fill == 'mean': fill = arr_in.mean() - n_missing = int((alpha**2.) - n_images) + n_missing = int((alpha_y * alpha_x) - n_images) missing = np.ones((n_missing, height, width), dtype=arr_in.dtype) * fill arr_out = np.vstack((arr_in, missing)) # -- reshape to 2d montage, step by step - arr_out = arr_out.reshape(alpha, alpha, height, width) + arr_out = arr_out.reshape(alpha_y, alpha_x, height, width) arr_out = arr_out.swapaxes(1, 2) - arr_out = arr_out.reshape(alpha * height, alpha * width) + arr_out = arr_out.reshape(alpha_y * height, alpha_x * width) return arr_out diff --git a/skimage/util/noise.py b/skimage/util/noise.py new file mode 100644 index 00000000..9283f537 --- /dev/null +++ b/skimage/util/noise.py @@ -0,0 +1,196 @@ +import numpy as np +from .dtype import img_as_float + + +__all__ = ['random_noise'] + + +def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): + """ + Function to add random noise of various types to a floating-point image. + + Parameters + ---------- + image : ndarray + Input image data. Will be converted to float. + mode : str + One of the following strings, selecting the type of noise to add: + + 'gaussian' Gaussian-distributed additive noise. + 'localvar' Gaussian-distributed additive noise, with specified + local variance at each point of `image` + 'poisson' Poisson-distributed noise generated from the data. + 'salt' Replaces random pixels with 1. + 'pepper' Replaces random pixels with 0. + 's&p' Replaces random pixels with 0 or 1. + 'speckle' Multiplicative noise using out = image + n*image, where + n is uniform noise with specified mean & variance. + seed : int + If provided, this will set the random seed before generating noise, + for valid pseudo-random comparisons. + clip : bool + If True (default), the output will be clipped after noise applied + for modes `'speckle'`, `'poisson'`, and `'gaussian'`. This is + needed to maintain the proper image data range. If False, clipping + is not applied, and the output may extend beyond the range [-1, 1]. + mean : float + Mean of random distribution. Used in 'gaussian' and 'speckle'. + Default : 0. + var : float + Variance of random distribution. Used in 'gaussian' and 'speckle'. + Note: variance = (standard deviation) ** 2. Default : 0.01 + local_vars : ndarray + Array of positive floats, same shape as `image`, defining the local + variance at every image point. Used in 'localvar'. + amount : float + Proportion of image pixels to replace with noise on range [0, 1]. + Used in 'salt', 'pepper', and 'salt & pepper'. Default : 0.05 + salt_vs_pepper : float + Proportion of salt vs. pepper noise for 's&p' on range [0, 1]. + Higher values represent more salt. Default : 0.5 (equal amounts) + + Returns + ------- + out : ndarray + Output floating-point image data on range [0, 1] or [-1, 1] if the + input `image` was unsigned or signed, respectively. + + Notes + ----- + Speckle, Poisson, Localvar, and Gaussian noise may generate noise outside + the valid image range. The default is to clip (not alias) these values, + but they may be preserved by setting `clip=False`. Note that in this case + the output may contain values outside the ranges [0, 1] or [-1, 1]. + Use this option with care. + + Because of the prevalence of exclusively positive floating-point images in + intermediate calculations, it is not possible to intuit if an input is + signed based on dtype alone. Instead, negative values are explicity + searched for. Only if found does this function assume signed input. + Unexpected results only occur in rare, poorly exposes cases (e.g. if all + values are above 50 percent gray in a signed `image`). In this event, + manually scaling the input to the positive domain will solve the problem. + + The Poisson distribution is only defined for positive integers. To apply + this noise type, the number of unique values in the image is found and + the next round power of two is used to scale up the floating-point result, + after which it is scaled back down to the floating-point image range. + + To generate Poisson noise against a signed image, the signed image is + temporarily converted to an unsigned image in the floating point domain, + Poisson noise is generated, then it is returned to the original range. + + """ + mode = mode.lower() + + # Detect if a signed image was input + if image.min() < 0: + low_clip = -1. + else: + low_clip = 0. + + image = img_as_float(image) + if seed is not None: + np.random.seed(seed=seed) + + allowedtypes = { + 'gaussian': 'gaussian_values', + 'localvar': 'localvar_values', + 'poisson': 'poisson_values', + 'salt': 'sp_values', + 'pepper': 'sp_values', + 's&p': 's&p_values', + 'speckle': 'gaussian_values'} + + kwdefaults = { + 'mean': 0., + 'var': 0.01, + 'amount': 0.05, + 'salt_vs_pepper': 0.5, + 'local_vars': np.zeros_like(image) + 0.01} + + allowedkwargs = { + 'gaussian_values': ['mean', 'var'], + 'localvar_values': ['local_vars'], + 'sp_values': ['amount'], + 's&p_values': ['amount', 'salt_vs_pepper'], + 'poisson_values': []} + + for key in kwargs: + if key not in allowedkwargs[allowedtypes[mode]]: + raise ValueError('%s keyword not in allowed keywords %s' % + (key, allowedkwargs[allowedtypes[mode]])) + + # Set kwarg defaults + for kw in allowedkwargs[allowedtypes[mode]]: + kwargs.setdefault(kw, kwdefaults[kw]) + + if mode == 'gaussian': + noise = np.random.normal(kwargs['mean'], kwargs['var'] ** 0.5, + image.shape) + out = image + noise + + elif mode == 'localvar': + # Ensure local variance input is correct + if (kwargs['local_vars'] <= 0).any(): + raise ValueError('All values of `local_vars` must be > 0.') + + # Safe shortcut usage broadcasts kwargs['local_vars'] as a ufunc + out = image + np.random.normal(0, kwargs['local_vars'] ** 0.5) + + elif mode == 'poisson': + # Determine unique values in image & calculate the next power of two + vals = len(np.unique(image)) + vals = 2 ** np.ceil(np.log2(vals)) + + # Ensure image is exclusively positive + if low_clip == -1.: + old_max = image.max() + image = (image + 1.) / (old_max + 1.) + + # Generating noise for each unique value in image. + out = np.random.poisson(image * vals) / float(vals) + + # Return image to original range if input was signed + if low_clip == -1.: + out = out * (old_max + 1.) - 1. + + elif mode == 'salt': + # Re-call function with mode='s&p' and p=1 (all salt noise) + out = random_noise(image, mode='s&p', seed=seed, + amount=kwargs['amount'], salt_vs_pepper=1.) + + elif mode == 'pepper': + # Re-call function with mode='s&p' and p=1 (all pepper noise) + out = random_noise(image, mode='s&p', seed=seed, + amount=kwargs['amount'], salt_vs_pepper=0.) + + elif mode == 's&p': + # This mode makes no effort to avoid repeat sampling. Thus, the + # exact number of replaced pixels is only approximate. + out = image.copy() + + # Salt mode + num_salt = np.ceil( + kwargs['amount'] * image.size * kwargs['salt_vs_pepper']) + coords = [np.random.randint(0, i - 1, int(num_salt)) + for i in image.shape] + out[coords] = 1 + + # Pepper mode + num_pepper = np.ceil( + kwargs['amount'] * image.size * (1. - kwargs['salt_vs_pepper'])) + coords = [np.random.randint(0, i - 1, int(num_pepper)) + for i in image.shape] + out[coords] = low_clip + + elif mode == 'speckle': + noise = np.random.normal(kwargs['mean'], kwargs['var'] ** 0.5, + image.shape) + out = image + image * noise + + # Clip back to original range, if necessary + if clip: + out = np.clip(out, low_clip, 1.0) + + return out diff --git a/skimage/util/shape.py b/skimage/util/shape.py index 0126d2e3..f91286c3 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -98,7 +98,7 @@ def view_as_blocks(arr_in, block_shape): return arr_out -def view_as_windows(arr_in, window_shape): +def view_as_windows(arr_in, window_shape, step=1): """Rolling window view of the input n-dimensional array. Windows are overlapping views of the input array, with adjacent windows @@ -108,10 +108,12 @@ def view_as_windows(arr_in, window_shape): ---------- arr_in: ndarray The n-dimensional input array. - window_shape: tuple Defines the shape of the elementary n-dimensional orthotope (better know as hyperrectangle [1]_) of the rolling window view. + step : int + Number of elements to skip when moving the window forward (by + default, move forward by one). Returns ------- @@ -206,26 +208,32 @@ def view_as_windows(arr_in, window_shape): # -- basic checks on arguments if not isinstance(arr_in, np.ndarray): - raise TypeError("'arr_in' must be a numpy ndarray") + raise TypeError("`arr_in` must be a numpy ndarray") if not isinstance(window_shape, tuple): - raise TypeError("'window_shape' must be a tuple") + raise TypeError("`window_shape` must be a tuple") if not (len(window_shape) == arr_in.ndim): - raise ValueError("'window_shape' is incompatible with 'arr_in.shape'") + raise ValueError("`window_shape` is incompatible with `arr_in.shape`") + + if step < 1: + raise ValueError("`step` must be >= 1") arr_shape = np.array(arr_in.shape) window_shape = np.array(window_shape, dtype=arr_shape.dtype) if ((arr_shape - window_shape) < 0).any(): - raise ValueError("'window_shape' is too large") + raise ValueError("`window_shape` is too large") if ((window_shape - 1) < 0).any(): - raise ValueError("'window_shape' is too small") + raise ValueError("`window_shape` is too small") # -- build rolling window view arr_in = np.ascontiguousarray(arr_in) - new_shape = tuple(arr_shape - window_shape + 1) + tuple(window_shape) - new_strides = arr_in.strides + arr_in.strides + new_shape = tuple((arr_shape - window_shape) // step + 1) + \ + tuple(window_shape) + + arr_strides = np.array(arr_in.strides) + new_strides = np.concatenate((arr_strides * step, arr_strides)) arr_out = as_strided(arr_in, shape=new_shape, strides=new_strides) diff --git a/skimage/util/tests/test_arraypad.py b/skimage/util/tests/test_arraypad.py new file mode 100644 index 00000000..008c3516 --- /dev/null +++ b/skimage/util/tests/test_arraypad.py @@ -0,0 +1,529 @@ +"""Tests for the pad functions. + +""" +from __future__ import division, absolute_import, print_function + +from numpy.testing import TestCase, run_module_suite, assert_array_equal +from numpy.testing import assert_raises, assert_array_almost_equal +import numpy as np +from skimage.util import pad + + +class TestStatistic(TestCase): + def test_check_mean_stat_length(self): + a = np.arange(100).astype('f') + a = pad(a, ((25, 20), ), 'mean', stat_length=((2, 3), )) + b = np.array([ + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, + + 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., 36., 37., 38., 39., + 40., 41., 42., 43., 44., 45., 46., 47., 48., 49., + 50., 51., 52., 53., 54., 55., 56., 57., 58., 59., + 60., 61., 62., 63., 64., 65., 66., 67., 68., 69., + 70., 71., 72., 73., 74., 75., 76., 77., 78., 79., + 80., 81., 82., 83., 84., 85., 86., 87., 88., 89., + 90., 91., 92., 93., 94., 95., 96., 97., 98., 99., + + 98., 98., 98., 98., 98., 98., 98., 98., 98., 98., + 98., 98., 98., 98., 98., 98., 98., 98., 98., 98.]) + assert_array_equal(a, b) + + def test_check_maximum_1(self): + a = np.arange(100) + a = pad(a, (25, 20), 'maximum') + b = np.array([ + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, + + 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, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99]) + assert_array_equal(a, b) + + def test_check_maximum_2(self): + a = np.arange(100) + 1 + a = pad(a, (25, 20), 'maximum') + b = np.array([ + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, + + 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, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, + 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, + 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, + 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, + + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100]) + assert_array_equal(a, b) + + def test_check_minimum_1(self): + a = np.arange(100) + a = pad(a, (25, 20), 'minimum') + b = 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, 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, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert_array_equal(a, b) + + def test_check_minimum_2(self): + a = np.arange(100) + 2 + a = pad(a, (25, 20), 'minimum') + b = np.array([ + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, + + 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, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, + 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, + 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, + 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, + + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) + assert_array_equal(a, b) + + def test_check_median(self): + a = np.arange(100).astype('f') + a = pad(a, (25, 20), 'median') + b = np.array([ + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, + + 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., 36., 37., 38., 39., + 40., 41., 42., 43., 44., 45., 46., 47., 48., 49., + 50., 51., 52., 53., 54., 55., 56., 57., 58., 59., + 60., 61., 62., 63., 64., 65., 66., 67., 68., 69., + 70., 71., 72., 73., 74., 75., 76., 77., 78., 79., + 80., 81., 82., 83., 84., 85., 86., 87., 88., 89., + 90., 91., 92., 93., 94., 95., 96., 97., 98., 99., + + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5]) + assert_array_equal(a, b) + + def test_check_median_01(self): + a = np.array([[3, 1, 4], [4, 5, 9], [9, 8, 2]]) + a = pad(a, 1, 'median') + b = np.array([ + [4, 4, 5, 4, 4], + + [3, 3, 1, 4, 3], + [5, 4, 5, 9, 5], + [8, 9, 8, 2, 8], + + [4, 4, 5, 4, 4]]) + assert_array_equal(a, b) + + def test_check_median_02(self): + a = np.array([[3, 1, 4], [4, 5, 9], [9, 8, 2]]) + a = pad(a.T, 1, 'median').T + b = np.array([ + [5, 4, 5, 4, 5], + + [3, 3, 1, 4, 3], + [5, 4, 5, 9, 5], + [8, 9, 8, 2, 8], + + [5, 4, 5, 4, 5]]) + assert_array_equal(a, b) + + def test_check_mean_shape_one(self): + a = [[4, 5, 6]] + a = pad(a, (5, 7), 'mean', stat_length=2) + b = np.array([ + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6]]) + assert_array_equal(a, b) + + def test_check_mean_2(self): + a = np.arange(100).astype('f') + a = pad(a, (25, 20), 'mean') + b = np.array([ + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, + + 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., 36., 37., 38., 39., + 40., 41., 42., 43., 44., 45., 46., 47., 48., 49., + 50., 51., 52., 53., 54., 55., 56., 57., 58., 59., + 60., 61., 62., 63., 64., 65., 66., 67., 68., 69., + 70., 71., 72., 73., 74., 75., 76., 77., 78., 79., + 80., 81., 82., 83., 84., 85., 86., 87., 88., 89., + 90., 91., 92., 93., 94., 95., 96., 97., 98., 99., + + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5]) + assert_array_equal(a, b) + + +class TestConstant(TestCase): + def test_check_constant(self): + a = np.arange(100) + a = pad(a, (25, 20), 'constant', constant_values=(10, 20)) + b = np.array([10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, + + 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, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20]) + assert_array_equal(a, b) + + +class TestLinearRamp(TestCase): + def test_check_simple(self): + a = np.arange(100).astype('f') + a = pad(a, (25, 20), 'linear_ramp', end_values=(4, 5)) + b = np.array([ + 4.00, 3.84, 3.68, 3.52, 3.36, 3.20, 3.04, 2.88, 2.72, 2.56, + 2.40, 2.24, 2.08, 1.92, 1.76, 1.60, 1.44, 1.28, 1.12, 0.96, + 0.80, 0.64, 0.48, 0.32, 0.16, + + 0.00, 1.00, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00, 9.00, + 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, + 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, + 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, + 40.0, 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, + 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, + 60.0, 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, + 70.0, 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, + 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, + 90.0, 91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, + + 94.3, 89.6, 84.9, 80.2, 75.5, 70.8, 66.1, 61.4, 56.7, 52.0, + 47.3, 42.6, 37.9, 33.2, 28.5, 23.8, 19.1, 14.4, 9.7, 5.]) + assert_array_almost_equal(a, b, decimal=5) + + +class TestReflect(TestCase): + def test_check_simple(self): + a = np.arange(100) + a = pad(a, (25, 20), 'reflect') + b = np.array([ + 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, + 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, + 5, 4, 3, 2, 1, + + 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, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + + 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, + 88, 87, 86, 85, 84, 83, 82, 81, 80, 79]) + assert_array_equal(a, b) + + def test_check_large_pad(self): + a = [[4, 5, 6], [6, 7, 8]] + a = pad(a, (5, 7), 'reflect') + b = np.array([ + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5]]) + assert_array_equal(a, b) + + def test_check_shape(self): + a = [[4, 5, 6]] + a = pad(a, (5, 7), 'reflect') + b = np.array([ + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5]]) + assert_array_equal(a, b) + + def test_check_01(self): + a = pad([1, 2, 3], 2, 'reflect') + b = np.array([3, 2, 1, 2, 3, 2, 1]) + assert_array_equal(a, b) + + def test_check_02(self): + a = pad([1, 2, 3], 3, 'reflect') + b = np.array([2, 3, 2, 1, 2, 3, 2, 1, 2]) + assert_array_equal(a, b) + + def test_check_03(self): + a = pad([1, 2, 3], 4, 'reflect') + b = np.array([1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3]) + assert_array_equal(a, b) + + +class TestWrap(TestCase): + def test_check_simple(self): + a = np.arange(100) + a = pad(a, (25, 20), 'wrap') + b = np.array([ + 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, + 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, + 95, 96, 97, 98, 99, + + 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, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + assert_array_equal(a, b) + + def test_check_large_pad(self): + a = np.arange(12) + a = np.reshape(a, (3, 4)) + a = pad(a, (10, 12), 'wrap') + b = np.array([ + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + + [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + + [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11]]) + assert_array_equal(a, b) + + def test_check_01(self): + a = pad([1, 2, 3], 3, 'wrap') + b = np.array([1, 2, 3, 1, 2, 3, 1, 2, 3]) + assert_array_equal(a, b) + + def test_check_02(self): + a = pad([1, 2, 3], 4, 'wrap') + b = np.array([3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1]) + assert_array_equal(a, b) + + +class TestStatLen(TestCase): + def test_check_simple(self): + a = np.arange(30) + a = np.reshape(a, (6, 5)) + a = pad(a, ((2, 3), (3, 2)), mode='mean', stat_length=(3,)) + b = np.array([[ 6, 6, 6, 5, 6, 7, 8, 9, 8, 8], + [ 6, 6, 6, 5, 6, 7, 8, 9, 8, 8], + + [ 1, 1, 1, 0, 1, 2, 3, 4, 3, 3], + [ 6, 6, 6, 5, 6, 7, 8, 9, 8, 8], + [11, 11, 11, 10, 11, 12, 13, 14, 13, 13], + [16, 16, 16, 15, 16, 17, 18, 19, 18, 18], + [21, 21, 21, 20, 21, 22, 23, 24, 23, 23], + [26, 26, 26, 25, 26, 27, 28, 29, 28, 28], + + [21, 21, 21, 20, 21, 22, 23, 24, 23, 23], + [21, 21, 21, 20, 21, 22, 23, 24, 23, 23], + [21, 21, 21, 20, 21, 22, 23, 24, 23, 23]]) + assert_array_equal(a, b) + + +class TestEdge(TestCase): + def test_check_simple(self): + a = np.arange(12) + a = np.reshape(a, (4, 3)) + a = pad(a, ((2, 3), (3, 2)), 'edge') + b = np.array([ + [0, 0, 0, 0, 1, 2, 2, 2], + [0, 0, 0, 0, 1, 2, 2, 2], + + [0, 0, 0, 0, 1, 2, 2, 2], + [3, 3, 3, 3, 4, 5, 5, 5], + [6, 6, 6, 6, 7, 8, 8, 8], + [9, 9, 9, 9, 10, 11, 11, 11], + + [9, 9, 9, 9, 10, 11, 11, 11], + [9, 9, 9, 9, 10, 11, 11, 11], + [9, 9, 9, 9, 10, 11, 11, 11]]) + assert_array_equal(a, b) + + +def test_check_too_many_pad_axes(): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + kwargs = dict(mode='mean', stat_length=(3, )) + assert_raises(ValueError, pad, arr, ((2, 3), (3, 2), (4, 5)), + **kwargs) + + +def test_check_negative_stat_length(): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + kwargs = dict(mode='mean', stat_length=(-3, )) + assert_raises(ValueError, pad, arr, ((2, 3), (3, 2)), + **kwargs) + + +def test_check_negative_pad_width(): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + kwargs = dict(mode='mean', stat_length=(3, )) + assert_raises(ValueError, pad, arr, ((-2, 3), (3, 2)), + **kwargs) + + +def test_pad_one_axis_three_ways(): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + kwargs = dict(mode='mean', stat_length=(3, )) + assert_raises(ValueError, pad, arr, ((2, 3, 4), (3, 2)), + **kwargs) + + +def test_zero_pad_width(): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + for pad_width in (0, (0, 0), ((0, 0), (0, 0))): + assert np.all(arr == pad(arr, pad_width, mode='constant')) + + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/util/tests/test_montage.py b/skimage/util/tests/test_montage.py index 47e5426d..cb7f92dc 100644 --- a/skimage/util/tests/test_montage.py +++ b/skimage/util/tests/test_montage.py @@ -51,6 +51,23 @@ def test_shape(): arr_out = montage2d(arr_in) assert_equal(arr_out.shape, (alpha * height, alpha * width)) + + +def test_grid_shape(): + n_images = 6 + height, width = 2, 2 + arr_in = np.arange(n_images * height * width, dtype=np.float32) + arr_in = arr_in.reshape(n_images, height, width) + arr_out = montage2d(arr_in, grid_shape=(3,2)) + correct_arr_out = np.array( + [[ 0., 1., 4., 5.], + [ 2., 3., 6., 7.], + [ 8., 9., 12., 13.], + [ 10., 11., 14., 15.], + [ 16., 17., 20., 21.], + [ 18., 19., 22., 23.]] + ) + assert_array_equal(arr_out, correct_arr_out) def test_rescale_intensity(): @@ -79,3 +96,7 @@ def test_rescale_intensity(): def test_error_ndim(): arr_error = np.random.randn(1, 2, 3, 4) montage2d(arr_error) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py new file mode 100644 index 00000000..39477cb0 --- /dev/null +++ b/skimage/util/tests/test_random_noise.py @@ -0,0 +1,201 @@ +from numpy.testing import assert_array_equal, assert_allclose, assert_raises + +import numpy as np +from skimage.data import camera +from skimage.util import random_noise, img_as_float + + +def test_set_seed(): + seed = 42 + cam = camera() + test = random_noise(cam, seed=seed) + assert_array_equal(test, random_noise(cam, seed=seed)) + + +def test_salt(): + seed = 42 + cam = img_as_float(camera()) + cam_noisy = random_noise(cam, seed=seed, mode='salt', amount=0.15) + saltmask = cam != cam_noisy + + # Ensure all changes are to 1.0 + assert_allclose(cam_noisy[saltmask], np.ones(saltmask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float(saltmask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.15 + + +def test_pepper(): + seed = 42 + cam = img_as_float(camera()) + data_signed = cam * 2. - 1. # Same image, on range [-1, 1] + + cam_noisy = random_noise(cam, seed=seed, mode='pepper', amount=0.15) + peppermask = cam != cam_noisy + + # Ensure all changes are to 1.0 + assert_allclose(cam_noisy[peppermask], np.zeros(peppermask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float(peppermask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.15 + + # Check to make sure pepper gets added properly to signed images + orig_zeros = (data_signed == -1).sum() + cam_noisy_signed = random_noise(data_signed, seed=seed, mode='pepper', + amount=.15) + + proportion = (float((cam_noisy_signed == -1).sum() - orig_zeros) / + (cam.shape[0] * cam.shape[1])) + assert 0.11 < proportion <= 0.15 + + +def test_salt_and_pepper(): + seed = 42 + cam = img_as_float(camera()) + cam_noisy = random_noise(cam, seed=seed, mode='s&p', amount=0.15, + salt_vs_pepper=0.25) + saltmask = np.logical_and(cam != cam_noisy, cam_noisy == 1.) + peppermask = np.logical_and(cam != cam_noisy, cam_noisy == 0.) + + # Ensure all changes are to 0. or 1. + assert_allclose(cam_noisy[saltmask], np.ones(saltmask.sum())) + assert_allclose(cam_noisy[peppermask], np.zeros(peppermask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float( + saltmask.sum() + peppermask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.18 + + # Verify the relative amount of salt vs. pepper is close to expected + assert 0.18 < saltmask.sum() / float(peppermask.sum()) < 0.32 + + +def test_gaussian(): + seed = 42 + data = np.zeros((128, 128)) + 0.5 + data_gaussian = random_noise(data, seed=seed, var=0.01) + assert 0.008 < data_gaussian.var() < 0.012 + + data_gaussian = random_noise(data, seed=seed, mean=0.3, var=0.015) + assert 0.28 < data_gaussian.mean() - 0.5 < 0.32 + assert 0.012 < data_gaussian.var() < 0.018 + + +def test_localvar(): + seed = 42 + data = np.zeros((128, 128)) + 0.5 + local_vars = np.zeros((128, 128)) + 0.001 + local_vars[:64, 64:] = 0.1 + local_vars[64:, :64] = 0.25 + local_vars[64:, 64:] = 0.45 + + data_gaussian = random_noise(data, mode='localvar', seed=seed, + local_vars=local_vars, clip=False) + assert 0. < data_gaussian[:64, :64].var() < 0.002 + assert 0.095 < data_gaussian[:64, 64:].var() < 0.105 + assert 0.245 < data_gaussian[64:, :64].var() < 0.255 + assert 0.445 < data_gaussian[64:, 64:].var() < 0.455 + + # Ensure local variance bounds checking works properly + bad_local_vars = np.zeros_like(data) + assert_raises(ValueError, random_noise, data, mode='localvar', seed=seed, + local_vars=bad_local_vars) + bad_local_vars += 0.1 + bad_local_vars[0, 0] = -1 + assert_raises(ValueError, random_noise, data, mode='localvar', seed=seed, + local_vars=bad_local_vars) + + +def test_speckle(): + seed = 42 + data = np.zeros((128, 128)) + 0.1 + np.random.seed(seed=seed) + noise = np.random.normal(0.1, 0.02 ** 0.5, (128, 128)) + expected = np.clip(data + data * noise, 0, 1) + + data_speckle = random_noise(data, mode='speckle', seed=seed, mean=0.1, + var=0.02) + assert_allclose(expected, data_speckle) + + +def test_poisson(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + cam_noisy = random_noise(data, mode='poisson', seed=seed) + cam_noisy2 = random_noise(data, mode='poisson', seed=seed, clip=False) + + np.random.seed(seed=seed) + expected = np.random.poisson(img_as_float(data) * 256) / 256. + assert_allclose(cam_noisy, np.clip(expected, 0., 1.)) + assert_allclose(cam_noisy2, expected) + + +def test_clip_poisson(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + data_signed = img_as_float(data) * 2. - 1. # Same image, on range [-1, 1] + + # Signed and unsigned, clipped + cam_poisson = random_noise(data, mode='poisson', seed=seed, clip=True) + cam_poisson2 = random_noise(data_signed, mode='poisson', seed=seed, + clip=True) + assert (cam_poisson.max() == 1.) and (cam_poisson.min() == 0.) + assert (cam_poisson2.max() == 1.) and (cam_poisson2.min() == -1.) + + # Signed and unsigned, unclipped + cam_poisson = random_noise(data, mode='poisson', seed=seed, clip=False) + cam_poisson2 = random_noise(data_signed, mode='poisson', seed=seed, + clip=False) + assert (cam_poisson.max() > 1.15) and (cam_poisson.min() == 0.) + assert (cam_poisson2.max() > 1.3) and (cam_poisson2.min() == -1.) + + +def test_clip_gaussian(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + data_signed = img_as_float(data) * 2. - 1. # Same image, on range [-1, 1] + + # Signed and unsigned, clipped + cam_gauss = random_noise(data, mode='gaussian', seed=seed, clip=True) + cam_gauss2 = random_noise(data_signed, mode='gaussian', seed=seed, + clip=True) + assert (cam_gauss.max() == 1.) and (cam_gauss.min() == 0.) + assert (cam_gauss2.max() == 1.) and (cam_gauss2.min() == -1.) + + # Signed and unsigned, unclipped + cam_gauss = random_noise(data, mode='gaussian', seed=seed, clip=False) + cam_gauss2 = random_noise(data_signed, mode='gaussian', seed=seed, + clip=False) + assert (cam_gauss.max() > 1.22) and (cam_gauss.min() < -0.36) + assert (cam_gauss2.max() > 1.219) and (cam_gauss2.min() < -1.337) + + +def test_clip_speckle(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + data_signed = img_as_float(data) * 2. - 1. # Same image, on range [-1, 1] + + # Signed and unsigned, clipped + cam_speckle = random_noise(data, mode='speckle', seed=seed, clip=True) + cam_speckle2 = random_noise(data_signed, mode='speckle', seed=seed, + clip=True) + assert (cam_speckle.max() == 1.) and (cam_speckle.min() == 0.) + assert (cam_speckle2.max() == 1.) and (cam_speckle2.min() == -1.) + + # Signed and unsigned, unclipped + cam_speckle = random_noise(data, mode='speckle', seed=seed, clip=False) + cam_speckle2 = random_noise(data_signed, mode='speckle', seed=seed, + clip=False) + assert (cam_speckle.max() > 1.219) and (cam_speckle.min() == 0.) + assert (cam_speckle2.max() > 1.219) and (cam_speckle2.min() < -1.306) + + +def test_bad_mode(): + data = np.zeros((64, 64)) + assert_raises(KeyError, random_noise, data, 'perlin') + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/tests/test_regular_grid.py b/skimage/util/tests/test_regular_grid.py new file mode 100644 index 00000000..61736a76 --- /dev/null +++ b/skimage/util/tests/test_regular_grid.py @@ -0,0 +1,40 @@ +import numpy as np +from numpy.testing import assert_equal +from skimage.util import regular_grid + + +def test_regular_grid_full(): + ar = np.zeros((2, 2)) + g = regular_grid(ar, 25) + assert_equal(g, [slice(None, None, None), slice(None, None, None)]) + ar[g] = 1 + assert_equal(ar.size, ar.sum()) + + +def test_regular_grid_2d_8(): + ar = np.zeros((20, 40)) + g = regular_grid(ar.shape, 8) + assert_equal(g, [slice(5.0, None, 10.0), slice(5.0, None, 10.0)]) + ar[g] = 1 + assert_equal(ar.sum(), 8) + + +def test_regular_grid_2d_32(): + ar = np.zeros((20, 40)) + g = regular_grid(ar.shape, 32) + assert_equal(g, [slice(2.0, None, 5.0), slice(2.0, None, 5.0)]) + ar[g] = 1 + assert_equal(ar.sum(), 32) + + +def test_regular_grid_3d_8(): + ar = np.zeros((3, 20, 40)) + g = regular_grid(ar.shape, 8) + assert_equal(g, [slice(1.0, None, 3.0), slice(5.0, None, 10.0), + slice(5.0, None, 10.0)]) + ar[g] = 1 + assert_equal(ar.sum(), 8) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/tests/test_shape.py b/skimage/util/tests/test_shape.py index 3b5f8d41..b6975d0f 100644 --- a/skimage/util/tests/test_shape.py +++ b/skimage/util/tests/test_shape.py @@ -139,3 +139,23 @@ def test_view_as_windows_2D(): [9, 10, 11], [13, 14, 15], [17, 18, 19]]]])) + + +def test_view_as_windows_With_skip(): + A = np.arange(20).reshape((5, 4)) + B = view_as_windows(A, (2, 2), step=2) + assert_equal(B, [[[[0, 1], + [4, 5]], + [[2, 3], + [6, 7]]], + [[[8, 9], + [12, 13]], + [[10, 11], + [14, 15]]]]) + + C = view_as_windows(A, (2, 2), step=4) + assert_equal(C.shape, (1, 1, 2, 2)) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/tests/test_unique_rows.py b/skimage/util/tests/test_unique_rows.py new file mode 100644 index 00000000..ad033b69 --- /dev/null +++ b/skimage/util/tests/test_unique_rows.py @@ -0,0 +1,40 @@ +import numpy as np +from numpy.testing import assert_equal, assert_raises +from skimage.util import unique_rows + + +def test_discontiguous_array(): + ar = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], np.uint8) + ar = ar[::2] + ar_out = unique_rows(ar) + desired_ar_out = np.array([[1, 0, 1]], np.uint8) + assert_equal(ar_out, desired_ar_out) + + +def test_uint8_array(): + ar = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], np.uint8) + ar_out = unique_rows(ar) + desired_ar_out = np.array([[0, 1, 0], [1, 0, 1]], np.uint8) + assert_equal(ar_out, desired_ar_out) + + +def test_float_array(): + ar = np.array([[1.1, 0.0, 1.1], [0.0, 1.1, 0.0], [1.1, 0.0, 1.1]], + np.float) + ar_out = unique_rows(ar) + desired_ar_out = np.array([[0.0, 1.1, 0.0], [1.1, 0.0, 1.1]], np.float) + assert_equal(ar_out, desired_ar_out) + + +def test_1d_array(): + ar = np.array([1, 0, 1, 1], np.uint8) + assert_raises(ValueError, unique_rows, ar) + + +def test_3d_array(): + ar = np.arange(8).reshape((2, 2, 2)) + assert_raises(ValueError, unique_rows, ar) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/unique.py b/skimage/util/unique.py new file mode 100644 index 00000000..635f6e89 --- /dev/null +++ b/skimage/util/unique.py @@ -0,0 +1,50 @@ +import numpy as np + + +def unique_rows(ar): + """Remove repeated rows from a 2D array. + + In particular, if given an array of coordinates of shape + (Npoints, Ndim), it will remove repeated points. + + Parameters + ---------- + ar : 2-D ndarray + The input array. + + Returns + ------- + ar_out : 2-D ndarray + A copy of the input array with repeated rows removed. + + Raises + ------ + ValueError : if `ar` is not two-dimensional. + + Notes + ----- + The function will generate a copy of `ar` if it is not + C-contiguous, which will negatively affect performance for large + input arrays. + + Examples + -------- + >>> ar = np.array([[1, 0, 1], + ... [0, 1, 0], + ... [1, 0, 1]], np.uint8) + >>> unique_rows(ar) + array([[0, 1, 0], + [1, 0, 1]], dtype=uint8) + """ + if ar.ndim != 2: + raise ValueError("unique_rows() only makes sense for 2D arrays, " + "got %dd" % ar.ndim) + # the view in the next line only works if the array is C-contiguous + ar = np.ascontiguousarray(ar) + # np.unique() finds identical items in a raveled array. To make it + # see each row as a single item, we create a view of each row as a + # byte string of length itemsize times number of columns in `ar` + ar_row_view = ar.view('|S%d' % (ar.itemsize * ar.shape[1])) + _, unique_row_indices = np.unique(ar_row_view, return_index=True) + ar_out = ar[unique_row_indices] + return ar_out diff --git a/skimage/viewer/__init__.py b/skimage/viewer/__init__.py index cfbeb0c0..5eed9589 100644 --- a/skimage/viewer/__init__.py +++ b/skimage/viewer/__init__.py @@ -1,4 +1 @@ -try: - from viewers import ImageViewer, CollectionViewer -except ImportError: - print("Could not import PyQt4 -- ImageViewer not available.") +from .viewers import ImageViewer, CollectionViewer diff --git a/skimage/viewer/canvastools/__init__.py b/skimage/viewer/canvastools/__init__.py new file mode 100644 index 00000000..f5b89e2c --- /dev/null +++ b/skimage/viewer/canvastools/__init__.py @@ -0,0 +1,3 @@ +from .linetool import LineTool, ThickLineTool +from .recttool import RectangleTool +from .painttool import PaintTool diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py new file mode 100644 index 00000000..6fcda9c4 --- /dev/null +++ b/skimage/viewer/canvastools/base.py @@ -0,0 +1,176 @@ +import numpy as np + +try: + from matplotlib import lines +except ImportError: + print("Could not import matplotlib -- skimage.viewer not available.") + + +__all__ = ['CanvasToolBase', 'ToolHandles'] + + +def _pass(*args): + pass + + +class CanvasToolBase(object): + """Base canvas tool for matplotlib axes. + + Parameters + ---------- + ax : :class:`matplotlib.axes.Axes` + Matplotlib axes where tool is displayed. + on_move : function + Function called whenever a control handle is moved. + This function must accept the end points of line as the only argument. + on_release : function + Function called whenever the control handle is released. + on_enter : function + Function called whenever the "enter" key is pressed. + useblit : bool + If True, update canvas by blitting, which is much faster than normal + redrawing (turn off for debugging purposes). + """ + def __init__(self, ax, on_move=None, on_enter=None, on_release=None, + useblit=True): + self.ax = ax + self.canvas = ax.figure.canvas + self.img_background = None + self.cids = [] + self._artists = [] + self.active = True + + if useblit: + self.connect_event('draw_event', self._blit_on_draw_event) + self.useblit = useblit + + self.callback_on_move = _pass if on_move is None else on_move + self.callback_on_enter = _pass if on_enter is None else on_enter + self.callback_on_release = _pass if on_release is None else on_release + + self.connect_event('key_press_event', self._on_key_press) + + def connect_event(self, event, callback): + """Connect callback with an event. + + This should be used in lieu of `figure.canvas.mpl_connect` since this + function stores call back ids for later clean up. + """ + cid = self.canvas.mpl_connect(event, callback) + self.cids.append(cid) + + def disconnect_events(self): + """Disconnect all events created by this widget.""" + for c in self.cids: + self.canvas.mpl_disconnect(c) + + def ignore(self, event): + """Return True if event should be ignored. + + This method (or a version of it) should be called at the beginning + of any event callback. + """ + return not self.active + + def set_visible(self, val): + for artist in self._artists: + artist.set_visible(val) + + def _blit_on_draw_event(self, event=None): + self.img_background = self.canvas.copy_from_bbox(self.ax.bbox) + self._draw_artists() + + def _draw_artists(self): + for artist in self._artists: + self.ax.draw_artist(artist) + + def remove(self): + """Remove artists and events from axes. + + Note that the naming here mimics the interface of Matplotlib artists. + """ + #TODO: For some reason, RectangleTool doesn't get properly removed + self.disconnect_events() + for a in self._artists: + a.remove() + + def redraw(self): + """Redraw image and canvas artists. + + This method should be called by subclasses when artists are updated. + """ + if self.useblit and self.img_background is not None: + self.canvas.restore_region(self.img_background) + self._draw_artists() + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw_idle() + + def _on_key_press(self, event): + if event.key == 'enter': + self.callback_on_enter(self.geometry) + self.set_visible(False) + self.redraw() + + @property + def geometry(self): + """Geometry information that gets passed to callback functions.""" + raise NotImplementedError + + +class ToolHandles(object): + """Control handles for canvas tools. + + Parameters + ---------- + ax : :class:`matplotlib.axes.Axes` + Matplotlib axes where tool handles are displayed. + x, y : 1D arrays + Coordinates of control handles. + marker : str + Shape of marker used to display handle. See `matplotlib.pyplot.plot`. + marker_props : dict + Additional marker properties. See :class:`matplotlib.lines.Line2D`. + """ + def __init__(self, ax, x, y, marker='o', marker_props=None): + self.ax = ax + + props = dict(marker=marker, markersize=7, mfc='w', ls='none', + alpha=0.5, visible=False) + props.update(marker_props if marker_props is not None else {}) + self._markers = lines.Line2D(x, y, animated=True, **props) + self.ax.add_line(self._markers) + self.artist = self._markers + + @property + def x(self): + return self._markers.get_xdata() + + @property + def y(self): + return self._markers.get_ydata() + + def set_data(self, pts, y=None): + """Set x and y positions of handles""" + if y is not None: + x = pts + pts = np.array([x, y]) + self._markers.set_data(pts) + + def set_visible(self, val): + self._markers.set_visible(val) + + def set_animated(self, val): + self._markers.set_animated(val) + + def draw(self): + self.ax.draw_artist(self._markers) + + def closest(self, x, y): + """Return index and pixel distance to closest index.""" + pts = np.transpose((self.x, self.y)) + # Transform data coordinates to pixel coordinates. + pts = self.ax.transData.transform(pts) + diff = pts - ((x, y)) + dist = np.sqrt(np.sum(diff**2, axis=1)) + return np.argmin(dist), np.min(dist) diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py new file mode 100644 index 00000000..c32d0ea7 --- /dev/null +++ b/skimage/viewer/canvastools/linetool.py @@ -0,0 +1,207 @@ +import numpy as np + +try: + from matplotlib import lines +except ImportError: + print("Could not import matplotlib -- skimage.viewer not available.") + +from skimage.viewer.canvastools.base import CanvasToolBase, ToolHandles + + +__all__ = ['LineTool', 'ThickLineTool'] + + +class LineTool(CanvasToolBase): + """Widget for line selection in a plot. + + Parameters + ---------- + ax : :class:`matplotlib.axes.Axes` + Matplotlib axes where tool is displayed. + on_move : function + Function called whenever a control handle is moved. + This function must accept the end points of line as the only argument. + on_release : function + Function called whenever the control handle is released. + on_enter : function + Function called whenever the "enter" key is pressed. + maxdist : float + Maximum pixel distance allowed when selecting control handle. + line_props : dict + Properties for :class:`matplotlib.lines.Line2D`. + + Attributes + ---------- + end_points : 2D array + End points of line ((x1, y1), (x2, y2)). + """ + def __init__(self, ax, on_move=None, on_release=None, on_enter=None, + maxdist=10, line_props=None): + super(LineTool, self).__init__(ax, on_move=on_move, on_enter=on_enter, + on_release=on_release) + + props = dict(color='r', linewidth=1, alpha=0.4, solid_capstyle='butt') + props.update(line_props if line_props is not None else {}) + self.linewidth = props['linewidth'] + self.maxdist = maxdist + self._active_pt = None + + x = (0, 0) + y = (0, 0) + self._end_pts = np.transpose([x, y]) + + self._line = lines.Line2D(x, y, visible=False, animated=True, **props) + ax.add_line(self._line) + + self._handles = ToolHandles(ax, x, y) + self._handles.set_visible(False) + self._artists = [self._line, self._handles.artist] + + if on_enter is None: + def on_enter(pts): + x, y = np.transpose(pts) + print("length = %0.2f" % np.sqrt(np.diff(x)**2 + np.diff(y)**2)) + self.callback_on_enter = on_enter + + self.connect_event('button_press_event', self.on_mouse_press) + self.connect_event('button_release_event', self.on_mouse_release) + self.connect_event('motion_notify_event', self.on_move) + + @property + def end_points(self): + return self._end_pts + + @end_points.setter + def end_points(self, pts): + self._end_pts = np.asarray(pts) + + self._line.set_data(np.transpose(pts)) + self._handles.set_data(np.transpose(pts)) + self._line.set_linewidth(self.linewidth) + + self.set_visible(True) + self.redraw() + + def on_mouse_press(self, event): + if event.button != 1 or not self.ax.in_axes(event): + return + self.set_visible(True) + idx, px_dist = self._handles.closest(event.x, event.y) + if px_dist < self.maxdist: + self._active_pt = idx + else: + self._active_pt = 0 + x, y = event.xdata, event.ydata + self._end_pts = np.array([[x, y], [x, y]]) + + def on_mouse_release(self, event): + if event.button != 1: + return + self._active_pt = None + self.callback_on_release(self.geometry) + + def on_move(self, event): + if event.button != 1 or self._active_pt is None: + return + if not self.ax.in_axes(event): + return + self.update(event.xdata, event.ydata) + self.callback_on_move(self.geometry) + + def update(self, x=None, y=None): + if x is not None: + self._end_pts[self._active_pt, :] = x, y + self.end_points = self._end_pts + + @property + def geometry(self): + return self.end_points + + +class ThickLineTool(LineTool): + """Widget for line selection in a plot. + + The thickness of the line can be varied using the mouse scroll wheel, or + with the '+' and '-' keys. + + Parameters + ---------- + ax : :class:`matplotlib.axes.Axes` + Matplotlib axes where tool is displayed. + on_move : function + Function called whenever a control handle is moved. + This function must accept the end points of line as the only argument. + on_release : function + Function called whenever the control handle is released. + on_enter : function + Function called whenever the "enter" key is pressed. + on_change : function + Function called whenever the line thickness is changed. + maxdist : float + Maximum pixel distance allowed when selecting control handle. + line_props : dict + Properties for :class:`matplotlib.lines.Line2D`. + + Attributes + ---------- + end_points : 2D array + End points of line ((x1, y1), (x2, y2)). + """ + + def __init__(self, ax, on_move=None, on_enter=None, on_release=None, + on_change=None, maxdist=10, line_props=None): + super(ThickLineTool, self).__init__(ax, + on_move=on_move, + on_enter=on_enter, + on_release=on_release, + maxdist=maxdist, + line_props=line_props) + + if on_change is None: + def on_change(*args): + pass + self.callback_on_change = on_change + + self.connect_event('scroll_event', self.on_scroll) + self.connect_event('key_press_event', self.on_key_press) + + 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 event.key == '+': + self._thicken_scan_line() + elif event.key == '-': + self._shrink_scan_line() + + def _thicken_scan_line(self): + self.linewidth += 1 + self.update() + self.callback_on_change(self.geometry) + + def _shrink_scan_line(self): + if self.linewidth > 1: + self.linewidth -= 1 + self.update() + self.callback_on_change(self.geometry) + + +if __name__ == '__main__': + import matplotlib.pyplot as plt + from skimage import data + + image = data.camera() + + f, ax = plt.subplots() + ax.imshow(image, interpolation='nearest') + h, w = image.shape + + # line_tool = LineTool(ax) + line_tool = ThickLineTool(ax) + line_tool.end_points = ([w/3, h/2], [2*w/3, h/2]) + plt.show() diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py new file mode 100644 index 00000000..3b4132f0 --- /dev/null +++ b/skimage/viewer/canvastools/painttool.py @@ -0,0 +1,198 @@ +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.colors as mcolors +LABELS_CMAP = mcolors.ListedColormap(['white', 'red', 'dodgerblue', 'gold', + 'greenyellow', 'blueviolet']) + +from skimage.viewer.canvastools.base import CanvasToolBase + + +__all__ = ['PaintTool'] + + +class PaintTool(CanvasToolBase): + """Widget for painting on top of a plot. + + Parameters + ---------- + ax : :class:`matplotlib.axes.Axes` + Matplotlib axes where tool is displayed. + overlay_shape : shape tuple + 2D shape tuple used to initialize overlay image. + alpha : float (between [0, 1]) + Opacity of overlay + on_move : function + Function called whenever a control handle is moved. + This function must accept the end points of line as the only argument. + on_release : function + Function called whenever the control handle is released. + on_enter : function + Function called whenever the "enter" key is pressed. + rect_props : dict + Properties for :class:`matplotlib.patches.Rectangle`. This class + redefines defaults in :class:`matplotlib.widgets.RectangleSelector`. + + Attributes + ---------- + overlay : array + Overlay of painted labels displayed on top of image. + label : int + Current paint color. + """ + def __init__(self, ax, overlay_shape, radius=5, alpha=0.3, on_move=None, + on_release=None, on_enter=None, rect_props=None): + super(PaintTool, self).__init__(ax, on_move=on_move, on_enter=on_enter, + on_release=on_release) + + props = dict(edgecolor='r', facecolor='0.7', alpha=0.5, animated=True) + props.update(rect_props if rect_props is not None else {}) + + self.alpha = alpha + self.cmap = LABELS_CMAP + self._overlay_plot = None + self._shape = overlay_shape + self.overlay = np.zeros(overlay_shape, dtype='uint8') + + self._cursor = plt.Rectangle((0, 0), 0, 0, **props) + self._cursor.set_visible(False) + self.ax.add_patch(self._cursor) + + # `label` and `radius` can only be set after initializing `_cursor` + self.label = 1 + self.radius = radius + + # Note that the order is important: Redraw cursor *after* overlay + self._artists = [self._overlay_plot, self._cursor] + + self.connect_event('button_press_event', self.on_mouse_press) + self.connect_event('button_release_event', self.on_mouse_release) + self.connect_event('motion_notify_event', self.on_move) + + @property + def label(self): + return self._label + + @label.setter + def label(self, value): + if value >= self.cmap.N: + raise ValueError('Maximum label value = %s' % len(self.cmap - 1)) + self._label = value + self._cursor.set_edgecolor(self.cmap(value)) + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, r): + self._radius = r + self._width = 2 * r + 1 + self._cursor.set_width(self._width) + self._cursor.set_height(self._width) + self.window = CenteredWindow(r, self._shape) + + @property + def overlay(self): + return self._overlay + + @overlay.setter + def overlay(self, image): + self._overlay = image + if image is None: + self.ax.images.remove(self._overlay_plot) + self._overlay_plot = None + elif self._overlay_plot is None: + props = dict(cmap=self.cmap, alpha=self.alpha, + norm=mcolors.no_norm(), animated=True) + self._overlay_plot = self.ax.imshow(image, **props) + else: + self._overlay_plot.set_data(image) + self.redraw() + + def _on_key_press(self, event): + if event.key == 'enter': + self.callback_on_enter(self.geometry) + self.redraw() + + def on_mouse_press(self, event): + if event.button != 1 or not self.ax.in_axes(event): + return + self.update_cursor(event.xdata, event.ydata) + self.update_overlay(event.xdata, event.ydata) + + def on_mouse_release(self, event): + if event.button != 1: + return + self.callback_on_release(self.geometry) + + def on_move(self, event): + if not self.ax.in_axes(event): + self._cursor.set_visible(False) + self.redraw() # make sure cursor is not visible + return + self._cursor.set_visible(True) + + self.update_cursor(event.xdata, event.ydata) + if event.button != 1: + self.redraw() # update cursor position + return + self.update_overlay(event.xdata, event.ydata) + self.callback_on_move(self.geometry) + + def update_overlay(self, x, y): + overlay = self.overlay + overlay[self.window.at(y, x)] = self.label + # Note that overlay calls `redraw` + self.overlay = overlay + + def update_cursor(self, x, y): + x = x - self.radius - 1 + y = y - self.radius - 1 + self._cursor.set_xy((x, y)) + + @property + def geometry(self): + return self.overlay + + +class CenteredWindow(object): + """Window that create slices numpy arrays over 2D windows. + + Example + ------- + >>> a = np.arange(16).reshape(4, 4) + >>> w = CenteredWindow(1, a.shape) + >>> a[w.at(1, 1)] + array([[ 0, 1, 2], + [ 4, 5, 6], + [ 8, 9, 10]]) + >>> a[w.at(0, 0)] + array([[0, 1], + [4, 5]]) + >>> a[w.at(4, 3)] + array([[14, 15]]) + """ + def __init__(self, radius, array_shape): + self.radius = radius + self.array_shape = array_shape + + def at(self, row, col): + h, w = self.array_shape + r = self.radius + xmin = max(0, col - r) + xmax = min(w, col + r + 1) + ymin = max(0, row - r) + ymax = min(h, row + r + 1) + return [slice(ymin, ymax), slice(xmin, xmax)] + + +if __name__ == '__main__': + np.testing.rundocs() + from skimage import data + + image = data.camera() + + f, ax = plt.subplots() + ax.imshow(image, interpolation='nearest') + paint_tool = PaintTool(ax, image.shape) + plt.show() diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py new file mode 100644 index 00000000..d4ae113e --- /dev/null +++ b/skimage/viewer/canvastools/recttool.py @@ -0,0 +1,214 @@ +try: + from matplotlib.widgets import RectangleSelector +except ImportError: + RectangleSelector = object + print("Could not import matplotlib -- skimage.viewer not available.") + +from skimage.viewer.canvastools.base import CanvasToolBase +from skimage.viewer.canvastools.base import ToolHandles + + +__all__ = ['RectangleTool'] + + +class RectangleTool(CanvasToolBase, RectangleSelector): + """Widget for selecting a rectangular region in a plot. + + After making the desired selection, press "Enter" to accept the selection + and call the `on_enter` callback function. + + Parameters + ---------- + ax : :class:`matplotlib.axes.Axes` + Matplotlib axes where tool is displayed. + on_move : function + Function called whenever a control handle is moved. + This function must accept the rectangle extents as the only argument. + on_release : function + Function called whenever the control handle is released. + on_enter : function + Function called whenever the "enter" key is pressed. + maxdist : float + Maximum pixel distance allowed when selecting control handle. + rect_props : dict + Properties for :class:`matplotlib.patches.Rectangle`. This class + redefines defaults in :class:`matplotlib.widgets.RectangleSelector`. + + Attributes + ---------- + extents : tuple + Rectangle extents: (xmin, xmax, ymin, ymax). + """ + + def __init__(self, ax, on_move=None, on_release=None, on_enter=None, + maxdist=10, rect_props=None): + CanvasToolBase.__init__(self, ax, on_move=on_move, + on_enter=on_enter, on_release=on_release) + + props = dict(edgecolor=None, facecolor='r', alpha=0.15) + props.update(rect_props if rect_props is not None else {}) + if props['edgecolor'] is None: + props['edgecolor'] = props['facecolor'] + RectangleSelector.__init__(self, ax, lambda *args: None, + rectprops=props, + useblit=self.useblit) + # Alias rectangle attribute, which is initialized in RectangleSelector. + self._rect = self.to_draw + self._rect.set_animated(True) + + self.maxdist = maxdist + self.active_handle = None + self._extents_on_press = None + + if on_enter is None: + def on_enter(extents): + print("(xmin=%.3g, xmax=%.3g, ymin=%.3g, ymax=%.3g)" % extents) + self.callback_on_enter = on_enter + + props = dict(mec=props['edgecolor']) + self._corner_order = ['NW', 'NE', 'SE', 'SW'] + xc, yc = self.corners + self._corner_handles = ToolHandles(ax, xc, yc, marker_props=props) + + self._edge_order = ['W', 'N', 'E', 'S'] + xe, ye = self.edge_centers + self._edge_handles = ToolHandles(ax, xe, ye, marker='s', + marker_props=props) + + self._artists = [self._rect, + self._corner_handles.artist, + self._edge_handles.artist] + + @property + def _rect_bbox(self): + x0 = self._rect.get_x() + y0 = self._rect.get_y() + width = self._rect.get_width() + height = self._rect.get_height() + return x0, y0, width, height + + @property + def corners(self): + """Corners of rectangle from lower left, moving clockwise.""" + x0, y0, width, height = self._rect_bbox + xc = x0, x0 + width, x0 + width, x0 + yc = y0, y0, y0 + height, y0 + height + return xc, yc + + @property + def edge_centers(self): + """Midpoint of rectangle edges from left, moving clockwise.""" + x0, y0, width, height = self._rect_bbox + w = width / 2. + h = height / 2. + xe = x0, x0 + w, x0 + width, x0 + w + ye = y0 + h, y0, y0 + h, y0 + height + return xe, ye + + @property + def extents(self): + """Return (xmin, xmax, ymin, ymax).""" + x0, y0, width, height = self._rect_bbox + xmin, xmax = sorted([x0, x0 + width]) + ymin, ymax = sorted([y0, y0 + height]) + return xmin, xmax, ymin, ymax + + @extents.setter + def extents(self, extents): + x1, x2, y1, y2 = extents + xmin, xmax = sorted([x1, x2]) + ymin, ymax = sorted([y1, y2]) + # Update displayed rectangle + self._rect.set_x(xmin) + self._rect.set_y(ymin) + self._rect.set_width(xmax - xmin) + self._rect.set_height(ymax - ymin) + # Update displayed handles + self._corner_handles.set_data(*self.corners) + self._edge_handles.set_data(*self.edge_centers) + + self.set_visible(True) + self.redraw() + + def release(self, event): + if event.button != 1: + return + if not self.ax.in_axes(event): + self.eventpress = None + return + RectangleSelector.release(self, event) + self._extents_on_press = None + # Undo hiding of rectangle and redraw. + self.set_visible(True) + self.redraw() + self.callback_on_release(self.geometry) + + def press(self, event): + if event.button != 1 or not self.ax.in_axes(event): + return + self._set_active_handle(event) + if self.active_handle is None: + # Clear previous rectangle before drawing new rectangle. + self.set_visible(False) + self.redraw() + self.set_visible(True) + RectangleSelector.press(self, event) + + def _set_active_handle(self, event): + """Set active handle based on the location of the mouse event""" + # Note: event.xdata/ydata in data coordinates, event.x/y in pixels + c_idx, c_dist = self._corner_handles.closest(event.x, event.y) + e_idx, e_dist = self._edge_handles.closest(event.x, event.y) + + # Set active handle as closest handle, if mouse click is close enough. + if c_dist > self.maxdist and e_dist > self.maxdist: + self.active_handle = None + return + elif c_dist < e_dist: + self.active_handle = self._corner_order[c_idx] + else: + self.active_handle = self._edge_order[e_idx] + + # Save coordinates of rectangle at the start of handle movement. + x1, x2, y1, y2 = self.extents + # Switch variables so that only x2 and/or y2 are updated on move. + if self.active_handle in ['W', 'SW', 'NW']: + x1, x2 = x2, event.xdata + if self.active_handle in ['N', 'NW', 'NE']: + y1, y2 = y2, event.ydata + self._extents_on_press = x1, x2, y1, y2 + + def onmove(self, event): + if self.eventpress is None or not self.ax.in_axes(event): + return + + if self.active_handle is None: + # New rectangle + x1 = self.eventpress.xdata + y1 = self.eventpress.ydata + x2, y2 = event.xdata, event.ydata + else: + x1, x2, y1, y2 = self._extents_on_press + if self.active_handle in ['E', 'W'] + self._corner_order: + x2 = event.xdata + if self.active_handle in ['N', 'S'] + self._corner_order: + y2 = event.ydata + self.extents = (x1, x2, y1, y2) + self.callback_on_move(self.geometry) + + @property + def geometry(self): + return self.extents + + +if __name__ == '__main__': + import matplotlib.pyplot as plt + from skimage import data + + f, ax = plt.subplots() + ax.imshow(data.camera(), interpolation='nearest') + + rect_tool = RectangleTool(ax) + plt.show() + print("Final selection:") + rect_tool.callback_on_enter(rect_tool.extents) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 198bac6a..50b7601c 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,23 +1,16 @@ """ 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.") +from warnings import warn -try: - import matplotlib as mpl -except ImportError: - print("Could not import matplotlib -- skimage.viewer not available.") +import numpy as np +from ..qt import QtGui +from ..qt.QtCore import Qt, Signal from ..utils import RequiredAttr, init_qtapp -class Plugin(QDialog): +class Plugin(QtGui.QDialog): """Base class for plugins that interact with an ImageViewer. A plugin connects an image filter (or another function) to an image viewer. @@ -49,8 +42,9 @@ class Plugin(QDialog): 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. + List of Matplotlib artists and canvastools. Any artists created by the + plugin should be added to this list so that it gets cleaned up on + close. Examples -------- @@ -79,16 +73,25 @@ class Plugin(QDialog): """ 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): + # Signals used when viewers are linked to the Plugin output. + image_changed = Signal(np.ndarray) + _started = Signal(int) + + def __init__(self, image_filter=None, height=0, width=400, useblit=True, + dock='bottom'): init_qtapp() super(Plugin, self).__init__() + self.dock = dock + self.image_viewer = None # If subclass defines `image_filter` method ignore input. if not hasattr(self, 'image_filter'): self.image_filter = image_filter + elif image_filter is not None: + warn("If the Plugin class defines an `image_filter` method, " + "then the `image_filter` argument is ignored.") self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) @@ -98,8 +101,6 @@ class Plugin(QDialog): 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 = [] @@ -121,10 +122,8 @@ class Plugin(QDialog): 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) + self.arguments = [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() @@ -155,15 +154,6 @@ class Plugin(QDialog): 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 @@ -176,14 +166,29 @@ class Plugin(QDialog): 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()]) + for name, a in self.keyword_arguments.items()]) filtered = self.image_filter(*arguments, **kwargs) + self.display_filtered_image(filtered) + self.image_changed.emit(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 _update_original_image(self, image): + """Update the original image argument passed to the filter function. + + This method is called by the viewer when the original image is updated. + """ + self.arguments[0] = image + self.filter_image() + + @property + def filtered_image(self): + """Return filtered image.""" + return self.image_viewer.image + def display_filtered_image(self, image): """Display the filtered image on image viewer. @@ -201,41 +206,32 @@ class Plugin(QDialog): """ setattr(self, name, value) + def show(self, main_window=True): + """Show plugin.""" + super(Plugin, self).show() + self.activateWindow() + self.raise_() + + # Emit signal with x-hint so new windows can be displayed w/o overlap. + size = self.frameGeometry() + x_hint = size.x() + size.width() + self._started.emit(x_hint) + 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`. + Note that artists must be appended to `self.artists`. """ - self.disconnect_image_events() + self.clean_up() + self.close() + + def clean_up(self): 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.""" + """Remove artists that are connected to the image viewer.""" for a in self.artists: - self.image_viewer.remove_artist(a) + a.remove() diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 7c7bfb3b..c2294ba8 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,5 +1,7 @@ -from skimage.filter import canny +import numpy as np +import skimage +from skimage.filter import canny from .overlayplugin import OverlayPlugin from ..widgets import Slider, ComboBox @@ -12,7 +14,17 @@ class CannyPlugin(OverlayPlugin): def __init__(self, *args, **kwargs): super(CannyPlugin, self).__init__(image_filter=canny, **kwargs) + def attach(self, image_viewer): + image = image_viewer.image + imin, imax = skimage.dtype_limits(image) + itype = 'float' if np.issubdtype(image.dtype, float) else 'int' 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(Slider('low threshold', imin, imax, value_type=itype, + update_on='release')) + self.add_widget(Slider('high threshold', imin, imax, value_type=itype, + update_on='release')) self.add_widget(ComboBox('color', self.color_names, ptype='plugin')) + # Call parent method at end b/c it calls `filter_image`, which needs + # the values specified by the widgets. Alternatively, move call to + # parent method to beginning and add a call to `self.filter_image()` + super(CannyPlugin,self).attach(image_viewer) diff --git a/skimage/viewer/plugins/color_histogram.py b/skimage/viewer/plugins/color_histogram.py new file mode 100644 index 00000000..39a75004 --- /dev/null +++ b/skimage/viewer/plugins/color_histogram.py @@ -0,0 +1,69 @@ +import numpy as np +import matplotlib.pyplot as plt + +from skimage import color +from skimage import exposure +from .plotplugin import PlotPlugin +from ..canvastools import RectangleTool + + +class ColorHistogram(PlotPlugin): + name = 'Color Histogram' + + def __init__(self, max_pct=0.99, **kwargs): + super(ColorHistogram, self).__init__(height=400, **kwargs) + self.max_pct = max_pct + + print(self.help()) + + def attach(self, image_viewer): + super(ColorHistogram, self).attach(image_viewer) + + self.rect_tool = RectangleTool(self.ax, on_release=self.ab_selected) + self.lab_image = color.rgb2lab(image_viewer.image) + + # Calculate color histogram in the Lab colorspace: + L, a, b = self.lab_image.T + left, right = -100, 100 + ab_extents = [left, right, right, left] + bins = np.arange(left, right) + hist, x_edges, y_edges = np.histogram2d(a.flatten(), b.flatten(), bins, + normed=True) + + # Clip bin heights that dominate a-b histogram + max_val = pct_total_area(hist, percentile=self.max_pct) + hist = exposure.rescale_intensity(hist, in_range=(0, max_val)) + self.ax.imshow(hist, extent=ab_extents, cmap=plt.cm.gray) + + self.ax.set_title('Color Histogram') + self.ax.set_xlabel('b') + self.ax.set_ylabel('a') + + def help(self): + helpstr = ("Color Histogram tool:", + "Select region of a-b colorspace to highlight on image.") + return '\n'.join(helpstr) + + def ab_selected(self, extents): + x0, x1, y0, y1 = extents + + lab_masked = self.lab_image.copy() + L, a, b = lab_masked.T + + mask = ((a > y0) & (a < y1)) & ((b > x0) & (b < x1)) + lab_masked[..., 1:][~mask.T] = 0 + + self.image_viewer.image = color.lab2rgb(lab_masked) + + +def pct_total_area(image, percentile=0.80): + """Return threshold value based on percentage of total area. + + The specified percent of pixels less than the given intensity threshold. + """ + idx = int((image.size - 1) * percentile) + sorted_pixels = np.sort(image.flat) + return sorted_pixels[idx] + + + diff --git a/skimage/viewer/plugins/crop.py b/skimage/viewer/plugins/crop.py new file mode 100644 index 00000000..61f61034 --- /dev/null +++ b/skimage/viewer/plugins/crop.py @@ -0,0 +1,35 @@ +from .base import Plugin +from ..canvastools import RectangleTool +from skimage.viewer.widgets import SaveButtons + + +__all__ = ['Crop'] + + +class Crop(Plugin): + name = 'Crop' + + def __init__(self, maxdist=10, **kwargs): + super(Crop, self).__init__(**kwargs) + self.maxdist = maxdist + self.add_widget(SaveButtons()) + print(self.help()) + + def attach(self, image_viewer): + super(Crop, self).attach(image_viewer) + + self.rect_tool = RectangleTool(self.image_viewer.ax, + maxdist=self.maxdist, + on_enter=self.crop) + self.artists.append(self.rect_tool) + + def help(self): + helpstr = ("Crop tool", + "Select rectangular region and press enter to crop.") + return '\n'.join(helpstr) + + def crop(self, extents): + xmin, xmax, ymin, ymax = extents + image = self.image_viewer.image[ymin:ymax+1, xmin:xmax+1] + self.image_viewer.image = image + self.image_viewer.ax.relim() diff --git a/skimage/viewer/plugins/labelplugin.py b/skimage/viewer/plugins/labelplugin.py new file mode 100644 index 00000000..b3c289f1 --- /dev/null +++ b/skimage/viewer/plugins/labelplugin.py @@ -0,0 +1,63 @@ +import numpy as np + +from .base import Plugin +from ..widgets import ComboBox, Slider +from ..canvastools import PaintTool + + +__all__ = ['LabelPainter'] + + +rad2deg = 180 / np.pi + + +class LabelPainter(Plugin): + name = 'LabelPainter' + + def __init__(self, max_radius=20, **kwargs): + super(LabelPainter, self).__init__(**kwargs) + + # These widgets adjust plugin properties instead of an image filter. + self._radius_widget = Slider('radius', low=1, high=max_radius, + value=5, value_type='int', ptype='plugin') + labels = [str(i) for i in range(6)] + labels[0] = 'Erase' + self._label_widget = ComboBox('label', labels, ptype='plugin') + self.add_widget(self._radius_widget) + self.add_widget(self._label_widget) + + print(self.help()) + + def help(self): + helpstr = ("Label painter", + "Hold left-mouse button and paint on canvas.") + return '\n'.join(helpstr) + + def attach(self, image_viewer): + super(LabelPainter, self).attach(image_viewer) + + image = image_viewer.original_image + self.paint_tool = PaintTool(self.image_viewer.ax, image.shape, + on_enter=self.on_enter) + self.paint_tool.radius = self.radius + self.paint_tool.label = self._label_widget.index = 1 + self.artists.append(self.paint_tool) + + def on_enter(self, overlay): + pass + + @property + def radius(self): + return self._radius_widget.val + + @radius.setter + def radius(self, val): + self.paint_tool.radius = val + + @property + def label(self): + return self._label_widget.val + + @label.setter + def label(self, val): + self.paint_tool.label = val diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 69c9ebfb..0d555eb5 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -1,14 +1,15 @@ +import warnings + import numpy as np import scipy.ndimage as ndi from skimage.util.dtype import dtype_range from .plotplugin import PlotPlugin +from ..canvastools import ThickLineTool __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. @@ -17,10 +18,10 @@ class LineProfile(PlotPlugin): Parameters ---------- - linewidth : float - Line width for interpolation. Wider lines average over more pixels. - epsilon : float + maxdist : float Maximum pixel distance allowed when selecting end point of scan line. + epsilon : float + Deprecated. Use `maxdist` instead. limits : tuple or {None, 'image', 'dtype'} (minimum, maximum) intensity limits for plotted profile. The following special values are defined: @@ -30,17 +31,17 @@ class LineProfile(PlotPlugin): '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): + def __init__(self, maxdist=10, epsilon='deprecated', + limits='image', **kwargs): super(LineProfile, self).__init__(**kwargs) - self.linewidth = linewidth - self.epsilon = epsilon - self._active_pt = None + + if not epsilon == 'deprecated': + warnings.warn("Parameter `epsilon` deprecated; use `maxdist`.") + maxdist = epsilon + self.maxdist = maxdist 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() + print(self.help()) def attach(self, image_viewer): super(LineProfile, self).attach(image_viewer) @@ -50,7 +51,7 @@ class LineProfile(PlotPlugin): 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] + 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: @@ -59,72 +60,40 @@ class LineProfile(PlotPlugin): 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() + h, w = image.shape[0:2] + x = [w / 3, 2 * w / 3] + y = [h / 2] * 2 - 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) + self.line_tool = ThickLineTool(self.image_viewer.ax, + maxdist=self.maxdist, + on_move=self.line_changed, + on_change=self.line_changed) + self.line_tool.end_points = np.transpose([x, y]) + + scan_data = profile_line(image, self.line_tool.end_points) + + self.reset_axes(scan_data) - 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): + def get_profiles(self): """Return intensity profile of the selected line. Returns ------- - end_pts: (2, 2) array + end_points: (2, 2) array The positions ((x1, y1), (x2, y2)) of the line ends. - profile: 1d array - Profile of intensity values. + profile: list of 1d arrays + Profile of intensity values. Length 1 (grayscale) or 3 (rgb). """ - 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) + profiles = [data.get_ydata() for data in self.profile] + return self.line_tool.end_points, profiles def _autoscale_view(self): if self.limits is None: @@ -132,78 +101,57 @@ class LineProfile(PlotPlugin): 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 line_changed(self, end_points): + x, y = np.transpose(end_points) + self.line_tool.end_points = end_points + scan = profile_line(self.image_viewer.original_image, end_points, + linewidth=self.line_tool.linewidth) - 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) + if scan.shape[1] != len(self.profile): + self.reset_axes(scan) - 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) + for i in range(len(scan[0])): + self.profile[i].set_xdata(np.arange(scan.shape[0])) + self.profile[i].set_ydata(scan[:, i]) 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 reset_axes(self, scan_data): + # Clear lines out + for line in self.ax.lines: + self.ax.lines = [] -def profile_line(img, end_pts, linewidth=1): + if scan_data.shape[1] == 1: + self.profile = self.ax.plot(scan_data, 'k-') + else: + self.profile = self.ax.plot(scan_data[:, 0], 'r-', + scan_data[:, 1], 'g-', + scan_data[:, 2], 'b-') + + +def _calc_vert(img, x1, x2, y1, y2, linewidth): + # Quick calculation if perfectly horizontal + pixels = img[min(y1, y2): max(y1, y2) + 1, + x1 - linewidth / 2: x1 + linewidth / 2 + 1] + + # Reverse index if necessary + if y2 > y1: + pixels = pixels[::-1, :] + + return pixels.mean(axis=1)[:, np.newaxis] + + +def profile_line(img, end_points, 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 + img : 2d or 3d array + The image, in grayscale (2d) or RGB (3d) format. + end_points: (2, 2) list End points ((x1, y1), (x2, y2)) of scan line. linewidth: int Width of the scan, perpendicular to the line @@ -214,21 +162,22 @@ def profile_line(img, end_pts, linewidth=1): 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 + point1, point2 = end_points x1, y1 = point1 = np.asarray(point1, dtype=float) x2, y2 = point2 = np.asarray(point2, dtype=float) dx, dy = point2 - point1 + channels = 1 + if img.ndim == 3: + channels = 3 - # Quick calculation if perfectly horizontal or vertical (remove?) + # Quick calculation if perfectly vertical; shortcuts div0 error 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) + if channels == 1: + img = img[:, :, np.newaxis] + + img = np.rollaxis(img, -1) + intensities = np.hstack([_calc_vert(im, x1, x2, y1, y2, linewidth) + for im in img]) return intensities theta = np.arctan2(dy, dx) @@ -236,7 +185,7 @@ def profile_line(img, end_pts, linewidth=1): b = y1 - a * x1 length = np.hypot(dx, dy) - line_x = np.linspace(min(x1, x2), max(x1, x2), np.ceil(length)) + line_x = np.linspace(x2, x1, 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, @@ -244,7 +193,17 @@ def profile_line(img, end_pts, linewidth=1): 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) + if img.ndim == 3: + pixels = [ndi.map_coordinates(img[..., i], perp_lines) + for i in range(3)] + pixels = np.transpose(np.asarray(pixels), (1, 2, 0)) + else: + pixels = ndi.map_coordinates(img, perp_lines) + pixels = pixels[..., np.newaxis] + intensities = pixels.mean(axis=1) - return intensities + if intensities.ndim == 1: + return intensities[..., np.newaxis] + else: + return intensities diff --git a/skimage/viewer/plugins/measure.py b/skimage/viewer/plugins/measure.py new file mode 100644 index 00000000..71412a3b --- /dev/null +++ b/skimage/viewer/plugins/measure.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +import numpy as np + +from .base import Plugin +from ..widgets import Text +from ..canvastools import LineTool + + +__all__ = ['Measure'] + + +rad2deg = 180 / np.pi + + +class Measure(Plugin): + name = 'Measure' + + def __init__(self, maxdist=10, **kwargs): + super(Measure, self).__init__(**kwargs) + + self.maxdist = maxdist + self._length = Text('Length:') + self._angle = Text('Angle:') + + self.add_widget(self._length) + self.add_widget(self._angle) + + print(self.help()) + + def attach(self, image_viewer): + super(Measure, self).attach(image_viewer) + + image = image_viewer.original_image + h, w = image.shape + self.line_tool = LineTool(self.image_viewer.ax, + maxdist=self.maxdist, + on_move=self.line_changed) + self.artists.append(self.line_tool) + + def help(self): + helpstr = ("Measure tool", + "Select line to measure distance and angle.") + return '\n'.join(helpstr) + + def line_changed(self, end_points): + x, y = np.transpose(end_points) + dx = np.diff(x)[0] + dy = np.diff(y)[0] + self._length.text = '%.1f' % np.hypot(dx, dy) + self._angle.text = u'%.1f°' % (180 - np.arctan2(dy, dx) * rad2deg) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index f9d37e59..dc5ca060 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -1,6 +1,18 @@ +from warnings import warn + from skimage.util.dtype import dtype_range from .base import Plugin -from ..utils import ClearColormap +from ..utils import ClearColormap, update_axes_image +from skimage._shared import six + + +__all__ = ['OverlayPlugin'] + + +def recent_mpl_version(): + import matplotlib + version = matplotlib.__version__.split('.') + return int(version[0]) == 1 and int(version[1]) >= 2 class OverlayPlugin(Plugin): @@ -25,6 +37,9 @@ class OverlayPlugin(Plugin): 'cyan': (0, 1, 1)} def __init__(self, **kwargs): + if not recent_mpl_version(): + msg = "Matplotlib >= 1.2 required for OverlayPlugin." + warn(RuntimeWarning(msg)) super(OverlayPlugin, self).__init__(**kwargs) self._overlay_plot = None self._overlay = None @@ -52,7 +67,8 @@ class OverlayPlugin(Plugin): self._overlay_plot = ax.imshow(image, cmap=self.cmap, vmin=vmin, vmax=vmax) else: - self._overlay_plot.set_array(image) + update_axes_image(self._overlay_plot, image) + self.image_viewer.redraw() @property @@ -62,7 +78,8 @@ class OverlayPlugin(Plugin): @color.setter def color(self, index): # Update colormap whenever color is changed. - if isinstance(index, basestring) and index not in self.color_names: + if isinstance(index, six.string_types) and \ + index not in self.color_names: raise ValueError("%s not defined in OverlayPlugin.colors" % index) else: name = self.color_names[index] @@ -74,6 +91,14 @@ class OverlayPlugin(Plugin): self._overlay_plot.set_cmap(self.cmap) self.image_viewer.redraw() + @property + def filtered_image(self): + """Return filtered image. + + This "filtered image" is used when saving from the plugin. + """ + return self.overlay + def display_filtered_image(self, image): """Display filtered image as an overlay on top of image in viewer.""" self.overlay = image diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index fa06088d..0ce5df73 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -1,22 +1,12 @@ import numpy as np -from PyQt4 import QtGui +from ..qt import QtGui -import matplotlib.pyplot as plt - -from ..utils import MatplotlibCanvas +from ..utils import new_plot from .base import Plugin -class PlotCanvas(MatplotlibCanvas): - """Canvas for displaying images. +__all__ = ['PlotPlugin'] - 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. @@ -27,6 +17,13 @@ class PlotPlugin(Plugin): See base Plugin class for additional details. """ + def __init__(self, image_filter=None, height=150, width=400, **kwargs): + super(PlotPlugin, self).__init__(image_filter=image_filter, + height=height, width=width, **kwargs) + + self._height = height + self._width = width + def attach(self, image_viewer): super(PlotPlugin, self).attach(image_viewer) # Add plot for displaying intensity profile. @@ -36,9 +33,12 @@ class PlotPlugin(Plugin): """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 + def add_plot(self): + self.fig, self.ax = new_plot() + self.fig.set_figwidth(self._width / float(self.fig.dpi)) + self.fig.set_figheight(self._height / float(self.fig.dpi)) + + self.canvas = self.fig.canvas #TODO: Converted color is slightly different than Qt background. qpalette = QtGui.QPalette() qcolor = qpalette.color(QtGui.QPalette.Window) @@ -46,5 +46,4 @@ class PlotPlugin(Plugin): 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/qt/QtCore.py b/skimage/viewer/qt/QtCore.py new file mode 100644 index 00000000..19b92a53 --- /dev/null +++ b/skimage/viewer/qt/QtCore.py @@ -0,0 +1,22 @@ +from . import qt_api + +if qt_api == 'pyside': + from PySide.QtCore import * +elif qt_api == 'pyqt': + from PyQt4.QtCore import * + # Use pyside names for signals and slots + Signal = pyqtSignal + Slot = pyqtSlot +else: + # Mock objects for buildbot (which doesn't have Qt, but imports viewer). + class Qt(object): + TopDockWidgetArea = None + BottomDockWidgetArea = None + LeftDockWidgetArea = None + RightDockWidgetArea = None + + def Signal(*args, **kwargs): + pass + + def Slot(*args, **kwargs): + pass diff --git a/skimage/viewer/qt/QtGui.py b/skimage/viewer/qt/QtGui.py new file mode 100644 index 00000000..12e9837f --- /dev/null +++ b/skimage/viewer/qt/QtGui.py @@ -0,0 +1,11 @@ +from . import qt_api + +if qt_api == 'pyside': + from PySide.QtGui import * +elif qt_api == 'pyqt': + from PyQt4.QtGui import * +else: + # Mock objects + QMainWindow = object + QDialog = object + QWidget = object diff --git a/skimage/viewer/qt/README.rst b/skimage/viewer/qt/README.rst new file mode 100644 index 00000000..993ae1a5 --- /dev/null +++ b/skimage/viewer/qt/README.rst @@ -0,0 +1,5 @@ +This qt subpackage provides a wrapper to allow use of either PySide or PyQt4. +In addition, if neither package is available, some mock objects are created to +prevent errors in the TravisCI build. Only the objects used in the global +namespace need to be mocked (e.g., a Qt object that gets subclassed is used +in the global namespace). diff --git a/skimage/viewer/qt/__init__.py b/skimage/viewer/qt/__init__.py new file mode 100644 index 00000000..8e7ab939 --- /dev/null +++ b/skimage/viewer/qt/__init__.py @@ -0,0 +1,22 @@ +import os +import warnings + +qt_api = os.environ.get('QT_API') + +if qt_api is None: + try: + import PySide + qt_api = 'pyside' + except ImportError: + try: + import PyQt4 + qt_api = 'pyqt' + except ImportError: + qt_api = None + # Note that we don't want to raise an error because that would + # cause the TravisCI build to fail. + warnings.warn("Could not import PyQt4: ImageViewer not available!") + + +if qt_api is not None: + os.environ['QT_API'] = qt_api diff --git a/skimage/viewer/utils/__init__.py b/skimage/viewer/utils/__init__.py index 5af24064..bb67a43f 100644 --- a/skimage/viewer/utils/__init__.py +++ b/skimage/viewer/utils/__init__.py @@ -1 +1 @@ -from core import * +from .core import * diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index cf632d5a..3b9b33fb 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -2,23 +2,29 @@ import warnings import numpy as np +from ..qt import qt_api + try: - import matplotlib.pyplot as plt + import matplotlib as mpl + from matplotlib.figure import Figure + from matplotlib import _pylab_helpers from matplotlib.colors import LinearSegmentedColormap - from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg + if qt_api is None: + raise ImportError + else: + from matplotlib.backends.backend_qt4 import FigureManagerQT + 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.") +from ..qt import QtGui __all__ = ['init_qtapp', 'start_qtapp', 'RequiredAttr', 'figimage', - 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas'] + 'LinearColormap', 'ClearColormap', 'FigureCanvas', 'new_plot', + 'update_axes_image'] QApp = None @@ -30,61 +36,53 @@ def init_qtapp(): The QApplication needs to be initialized before creating any QWidgets """ global QApp + QApp = QtGui.QApplication.instance() if QApp is None: QApp = QtGui.QApplication([]) + return QApp -def start_qtapp(): +def is_event_loop_running(app=None): + """Return True if event loop is running.""" + if app is None: + app = init_qtapp() + if hasattr(app, '_in_event_loop'): + return app._in_event_loop + else: + return False + + +def start_qtapp(app=None): """Start Qt mainloop""" - QApp.exec_() + if app is None: + app = init_qtapp() + if not is_event_loop_running(app): + app._in_event_loop = True + app.exec_() + app._in_event_loop = False + else: + app._in_event_loop = True class RequiredAttr(object): """A class attribute that must be set before use.""" - def __init__(self, msg): + instances = dict() + + def __init__(self, msg='Required attribute not set', init_val=None): + self.instances[self, None] = init_val self.msg = msg - self.val = None def __get__(self, obj, objtype): - if self.val is None: + value = self.instances[self, obj] + if value is None: + # Should raise an error but that causes issues with the buildbot. warnings.warn(self.msg) - return self.val + self.__set__(obj, self.init_val) + return value - 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 + def __set__(self, obj, value): + self.instances[self, obj] = value class LinearColormap(LinearSegmentedColormap): @@ -108,7 +106,7 @@ class LinearColormap(LinearSegmentedColormap): """ 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()) + for key, value in segmented_data.items()) LinearSegmentedColormap.__init__(self, name, segmented_data, **kwargs) @@ -124,14 +122,104 @@ class ClearColormap(LinearColormap): LinearColormap.__init__(self, name, cg_speq) -class MatplotlibCanvas(FigureCanvasQTAgg): +class FigureCanvas(FigureCanvasQTAgg): """Canvas for displaying images.""" - def __init__(self, parent, figure, **kwargs): + def __init__(self, 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) + + def resizeEvent(self, event): + FigureCanvasQTAgg.resizeEvent(self, event) + # Call to `resize_event` missing in FigureManagerQT. + # See https://github.com/matplotlib/matplotlib/pull/1585 + self.resize_event() + + +def new_canvas(*args, **kwargs): + """Return a new figure canvas.""" + allnums = _pylab_helpers.Gcf.figs.keys() + num = max(allnums) + 1 if allnums else 1 + + FigureClass = kwargs.pop('FigureClass', Figure) + figure = FigureClass(*args, **kwargs) + canvas = FigureCanvas(figure) + fig_manager = FigureManagerQT(canvas, num) + return fig_manager.canvas + + +def new_plot(parent=None, subplot_kw=None, **fig_kw): + """Return new figure and axes. + + Parameters + ---------- + parent : QtWidget + Qt widget that displays the plot objects. If None, you must manually + call ``canvas.setParent`` and pass the parent widget. + subplot_kw : dict + Keyword arguments passed ``matplotlib.figure.Figure.add_subplot``. + fig_kw : dict + Keyword arguments passed ``matplotlib.figure.Figure``. + """ + if subplot_kw is None: + subplot_kw = {} + canvas = new_canvas(**fig_kw) + canvas.setParent(parent) + + fig = canvas.figure + ax = fig.add_subplot(1, 1, 1, **subplot_kw) + return fig, ax + + +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 mpl.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 = new_plot(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 + + +def update_axes_image(image_axes, image): + """Update the image displayed by an image plot. + + This sets the image plot's array and updates its shape appropriately + + Parameters + ---------- + image_axes : `matplotlib.image.AxesImage` + Image axes to update. + image : array + Image array. + """ + image_axes.set_array(image) + + # Adjust size if new image shape doesn't match the original + h, w = image.shape[:2] + image_axes.set_extent((0, w, h, 0)) diff --git a/skimage/viewer/utils/dialogs.py b/skimage/viewer/utils/dialogs.py new file mode 100644 index 00000000..f160531d --- /dev/null +++ b/skimage/viewer/utils/dialogs.py @@ -0,0 +1,35 @@ +import os + +from ..qt import QtGui + + +__all__ = ['open_file_dialog', 'save_file_dialog'] + + +def _format_filename(filename): + if isinstance(filename, tuple): + # Handle discrepancy between PyQt4 and PySide APIs. + filename = filename[0] + if len(filename) == 0: + return None + return str(filename) + + +def open_file_dialog(): + """Return user-selected file path.""" + filename = QtGui.QFileDialog.getOpenFileName() + filename = _format_filename(filename) + return filename + + +def save_file_dialog(default_format='png'): + """Return user-selected file path.""" + filename = QtGui.QFileDialog.getSaveFileName() + filename = _format_filename(filename) + if filename is None: + return None + #TODO: io plugins should assign default image formats + basename, ext = os.path.splitext(filename) + if not ext: + filename = '%s.%s' % (filename, default_format) + return filename diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index e11967f7..c3ffeb1e 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -1,29 +1,35 @@ """ 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 ..qt import QtGui +from ..qt.QtCore import Qt, Signal +from skimage import io, img_as_float from skimage.util.dtype import dtype_range +from skimage.exposure import rescale_intensity +import numpy as np from .. import utils from ..widgets import Slider +from ..utils import dialogs +from ..plugins.base import Plugin __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) +def mpl_image_to_rgba(mpl_image): + """Return RGB image from the given matplotlib image object. + + Each image in a matplotlib figure has it's own colormap and normalization + function. Return RGBA (RGB + alpha channel) image with float dtype. + """ + input_range = (mpl_image.norm.vmin, mpl_image.norm.vmax) + image = rescale_intensity(mpl_image.get_array(), in_range=input_range) + image = mpl_image.cmap(img_as_float(image)) # cmap complains on bool arrays + return img_as_float(image) -class ImageViewer(QMainWindow): +class ImageViewer(QtGui.QMainWindow): """Viewer for displaying images. This viewer is a simple container object that holds a Matplotlib axes @@ -54,6 +60,15 @@ class ImageViewer(QMainWindow): >>> # viewer.show() """ + + dock_areas = {'top': Qt.TopDockWidgetArea, + 'bottom': Qt.BottomDockWidgetArea, + 'left': Qt.LeftDockWidgetArea, + 'right': Qt.RightDockWidgetArea} + + # Signal that the original image has been changed + original_image_changed = Signal(np.ndarray) + def __init__(self, image): # Start main loop utils.init_qtapp() @@ -61,36 +76,38 @@ class ImageViewer(QMainWindow): #TODO: Add ImageViewer to skimage.io window manager - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setAttribute(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.file_menu.addAction('Open file', self.open_file, + Qt.CTRL + Qt.Key_O) + self.file_menu.addAction('Save to file', self.save_to_file, + Qt.CTRL + Qt.Key_S) + self.file_menu.addAction('Quit', self.close, + Qt.CTRL + 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 + if isinstance(image, Plugin): + plugin = image + image = plugin.filtered_image + plugin.image_changed.connect(self._update_original_image) + # When plugin is started, start + plugin._started.connect(self._show) + + self.fig, self.ax = utils.figimage(image) + self.canvas = self.fig.canvas + self.canvas.setParent(self) + self.ax.autoscale(enable=False) self._image_plot = self.ax.images[0] - - self.original_image = image - self.image = image.copy() + self._update_original_image(image) 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) @@ -105,32 +122,96 @@ class ImageViewer(QMainWindow): def __add__(self, plugin): """Add plugin to ImageViewer""" plugin.attach(self) + self.original_image_changed.connect(plugin._update_original_image) + + if plugin.dock: + location = self.dock_areas[plugin.dock] + dock_location = Qt.DockWidgetArea(location) + dock = QtGui.QDockWidget() + dock.setWidget(plugin) + dock.setWindowTitle(plugin.name) + self.addDockWidget(dock_location, dock) + + horiz = (self.dock_areas['left'], self.dock_areas['right']) + dimension = 'width' if location in horiz else 'height' + self._add_widget_size(plugin, dimension=dimension) + return self + def _add_widget_size(self, widget, dimension='width'): + widget_size = widget.sizeHint() + viewer_size = self.frameGeometry() + + dx = dy = 0 + if dimension == 'width': + dx = widget_size.width() + elif dimension == 'height': + dy = widget_size.height() + + w = viewer_size.width() + h = viewer_size.height() + self.resize(w + dx, h + dy) + + def open_file(self): + """Open image file and display in viewer.""" + filename = dialogs.open_file_dialog() + if filename is None: + return + image = io.imread(filename) + self._update_original_image(image) + + def _update_original_image(self, image): + self.original_image = image # update saved image + self.image = image.copy() # update displayed image + self.original_image_changed.emit(image) + + def save_to_file(self): + """Save current image to file. + + The current behavior is not ideal: It saves the image displayed on + screen, so all images will be converted to RGB, and the image size is + not preserved (resizing the viewer window will alter the size of the + saved image). + """ + filename = dialogs.save_file_dialog() + if filename is None: + return + if len(self.ax.images) == 1: + io.imsave(filename, self.image) + else: + underlay = mpl_image_to_rgba(self.ax.images[0]) + overlay = mpl_image_to_rgba(self.ax.images[1]) + alpha = overlay[:, :, 3] + + # alpha can be set by channel of array or by a scalar value. + # Prefer the alpha channel, but fall back to scalar value. + if np.all(alpha == 1): + alpha = np.ones_like(alpha) * self.ax.images[1].get_alpha() + + alpha = alpha[:, :, np.newaxis] + composite = (overlay[:, :, :3] * alpha + + underlay[:, :, :3] * (1 - alpha)) + io.imsave(filename, composite) + 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). + def _show(self, x=0): + self.move(x, 0) for p in self.plugins: - p.move(w, y) - y += p.geometry().height() + p.show() + super(ImageViewer, self).show() + self.activateWindow() + self.raise_() - def show(self): + def show(self, main_window=True): """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() + self._show() + if main_window: + utils.start_qtapp() def redraw(self): self.canvas.draw_idle() @@ -142,11 +223,19 @@ class ImageViewer(QMainWindow): @image.setter def image(self, image): self._img = image - self._image_plot.set_array(image) + utils.update_axes_image(self._image_plot, image) + + # update display (otherwise image doesn't fill the canvas) + h, w = image.shape[:2] + self.ax.set_xlim(0, w) + self.ax.set_ylim(h, 0) + + # update color range 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): @@ -161,25 +250,6 @@ class ImageViewer(QMainWindow): """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)) @@ -217,7 +287,7 @@ class CollectionViewer(ImageViewer): ---------- image_collection : list of images List of images to be displayed. - update_on : {'on_slide' | 'on_release'} + update_on : {'move' | '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 @@ -265,7 +335,7 @@ class CollectionViewer(ImageViewer): This method can be overridden or extended in subclasses and plugins to react to image changes. """ - self.image = image + self._update_original_image(image) def keyPressEvent(self, event): if type(event) == QtGui.QKeyEvent: @@ -274,6 +344,8 @@ class CollectionViewer(ImageViewer): if 48 <= key < 58: index = 0.1 * int(key - 48) * self.num_images self.update_index('', index) - event.accept() + event.accept() + else: + event.ignore() else: event.ignore() diff --git a/skimage/viewer/widgets/__init__.py b/skimage/viewer/widgets/__init__.py index 6552a313..efa9fe9d 100644 --- a/skimage/viewer/widgets/__init__.py +++ b/skimage/viewer/widgets/__init__.py @@ -1,2 +1,2 @@ -from core import * -from history import * +from .core import * +from .history import * diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 9382652b..b9714d38 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -15,22 +15,17 @@ parameter type specified by its `ptype` attribute, which can be: 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 ..qt import QtGui +from ..qt import QtCore +from ..qt.QtCore import Qt from ..utils import RequiredAttr -__all__ = ['BaseWidget', 'Slider', 'ComboBox'] +__all__ = ['BaseWidget', 'Slider', 'ComboBox', 'Text'] -class BaseWidget(QWidget): +class BaseWidget(QtGui.QWidget): plugin = RequiredAttr("Widget is not attached to a Plugin.") @@ -50,6 +45,28 @@ class BaseWidget(QWidget): self.callback(self.name, value) +class Text(BaseWidget): + + def __init__(self, name=None, text=''): + super(Text, self).__init__(name) + self._label = QtGui.QLabel() + self.text = text + self.layout = QtGui.QHBoxLayout(self) + if name is not None: + name_label = QtGui.QLabel() + name_label.setText(name) + self.layout.addWidget(name_label) + self.layout.addWidget(self._label) + + @property + def text(self): + return self._label.text() + + @text.setter + def text(self, text_str): + self._label.setText(text_str) + + class Slider(BaseWidget): """Slider widget for adjusting numeric parameters. @@ -64,7 +81,7 @@ class Slider(BaseWidget): Range of slider values. value : float Default slider value. If None, use midpoint between `low` and `high`. - value : {'float' | 'int'} + value_type : {'float' | 'int'} Numeric type of slider value. ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. @@ -73,12 +90,12 @@ class Slider(BaseWidget): is typically set when the widget is added to a plugin. orientation : {'horizontal' | 'vertical'} Slider orientation. - update_on : {'move' | 'release'} + update_on : {'release' | 'move'} 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'): + orientation='horizontal', update_on='release'): super(Slider, self).__init__(name, ptype, callback) if value is None: @@ -131,6 +148,7 @@ class Slider(BaseWidget): self.slider.sliderReleased.connect(self._on_slider_changed) else: raise ValueError("Unexpected value %s for 'update_on'" % update_on) + self.slider.setFocusPolicy(QtCore.Qt.StrongFocus) self.name_label = QtGui.QLabel() self.name_label.setText(self.name) @@ -142,9 +160,9 @@ class Slider(BaseWidget): 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) + self.layout.addWidget(self.name_label) + self.layout.addWidget(self.slider) + self.layout.addWidget(self.editbox) def _on_slider_changed(self): """Call callback function with slider's name and value as parameters""" @@ -218,7 +236,7 @@ class ComboBox(BaseWidget): self.layout = QtGui.QHBoxLayout(self) self.layout.addWidget(self.name_label) - self.layout.addWidget(self._combo_box, alignment=QtCore.Qt.AlignLeft) + self.layout.addWidget(self._combo_box) self._combo_box.currentIndexChanged.connect(self._value_changed) # self.connect(self._combo_box, @@ -227,3 +245,11 @@ class ComboBox(BaseWidget): @property def val(self): return self._combo_box.value() + + @property + def index(self): + return self._combo_box.currentIndex() + + @index.setter + def index(self, i): + self._combo_box.setCurrentIndex(i) diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py index efc8a21c..ce20b559 100644 --- a/skimage/viewer/widgets/history.py +++ b/skimage/viewer/widgets/history.py @@ -1,13 +1,14 @@ -import os from textwrap import dedent -try: - from PyQt4 import QtGui -except ImportError: - print("Could not import PyQt4 -- skimage.viewer not available.") +from ..qt import QtGui +from ..qt import QtCore +import numpy as np + +import skimage from skimage import io from .core import BaseWidget +from ..utils import dialogs __all__ = ['OKCancelButtons', 'SaveButtons'] @@ -26,9 +27,11 @@ class OKCancelButtons(BaseWidget): self.ok = QtGui.QPushButton('OK') self.ok.clicked.connect(self.update_original_image) self.ok.setMaximumWidth(button_width) + self.ok.setFocusPolicy(QtCore.Qt.NoFocus) self.cancel = QtGui.QPushButton('Cancel') self.cancel.clicked.connect(self.close_plugin) self.cancel.setMaximumWidth(button_width) + self.cancel.setFocusPolicy(QtCore.Qt.NoFocus) self.layout = QtGui.QHBoxLayout(self) self.layout.addStretch() @@ -58,8 +61,10 @@ class SaveButtons(BaseWidget): self.save_file = QtGui.QPushButton('File') self.save_file.clicked.connect(self.save_to_file) + self.save_file.setFocusPolicy(QtCore.Qt.NoFocus) self.save_stack = QtGui.QPushButton('Stack') self.save_stack.clicked.connect(self.save_to_stack) + self.save_stack.setFocusPolicy(QtCore.Qt.NoFocus) self.layout = QtGui.QHBoxLayout(self) self.layout.addWidget(self.name_label) @@ -67,7 +72,7 @@ class SaveButtons(BaseWidget): self.layout.addWidget(self.save_file) def save_to_stack(self): - image = self.plugin.image_viewer.image.copy() + image = self.plugin.filtered_image.copy() io.push(image) msg = dedent('''\ @@ -77,14 +82,14 @@ class SaveButtons(BaseWidget): notify(msg) def save_to_file(self): - filename = str(QtGui.QFileDialog.getSaveFileName()) - if len(filename) == 0: + filename = dialogs.save_file_dialog() + if filename is None: 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) + image = self.plugin.filtered_image + if image.dtype == np.bool: + #TODO: This check/conversion should probably be in `imsave`. + image = skimage.img_as_ubyte(image) + io.imsave(filename, image) def notify(msg): diff --git a/viewer_examples/plugins/canny_simple.py b/viewer_examples/plugins/canny_simple.py index 49bc15eb..c26ca08d 100644 --- a/viewer_examples/plugins/canny_simple.py +++ b/viewer_examples/plugins/canny_simple.py @@ -3,18 +3,22 @@ from skimage.filter import canny from skimage.viewer import ImageViewer from skimage.viewer.widgets import Slider +from skimage.viewer.widgets.history import SaveButtons 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. +plugin += Slider('sigma', 0, 5) +plugin += Slider('low threshold', 0, 255) +plugin += Slider('high threshold', 0, 255) +# ... and we can also add buttons to save the overlay: +plugin += SaveButtons(name='Save overlay to:') + +# Finally, attach the plugin to an image viewer. +viewer = ImageViewer(image) viewer += plugin viewer.show() diff --git a/viewer_examples/plugins/collection_overlay.py b/viewer_examples/plugins/collection_overlay.py new file mode 100644 index 00000000..4c7002f2 --- /dev/null +++ b/viewer_examples/plugins/collection_overlay.py @@ -0,0 +1,21 @@ +""" +============================================== +``CollectionViewer`` with an ``OverlayPlugin`` +============================================== + +Demo of a CollectionViewer for viewing collections of images with an +overlay plugin. + +""" +from skimage import data + +from skimage.viewer import CollectionViewer +from skimage.viewer.plugins.canny import CannyPlugin + + +img_collection = [data.camera(), data.coins(), data.text()] + +viewer = CollectionViewer(img_collection) +viewer += CannyPlugin() + +viewer.show() diff --git a/viewer_examples/plugins/collection_plugin.py b/viewer_examples/plugins/collection_plugin.py new file mode 100644 index 00000000..65ff1f21 --- /dev/null +++ b/viewer_examples/plugins/collection_plugin.py @@ -0,0 +1,33 @@ +""" +================================== +``CollectionViewer`` with a plugin +================================== + +Demo of a CollectionViewer for viewing collections of images with the +`autolevel` rank filter connected as a plugin. + +""" +from skimage import data +from skimage.filter import rank +from skimage.morphology import disk + +from skimage.viewer import CollectionViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.plugins.base import Plugin + + +# Wrap autolevel function to make the disk size a filter argument. +def autolevel(image, disk_size): + return rank.autolevel(image, disk(disk_size)) + + +img_collection = [data.camera(), data.coins(), data.text()] + +plugin = Plugin(image_filter=autolevel) +plugin += Slider('disk_size', 2, 8, value_type='int') +plugin.name = "Autolevel" + +viewer = CollectionViewer(img_collection) +viewer += plugin + +viewer.show() diff --git a/viewer_examples/plugins/color_histogram.py b/viewer_examples/plugins/color_histogram.py new file mode 100644 index 00000000..6b091d69 --- /dev/null +++ b/viewer_examples/plugins/color_histogram.py @@ -0,0 +1,9 @@ +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.color_histogram import ColorHistogram +from skimage import data + + +image = data.load('color.png') +viewer = ImageViewer(image) +viewer += ColorHistogram(dock='right') +viewer.show() diff --git a/viewer_examples/plugins/croptool.py b/viewer_examples/plugins/croptool.py new file mode 100644 index 00000000..0eb44903 --- /dev/null +++ b/viewer_examples/plugins/croptool.py @@ -0,0 +1,9 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.crop import Crop + + +image = data.camera() +viewer = ImageViewer(image) +viewer += Crop() +viewer.show() diff --git a/viewer_examples/plugins/lineprofile_rgb.py b/viewer_examples/plugins/lineprofile_rgb.py new file mode 100644 index 00000000..86b71d5d --- /dev/null +++ b/viewer_examples/plugins/lineprofile_rgb.py @@ -0,0 +1,9 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.lineprofile import LineProfile + + +image = data.chelsea() +viewer = ImageViewer(image) +viewer += LineProfile() +viewer.show() diff --git a/viewer_examples/plugins/measure.py b/viewer_examples/plugins/measure.py new file mode 100644 index 00000000..6200a717 --- /dev/null +++ b/viewer_examples/plugins/measure.py @@ -0,0 +1,9 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.measure import Measure + + +image = data.camera() +viewer = ImageViewer(image) +viewer += Measure() +viewer.show() diff --git a/viewer_examples/plugins/median_filter.py b/viewer_examples/plugins/median_filter.py index 3a050382..36593c3d 100644 --- a/viewer_examples/plugins/median_filter.py +++ b/viewer_examples/plugins/median_filter.py @@ -2,8 +2,7 @@ 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.widgets import Slider, OKCancelButtons, SaveButtons from skimage.viewer.plugins.base import Plugin @@ -11,7 +10,7 @@ image = data.coins() viewer = ImageViewer(image) plugin = Plugin(image_filter=median_filter) -plugin += Slider('radius', 2, 10, value_type='int', update_on='release') +plugin += Slider('radius', 2, 10, value_type='int') plugin += SaveButtons() plugin += OKCancelButtons() diff --git a/viewer_examples/plugins/probabilistic_hough.py b/viewer_examples/plugins/probabilistic_hough.py new file mode 100644 index 00000000..98052f87 --- /dev/null +++ b/viewer_examples/plugins/probabilistic_hough.py @@ -0,0 +1,46 @@ +import numpy as np + +from skimage import data +from skimage import draw +from skimage.transform import probabilistic_hough_line + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.plugins.overlayplugin import OverlayPlugin +from skimage.viewer.plugins.canny import CannyPlugin + + +def line_image(shape, lines): + image = np.zeros(shape, dtype=bool) + for end_points in lines: + # hough lines returns (x, y) points, draw.line wants (row, columns) + end_points = np.asarray(end_points)[:, ::-1] + image[draw.line(*np.ravel(end_points))] = 1 + return image + + +def hough_lines(image, *args, **kwargs): + # Set threshold to 0.5 since we're working with a binary image (from canny) + lines = probabilistic_hough_line(image, threshold=0.5, *args, **kwargs) + image = line_image(image.shape, lines) + return image + + +image = data.camera() +canny_viewer = ImageViewer(image) +canny_plugin = CannyPlugin() +canny_viewer += canny_plugin + +hough_plugin = OverlayPlugin(image_filter=hough_lines) +hough_plugin.name = 'Hough Lines' + +hough_plugin += Slider('line length', 0, 100) +hough_plugin += Slider('line gap', 0, 20) + +# Passing a plugin to a viewer connects the output of the plugin to the viewer. +hough_viewer = ImageViewer(canny_plugin) +hough_viewer += hough_plugin + +# Show viewers displays both viewers since `hough_viewer` is connected to +# `canny_viewer` through `canny_plugin` +canny_viewer.show() diff --git a/viewer_examples/plugins/watershed_demo.py b/viewer_examples/plugins/watershed_demo.py new file mode 100644 index 00000000..612ec6c9 --- /dev/null +++ b/viewer_examples/plugins/watershed_demo.py @@ -0,0 +1,43 @@ +import matplotlib.pyplot as plt + +from skimage import data +from skimage import filter +from skimage import morphology +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import history +from skimage.viewer.plugins.labelplugin import LabelPainter + + +class OKCancelButtons(history.OKCancelButtons): + + def update_original_image(self): + # OKCancelButtons updates the original image with the filtered image + # by default. Override this method to update the overlay. + self.plugin._show_watershed() + self.plugin.close() + + +class WatershedPlugin(LabelPainter): + + def help(self): + helpstr = ("Watershed plugin", + "----------------", + "Use mouse to paint each region with a different label.", + "Press OK to display segmented image.") + return '\n'.join(helpstr) + + def _show_watershed(self): + viewer = self.image_viewer + edge_image = filter.sobel(viewer.image) + labels = morphology.watershed(edge_image, self.paint_tool.overlay) + viewer.ax.imshow(labels, cmap=plt.cm.jet, alpha=0.5) + viewer.redraw() + + +image = data.coins() +plugin = WatershedPlugin() +plugin += OKCancelButtons() + +viewer = ImageViewer(image) +viewer += plugin +viewer.show() diff --git a/viewer_examples/viewers/collection_viewer.py b/viewer_examples/viewers/collection_viewer.py index 62cdbb26..61df3dd9 100644 --- a/viewer_examples/viewers/collection_viewer.py +++ b/viewer_examples/viewers/collection_viewer.py @@ -18,7 +18,6 @@ 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