diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..5a372d61 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ +# Configuration for coverage.py + +[run] +branch = True +source = skimage +include = */skimage/* +omit = + */setup.py + +[report] +exclude_lines = + def __repr__ + if __name__ == .__main__.: diff --git a/.travis.yml b/.travis.yml index 91b9b5fa..169b512b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,42 +1,94 @@ # vim ft=yaml -# travis-ci.org definition for skimage build -# -# 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 -language: erlang -env: - - PYTHON=python PYSUF='' PYVER=2.7 - - PYTHON=python3 PYSUF='3' PYVER=3.2 -install: - - sudo apt-get update # needed for python3-numpy - - sudo apt-get install $PYTHON-dev +# After changing this file, check it on: +# http://lint.travis-ci.org/ + + +language: python + +python: + - 2.6 + +matrix: + include: + - python: 2.7 + env: + - PYTHON=python + - PYTHONWARNINGS=all + - PYTHONX=python + - PYVER=2.x + - python: 3.2 + env: + - PYTHON=python3 + - PYTHONWARNINGS=all + - PYTHONX=python3 + - PYVER=3.x + exclude: + - python: 2.6 + +virtualenv: + system_site_packages: true + +before_install: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + + - sudo apt-get update + - sudo apt-get install $PYTHON-numpy + - wget https://raw.githubusercontent.com/numpy/numpy/master/numpy/_import_tools.py -O /home/travis/virtualenv/python3.2_with_system_site_packages/lib/python3.2/site-packages/numpy/_import_tools.py + - sudo apt-get install $PYTHON-scipy - - sudo apt-get install $PYTHON-setuptools - - sudo apt-get install $PYTHON-nose - - sudo easy_install$PYSUF pip - - sudo pip-$PYVER install cython - sudo apt-get install libfreeimage3 - - 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 + + - if [[ $PYVER == '2.x' ]]; then + - sudo apt-get install $PYTHON-qt4; + - sudo apt-get install $PYTHON-matplotlib; + - fi + - if [[ $PYVER == '3.x' ]]; then + - sudo apt-get install $PYTHON-pyqt4; + - pip install --use-mirrors matplotlib; + - fi + + - pip install pillow + - pip install cython + - pip install flake8 + - pip install six + + - pip install nose-cov + - pip install coveralls + + - python check_bento_build.py + +install: + - tools/header.py "Dependency versions" + - tools/build_versions.py + + - python setup.py build_ext --inplace + 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 + # Matplotlib settings + - mkdir -p $HOME/.matplotlib + - touch $HOME/.matplotlib/matplotlibrc - "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 all tests + - if [[ $PYVER == '3.x' ]]; then + - nosetests --exe -v --with-doctest --with-cov --cov skimage --cov-config=.coveragerc skimage + - fi + - if [[ $PYVER == '2.x' ]]; then + - nosetests --exe -v --with-doctest skimage + - fi + # Run all doc examples + - export PYTHONPATH=$(pwd):$PYTHONPATH + - for f in doc/examples/*.py; do $PYTHONX "$f"; if [ $? -ne 0 ]; then exit 1; fi done + - for f in doc/examples/applications/*.py; do $PYTHONX "$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 + +after_success: + - if [[ $PYVER == '3.x' ]]; then + - coveralls + - fi diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 331b4ed8..202210a1 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -13,7 +13,7 @@ Here's the long and short of it: git clone git@github.com:your-username/scikit-image.git - * Add upstream repository:: + * Add the upstream repository:: git remote add upstream git@github.com:scikit-image/scikit-image.git @@ -43,8 +43,8 @@ Here's the long and short of it: git push origin transform-speedups - * Go to GitHub. The new branch will show up with a Pull Request button - - click it. + * Go to GitHub. The new branch will show up with a green Pull Request + button - click it. * If you want, post on the `mailing list `_ to explain your changes or @@ -54,11 +54,34 @@ For a more detailed discussion, read these :doc:`detailed documents ` on how to use Git with ``scikit-image`` (``_). +4. Review process: + + * Reviewers (the other developers and interested community members) will + write inline and/or general comments on your Pull Request (PR) to help + you improve its implementation, documentation and style. Every single + developer working on the project has their code reviewed, and we've come + to see it as friendly conversation from which we all learn and the + overall code quality benefits. Therefore, please don't let the review + discourage you from contributing: its only aim is to improve the quality + of project, not to criticize (we are, after all, very grateful for the + time you're donating!). + + * To update your pull request, make your changes on your local repository + and commit. As soon as those changes are pushed up (to the same branch as + before) the pull request will update automatically. + + * `Travis-CI `__, a continuous integration service, + is triggered after each Pull Request update to build the code, run unit + tests, measure code coverage and check coding style (PEP8) of your + branch. The Travis tests must pass before your PR can be merged. If + Travis fails, you can find out why by clicking on the "failed" icon (red + cross) and inspecting the build and test log. + .. 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. + To reviewers: if it is not obvious, add a short explanation of what a branch + did to the merge message and, if closing a bug, also add "Closes #123" + where 123 is the issue number. Divergence between ``upstream master`` and your feature branch @@ -98,7 +121,7 @@ Guidelines as NumPy and SciPy. * For new functionality, always add an example to the gallery. -* No changes should be committed without review. Ask on the +* No changes are ever committed without review. Ask on the `mailing list `_ if you get no response to your pull request. **Never merge your own pull request.** @@ -154,6 +177,7 @@ detailing the test coverage:: skimage/filter/__init__ 1 1 100% ... + Activate Travis-CI for your fork (optional) ------------------------------------------- @@ -174,6 +198,10 @@ It corresponds to steps one and two in 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. +Every time Travis is triggered, it also calls on `Coveralls +`_ to inspect the current test overage. + + Bugs ---- diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index f8f78c52..140842ab 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -144,17 +144,41 @@ 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 + Tomography: radon/iradon improvements and SART implementation + Phase unwrapping integration - Matt Terry Color difference functions - Eugene Dvoretsky - Yen threshold implementation. + Yen, Ridler-Calvard (ISODATA) threshold implementations. - Riaan van den Dool skimage.io plugin: GDAL - Fedor Morozov Drawing: Wu's anti-aliased circle + +- Michael Hansen + novice submodule + +- Munther Gdeisat + Phase unwrapping implementation + +- Miguel Arevallilo Herraez + Phase unwrapping implementation + +- Hussein Abdul-Rahman + Phase unwrapping implementation + +- Gregor Thalhammer + Phase unwrapping integration + +- François Orieux + Image deconvolution http://research.orieux.fr + +- Vighnesh Birodkar + Blob Detection + +- Axel Donath + Blob Detection diff --git a/DEPENDS.txt b/DEPENDS.txt index 04b745c2..c4cc8679 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -46,6 +46,12 @@ functionality is only available with the following installed: The ``pyamg`` module is used for the fast `cg_mg` mode of random walker segmentation. +* `Pillow `__ + (or <`PIL http://www.pythonware.com/products/pil/>`__) + The ``Pillow`` library (or equivalently ``PIL``) is used for Input/Output. + +* `Astropy `__ is required to use the FITS io plug-in. + Testing requirements -------------------- * `Nose `__ diff --git a/MANIFEST.in b/MANIFEST.in index a2ef2eaf..4f6786be 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,7 @@ include setup*.py include MANIFEST.in include *.txt include Makefile -recursive-include skimage *.pyx *.pxd *.pxi *.py *.c *.h *.ini *.md5 *.rst *.txt +recursive-include skimage *.pyx *.pxd *.pxi *.py *.c *.h *.ini *.md5 recursive-include skimage/data * include doc/Makefile diff --git a/Makefile b/Makefile index a0ca9745..2ace534e 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,10 @@ clean: find . -name "*.so" -o -name "*.pyc" -o -name "*.pyx.md5" | xargs rm -f test: - nosetests skimage + python -c "import skimage, sys, io; sys.exit(skimage.test_verbose())" + +doctest: + python -c "import skimage, sys, io; sys.exit(skimage.doctest_verbose())" coverage: nosetests skimage --with-coverage --cover-package=skimage diff --git a/RELEASE.txt b/RELEASE.txt index 5772cf77..36c3acf5 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -13,8 +13,8 @@ How to make a new release of ``skimage`` - Update the docs: - 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. + - Build a clean version of the docs. Run ``python setup.py install`` 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! @@ -51,7 +51,7 @@ How to make a new release of ``skimage`` - Add previous stable version documentation path to disallowed paths in `robots.txt` - Build using ``make gh-pages``. - - Push upstream: ``git push`` in ``gh-pages``. + - Push upstream: ``git push origin master`` in ``gh-pages``. - Update the development docs for the new version ``0.Xdev`` just like above diff --git a/TODO.txt b/TODO.txt index 3cbf4b53..adc70f48 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,15 +1,17 @@ -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 +Remember to list any API changes below in `doc/source/api_changes.txt`. +Version 0.12 +------------ +* Change `label` to mark background as 0, not -1, which is consistent with + SciPy's labelling. +* Remove `skimage.morphology.label` from `skimage.morphology.__init__`--it now + lives in `skimage.measure.label`. +* Remove deprecated `reverse_map` parameter of `skimage.transform.warp` +* Change depecrated `enforce_connectivity=False` on skimage.segmentation.slic + and set it to True as default +* Remove deprecated `skimage.measure.fit.BaseModel._params` attribute +* Remove deprecated `skimage.measure.fit.BaseModel._params`, + `skimage.transform.ProjectiveTransform._matrix`, + `skimage.transform.PolynomialTransform._params`, + `skimage.transform.PiecewiseAffineTransform.affines_*` attributes +* Remove deprecated functions `skimage.filter.denoise_*` diff --git a/bento.info b/bento.info index b01a09d1..18310623 100644 --- a/bento.info +++ b/bento.info @@ -1,5 +1,5 @@ Name: scikit-image -Version: 0.9.3 +Version: 0.10.0 Summary: Image processing routines for SciPy Url: http://scikit-image.org DownloadUrl: http://github.com/scikit-image/scikit-image @@ -35,19 +35,17 @@ Library: skimage, skimage.color, skimage.data, skimage.draw, skimage.exposure, skimage.feature, skimage.filter, skimage.graph, skimage.io, skimage.io._plugins, skimage.measure, skimage.morphology, - skimage.scripts, skimage.segmentation, skimage.transform, skimage.util + skimage.scripts, skimage.restoration, skimage.segmentation, + skimage.transform, skimage.util Extension: skimage.morphology._pnpoly Sources: skimage/morphology/_pnpoly.pyx - Extension: skimage.feature._template - Sources: - skimage/feature/_template.pyx Extension: skimage.io._plugins._colormixer Sources: skimage/io/_plugins/_colormixer.pyx - Extension: skimage.measure._find_contours + Extension: skimage.measure._find_contours_cy Sources: - skimage/measure/_find_contours.pyx + skimage/measure/_find_contours_cy.pyx Extension: skimage.measure._moments Sources: skimage/measure/_moments.pyx @@ -66,12 +64,9 @@ Library: Extension: skimage.filter._ctmf Sources: skimage/filter/_ctmf.pyx - Extension: skimage.filter._denoise_cy + Extension: skimage.measure._ccomp Sources: - skimage/filter/_denoise_cy.pyx - Extension: skimage.morphology.ccomp - Sources: - skimage/morphology/ccomp.pyx + skimage/measure/_ccomp.pyx Extension: skimage.morphology._watershed Sources: skimage/morphology/_watershed.pyx @@ -96,9 +91,12 @@ Library: Extension: skimage.feature.censure_cy Sources: skimage/feature/censure_cy.pyx - Extension: skimage.feature._brief_cy + Extension: skimage.feature.orb_cy Sources: - skimage/feature/_brief_cy.pyx + skimage/feature/orb_cy.pyx + Extension: skimage.feature.brief_cy + Sources: + skimage/feature/brief_cy.pyx Extension: skimage.feature.corner_cy Sources: skimage/feature/corner_cy.pyx @@ -123,9 +121,6 @@ Library: Extension: skimage.transform._warps_cy Sources: skimage/transform/_warps_cy.pyx - Extension: skimage._shared.interpolation - Sources: - skimage/_shared/interpolation.pyx Extension: skimage.segmentation._felzenszwalb_cy Sources: skimage/segmentation/_felzenszwalb_cy.pyx @@ -144,6 +139,21 @@ Library: Extension: skimage.filter.rank.bilateral_cy Sources: skimage/filter/rank/bilateral_cy.pyx + Extension: skimage.restoration._unwrap_1d + Sources: + skimage/restoration/_unwrap_1d.pyx + Extension: skimage.restoration._unwrap_2d + Sources: + skimage/restoration/_unwrap_2d.pyx skimage/exposure/unwrap_2d_ljmu.c + Extension: skimage.restoration._unwrap_3d + Sources: + skimage/restoration/_unwrap_3d.pyx skimage/exposure/unwrap_3d_ljmu.c + Extension: skimage.restoration._denoise_cy + Sources: + skimage/restoration/_denoise_cy.pyx + Extension: skimage.feature._hessian_det_appx + Sources: + skimage/exposure/_hessian_det_appx.pyx Executable: skivi Module: skimage.scripts.skivi diff --git a/doc/examples/applications/plot_coins_segmentation.py b/doc/examples/applications/plot_coins_segmentation.py index fa7aa1f4..49ff399f 100644 --- a/doc/examples/applications/plot_coins_segmentation.py +++ b/doc/examples/applications/plot_coins_segmentation.py @@ -16,13 +16,11 @@ from skimage import data coins = data.coins() hist = np.histogram(coins, bins=np.arange(0, 256)) -plt.figure(figsize=(8, 3)) -plt.subplot(121) -plt.imshow(coins, cmap=plt.cm.gray, interpolation='nearest') -plt.axis('off') -plt.subplot(122) -plt.plot(hist[1][:-1], hist[0], lw=2) -plt.title('histogram of grey values') +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 3)) +ax1.imshow(coins, cmap=plt.cm.gray, interpolation='nearest') +ax1.axis('off') +ax2.plot(hist[1][:-1], hist[0], lw=2) +ax2.set_title('histogram of grey values') """ .. image:: PLOT2RST.current_figure @@ -37,17 +35,15 @@ background with the coins: """ -plt.figure(figsize=(6, 3)) -plt.subplot(121) -plt.imshow(coins > 100, cmap=plt.cm.gray, interpolation='nearest') -plt.title('coins > 100') -plt.axis('off') -plt.subplot(122) -plt.imshow(coins > 150, cmap=plt.cm.gray, interpolation='nearest') -plt.title('coins > 150') -plt.axis('off') +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(6, 3)) +ax1.imshow(coins > 100, cmap=plt.cm.gray, interpolation='nearest') +ax1.set_title('coins > 100') +ax1.axis('off') +ax2.imshow(coins > 150, cmap=plt.cm.gray, interpolation='nearest') +ax2.set_title('coins > 150') +ax2.axis('off') margins = dict(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) -plt.subplots_adjust(**margins) +fig.subplots_adjust(**margins) """ .. image:: PLOT2RST.current_figure @@ -64,10 +60,10 @@ edge-detector. from skimage.filter import canny edges = canny(coins/255.) -plt.figure(figsize=(4, 3)) -plt.imshow(edges, cmap=plt.cm.gray, interpolation='nearest') -plt.axis('off') -plt.title('Canny detector') +fig, ax = plt.subplots(figsize=(4, 3)) +ax.imshow(edges, cmap=plt.cm.gray, interpolation='nearest') +ax.axis('off') +ax.set_title('Canny detector') """ .. image:: PLOT2RST.current_figure @@ -79,10 +75,10 @@ from scipy import ndimage fill_coins = ndimage.binary_fill_holes(edges) -plt.figure(figsize=(4, 3)) -plt.imshow(fill_coins, cmap=plt.cm.gray, interpolation='nearest') -plt.axis('off') -plt.title('Filling the holes') +fig, ax = plt.subplots(figsize=(4, 3)) +ax.imshow(fill_coins, cmap=plt.cm.gray, interpolation='nearest') +ax.axis('off') +ax.set_title('Filling the holes') """ .. image:: PLOT2RST.current_figure @@ -93,10 +89,10 @@ objects. from skimage import morphology coins_cleaned = morphology.remove_small_objects(fill_coins, 21) -plt.figure(figsize=(4, 3)) -plt.imshow(coins_cleaned, cmap=plt.cm.gray, interpolation='nearest') -plt.axis('off') -plt.title('Removing small objects') +fig, ax = plt.subplots(figsize=(4, 3)) +ax.imshow(coins_cleaned, cmap=plt.cm.gray, interpolation='nearest') +ax.axis('off') +ax.set_title('Removing small objects') """ .. image:: PLOT2RST.current_figure @@ -117,10 +113,10 @@ from skimage.filter import sobel elevation_map = sobel(coins) -plt.figure(figsize=(4, 3)) -plt.imshow(elevation_map, cmap=plt.cm.jet, interpolation='nearest') -plt.axis('off') -plt.title('elevation_map') +fig, ax = plt.subplots(figsize=(4, 3)) +ax.imshow(elevation_map, cmap=plt.cm.jet, interpolation='nearest') +ax.axis('off') +ax.set_title('elevation_map') """ .. image:: PLOT2RST.current_figure @@ -133,10 +129,10 @@ markers = np.zeros_like(coins) markers[coins < 30] = 1 markers[coins > 150] = 2 -plt.figure(figsize=(4, 3)) -plt.imshow(markers, cmap=plt.cm.spectral, interpolation='nearest') -plt.axis('off') -plt.title('markers') +fig, ax = plt.subplots(figsize=(4, 3)) +ax.imshow(markers, cmap=plt.cm.spectral, interpolation='nearest') +ax.axis('off') +ax.set_title('markers') """ .. image:: PLOT2RST.current_figure @@ -147,10 +143,10 @@ starting from the markers determined above: """ segmentation = morphology.watershed(elevation_map, markers) -plt.figure(figsize=(4, 3)) -plt.imshow(segmentation, cmap=plt.cm.gray, interpolation='nearest') -plt.axis('off') -plt.title('segmentation') +fig, ax = plt.subplots(figsize=(4, 3)) +ax.imshow(segmentation, cmap=plt.cm.gray, interpolation='nearest') +ax.axis('off') +ax.set_title('segmentation') """ .. image:: PLOT2RST.current_figure @@ -166,16 +162,14 @@ 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) -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(image_label_overlay, interpolation='nearest') -plt.axis('off') +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(6, 3)) +ax1.imshow(coins, cmap=plt.cm.gray, interpolation='nearest') +ax1.contour(segmentation, [0.5], linewidths=1.2, colors='y') +ax1.axis('off') +ax2.imshow(image_label_overlay, interpolation='nearest') +ax2.axis('off') -plt.subplots_adjust(**margins) +fig.subplots_adjust(**margins) """ .. image:: PLOT2RST.current_figure diff --git a/doc/examples/applications/plot_geometric.py b/doc/examples/applications/plot_geometric.py index 141ef51f..86dd5aaa 100644 --- a/doc/examples/applications/plot_geometric.py +++ b/doc/examples/applications/plot_geometric.py @@ -33,14 +33,14 @@ 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.params) """ Alternatively you can define a transformation by the transformation matrix itself: """ -matrix = tform._matrix.copy() +matrix = tform.params.copy() matrix[1, 2] = 2 tform2 = tf.SimilarityTransform(matrix) diff --git a/doc/examples/applications/plot_morphology.py b/doc/examples/applications/plot_morphology.py index 84cf2972..68ecc6d6 100644 --- a/doc/examples/applications/plot_morphology.py +++ b/doc/examples/applications/plot_morphology.py @@ -30,9 +30,9 @@ 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) +fig, ax = plt.subplots() +ax.imshow(phantom, cmap=plt.cm.gray) """ .. image:: PLOT2RST.current_figure @@ -43,10 +43,10 @@ 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.imshow(original, cmap=plt.cm.gray) ax1.set_title('original') ax1.axis('off') - ax2.imshow(filtered) + ax2.imshow(filtered, cmap=plt.cm.gray) ax2.set_title(filter_name) ax2.axis('off') diff --git a/doc/examples/applications/plot_rank_filters.py b/doc/examples/applications/plot_rank_filters.py index 87dad0d0..6530c6d6 100644 --- a/doc/examples/applications/plot_rank_filters.py +++ b/doc/examples/applications/plot_rank_filters.py @@ -44,13 +44,11 @@ from skimage import data 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(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') +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 3)) +ax1.imshow(noisy_image, interpolation='nearest', cmap=plt.cm.gray) +ax1.axis('off') +ax2.plot(hist[1][:-1], hist[0], lw=2) +ax2.set_title('Histogram of grey values') """ @@ -62,11 +60,6 @@ Noise removal Some noise is added to the image, 1% of pixels are randomly set to 255, 1% are randomly set to 0. The **median** filter is applied to remove the noise. -.. note:: - - There are different implementations of median filter: - `skimage.filter.median_filter` and `skimage.filter.rank.median` - """ from skimage.filter.rank import median @@ -77,27 +70,24 @@ 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)) +fig, ax = plt.subplots(2, 2, figsize=(10, 7)) +ax1, ax2, ax3, ax4 = ax.ravel() -plt.subplot(2, 2, 1) -plt.imshow(noisy_image, vmin=0, vmax=255) -plt.title('Noisy image') -plt.axis('off') +ax1.imshow(noisy_image, vmin=0, vmax=255, cmap=plt.cm.gray) +ax1.set_title('Noisy image') +ax1.axis('off') -plt.subplot(2, 2, 2) -plt.imshow(median(noisy_image, disk(1)), vmin=0, vmax=255) -plt.title('Median $r=1$') -plt.axis('off') +ax2.imshow(median(noisy_image, disk(1)), vmin=0, vmax=255, cmap=plt.cm.gray) +ax2.set_title('Median $r=1$') +ax2.axis('off') -plt.subplot(2, 2, 3) -plt.imshow(median(noisy_image, disk(5)), vmin=0, vmax=255) -plt.title('Median $r=5$') -plt.axis('off') +ax3.imshow(median(noisy_image, disk(5)), vmin=0, vmax=255, cmap=plt.cm.gray) +ax3.set_title('Median $r=5$') +ax3.axis('off') -plt.subplot(2, 2, 4) -plt.imshow(median(noisy_image, disk(20)), vmin=0, vmax=255) -plt.title('Median $r=20$') -plt.axis('off') +ax4.imshow(median(noisy_image, disk(20)), vmin=0, vmax=255, cmap=plt.cm.gray) +ax4.set_title('Median $r=20$') +ax4.axis('off') """ @@ -119,19 +109,17 @@ image. from skimage.filter.rank import mean -fig = plt.figure(figsize=[10, 7]) +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=[10, 7]) loc_mean = mean(noisy_image, disk(10)) -plt.subplot(1, 2, 1) -plt.imshow(noisy_image, vmin=0, vmax=255) -plt.title('Original') -plt.axis('off') +ax1.imshow(noisy_image, vmin=0, vmax=255, cmap=plt.cm.gray) +ax1.set_title('Original') +ax1.axis('off') -plt.subplot(1, 2, 2) -plt.imshow(loc_mean, vmin=0, vmax=255) -plt.title('Local mean $r=10$') -plt.axis('off') +ax2.imshow(loc_mean, vmin=0, vmax=255, cmap=plt.cm.gray) +ax2.set_title('Local mean $r=10$') +ax2.axis('off') """ @@ -149,31 +137,28 @@ the central one. """ -from skimage.filter.rank import bilateral_mean +from skimage.filter.rank import mean_bilateral noisy_image = img_as_ubyte(data.camera()) -bilat = bilateral_mean(noisy_image.astype(np.uint16), disk(20), s0=10, s1=10) +bilat = mean_bilateral(noisy_image.astype(np.uint16), disk(20), s0=10, s1=10) -fig = plt.figure(figsize=[10, 7]) +fig, ax = plt.subplots(2, 2, figsize=(10, 7)) +ax1, ax2, ax3, ax4 = ax.ravel() -plt.subplot(2, 2, 1) -plt.imshow(noisy_image, cmap=plt.cm.gray) -plt.title('Original') -plt.axis('off') +ax1.imshow(noisy_image, cmap=plt.cm.gray) +ax1.set_title('Original') +ax1.axis('off') -plt.subplot(2, 2, 3) -plt.imshow(bilat, cmap=plt.cm.gray) -plt.title('Bilateral mean') -plt.axis('off') +ax2.imshow(bilat, cmap=plt.cm.gray) +ax2.set_title('Bilateral mean') +ax2.axis('off') -plt.subplot(2, 2, 2) -plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) -plt.axis('off') +ax3.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +ax3.axis('off') -plt.subplot(2, 2, 4) -plt.imshow(bilat[200:350, 350:450], cmap=plt.cm.gray) -plt.axis('off') +ax4.imshow(bilat[200:350, 350:450], cmap=plt.cm.gray) +ax4.axis('off') """ @@ -203,7 +188,7 @@ from skimage.filter import rank noisy_image = img_as_ubyte(data.camera()) # equalize globally and locally -glob = exposure.equalize(noisy_image) * 255 +glob = exposure.equalize_hist(noisy_image) * 255 loc = rank.equalize(noisy_image, disk(20)) # extract histogram for each image @@ -211,31 +196,26 @@ 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)) +fig, ax = plt.subplots(3, 2, figsize=(10, 10)) +ax1, ax2, ax3, ax4, ax5, ax6 = ax.ravel() -plt.subplot(321) -plt.imshow(noisy_image, interpolation='nearest') -plt.axis('off') +ax1.imshow(noisy_image, interpolation='nearest', cmap=plt.cm.gray) +ax1.axis('off') -plt.subplot(322) -plt.plot(hist[1][:-1], hist[0], lw=2) -plt.title('Histogram of gray values') +ax2.plot(hist[1][:-1], hist[0], lw=2) +ax2.set_title('Histogram of gray values') -plt.subplot(323) -plt.imshow(glob, interpolation='nearest') -plt.axis('off') +ax3.imshow(glob, interpolation='nearest', cmap=plt.cm.gray) +ax3.axis('off') -plt.subplot(324) -plt.plot(glob_hist[1][:-1], glob_hist[0], lw=2) -plt.title('Histogram of gray values') +ax4.plot(glob_hist[1][:-1], glob_hist[0], lw=2) +ax4.set_title('Histogram of gray values') -plt.subplot(325) -plt.imshow(loc, interpolation='nearest') -plt.axis('off') +ax5.imshow(loc, interpolation='nearest', cmap=plt.cm.gray) +ax5.axis('off') -plt.subplot(326) -plt.plot(loc_hist[1][:-1], loc_hist[0], lw=2) -plt.title('Histogram of gray values') +ax6.plot(loc_hist[1][:-1], loc_hist[0], lw=2) +ax6.set_title('Histogram of gray values') """ @@ -256,17 +236,15 @@ noisy_image = img_as_ubyte(data.camera()) auto = autolevel(noisy_image.astype(np.uint16), disk(20)) -fig = plt.figure(figsize=[10, 7]) +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=[10, 7]) -plt.subplot(1, 2, 1) -plt.imshow(noisy_image, cmap=plt.cm.gray) -plt.title('Original') -plt.axis('off') +ax1.imshow(noisy_image, cmap=plt.cm.gray) +ax1.set_title('Original') +ax1.axis('off') -plt.subplot(1, 2, 2) -plt.imshow(auto, cmap=plt.cm.gray) -plt.title('Local autolevel') -plt.axis('off') +ax2.imshow(auto, cmap=plt.cm.gray) +ax2.set_title('Local autolevel') +ax2.axis('off') """ @@ -297,7 +275,7 @@ fig, axes = plt.subplots(nrows=3, figsize=(7, 8)) ax0, ax1, ax2 = axes plt.gray() -ax0.imshow(np.hstack((image, loc_autolevel))) +ax0.imshow(np.hstack((image, loc_autolevel)), cmap=plt.cm.gray) ax0.set_title('Original / auto-level') ax1.imshow( @@ -326,24 +304,22 @@ noisy_image = img_as_ubyte(data.camera()) enh = enhance_contrast(noisy_image, disk(5)) -fig = plt.figure(figsize=[10, 7]) -plt.subplot(2, 2, 1) -plt.imshow(noisy_image, cmap=plt.cm.gray) -plt.title('Original') -plt.axis('off') +fig, ax = plt.subplots(2, 2, figsize=[10, 7]) +ax1, ax2, ax3, ax4 = ax.ravel() -plt.subplot(2, 2, 3) -plt.imshow(enh, cmap=plt.cm.gray) -plt.title('Local morphological contrast enhancement') -plt.axis('off') +ax1.imshow(noisy_image, cmap=plt.cm.gray) +ax1.set_title('Original') +ax1.axis('off') -plt.subplot(2, 2, 2) -plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) -plt.axis('off') +ax2.imshow(enh, cmap=plt.cm.gray) +ax2.set_title('Local morphological contrast enhancement') +ax2.axis('off') -plt.subplot(2, 2, 4) -plt.imshow(enh[200:350, 350:450], cmap=plt.cm.gray) -plt.axis('off') +ax3.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +ax3.axis('off') + +ax4.imshow(enh[200:350, 350:450], cmap=plt.cm.gray) +ax4.axis('off') """ @@ -360,24 +336,22 @@ noisy_image = img_as_ubyte(data.camera()) penh = enhance_contrast_percentile(noisy_image, disk(5), p0=.1, p1=.9) -fig = plt.figure(figsize=[10, 7]) -plt.subplot(2, 2, 1) -plt.imshow(noisy_image, cmap=plt.cm.gray) -plt.title('Original') -plt.axis('off') +fig, ax = plt.subplots(2, 2, figsize=[10, 7]) +ax1, ax2, ax3, ax4 = ax.ravel() -plt.subplot(2, 2, 3) -plt.imshow(penh, cmap=plt.cm.gray) -plt.title('Local percentile morphological\n contrast enhancement') -plt.axis('off') +ax1.imshow(noisy_image, cmap=plt.cm.gray) +ax1.set_title('Original') +ax1.axis('off') -plt.subplot(2, 2, 2) -plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) -plt.axis('off') +ax2.imshow(penh, cmap=plt.cm.gray) +ax2.set_title('Local percentile morphological\n contrast enhancement') +ax2.axis('off') -plt.subplot(2, 2, 4) -plt.imshow(penh[200:350, 350:450], cmap=plt.cm.gray) -plt.axis('off') +ax3.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +ax3.axis('off') + +ax4.imshow(penh[200:350, 350:450], cmap=plt.cm.gray) +ax4.axis('off') """ @@ -419,29 +393,24 @@ loc_otsu = p8 >= t_loc_otsu t_glob_otsu = threshold_otsu(p8) glob_otsu = p8 >= t_glob_otsu -plt.figure() +fig, ax = plt.subplots(2, 2) +ax1, ax2, ax3, ax4 = ax.ravel() -plt.subplot(2, 2, 1) -plt.imshow(p8, cmap=plt.cm.gray) -plt.title('Original') -plt.colorbar() -plt.axis('off') +fig.colorbar(ax1.imshow(p8, cmap=plt.cm.gray), ax=ax1) +ax1.set_title('Original') +ax1.axis('off') -plt.subplot(2, 2, 2) -plt.imshow(t_loc_otsu, cmap=plt.cm.gray) -plt.title('Local Otsu ($r=%d$)' % radius) -plt.colorbar() -plt.axis('off') +fig.colorbar(ax2.imshow(t_loc_otsu, cmap=plt.cm.gray), ax=ax2) +ax2.set_title('Local Otsu ($r=%d$)' % radius) +ax2.axis('off') -plt.subplot(2, 2, 3) -plt.imshow(p8 >= t_loc_otsu, cmap=plt.cm.gray) -plt.title('Original >= local Otsu' % t_glob_otsu) -plt.axis('off') +ax3.imshow(p8 >= t_loc_otsu, cmap=plt.cm.gray) +ax3.set_title('Original >= local Otsu' % t_glob_otsu) +ax3.axis('off') -plt.subplot(2, 2, 4) -plt.imshow(glob_otsu, cmap=plt.cm.gray) -plt.title('Global Otsu ($t=%d$)' % t_glob_otsu) -plt.axis('off') +ax4.imshow(glob_otsu, cmap=plt.cm.gray) +ax4.set_title('Global Otsu ($t=%d$)' % t_glob_otsu) +ax4.axis('off') """ @@ -460,17 +429,15 @@ 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() +fig, (ax1, ax2) = plt.subplots(1, 2) -plt.subplot(1, 2, 1) -plt.imshow(m) -plt.title('Original') -plt.axis('off') +ax1.imshow(m) +ax1.set_title('Original') +ax1.axis('off') -plt.subplot(1, 2, 2) -plt.imshow(m >= t, interpolation='nearest') -plt.title('Local Otsu ($r=%d$)' % radius) -plt.axis('off') +ax2.imshow(m >= t, interpolation='nearest') +ax2.set_title('Local Otsu ($r=%d$)' % radius) +ax2.axis('off') """ @@ -501,27 +468,24 @@ opening = minimum(maximum(noisy_image, disk(5)), disk(5)) grad = gradient(noisy_image, disk(5)) # display results -fig = plt.figure(figsize=[10, 7]) +fig, ax = plt.subplots(2, 2, figsize=[10, 7]) +ax1, ax2, ax3, ax4 = ax.ravel() -plt.subplot(2, 2, 1) -plt.imshow(noisy_image, cmap=plt.cm.gray) -plt.title('Original') -plt.axis('off') +ax1.imshow(noisy_image, cmap=plt.cm.gray) +ax1.set_title('Original') +ax1.axis('off') -plt.subplot(2, 2, 2) -plt.imshow(closing, cmap=plt.cm.gray) -plt.title('Gray-level closing') -plt.axis('off') +ax2.imshow(closing, cmap=plt.cm.gray) +ax2.set_title('Gray-level closing') +ax2.axis('off') -plt.subplot(2, 2, 3) -plt.imshow(opening, cmap=plt.cm.gray) -plt.title('Gray-level opening') -plt.axis('off') +ax3.imshow(opening, cmap=plt.cm.gray) +ax3.set_title('Gray-level opening') +ax3.axis('off') -plt.subplot(2, 2, 4) -plt.imshow(grad, cmap=plt.cm.gray) -plt.title('Morphological gradient') -plt.axis('off') +ax4.imshow(grad, cmap=plt.cm.gray) +ax4.set_title('Morphological gradient') +ax4.axis('off') """ @@ -554,19 +518,15 @@ import matplotlib.pyplot as plt image = data.camera() -plt.figure(figsize=(10, 4)) +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) -plt.subplot(1, 2, 1) -plt.imshow(image, cmap=plt.cm.gray) -plt.title('Image') -plt.colorbar() -plt.axis('off') +fig.colorbar(ax1.imshow(image, cmap=plt.cm.gray), ax=ax1) +ax1.set_title('Image') +ax1.axis('off') -plt.subplot(1, 2, 2) -plt.imshow(entropy(image, disk(5)), cmap=plt.cm.jet) -plt.title('Entropy') -plt.colorbar() -plt.axis('off') +fig.colorbar(ax2.imshow(entropy(image, disk(5)), cmap=plt.cm.jet), ax=ax2) +ax2.set_title('Entropy') +ax2.axis('off') """ @@ -589,7 +549,6 @@ from time import time from scipy.ndimage.filters import percentile_filter from skimage.morphology import dilation -from skimage.filter import median_filter from skimage.filter.rank import median, maximum @@ -619,11 +578,6 @@ def cm_dil(image, selem): return dilation(image=image, selem=selem) -@exec_and_timeit -def ctmf_med(image, radius): - return median_filter(image=image, radius=radius) - - @exec_and_timeit def ndi_med(image, n): return percentile_filter(image, 50, size=n * 2 - 1) @@ -651,12 +605,12 @@ for r in e_range: rec = np.asarray(rec) -plt.figure() -plt.title('Performance with respect to element size') -plt.ylabel('Time (ms)') -plt.title('Element radius') -plt.plot(e_range, rec) -plt.legend(['filter.rank.maximum', 'morphology.dilate']) +fig, ax = plt.subplots() +ax.set_title('Performance with respect to element size') +ax.set_ylabel('Time (ms)') +ax.set_xlabel('Element radius') +ax.plot(e_range, rec) +ax.legend(['filter.rank.maximum', 'morphology.dilate']) """ @@ -679,12 +633,12 @@ for s in s_range: rec = np.asarray(rec) -plt.figure() -plt.title('Performance with respect to image size') -plt.ylabel('Time (ms)') -plt.title('Image size') -plt.plot(s_range, rec) -plt.legend(['filter.rank.maximum', 'morphology.dilate']) +fig, ax = plt.subplots() +ax.set_title('Performance with respect to image size') +ax.set_ylabel('Time (ms)') +ax.set_xlabel('Image size') +ax.plot(s_range, rec) +ax.legend(['filter.rank.maximum', 'morphology.dilate']) """ @@ -694,7 +648,6 @@ plt.legend(['filter.rank.maximum', 'morphology.dilate']) Comparison between: * `filter.rank.median` -* `filter.median_filter` * `scipy.ndimage.percentile` on increasing structuring element size: @@ -708,19 +661,17 @@ e_range = range(2, 30, 4) for r in e_range: elem = disk(r + 1) rc, ms_rc = cr_med(a, elem) - rctmf, ms_rctmf = ctmf_med(a, r) rndi, ms_ndi = ndi_med(a, r) - rec.append((ms_rc, ms_rctmf, ms_ndi)) + rec.append((ms_rc, ms_ndi)) rec = np.asarray(rec) -plt.figure() -plt.title('Performance with respect to element size') -plt.plot(e_range, rec) -plt.legend(['filter.rank.median', 'filter.median_filter', - 'scipy.ndimage.percentile']) -plt.ylabel('Time (ms)') -plt.title('Element radius') +fig, ax = plt.subplots() +ax.set_title('Performance with respect to element size') +ax.plot(e_range, rec) +ax.legend(['filter.rank.median', 'scipy.ndimage.percentile']) +ax.set_ylabel('Time (ms)') +ax.set_xlabel('Element radius') """ .. image:: PLOT2RST.current_figure @@ -729,10 +680,10 @@ Comparison of outcome of the three methods: """ -plt.figure() -plt.imshow(np.hstack((rc, rctmf, rndi))) -plt.title('filter.rank.median vs filtermedian_filter vs scipy.ndimage.percentile') -plt.axis('off') +fig, ax = plt.subplots() +ax.imshow(np.hstack((rc, rndi))) +ax.set_title('filter.rank.median vs. scipy.ndimage.percentile') +ax.axis('off') """ .. image:: PLOT2RST.current_figure @@ -749,19 +700,17 @@ s_range = [100, 200, 500, 1000] for s in s_range: 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) - rec.append((ms_rc, ms_rctmf, ms_ndi)) + rec.append((ms_rc, ms_ndi)) rec = np.asarray(rec) -plt.figure() -plt.title('Performance with respect to image size') -plt.plot(s_range, rec) -plt.legend(['filter.rank.median', 'filter.median_filter', - 'scipy.ndimage.percentile']) -plt.ylabel('Time (ms)') -plt.title('Image size') +fig, ax = plt.subplots() +ax.set_title('Performance with respect to image size') +ax.plot(s_range, rec) +ax.legend(['filter.rank.median', 'scipy.ndimage.percentile']) +ax.set_ylabel('Time (ms)') +ax.set_xlabel('Image size') """ .. image:: PLOT2RST.current_figure diff --git a/doc/examples/plot_blob.py b/doc/examples/plot_blob.py new file mode 100644 index 00000000..33cc44c2 --- /dev/null +++ b/doc/examples/plot_blob.py @@ -0,0 +1,73 @@ +""" +============== +Blob Detection +============== + +Blobs are bright on dark or dark on bright regions in an image. In +this example, blobs are detected using 3 algorithms. The image used +in this case is the Hubble eXtreme Deep Field. Each bright dot in the +image is a star or a galaxy. + +Laplacian of Gaussian (LoG) +----------------------------- +This is the most accurate and slowest approach. It computes the Laplacian +of Gaussian images with successively increasing standard deviation and +stacks them up in a cube. Blobs are local maximas in this cube. Detecting +larger blobs is especially slower because of larger kernel sizes during +convolution. Only bright blobs on dark backgrounds are detected. See +:py:meth:`skimage.feature.blob_log` for usage. + +Difference of Gaussian (DoG) +---------------------------- +This is a faster approximation of LoG approach. In this case the image is +blurred with increasing standard deviations and the difference between +two successively blurred images are stacked up in a cube. This method +suffers from the same disadvantage as LoG approach for detecting larger +blobs. Blobs are again assumed to be bright on dark. See +:py:meth:`skimage.feature.blob_dog` for usage. + +Determinant of Hessian (DoH) +---------------------------- +This is the fastest approach. It detects blobs by finding maximas in the +matrix of the Determinant of Hessian of the image. The detection speed is +independent of the size of blobs as internally the implementation uses +box filters instead of convolutions. Bright on dark as well as dark on +bright blobs are detected. The downside is that small blobs (<3px) are not +detected accurately. See :py:meth:`skimage.feature.blob_doh` for usage. + +""" + +from matplotlib import pyplot as plt +from skimage import data +from skimage.feature import blob_dog, blob_log, blob_doh +from math import sqrt +from skimage.color import rgb2gray + +image = data.hubble_deep_field()[0:500, 0:500] +image_gray = rgb2gray(image) + +blobs_log = blob_log(image_gray, max_sigma=30, num_sigma=10, threshold=.1) +# Compute radii in the 3rd column. +blobs_log[:, 2] = blobs_log[:, 2] * sqrt(2) + +blobs_dog = blob_dog(image_gray, max_sigma=30, threshold=.1) +blobs_dog[:, 2] = blobs_dog[:, 2] * sqrt(2) + +blobs_doh = blob_doh(image_gray, max_sigma=30, threshold=.01) + +blobs_list = [blobs_log, blobs_dog, blobs_doh] +colors = ['yellow', 'lime', 'red'] +titles = ['Laplacian of Gaussian', 'Difference of Gaussian', + 'Determinant of Hessian'] +sequence = zip(blobs_list, colors, titles) + +for blobs, color, title in sequence: + fig, ax = plt.subplots(1, 1) + ax.set_title(title) + ax.imshow(image, interpolation='nearest') + for blob in blobs: + y, x, r = blob + c = plt.Circle((x, y), r, color=color, linewidth=2, fill=False) + ax.add_patch(c) + +plt.show() diff --git a/doc/examples/plot_brief.py b/doc/examples/plot_brief.py new file mode 100644 index 00000000..47c9ad5f --- /dev/null +++ b/doc/examples/plot_brief.py @@ -0,0 +1,61 @@ +""" +======================= +BRIEF binary descriptor +======================= + +This example demonstrates the BRIEF binary description algorithm. + +The descriptor consists of relatively few bits and can be computed using +a set of intensity difference tests. The short binary descriptor results +in low memory footprint and very efficient matching based on the Hamming +distance metric. + +BRIEF does not provide rotation-invariance. Scale-invariance can be achieved by +detecting and extracting features at different scales. + +""" +from skimage import data +from skimage import transform as tf +from skimage.feature import (match_descriptors, corner_peaks, corner_harris, + plot_matches, BRIEF) +from skimage.color import rgb2gray +import matplotlib.pyplot as plt + + +img1 = rgb2gray(data.lena()) +tform = tf.AffineTransform(scale=(1.2, 1.2), translation=(0, -100)) +img2 = tf.warp(img1, tform) +img3 = tf.rotate(img1, 25) + +keypoints1 = corner_peaks(corner_harris(img1), min_distance=5) +keypoints2 = corner_peaks(corner_harris(img2), min_distance=5) +keypoints3 = corner_peaks(corner_harris(img3), min_distance=5) + +extractor = BRIEF() + +extractor.extract(img1, keypoints1) +keypoints1 = keypoints1[extractor.mask] +descriptors1 = extractor.descriptors + +extractor.extract(img2, keypoints2) +keypoints2 = keypoints2[extractor.mask] +descriptors2 = extractor.descriptors + +extractor.extract(img3, keypoints3) +keypoints3 = keypoints3[extractor.mask] +descriptors3 = extractor.descriptors + +matches12 = match_descriptors(descriptors1, descriptors2, cross_check=True) +matches13 = match_descriptors(descriptors1, descriptors3, cross_check=True) + +fig, ax = plt.subplots(nrows=2, ncols=1) + +plt.gray() + +plot_matches(ax[0], img1, img2, keypoints1, keypoints2, matches12) +ax[0].axis('off') + +plot_matches(ax[1], img1, img3, keypoints1, keypoints3, matches13) +ax[1].axis('off') + +plt.show() diff --git a/doc/examples/plot_canny.py b/doc/examples/plot_canny.py index f8269d70..f1caf264 100644 --- a/doc/examples/plot_canny.py +++ b/doc/examples/plot_canny.py @@ -35,24 +35,21 @@ edges1 = filter.canny(im) edges2 = filter.canny(im, sigma=3) # display results -plt.figure(figsize=(8, 3)) +fig, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3, figsize=(8, 3)) -plt.subplot(131) -plt.imshow(im, cmap=plt.cm.jet) -plt.axis('off') -plt.title('noisy image', fontsize=20) +ax1.imshow(im, cmap=plt.cm.jet) +ax1.axis('off') +ax1.set_title('noisy image', fontsize=20) -plt.subplot(132) -plt.imshow(edges1, cmap=plt.cm.gray) -plt.axis('off') -plt.title('Canny filter, $\sigma=1$', fontsize=20) +ax2.imshow(edges1, cmap=plt.cm.gray) +ax2.axis('off') +ax2.set_title('Canny filter, $\sigma=1$', fontsize=20) -plt.subplot(133) -plt.imshow(edges2, cmap=plt.cm.gray) -plt.axis('off') -plt.title('Canny filter, $\sigma=3$', fontsize=20) +ax3.imshow(edges2, cmap=plt.cm.gray) +ax3.axis('off') +ax3.set_title('Canny filter, $\sigma=3$', fontsize=20) -plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, +fig.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_censure.py b/doc/examples/plot_censure.py new file mode 100644 index 00000000..c7d70ea5 --- /dev/null +++ b/doc/examples/plot_censure.py @@ -0,0 +1,43 @@ +""" +======================== +CENSURE feature detector +======================== + +The CENSURE feature detector is a scale-invariant center-surround detector +(CENSURE) that claims to outperform other detectors and is capable of real-time +implementation. + +""" +from skimage import data +from skimage import transform as tf +from skimage.feature import CENSURE +from skimage.color import rgb2gray +import matplotlib.pyplot as plt + + +img1 = rgb2gray(data.lena()) +tform = tf.AffineTransform(scale=(1.5, 1.5), rotation=0.5, + translation=(150, -200)) +img2 = tf.warp(img1, tform) + +detector = CENSURE() + +fig, ax = plt.subplots(nrows=1, ncols=2) + +plt.gray() + +detector.detect(img1) + +ax[0].imshow(img1) +ax[0].axis('off') +ax[0].scatter(detector.keypoints[:, 1], detector.keypoints[:, 0], + 2 ** detector.scales, facecolors='none', edgecolors='r') + +detector.detect(img2) + +ax[1].imshow(img2) +ax[1].axis('off') +ax[1].scatter(detector.keypoints[:, 1], detector.keypoints[:, 0], + 2 ** detector.scales, facecolors='none', edgecolors='r') + +plt.show() diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index 7fb67046..fbdd4f2c 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -48,7 +48,7 @@ from skimage.util import img_as_ubyte 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)) +fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(5, 2)) # Detect two radii hough_radii = np.arange(15, 30, 2) @@ -77,6 +77,8 @@ ax.imshow(image, cmap=plt.cm.gray) """ +.. image:: PLOT2RST.current_figure + Ellipse detection ================= @@ -137,7 +139,7 @@ image_rgb[cy, cx] = (0, 0, 255) edges = color.gray2rgb(edges) edges[cy, cx] = (250, 0, 0) -fig2, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(10, 6)) +fig2, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(8, 4)) ax1.set_title('Original picture') ax1.imshow(image_rgb) @@ -146,3 +148,8 @@ ax2.set_title('Edge (white) and result (red)') ax2.imshow(edges) plt.show() + +""" +.. image:: PLOT2RST.current_figure + +""" diff --git a/doc/examples/plot_contours.py b/doc/examples/plot_contours.py index d23b2ae5..e686b4ac 100644 --- a/doc/examples/plot_contours.py +++ b/doc/examples/plot_contours.py @@ -29,12 +29,13 @@ r = np.sin(np.exp((np.sin(x)**3 + np.cos(y)**2))) contours = measure.find_contours(r, 0.8) # Display the image and plot all contours found -plt.imshow(r, interpolation='nearest') +fig, ax = plt.subplots() +ax.imshow(r, interpolation='nearest', cmap=plt.cm.gray) for n, contour in enumerate(contours): - plt.plot(contour[:, 1], contour[:, 0], linewidth=2) + ax.plot(contour[:, 1], contour[:, 0], linewidth=2) -plt.axis('image') -plt.xticks([]) -plt.yticks([]) +ax.axis('image') +ax.set_xticks([]) +ax.set_yticks([]) plt.show() diff --git a/doc/examples/plot_convex_hull.py b/doc/examples/plot_convex_hull.py index 31398e6d..e811c1ee 100644 --- a/doc/examples/plot_convex_hull.py +++ b/doc/examples/plot_convex_hull.py @@ -40,11 +40,9 @@ image[chull] += 1 # [ 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') +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 6)) +ax1.set_title('Original picture') +ax1.imshow(original_image, cmap=plt.cm.gray, interpolation='nearest') +ax2.set_title('Transformed picture') +ax2.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 a3233188..4e962e17 100644 --- a/doc/examples/plot_corner.py +++ b/doc/examples/plot_corner.py @@ -29,9 +29,9 @@ image[230:280, 60:110] = 1 coords = corner_peaks(corner_harris(image), min_distance=5) coords_subpix = corner_subpix(image, coords, window_size=13) -plt.gray() -plt.imshow(image, interpolation='nearest') -plt.plot(coords[:, 1], coords[:, 0], '.b', markersize=3) -plt.plot(coords_subpix[:, 1], coords_subpix[:, 0], '+r', markersize=15) -plt.axis((0, 350, 350, 0)) +fig, ax = plt.subplots() +ax.imshow(image, interpolation='nearest', cmap=plt.cm.gray) +ax.plot(coords[:, 1], coords[:, 0], '.b', markersize=3) +ax.plot(coords_subpix[:, 1], coords_subpix[:, 0], '+r', markersize=15) +ax.axis((0, 350, 350, 0)) plt.show() diff --git a/doc/examples/plot_daisy.py b/doc/examples/plot_daisy.py index fbde5c8d..128d6e67 100644 --- a/doc/examples/plot_daisy.py +++ b/doc/examples/plot_daisy.py @@ -20,8 +20,9 @@ img = data.camera() descs, descs_img = daisy(img, step=180, radius=58, rings=2, histograms=6, orientations=8, visualize=True) -plt.axis('off') -plt.imshow(descs_img) +fig, ax = plt.subplots() +ax.axis('off') +ax.imshow(descs_img) descs_num = descs.shape[0] * descs.shape[1] -plt.title('%i DAISY descriptors extracted:' % descs_num) +ax.set_title('%i DAISY descriptors extracted:' % descs_num) plt.show() diff --git a/doc/examples/plot_denoise.py b/doc/examples/plot_denoise.py index 200036ae..3a05c99f 100644 --- a/doc/examples/plot_denoise.py +++ b/doc/examples/plot_denoise.py @@ -29,7 +29,7 @@ import numpy as np import matplotlib.pyplot as plt from skimage import data, img_as_float -from skimage.filter import denoise_tv_chambolle, denoise_bilateral +from skimage.restoration import denoise_tv_chambolle, denoise_bilateral lena = img_as_float(data.lena()) diff --git a/doc/examples/plot_entropy.py b/doc/examples/plot_entropy.py index 9f208c73..b92a5bf3 100644 --- a/doc/examples/plot_entropy.py +++ b/doc/examples/plot_entropy.py @@ -22,11 +22,11 @@ fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10, 4)) img0 = ax0.imshow(image, cmap=plt.cm.gray) ax0.set_title('Image') ax0.axis('off') -plt.colorbar(img0, ax=ax0) +fig.colorbar(img0, ax=ax0) img1 = ax1.imshow(entropy(image, disk(5)), cmap=plt.cm.jet) ax1.set_title('Entropy') ax1.axis('off') -plt.colorbar(img1, ax=ax1) +fig.colorbar(img1, ax=ax1) plt.show() diff --git a/doc/examples/plot_equalize.py b/doc/examples/plot_equalize.py index 52121e3a..2d04e22a 100644 --- a/doc/examples/plot_equalize.py +++ b/doc/examples/plot_equalize.py @@ -17,6 +17,8 @@ that fall within the 2nd and 98th percentiles [2]_. .. [2] http://homepages.inf.ed.ac.uk/rbf/HIPR2/stretch.htm """ + +import matplotlib import matplotlib.pyplot as plt import numpy as np @@ -24,6 +26,9 @@ from skimage import data, img_as_float from skimage import exposure +matplotlib.rcParams['font.size'] = 8 + + def plot_img_and_hist(img, axes, bins=256): """Plot an image along with its histogram and cumulative histogram. @@ -55,8 +60,7 @@ def plot_img_and_hist(img, axes, bins=256): img = data.moon() # Contrast stretching -p2 = np.percentile(img, 2) -p98 = np.percentile(img, 98) +p2, p98 = np.percentile(img, (2, 98)) img_rescale = exposure.rescale_intensity(img, in_range=(p2, p98)) # Equalization @@ -66,7 +70,7 @@ img_eq = exposure.equalize_hist(img) img_adapteq = exposure.equalize_adapthist(img, clip_limit=0.03) # Display results -f, axes = plt.subplots(2, 4, figsize=(8, 4)) +fig, axes = plt.subplots(nrows=2, ncols=4, figsize=(8, 5)) ax_img, ax_hist, ax_cdf = plot_img_and_hist(img, axes[:, 0]) ax_img.set_title('Low contrast image') @@ -88,5 +92,5 @@ ax_cdf.set_ylabel('Fraction of total intensity') ax_cdf.set_yticks(np.linspace(0, 1, 5)) # prevent overlap of y-axis labels -plt.subplots_adjust(wspace=0.4) +fig.subplots_adjust(wspace=0.4) plt.show() diff --git a/doc/examples/plot_gabor.py b/doc/examples/plot_gabor.py index db1ff7d6..87d99589 100644 --- a/doc/examples/plot_gabor.py +++ b/doc/examples/plot_gabor.py @@ -24,9 +24,6 @@ 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): @@ -104,24 +101,24 @@ for theta in (0, 1): # 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)) +fig, axes = plt.subplots(nrows=5, ncols=4, figsize=(5, 6)) plt.gray() -fig.suptitle('Image responses for Gabor filter kernels', fontsize=15) +fig.suptitle('Image responses for Gabor filter kernels', fontsize=12) 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.set_title(label, fontsize=9) 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_ylabel(label, fontsize=7) ax.set_xticks([]) ax.set_yticks([]) diff --git a/doc/examples/plot_glcm.py b/doc/examples/plot_glcm.py index 7ee54d57..af9b4168 100644 --- a/doc/examples/plot_glcm.py +++ b/doc/examples/plot_glcm.py @@ -53,44 +53,45 @@ for i, patch in enumerate(grass_patches + sky_patches): ys.append(greycoprops(glcm, 'correlation')[0, 0]) # create the figure -plt.figure(figsize=(8, 8)) +fig = plt.figure(figsize=(8, 8)) + +# display original image with locations of patches +ax = fig.add_subplot(3, 2, 1) +ax.imshow(image, cmap=plt.cm.gray, interpolation='nearest', + vmin=0, vmax=255) +for (y, x) in grass_locations: + ax.plot(x + PATCH_SIZE / 2, y + PATCH_SIZE / 2, 'gs') +for (y, x) in sky_locations: + ax.plot(x + PATCH_SIZE / 2, y + PATCH_SIZE / 2, 'bs') +ax.set_xlabel('Original Image') +ax.set_xticks([]) +ax.set_yticks([]) +ax.axis('image') + +# for each patch, plot (dissimilarity, correlation) +ax = fig.add_subplot(3, 2, 2) +ax.plot(xs[:len(grass_patches)], ys[:len(grass_patches)], 'go', + label='Grass') +ax.plot(xs[len(grass_patches):], ys[len(grass_patches):], 'bo', + label='Sky') +ax.set_xlabel('GLCM Dissimilarity') +ax.set_ylabel('GLVM Correlation') +ax.legend() # display the image patches for i, patch in enumerate(grass_patches): - plt.subplot(3, len(grass_patches), len(grass_patches) * 1 + i + 1) - plt.imshow(patch, cmap=plt.cm.gray, interpolation='nearest', - vmin=0, vmax=255) - plt.xlabel('Grass %d' % (i + 1)) + ax = fig.add_subplot(3, len(grass_patches), len(grass_patches)*1 + i + 1) + ax.imshow(patch, cmap=plt.cm.gray, interpolation='nearest', + vmin=0, vmax=255) + ax.set_xlabel('Grass %d' % (i + 1)) for i, patch in enumerate(sky_patches): - plt.subplot(3, len(grass_patches), len(grass_patches) * 2 + i + 1) - plt.imshow(patch, cmap=plt.cm.gray, interpolation='nearest', - vmin=0, vmax=255) - plt.xlabel('Sky %d' % (i + 1)) + ax = fig.add_subplot(3, len(sky_patches), len(sky_patches)*2 + i + 1) + ax.imshow(patch, cmap=plt.cm.gray, interpolation='nearest', + vmin=0, vmax=255) + ax.set_xlabel('Sky %d' % (i + 1)) -# display original image with locations of patches -plt.subplot(3, 2, 1) -plt.imshow(image, cmap=plt.cm.gray, interpolation='nearest', - vmin=0, vmax=255) -for (y, x) in grass_locations: - plt.plot(x + PATCH_SIZE / 2, y + PATCH_SIZE / 2, 'gs') -for (y, x) in sky_locations: - plt.plot(x + PATCH_SIZE / 2, y + PATCH_SIZE / 2, 'bs') -plt.xlabel('Original Image') -plt.xticks([]) -plt.yticks([]) -plt.axis('image') - -# for each patch, plot (dissimilarity, correlation) -plt.subplot(3, 2, 2) -plt.plot(xs[:len(grass_patches)], ys[:len(grass_patches)], 'go', - label='Grass') -plt.plot(xs[len(grass_patches):], ys[len(grass_patches):], 'bo', - label='Sky') -plt.xlabel('GLCM Dissimilarity') -plt.ylabel('GLVM Correlation') -plt.legend() # display the patches and plot -plt.suptitle('Grey level co-occurrence matrix features', fontsize=14) +fig.suptitle('Grey level co-occurrence matrix features', fontsize=14) plt.show() diff --git a/doc/examples/plot_hog.py b/doc/examples/plot_hog.py index 4a339a1b..4899c590 100644 --- a/doc/examples/plot_hog.py +++ b/doc/examples/plot_hog.py @@ -90,16 +90,16 @@ image = color.rgb2gray(data.lena()) fd, hog_image = hog(image, orientations=8, pixels_per_cell=(16, 16), cells_per_block=(1, 1), visualise=True) -plt.figure(figsize=(8, 4)) +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4)) -plt.subplot(121).set_axis_off() -plt.imshow(image, cmap=plt.cm.gray) -plt.title('Input image') +ax1.axis('off') +ax1.imshow(image, cmap=plt.cm.gray) +ax1.set_title('Input image') # Rescale histogram for better display hog_image_rescaled = exposure.rescale_intensity(hog_image, in_range=(0, 0.02)) -plt.subplot(122).set_axis_off() -plt.imshow(hog_image_rescaled, cmap=plt.cm.gray) -plt.title('Histogram of Oriented Gradients') +ax2.axis('off') +ax2.imshow(hog_image_rescaled, cmap=plt.cm.gray) +ax2.set_title('Histogram of Oriented Gradients') plt.show() diff --git a/doc/examples/plot_holes_and_peaks.py b/doc/examples/plot_holes_and_peaks.py index e40b777e..791c69ee 100644 --- a/doc/examples/plot_holes_and_peaks.py +++ b/doc/examples/plot_holes_and_peaks.py @@ -22,13 +22,13 @@ image = data.moon() image = rescale_intensity(image, in_range=(50, 200)) # convenience function for plotting images -def imshow(image, **kwargs): - plt.figure(figsize=(5, 4)) - plt.imshow(image, **kwargs) - plt.axis('off') +def imshow(image, title, **kwargs): + fig, ax = plt.subplots(figsize=(5, 4)) + ax.imshow(image, **kwargs) + ax.axis('off') + ax.set_title(title) -imshow(image) -plt.title('original image') +imshow(image, 'Original image') """ .. image:: PLOT2RST.current_figure @@ -50,8 +50,7 @@ mask = image filled = reconstruction(seed, mask, method='erosion') -imshow(filled, vmin=image.min(), vmax=image.max()) -plt.title('after filling holes') +imshow(filled, 'after filling holes',vmin=image.min(), vmax=image.max()) """ .. image:: PLOT2RST.current_figure @@ -62,8 +61,8 @@ isolate the dark regions by subtracting the reconstructed image from the original image. """ -imshow(image - filled) -plt.title('holes') +imshow(image - filled, 'holes') +# plt.title('holes') """ .. image:: PLOT2RST.current_figure @@ -78,8 +77,7 @@ intensity instead of the maximum. The remainder of the process is the same. seed = np.copy(image) seed[1:-1, 1:-1] = image.min() rec = reconstruction(seed, mask, method='dilation') -imshow(image - rec) -plt.title('peaks') +imshow(image - rec, 'peaks') plt.show() """ diff --git a/doc/examples/plot_ihc_color_separation.py b/doc/examples/plot_ihc_color_separation.py index f89f28fe..3e297386 100644 --- a/doc/examples/plot_ihc_color_separation.py +++ b/doc/examples/plot_ihc_color_separation.py @@ -61,10 +61,10 @@ 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') +fig, ax = plt.subplots() +ax.imshow(zdh) +ax.set_title("Stain separated image (rescaled)") +ax.axis('off') plt.show() """ diff --git a/doc/examples/plot_join_segmentations.py b/doc/examples/plot_join_segmentations.py index 8ccc5038..849f087c 100644 --- a/doc/examples/plot_join_segmentations.py +++ b/doc/examples/plot_join_segmentations.py @@ -58,5 +58,5 @@ axes[3].set_title('Join') for ax in axes: ax.axis('off') -plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) +fig.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) plt.show() diff --git a/doc/examples/plot_label.py b/doc/examples/plot_label.py index 0542d68c..9912eddf 100644 --- a/doc/examples/plot_label.py +++ b/doc/examples/plot_label.py @@ -43,14 +43,14 @@ image_label_overlay = label2rgb(label_image, image=image) fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) ax.imshow(image_label_overlay) -for region in regionprops(label_image, ['Area', 'BoundingBox']): +for region in regionprops(label_image): # skip small images - if region['Area'] < 100: + if region.area < 100: continue # draw rectangle around segmented coins - minr, minc, maxr, maxc = region['BoundingBox'] + minr, minc, maxr, maxc = region.bbox rect = mpatches.Rectangle((minc, minr), maxc - minc, maxr - minr, fill=False, edgecolor='red', linewidth=2) ax.add_patch(rect) diff --git a/doc/examples/plot_line_hough_transform.py b/doc/examples/plot_line_hough_transform.py index cd0ae008..bdb05661 100644 --- a/doc/examples/plot_line_hough_transform.py +++ b/doc/examples/plot_line_hough_transform.py @@ -77,30 +77,30 @@ image[idx, idx] = 255 h, theta, d = hough_line(image) -plt.figure(figsize=(8, 4)) +fig, ax = plt.subplots(1, 3, figsize=(8, 4)) -plt.subplot(131) -plt.imshow(image, cmap=plt.cm.gray) -plt.title('Input image') +ax[0].imshow(image, cmap=plt.cm.gray) +ax[0].set_title('Input image') +ax[0].axis('image') -plt.subplot(132) -plt.imshow(np.log(1 + h), +ax[1].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)') +ax[1].set_title('Hough transform') +ax[1].set_xlabel('Angles (degrees)') +ax[1].set_ylabel('Distance (pixels)') +ax[1].axis('image') -plt.subplot(133) -plt.imshow(image, cmap=plt.cm.gray) +ax[2].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') + ax[2].plot((0, cols), (y0, y1), '-r') +ax[2].axis((0, cols, rows, 0)) +ax[2].set_title('Detected lines') +ax[2].axis('image') # Line finding, using the Probabilistic Hough Transform @@ -108,23 +108,22 @@ 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)) +fig2, ax = plt.subplots(1, 3, figsize=(8, 3)) -plt.subplot(131) -plt.imshow(image, cmap=plt.cm.gray) -plt.title('Input image') +ax[0].imshow(image, cmap=plt.cm.gray) +ax[0].set_title('Input image') +ax[0].axis('image') -plt.subplot(132) -plt.imshow(edges, cmap=plt.cm.gray) -plt.title('Canny edges') +ax[1].imshow(edges, cmap=plt.cm.gray) +ax[1].set_title('Canny edges') +ax[1].axis('image') -plt.subplot(133) -plt.imshow(edges * 0) +ax[2].imshow(edges * 0) for line in lines: p0, p1 = line - plt.plot((p0[0], p1[0]), (p0[1], p1[1])) + ax[2].plot((p0[0], p1[0]), (p0[1], p1[1])) -plt.title('Probabilistic Hough') -plt.axis('image') +ax[2].set_title('Probabilistic Hough') +ax[2].axis('image') plt.show() diff --git a/doc/examples/plot_local_equalize.py b/doc/examples/plot_local_equalize.py index 1fd8325f..b3c11d55 100644 --- a/doc/examples/plot_local_equalize.py +++ b/doc/examples/plot_local_equalize.py @@ -20,6 +20,7 @@ References """ import numpy as np +import matplotlib import matplotlib.pyplot as plt from skimage import data @@ -30,6 +31,9 @@ from skimage.morphology import disk from skimage.filter import rank +matplotlib.rcParams['font.size'] = 9 + + def plot_img_and_hist(img, axes, bins=256): """Plot an image along with its histogram and cumulative histogram. @@ -59,9 +63,7 @@ def plot_img_and_hist(img, axes, bins=256): # Load an example image img = img_as_ubyte(data.moon()) -# Contrast stretching -p2 = np.percentile(img, 2) -p98 = np.percentile(img, 98) +# Global equalize img_rescale = exposure.equalize_hist(img) # Equalization @@ -70,7 +72,7 @@ img_eq = rank.equalize(img, selem=selem) # Display results -f, axes = plt.subplots(2, 3, figsize=(8, 4)) +fig, axes = plt.subplots(2, 3, figsize=(8, 5)) ax_img, ax_hist, ax_cdf = plot_img_and_hist(img, axes[:, 0]) ax_img.set_title('Low contrast image') @@ -85,5 +87,5 @@ ax_cdf.set_ylabel('Fraction of total intensity') # prevent overlap of y-axis labels -plt.subplots_adjust(wspace=0.4) +fig.subplots_adjust(wspace=0.4) plt.show() diff --git a/doc/examples/plot_local_otsu.py b/doc/examples/plot_local_otsu.py index 3a321780..d40af6b6 100644 --- a/doc/examples/plot_local_otsu.py +++ b/doc/examples/plot_local_otsu.py @@ -15,6 +15,7 @@ The example compares the local threshold with the global threshold. .. [1] http://en.wikipedia.org/wiki/Otsu's_method """ +import matplotlib import matplotlib.pyplot as plt from skimage import data @@ -23,29 +24,38 @@ from skimage.filter import threshold_otsu, rank from skimage.util import img_as_ubyte -p8 = img_as_ubyte(data.page()) +matplotlib.rcParams['font.size'] = 9 -radius = 10 + +img = img_as_ubyte(data.page()) + +radius = 15 selem = disk(radius) -loc_otsu = rank.otsu(p8, selem) -t_glob_otsu = threshold_otsu(p8) -glob_otsu = p8 >= t_glob_otsu +local_otsu = rank.otsu(img, selem) +threshold_global_otsu = threshold_otsu(img) +global_otsu = img >= threshold_global_otsu -plt.figure() -plt.subplot(2, 2, 1) -plt.imshow(p8, cmap=plt.cm.gray) -plt.xlabel('original') -plt.colorbar() -plt.subplot(2, 2, 2) -plt.imshow(loc_otsu, cmap=plt.cm.gray) -plt.xlabel('local Otsu ($radius=%d$)' % radius) -plt.colorbar() -plt.subplot(2, 2, 3) -plt.imshow(p8 >= loc_otsu, cmap=plt.cm.gray) -plt.xlabel('original >= local Otsu' % t_glob_otsu) -plt.subplot(2, 2, 4) -plt.imshow(glob_otsu, cmap=plt.cm.gray) -plt.xlabel('global Otsu ($t = %d$)' % t_glob_otsu) +fig, ax = plt.subplots(2, 2, figsize=(8, 5)) +ax1, ax2, ax3, ax4 = ax.ravel() + +fig.colorbar(ax1.imshow(img, cmap=plt.cm.gray), + ax=ax1, orientation='horizontal') +ax1.set_title('Original') +ax1.axis('off') + +fig.colorbar(ax2.imshow(local_otsu, cmap=plt.cm.gray), + ax=ax2, orientation='horizontal') +ax2.set_title('Local Otsu (radius=%d)' % radius) +ax2.axis('off') + +ax3.imshow(img >= local_otsu, cmap=plt.cm.gray) +ax3.set_title('Original >= Local Otsu' % threshold_global_otsu) +ax3.axis('off') + +ax4.imshow(global_otsu, cmap=plt.cm.gray) +ax4.set_title('Global Otsu (threshold = %d)' % threshold_global_otsu) +ax4.axis('off') + plt.show() diff --git a/doc/examples/plot_marked_watershed.py b/doc/examples/plot_marked_watershed.py index d7f2c354..0e1f1c58 100644 --- a/doc/examples/plot_marked_watershed.py +++ b/doc/examples/plot_marked_watershed.py @@ -51,5 +51,5 @@ ax3.imshow(labels, cmap=plt.cm.spectral, interpolation='nearest', alpha=.7) for ax in axes: ax.axis('off') -plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) +fig.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) plt.show() diff --git a/doc/examples/plot_matching.py b/doc/examples/plot_matching.py index bae6d6e2..314c45d2 100644 --- a/doc/examples/plot_matching.py +++ b/doc/examples/plot_matching.py @@ -27,7 +27,8 @@ 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.feature import (corner_harris, corner_subpix, corner_peaks, + plot_matches) from skimage.transform import warp, AffineTransform from skimage.exposure import rescale_intensity from skimage.color import rgb2gray @@ -71,7 +72,7 @@ def gaussian_weights(window_ext, sigma=1): def match_corner(coord, window_ext=5): - r, c = np.round(coord) + r, c = np.round(coord).astype(np.intp) window_orig = img_orig[r-window_ext:r+window_ext+1, c-window_ext:c+window_ext+1, :] @@ -117,28 +118,21 @@ 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) - +# visualize correspondence fig, ax = plt.subplots(nrows=2, ncols=1) + plt.gray() -ax[0].imshow(img_combined, interpolation='nearest') +inlier_idxs = np.nonzero(inliers)[0] +plot_matches(ax[0], img_orig_gray, img_warped_gray, src, dst, + np.column_stack((inlier_idxs, inlier_idxs)), matches_color='b') ax[0].axis('off') -ax[0].axis((0, 400, 200, 0)) ax[0].set_title('Correct correspondences') -ax[1].imshow(img_combined, interpolation='nearest') + +outlier_idxs = np.nonzero(outliers)[0] +plot_matches(ax[1], img_orig_gray, img_warped_gray, src, dst, + np.column_stack((outlier_idxs, outlier_idxs)), matches_color='r') 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 f0792e50..1a8469b2 100644 --- a/doc/examples/plot_medial_transform.py +++ b/doc/examples/plot_medial_transform.py @@ -55,14 +55,12 @@ skel, distance = medial_axis(data, return_distance=True) # Distance to the background for pixels of the skeleton dist_on_skel = distance * skel -plt.figure(figsize=(8, 4)) -plt.subplot(121) -plt.imshow(data, cmap=plt.cm.gray, interpolation='nearest') -plt.axis('off') -plt.subplot(122) -plt.imshow(dist_on_skel, cmap=plt.cm.spectral, interpolation='nearest') -plt.contour(data, [0.5], colors='w') -plt.axis('off') +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4)) +ax1.imshow(data, cmap=plt.cm.gray, interpolation='nearest') +ax1.axis('off') +ax2.imshow(dist_on_skel, cmap=plt.cm.spectral, interpolation='nearest') +ax2.contour(data, [0.5], colors='w') +ax2.axis('off') -plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) +fig.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) plt.show() diff --git a/doc/examples/plot_orb.py b/doc/examples/plot_orb.py new file mode 100644 index 00000000..1a73fc7f --- /dev/null +++ b/doc/examples/plot_orb.py @@ -0,0 +1,56 @@ +""" +========================================== +ORB feature detector and binary descriptor +========================================== + +This example demonstrates the ORB feature detection and binary description +algorithm. It uses an oriented FAST detection method and the rotated BRIEF +descriptors. + +Unlike BRIEF, ORB is comparatively scale- and rotation-invariant while still +employing the very efficient Hamming distance metric for matching. As such, it +is preferred for real-time applications. + +""" +from skimage import data +from skimage import transform as tf +from skimage.feature import (match_descriptors, corner_harris, + corner_peaks, ORB, plot_matches) +from skimage.color import rgb2gray +import matplotlib.pyplot as plt + + +img1 = rgb2gray(data.lena()) +img2 = tf.rotate(img1, 180) +tform = tf.AffineTransform(scale=(1.3, 1.1), rotation=0.5, + translation=(0, -200)) +img3 = tf.warp(img1, tform) + +descriptor_extractor = ORB(n_keypoints=200) + +descriptor_extractor.detect_and_extract(img1) +keypoints1 = descriptor_extractor.keypoints +descriptors1 = descriptor_extractor.descriptors + +descriptor_extractor.detect_and_extract(img2) +keypoints2 = descriptor_extractor.keypoints +descriptors2 = descriptor_extractor.descriptors + +descriptor_extractor.detect_and_extract(img3) +keypoints3 = descriptor_extractor.keypoints +descriptors3 = descriptor_extractor.descriptors + +matches12 = match_descriptors(descriptors1, descriptors2, cross_check=True) +matches13 = match_descriptors(descriptors1, descriptors3, cross_check=True) + +fig, ax = plt.subplots(nrows=2, ncols=1) + +plt.gray() + +plot_matches(ax[0], img1, img2, keypoints1, keypoints2, matches12) +ax[0].axis('off') + +plot_matches(ax[1], img1, img3, keypoints1, keypoints3, matches13) +ax[1].axis('off') + +plt.show() diff --git a/doc/examples/plot_otsu.py b/doc/examples/plot_otsu.py index 5221e934..059093ab 100644 --- a/doc/examples/plot_otsu.py +++ b/doc/examples/plot_otsu.py @@ -14,30 +14,31 @@ the intra-class variance. .. [1] http://en.wikipedia.org/wiki/Otsu's_method """ +import matplotlib import matplotlib.pyplot as plt from skimage.data import camera from skimage.filter import threshold_otsu +matplotlib.rcParams['font.size'] = 9 + + image = camera() thresh = threshold_otsu(image) binary = image > thresh -plt.figure(figsize=(8, 2.5)) -plt.subplot(1, 3, 1) -plt.imshow(image, cmap=plt.cm.gray) -plt.title('Original') -plt.axis('off') +fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(8, 2.5)) +ax1.imshow(image, cmap=plt.cm.gray) +ax1.set_title('Original') +ax1.axis('off') -plt.subplot(1, 3, 2, aspect='equal') -plt.hist(image) -plt.title('Histogram') -plt.axvline(thresh, color='r') +ax2.hist(image) +ax2.set_title('Histogram') +ax2.axvline(thresh, color='r') -plt.subplot(1, 3, 3) -plt.imshow(binary, cmap=plt.cm.gray) -plt.title('Thresholded') -plt.axis('off') +ax3.imshow(binary, cmap=plt.cm.gray) +ax3.set_title('Thresholded') +ax3.axis('off') plt.show() diff --git a/doc/examples/plot_peak_local_max.py b/doc/examples/plot_peak_local_max.py index 3a2dccfe..03981578 100644 --- a/doc/examples/plot_peak_local_max.py +++ b/doc/examples/plot_peak_local_max.py @@ -25,25 +25,23 @@ image_max = ndimage.maximum_filter(im, size=20, mode='constant') coordinates = peak_local_max(im, min_distance=20) # display results -plt.figure(figsize=(8, 3)) -plt.subplot(131) -plt.imshow(im, cmap=plt.cm.gray) -plt.axis('off') -plt.title('Original') +fig, ax = plt.subplots(1, 3, figsize=(8, 3)) +ax1, ax2, ax3 = ax.ravel() +ax1.imshow(im, cmap=plt.cm.gray) +ax1.axis('off') +ax1.set_title('Original') -plt.subplot(132) -plt.imshow(image_max, cmap=plt.cm.gray) -plt.axis('off') -plt.title('Maximum filter') +ax2.imshow(image_max, cmap=plt.cm.gray) +ax2.axis('off') +ax2.set_title('Maximum filter') -plt.subplot(133) -plt.imshow(im, cmap=plt.cm.gray) -plt.autoscale(False) -plt.plot([p[1] for p in coordinates], [p[0] for p in coordinates], 'r.') -plt.axis('off') -plt.title('Peak local max') +ax3.imshow(im, cmap=plt.cm.gray) +ax3.autoscale(False) +ax3.plot(coordinates[:, 1], coordinates[:, 0], 'r.') +ax3.axis('off') +ax3.set_title('Peak local max') -plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, +fig.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_phase_unwrap.py b/doc/examples/plot_phase_unwrap.py new file mode 100644 index 00000000..ded87ada --- /dev/null +++ b/doc/examples/plot_phase_unwrap.py @@ -0,0 +1,106 @@ +""" +================ +Phase Unwrapping +================ + +Some signals can only be observed modulo 2*pi, and this can also apply to +two- and three dimensional images. In these cases phase unwrapping is +needed to recover the underlying, unwrapped signal. In this example we will +demonstrate an algorithm [1]_ implemented in ``skimage`` at work for such a +problem. One-, two- and three dimensional images can all be unwrapped using +skimage. Here we will demonstrate phase unwrapping in the two dimensional case. +""" + +import numpy as np +from matplotlib import pyplot as plt +from skimage import data, img_as_float, color, exposure +from skimage.restoration import unwrap_phase + + +# Load an image as a floating-point grayscale +image = color.rgb2gray(img_as_float(data.chelsea())) +# Scale the image to [0, 4*pi] +image = exposure.rescale_intensity(image, out_range=(0, 4 * np.pi)) +# Create a phase-wrapped image in the interval [-pi, pi) +image_wrapped = np.angle(np.exp(1j * image)) +# Perform phase unwrapping +image_unwrapped = unwrap_phase(image_wrapped) + +fig, ax = plt.subplots(2, 2) +ax1, ax2, ax3, ax4 = ax.ravel() + +fig.colorbar(ax1.imshow(image, cmap='gray', vmin=0, vmax=4 * np.pi), ax=ax1) +ax1.set_title('Original') + +fig.colorbar(ax2.imshow(image_wrapped, cmap='gray', vmin=-np.pi, vmax=np.pi), ax=ax2) +ax2.set_title('Wrapped phase') + +fig.colorbar(ax3.imshow(image_unwrapped, cmap='gray'), ax=ax3) +ax3.set_title('After phase unwrapping') + +fig.colorbar(ax4.imshow(image_unwrapped - image, cmap='gray'), ax=ax4) +ax4.set_title('Unwrapped minus original') + +""" +.. image:: PLOT2RST.current_figure + +The unwrapping procedure accepts masked arrays, and can also optionally +assume cyclic boundaries to connect edges of an image. In the example below, +we study a simple phase ramp which has been split in two by masking +a row of the image. +""" + +# Create a simple ramp +image = np.ones((100, 100)) * np.linspace(0, 8 * np.pi, 100).reshape((-1, 1)) +# Mask the image to split it in two horizontally +mask = np.zeros_like(image, dtype=np.bool) +mask[image.shape[0] // 2, :] = True + +image_wrapped = np.ma.array(np.angle(np.exp(1j * image)), mask=mask) +# Unwrap image without wrap around +image_unwrapped_no_wrap_around = unwrap_phase(image_wrapped, + wrap_around=(False, False)) +# Unwrap with wrap around enabled for the 0th dimension +image_unwrapped_wrap_around = unwrap_phase(image_wrapped, + wrap_around=(True, False)) + +fig, ax = plt.subplots(2, 2) +ax1, ax2, ax3, ax4 = ax.ravel() + +fig.colorbar(ax1.imshow(np.ma.array(image, mask=mask), cmap='jet'), ax=ax1) +ax1.set_title('Original') + +fig.colorbar(ax2.imshow(image_wrapped, cmap='jet', vmin=-np.pi, vmax=np.pi), + ax=ax2) +ax2.set_title('Wrapped phase') + +fig.colorbar(ax3.imshow(image_unwrapped_no_wrap_around, cmap='jet'), + ax=ax3) +ax3.set_title('Unwrapped without wrap_around') + +fig.colorbar(ax4.imshow(image_unwrapped_wrap_around, cmap='jet'), ax=ax4) +ax4.set_title('Unwrapped with wrap_around') + +plt.show() + +""" +.. image:: PLOT2RST.current_figure + +In the figures above, the masked row can be seen as a white line across +the image. The difference between the two unwrapped images in the bottom row +is clear: Without unwrapping (lower left), the regions above and below the +masked boundary do not interact at all, resulting in an offset between the +two regions of an arbitrary integer times two pi. We could just as well have +unwrapped the regions as two separate images. With wrap around enabled for the +vertical direction (lower rigth), the situation changes: Unwrapping paths are +now allowed to pass from the bottom to the top of the image and vice versa, in +effect providing a way to determine the offset between the two regions. + +References +---------- + +.. [1] Miguel Arevallilo Herraez, David R. Burton, Michael J. Lalor, + and Munther A. Gdeisat, "Fast two-dimensional phase-unwrapping + algorithm based on sorting by reliability following a noncontinuous + path", Journal Applied Optics, Vol. 41, No. 35, pp. 7437, 2002 +""" diff --git a/doc/examples/plot_piecewise_affine.py b/doc/examples/plot_piecewise_affine.py index a0ad7d8f..b1fb551f 100644 --- a/doc/examples/plot_piecewise_affine.py +++ b/doc/examples/plot_piecewise_affine.py @@ -35,7 +35,8 @@ out_rows = image.shape[0] - 1.5 * 50 out_cols = cols out = warp(image, tform, output_shape=(out_rows, out_cols)) -plt.imshow(out) -plt.plot(tform.inverse(src)[:, 0], tform.inverse(src)[:, 1], '.b') -plt.axis((0, out_cols, out_rows, 0)) +fig, ax = plt.subplots() +ax.imshow(out) +ax.plot(tform.inverse(src)[:, 0], tform.inverse(src)[:, 1], '.b') +ax.axis((0, out_cols, out_rows, 0)) plt.show() diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py index c4853c2f..8e309edf 100644 --- a/doc/examples/plot_pyramid.py +++ b/doc/examples/plot_pyramid.py @@ -30,5 +30,6 @@ for p in pyramid[1:]: composite_image[i_row:i_row + n_rows, cols:cols + n_cols] = p i_row += n_rows -plt.imshow(composite_image) +fig, ax = plt.subplots() +ax.imshow(composite_image) plt.show() diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index 0dab82e6..04cf7bab 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -60,22 +60,20 @@ from skimage.transform import radon, rescale image = imread(data_dir + "/phantom.png", as_grey=True) image = rescale(image, scale=0.4) -plt.figure(figsize=(8, 4.5)) +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4.5)) -plt.subplot(121) -plt.title("Original") -plt.imshow(image, cmap=plt.cm.Greys_r) +ax1.set_title("Original") +ax1.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, +ax2.set_title("Radon transform\n(Sinogram)") +ax2.set_xlabel("Projection angle (deg)") +ax2.set_ylabel("Projection position (pixels)") +ax2.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) +fig.subplots_adjust(hspace=0.4, wspace=0.5) plt.show() """ @@ -103,13 +101,11 @@ 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) +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4.5)) +ax1.set_title("Reconstruction\nFiltered back projection") +ax1.imshow(reconstruction_fbp, cmap=plt.cm.Greys_r) +ax2.set_title("Reconstruction error\nFiltered back projection") +ax2.imshow(reconstruction_fbp - image, cmap=plt.cm.Greys_r, **imkwargs) plt.show() """ @@ -156,14 +152,12 @@ 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("Reconstruction\nSART") -plt.imshow(reconstruction_sart, cmap=plt.cm.Greys_r) -plt.subplot(222) -plt.title("Reconstruction error\nSART") -plt.imshow(reconstruction_sart - image, cmap=plt.cm.Greys_r, **imkwargs) +fig, ax = plt.subplots(2, 2, figsize=(8, 8.5)) +ax1, ax2, ax3, ax4 = ax.ravel() +ax1.set_title("Reconstruction\nSART") +ax1.imshow(reconstruction_sart, cmap=plt.cm.Greys_r) +ax2.set_title("Reconstruction error\nSART") +ax2.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 @@ -173,12 +167,10 @@ error = reconstruction_sart2 - image print('SART (2 iterations) rms reconstruction error: %.3g' % np.sqrt(np.mean(error**2))) -plt.subplot(223) -plt.title("Reconstruction\nSART, 2 iterations") -plt.imshow(reconstruction_sart2, cmap=plt.cm.Greys_r) -plt.subplot(224) -plt.title("Reconstruction error\nSART, 2 iterations") -plt.imshow(reconstruction_sart2 - image, cmap=plt.cm.Greys_r, **imkwargs) +ax3.set_title("Reconstruction\nSART, 2 iterations") +ax3.imshow(reconstruction_sart2, cmap=plt.cm.Greys_r) +ax4.set_title("Reconstruction error\nSART, 2 iterations") +ax4.imshow(reconstruction_sart2 - image, cmap=plt.cm.Greys_r, **imkwargs) plt.show() """ diff --git a/doc/examples/plot_random_walker_segmentation.py b/doc/examples/plot_random_walker_segmentation.py index 8c960a8b..fa70e503 100644 --- a/doc/examples/plot_random_walker_segmentation.py +++ b/doc/examples/plot_random_walker_segmentation.py @@ -58,20 +58,17 @@ markers[data > 1.3] = 2 labels = random_walker(data, markers, beta=10, mode='bf') # Plot results -plt.figure(figsize=(8, 3.2)) -plt.subplot(131) -plt.imshow(data, cmap='gray', interpolation='nearest') -plt.axis('off') -plt.title('Noisy data') -plt.subplot(132) -plt.imshow(markers, cmap='hot', interpolation='nearest') -plt.axis('off') -plt.title('Markers') -plt.subplot(133) -plt.imshow(labels, cmap='gray', interpolation='nearest') -plt.axis('off') -plt.title('Segmentation') +fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(8, 3.2)) +ax1.imshow(data, cmap='gray', interpolation='nearest') +ax1.axis('off') +ax1.set_title('Noisy data') +ax2.imshow(markers, cmap='hot', interpolation='nearest') +ax2.axis('off') +ax2.set_title('Markers') +ax3.imshow(labels, cmap='gray', interpolation='nearest') +ax3.axis('off') +ax3.set_title('Segmentation') -plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, +fig.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) plt.show() diff --git a/doc/examples/plot_ransac.py b/doc/examples/plot_ransac.py index 2de76619..770e8cd1 100644 --- a/doc/examples/plot_ransac.py +++ b/doc/examples/plot_ransac.py @@ -45,11 +45,12 @@ 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') +fig, ax = plt.subplots() +ax.plot(data[inliers, 0], data[inliers, 1], '.b', alpha=0.6, + label='Inlier data') +ax.plot(data[outliers, 0], data[outliers, 1], '.r', alpha=0.6, + label='Outlier data') +ax.plot(line_x, line_y, '-k', label='Line model from all data') +ax.plot(line_x, line_y_robust, '-b', label='Robust line model') +ax.legend(loc='lower left') plt.show() diff --git a/doc/examples/plot_regional_maxima.py b/doc/examples/plot_regional_maxima.py index f7b125f1..dd676014 100644 --- a/doc/examples/plot_regional_maxima.py +++ b/doc/examples/plot_regional_maxima.py @@ -50,7 +50,7 @@ ax3.imshow(image - dilated) ax3.set_title('image - dilated') ax3.axis('off') -plt.tight_layout() +fig.tight_layout() """ @@ -98,7 +98,7 @@ ax3.axhline(yslice, color='r', alpha=0.4) ax3.set_title('image - dilated') ax3.axis('off') -plt.tight_layout() +fig.tight_layout() plt.show() """ diff --git a/doc/examples/plot_regionprops.py b/doc/examples/plot_regionprops.py index f675d11c..065bae55 100644 --- a/doc/examples/plot_regionprops.py +++ b/doc/examples/plot_regionprops.py @@ -26,7 +26,8 @@ image = rotate(image, angle=15, order=0) label_img = label(image) regions = regionprops(label_img) -plt.imshow(image) +fig, ax = plt.subplots() +ax.imshow(image, cmap=plt.cm.gray) for props in regions: y0, x0 = props.centroid @@ -36,15 +37,14 @@ for props in regions: 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) + ax.plot((x0, x1), (y0, y1), '-r', linewidth=2.5) + ax.plot((x0, x2), (y0, y2), '-r', linewidth=2.5) + ax.plot(x0, y0, '.g', markersize=15) 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) + ax.plot(bx, by, '-b', linewidth=2.5) -plt.gray() -plt.axis((0, 600, 600, 0)) +ax.axis((0, 600, 600, 0)) plt.show() diff --git a/doc/examples/plot_restoration.py b/doc/examples/plot_restoration.py new file mode 100644 index 00000000..fdffd953 --- /dev/null +++ b/doc/examples/plot_restoration.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +""" +===================== +Deconvolution of Lena +===================== + +In this example, we deconvolve a noisy version of Lena using Wiener +and unsupervised Wiener algorithms. This algorithms are based on +linear models that can't restore sharp edge as much as non-linear +methods (like TV restoration) but are much faster. + +Wiener filter +------------- +The inverse filter based on the PSF (Point Spread Function), +the prior regularisation (penalisation of high frequency) and the +tradeoff between the data and prior adequacy. The regularization +parameter must be hand tuned. + +Unsupervised Wiener +------------------- +This algorithm has a self-tuned regularisation parameters based on +data learning. This is not common and based on the following +publication. The algorithm is based on a iterative Gibbs sampler that +draw alternatively samples of posterior conditionnal law of the image, +the noise power and the image frequency power. + +.. [1] François Orieux, Jean-François Giovannelli, and Thomas + Rodet, "Bayesian estimation of regularization and point + spread function parameters for Wiener-Hunt deconvolution", + J. Opt. Soc. Am. A 27, 1593-1607 (2010) +""" +import numpy as np +import matplotlib.pyplot as plt + +from skimage import color, data, restoration + +lena = color.rgb2gray(data.lena()) +from scipy.signal import convolve2d as conv2 +psf = np.ones((5, 5)) / 25 +lena = conv2(lena, psf, 'same') +lena += 0.1 * lena.std() * np.random.standard_normal(lena.shape) + +deconvolved, _ = restoration.unsupervised_wiener(lena, psf) + +fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(8, 5)) + +plt.gray() + +ax[0].imshow(lena, vmin=deconvolved.min(), vmax=deconvolved.max()) +ax[0].axis('off') +ax[0].set_title('Data') + +ax[1].imshow(deconvolved) +ax[1].axis('off') +ax[1].set_title('Self tuned restoration') + +fig.subplots_adjust(wspace=0.02, hspace=0.2, + top=0.9, bottom=0.05, left=0, right=1) + +plt.show() diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index 55bb6082..abbf352d 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -51,7 +51,7 @@ and image location and is therefore closely related to quickshift. As the clustering method is simpler, it is very efficient. It is essential for this algorithm to work in Lab color space to obtain good results. The algorithm quickly gained momentum and is now widely used. See [3] for details. The -``ratio`` parameter trades off color-similarity and proximity, as in the case +``compactness`` parameter trades off color-similarity and proximity, as in the case of Quickshift, while ``n_segments`` chooses the number of centers for kmeans. .. [3] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, @@ -70,7 +70,7 @@ from skimage.util import img_as_float img = img_as_float(lena()[::2, ::2]) segments_fz = felzenszwalb(img, scale=100, sigma=0.5, min_size=50) -segments_slic = slic(img, ratio=10, n_segments=250, sigma=1) +segments_slic = slic(img, n_segments=250, compactness=10, sigma=1) segments_quick = quickshift(img, kernel_size=3, max_dist=6, ratio=0.5) print("Felzenszwalb's number of segments: %d" % len(np.unique(segments_fz))) @@ -79,7 +79,7 @@ print("Quickshift number of segments: %d" % len(np.unique(segments_quick))) fig, ax = plt.subplots(1, 3) fig.set_size_inches(8, 3, forward=True) -plt.subplots_adjust(0.05, 0.05, 0.95, 0.95, 0.05, 0.05) +fig.subplots_adjust(0.05, 0.05, 0.95, 0.95, 0.05, 0.05) ax[0].imshow(mark_boundaries(img, segments_fz)) ax[0].set_title("Felzenszwalbs's method") diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 2ae842a5..34d1108c 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -4,11 +4,17 @@ Shapes ====== This example shows how to draw several different shapes: -* line -* Bezier curve -* polygon -* circle -* ellipse + + - line + - Bezier curve + - polygon + - circle + - ellipse + +Anti-aliased drawing for: + + - line + - circle """ import math @@ -69,13 +75,6 @@ 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 diff --git a/doc/examples/plot_skeleton.py b/doc/examples/plot_skeleton.py index a0bfd267..a4a3a60c 100644 --- a/doc/examples/plot_skeleton.py +++ b/doc/examples/plot_skeleton.py @@ -47,19 +47,17 @@ image[circle2] = 0 skeleton = skeletonize(image) # display results -plt.figure(figsize=(8, 4.5)) +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4.5)) -plt.subplot(121) -plt.imshow(image, cmap=plt.cm.gray) -plt.axis('off') -plt.title('original', fontsize=20) +ax1.imshow(image, cmap=plt.cm.gray) +ax1.axis('off') +ax1.set_title('original', fontsize=20) -plt.subplot(122) -plt.imshow(skeleton, cmap=plt.cm.gray) -plt.axis('off') -plt.title('skeleton', fontsize=20) +ax2.imshow(skeleton, cmap=plt.cm.gray) +ax2.axis('off') +ax2.set_title('skeleton', fontsize=20) -plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.98, +fig.subplots_adjust(wspace=0.02, hspace=0.02, top=0.98, bottom=0.02, left=0.02, right=0.98) plt.show() diff --git a/doc/examples/plot_ssim.py b/doc/examples/plot_ssim.py index 9fe0c930..319c72fe 100644 --- a/doc/examples/plot_ssim.py +++ b/doc/examples/plot_ssim.py @@ -22,12 +22,16 @@ but with very different mean structural similarity indices. """ import numpy as np +import matplotlib import matplotlib.pyplot as plt from skimage import data, img_as_float from skimage.measure import structural_similarity as ssim +matplotlib.rcParams['font.size'] = 9 + + img = img_as_float(data.camera()) rows, cols = img.shape @@ -41,7 +45,7 @@ def mse(x, y): img_noise = img + noise img_const = img + abs(noise) -f, (ax0, ax1, ax2) = plt.subplots(1, 3) +fig, (ax0, ax1, ax2) = plt.subplots(nrows=1, ncols=3, figsize=(8, 4)) mse_none = mse(img, img) ssim_none = ssim(img, img, dynamic_range=img.max() - img.min()) diff --git a/doc/examples/plot_swirl.py b/doc/examples/plot_swirl.py index 98226a20..8f79db94 100644 --- a/doc/examples/plot_swirl.py +++ b/doc/examples/plot_swirl.py @@ -74,7 +74,7 @@ from skimage.transform import swirl image = data.checkerboard() swirled = swirl(image, rotation=0, strength=10, radius=120, order=2) -f, (ax0, ax1) = plt.subplots(1, 2, figsize=(8, 3)) +fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(8, 3)) ax0.imshow(image, cmap=plt.cm.gray, interpolation='none') ax0.axis('off') diff --git a/doc/examples/plot_tinting_grayscale_images.py b/doc/examples/plot_tinting_grayscale_images.py new file mode 100644 index 00000000..8b07b41d --- /dev/null +++ b/doc/examples/plot_tinting_grayscale_images.py @@ -0,0 +1,153 @@ +""" +========================= +Tinting gray-scale images +========================= + +It can be useful to artificially tint an image with some color, either to +highlight particular regions of an image or maybe just to liven up a grayscale +image. This example demonstrates image-tinting by scaling RGB values and by +adjusting colors in the HSV color-space. + +In 2D, color images are often represented in RGB---3 layers of 2D arrays, where +the 3 layers represent (R)ed, (G)reen and (B)lue channels of the image. The +simplest way of getting a tinted image is to set each RGB channel to the +grayscale image scaled by a different multiplier for each channel. For example, +multiplying the green and blue channels by 0 leaves only the red channel and +produces a bright red image. Similarly, zeroing-out the blue channel leaves +only the red and green channels, which combine to form yellow. +""" + +import matplotlib.pyplot as plt +from skimage import data +from skimage import color +from skimage import img_as_float + +grayscale_image = img_as_float(data.camera()[::2, ::2]) +image = color.gray2rgb(grayscale_image) + +red_multiplier = [1, 0, 0] +yellow_multiplier = [1, 1, 0] + +fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8, 4)) +ax1.imshow(red_multiplier * image) +ax2.imshow(yellow_multiplier * image) + +""" +.. image:: PLOT2RST.current_figure + +In many cases, dealing with RGB values may not be ideal. Because of that, there +are many other `color spaces`_ in which you can represent a color image. One +popular color space is called HSV_, which represents hue (~the color), +saturation (~colorfulness), and value (~brightness). For example, a color +(hue) might be green, but its saturation is how intense that green is---where +olive is on the low end and neon on the high end. + +In some implementations, the hue in HSV goes from 0 to 360, since hues wrap +around in a circle. In scikit-image, however, hues are float values from 0 to +1, so that hue, saturation, and value all share the same scale. + +Below, we plot a linear gradient in the hue, with the saturation and value +turned all the way up: +""" +import numpy as np + +hue_gradient = np.linspace(0, 1) +hsv = np.ones(shape=(1, len(hue_gradient), 3), dtype=float) +hsv[:, :, 0] = hue_gradient + +all_hues = color.hsv2rgb(hsv) + +fig, ax = plt.subplots(figsize=(5, 2)) +# Set image extent so hues go from 0 to 1 and the image is a nice aspect ratio. +ax.imshow(all_hues, extent=(0, 1, 0, 0.2)) +ax.set_axis_off() + +""" +.. image:: PLOT2RST.current_figure + +Notice how the colors at the far left and far right are the same. That reflects +the fact that the hues wrap around like the color wheel (see HSV_ for more +info). + +Now, let's create a little utility function to take an RGB image and: + +1. Transform the RGB image to HSV +2. Set the hue and saturation +3. Transform the HSV image back to RGB + +""" + +def colorize(image, hue, saturation=1): + """ Add color of the given hue to an RGB image. + + By default, set the saturation to 1 so that the colors pop! + """ + hsv = color.rgb2hsv(image) + hsv[:, :, 1] = saturation + hsv[:, :, 0] = hue + return color.hsv2rgb(hsv) + +""" +Notice that we need to bump up the saturation; images with zero saturation are +grayscale, so we need to a non-zero value to actually see the color we've set. + +Using the function above, we plot six images with a linear gradient in the hue +and a non-zero saturation: +""" + +hue_rotations = np.linspace(0, 1, 6) + +fig, axes = plt.subplots(nrows=2, ncols=3) + +for ax, hue in zip(axes.flat, hue_rotations): + # Turn down the saturation to give it that vintage look. + tinted_image = colorize(image, hue, saturation=0.3) + ax.imshow(tinted_image, vmin=0, vmax=1) + ax.set_axis_off() +fig.tight_layout() + +""" +.. image:: PLOT2RST.current_figure + +You can combine this tinting effect with numpy slicing and fancy-indexing to +selectively tint your images. In the example below, we set the hue of some +rectangles using slicing and scale the RGB values of some pixels found by +thresholding. In practice, you might want to define a region for tinting based +on segmentation results or blob detection methods. +""" + +from skimage.filter import rank + +# Square regions defined as slices over the first two dimensions. +top_left = (slice(100),) * 2 +bottom_right = (slice(-100, None),) * 2 + +sliced_image = image.copy() +sliced_image[top_left] = colorize(image[top_left], 0.82, saturation=0.5) +sliced_image[bottom_right] = colorize(image[bottom_right], 0.5, saturation=0.5) + +# Create a mask selecting regions with interesting texture. +noisy = rank.entropy(grayscale_image, np.ones((9, 9))) +textured_regions = noisy > 4 +# Note that using `colorize` here is a bit more difficult, since `rgb2hsv` +# expects an RGB image (height x width x channel), but fancy-indexing returns +# a set of RGB pixels (# pixels x channel). +masked_image = image.copy() +masked_image[textured_regions, :] *= red_multiplier + +fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8, 4)) +ax1.imshow(sliced_image) +ax2.imshow(masked_image) + +plt.show() + +""" +.. image:: PLOT2RST.current_figure + +For coloring multiple regions, you may also be interested in +`skimage.color.label2rgb `_. + +.. _color spaces: + http://en.wikipedia.org/wiki/List_of_color_spaces_and_their_uses +.. _HSV: http://en.wikipedia.org/wiki/HSL_and_HSV +""" diff --git a/doc/examples/plot_view_as_blocks.py b/doc/examples/plot_view_as_blocks.py index c58a74a6..2237e50b 100644 --- a/doc/examples/plot_view_as_blocks.py +++ b/doc/examples/plot_view_as_blocks.py @@ -60,5 +60,5 @@ ax2.imshow(max_view, cmap=cm.Greys_r) ax3.set_title("Block view with\n local median pooling") ax3.imshow(median_view, cmap=cm.Greys_r) -plt.subplots_adjust(hspace=0.4, wspace=0.4) +fig.subplots_adjust(hspace=0.4, wspace=0.4) plt.show() diff --git a/doc/examples/plot_watershed.py b/doc/examples/plot_watershed.py index f003c122..27f7016e 100644 --- a/doc/examples/plot_watershed.py +++ b/doc/examples/plot_watershed.py @@ -61,6 +61,6 @@ ax2.set_title('Separated objects') for ax in axes: ax.axis('off') -plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, +fig.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) plt.show() diff --git a/doc/ext/docscrape.py b/doc/ext/docscrape.py index b90b49c8..615ea11f 100644 --- a/doc/ext/docscrape.py +++ b/doc/ext/docscrape.py @@ -9,7 +9,6 @@ import pydoc from StringIO import StringIO from warnings import warn - class Reader(object): """A line-based string reader. @@ -233,7 +232,8 @@ class NumpyDocString(object): current_func = None if ',' in line: for func in line.split(','): - push_item(func, []) + if func.strip(): + push_item(func, []) elif line.strip(): current_func = line elif current_func is not None: @@ -284,8 +284,8 @@ class NumpyDocString(object): for (section,content) in self._read_sections(): if not section.startswith('..'): section = ' '.join([s.capitalize() for s in section.split(' ')]) - if section in ('Parameters', 'Attributes', 'Methods', - 'Returns', 'Raises', 'Warns'): + if section in ('Parameters', 'Returns', 'Raises', 'Warns', + 'Other Parameters', 'Attributes', 'Methods'): self[section] = self._parse_param_list(content) elif section.startswith('.. index::'): self['index'] = self._parse_index(section, content) @@ -370,7 +370,7 @@ class NumpyDocString(object): idx = self['index'] out = [] out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.items(): + for section, references in idx.iteritems(): if section == 'default': continue out += [' :%s: %s' % (section, ', '.join(references))] @@ -381,7 +381,8 @@ class NumpyDocString(object): out += self._str_signature() out += self._str_summary() out += self._str_extended_summary() - for param_list in ('Parameters','Returns','Raises'): + for param_list in ('Parameters', 'Returns', 'Other Parameters', + 'Raises', 'Warns'): out += self._str_param_list(param_list) out += self._str_section('Warnings') out += self._str_see_also(func_role) diff --git a/doc/ext/docscrape_sphinx.py b/doc/ext/docscrape_sphinx.py index 9e66c4be..e44e770e 100644 --- a/doc/ext/docscrape_sphinx.py +++ b/doc/ext/docscrape_sphinx.py @@ -2,7 +2,6 @@ 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) @@ -128,7 +127,7 @@ class SphinxDocString(NumpyDocString): return out out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.items(): + for section, references in idx.iteritems(): if section == 'default': continue elif section == 'refguide': @@ -179,7 +178,8 @@ class SphinxDocString(NumpyDocString): out += self._str_index() + [''] out += self._str_summary() out += self._str_extended_summary() - for param_list in ('Parameters', 'Returns', 'Raises'): + for param_list in ('Parameters', 'Returns', 'Other Parameters', + 'Raises', 'Warns'): out += self._str_param_list(param_list) out += self._str_warnings() out += self._str_see_also(func_role) diff --git a/doc/ext/notebook.py b/doc/ext/notebook.py new file mode 100644 index 00000000..054a9c44 --- /dev/null +++ b/doc/ext/notebook.py @@ -0,0 +1,126 @@ +__all__ = ['python_to_notebook', 'Notebook'] + +import json +import copy +import warnings + + +# Skeleton notebook in JSON format +skeleton_nb = """{ + "metadata": { + "name":"" + }, + "nbformat": 3, + "nbformat_minor": 0, + "worksheets": [ + { + "cells": [ + { + "cell_type": "code", + "collapsed": false, + "input": [ + "%matplotlib inline" + ], + "language": "python", + "metadata": {}, + "outputs": [] + } + ], + "metadata": {} + } + ] +}""" + + +class Notebook(object): + """ + Notebook object for building an IPython notebook cell-by-cell. + """ + + def __init__(self): + # cell type code + self.cell_code = { + 'cell_type': 'code', + 'collapsed': False, + 'input': [ + '# Code Goes Here' + ], + 'language': 'python', + 'metadata': {}, + 'outputs': [] + } + + # cell type markdown + self.cell_md = { + 'cell_type': 'markdown', + 'metadata': {}, + 'source': [ + 'Markdown Goes Here' + ] + } + + self.template = json.loads(skeleton_nb) + self.cell_type = {'input': self.cell_code, 'source': self.cell_md} + self.valuetype_to_celltype = {'code': 'input', 'markdown': 'source'} + + def add_cell(self, value, cell_type='code'): + """Add a notebook cell. + + Parameters + ---------- + value : str + Cell content. + cell_type : {'code', 'markdown'} + Type of content (default is 'code'). + + """ + if cell_type in ['markdown', 'code']: + key = self.valuetype_to_celltype[cell_type] + cells = self.template['worksheets'][0]['cells'] + cells.append(copy.deepcopy(self.cell_type[key])) + # assign value to the last cell + cells[-1][key] = value + else: + warnings.warn('Ignoring unsupported cell type (%s)' % cell_type) + + def json(self): + """Return a JSON representation of the notebook. + + Returns + ------- + str + JSON notebook. + + """ + return json.dumps(self.template, indent=2) + + +def test_notebook_basic(): + nb = Notebook() + assert(json.loads(nb.json()) == json.loads(skeleton_nb)) + + +def test_notebook_add(): + nb = Notebook() + + str1 = 'hello world' + str2 = 'f = lambda x: x * x' + + nb.add_cell(str1, cell_type='markdown') + nb.add_cell(str2, cell_type='code') + + d = json.loads(nb.json()) + cells = d['worksheets'][0]['cells'] + values = [c['input'] if c['cell_type'] == 'code' else c['source'] + for c in cells] + + assert values[1] == str1 + assert values[2] == str2 + + assert cells[1]['cell_type'] == 'markdown' + assert cells[2]['cell_type'] == 'code' + + +if __name__ == "__main__": + import numpy.testing as npt + npt.run_module_suite() diff --git a/doc/ext/plot2rst.py b/doc/ext/plot2rst.py index 3b26aeb9..258a6929 100644 --- a/doc/ext/plot2rst.py +++ b/doc/ext/plot2rst.py @@ -80,6 +80,10 @@ from skimage import io from skimage import transform from skimage.util.dtype import dtype_range +from notebook import Notebook + +from docutils.core import publish_parts + LITERALINCLUDE = """ .. literalinclude:: {src_name} @@ -94,6 +98,13 @@ CODE_LINK = """ """ +NOTEBOOK_LINK = """ + +**IPython Notebook:** :download:`download <{0}>` +(generated using ``skimage`` |version|) + +""" + TOCTREE_TEMPLATE = """ .. toctree:: :hidden: @@ -305,16 +316,20 @@ def write_example(src_name, src_dir, rst_dir, cfg): image_dir = rst_dir.pjoin('images') thumb_dir = image_dir.pjoin('thumb') + notebook_dir = rst_dir.pjoin('notebook') image_dir.makedirs() thumb_dir.makedirs() + notebook_dir.makedirs() base_image_name = os.path.splitext(src_name)[0] image_path = image_dir.pjoin(base_image_name + '_{0}.png') basename, py_ext = os.path.splitext(src_name) rst_path = rst_dir.pjoin(basename + cfg.source_suffix) + notebook_path = notebook_dir.pjoin(basename + '.ipynb') - if _plots_are_current(src_path, image_path) and rst_path.exists: + if _plots_are_current(src_path, image_path) and rst_path.exists and \ + notebook_path.exists: return blocks = split_code_and_text_blocks(example_file) @@ -341,8 +356,11 @@ def write_example(src_name, src_dir, rst_dir, cfg): example_rst += LITERALINCLUDE.format(**code_info) example_rst += CODE_LINK.format(src_name) + ipnotebook_name = src_name.replace('.py', '.ipynb') + ipnotebook_name = './notebook/' + ipnotebook_name + example_rst += NOTEBOOK_LINK.format(ipnotebook_name) - f = open(rst_path,'w') + f = open(rst_path, 'w') f.write(example_rst) f.flush() @@ -359,6 +377,24 @@ def write_example(src_name, src_dir, rst_dir, cfg): else: shutil.copy(cfg.plot2rst_default_thumb, thumb_path) + # Export example to IPython notebook + nb = Notebook() + + for (cell_type, _, content) in blocks: + content = content.rstrip('\n') + + if cell_type == 'code': + nb.add_cell(content, cell_type='code') + else: + content = content.replace('"""', '') + content = '\n'.join([line for line in content.split('\n') if + not line.startswith('.. image')]) + html = publish_parts(content, writer_name='html')['html_body'] + nb.add_cell(html, cell_type='markdown') + + with open(notebook_path, 'w') as f: + f.write(nb.json()) + def save_thumbnail(image, thumb_path, shape): """Save image as a thumbnail with the specified shape. diff --git a/doc/gh-pages.py b/doc/gh-pages.py old mode 100644 new mode 100755 index af992158..7bcf802c --- a/doc/gh-pages.py +++ b/doc/gh-pages.py @@ -30,7 +30,7 @@ from subprocess import Popen, PIPE, CalledProcessError, check_call pages_dir = 'gh-pages' html_dir = 'build/html' pdf_dir = 'build/latex' -pages_repo = 'git@github.com:scikit-image/docs.git' +pages_repo = 'https://github.com/scikit-image/docs.git' #----------------------------------------------------------------------------- # Functions @@ -86,13 +86,14 @@ if __name__ == '__main__': if l.startswith('VERSION'): tag = l.split("'")[1] - # Rename to, e.g., 0.9.x - tag = '.'.join(tag.split('.')[:-1] + ['x']) + if "dev" in tag: + tag = "dev" + else: + # Rename e.g. 0.9.0 to 0.9.x + tag = '.'.join(tag.split('.')[:-1] + ['x']) break - if "dev" in tag: - tag = "dev" startdir = os.getcwd() if not os.path.exists(pages_dir): @@ -124,7 +125,7 @@ if __name__ == '__main__': sh("touch .nojekyll") sh('git add .nojekyll') sh('git add index.html') - sh('git add %s' % tag) + sh('git add --all %s' % tag) sh2('git commit -m"Updated doc release: %s"' % tag) print('Most recent commit:') diff --git a/doc/release/contribs.py b/doc/release/contribs.py index 08a59fc9..58cb11d2 100755 --- a/doc/release/contribs.py +++ b/doc/release/contribs.py @@ -5,16 +5,16 @@ import string import shlex if len(sys.argv) != 2: - print "Usage: ./contributors.py tag-of-previous-release" + 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') + return subprocess.check_output(shlex.split(cmd), universal_newlines=True).split('\n') tag_date = call("git show --format='%%ci' %s" % tag)[0] -print "Release %s was on %s" % (tag, tag_date) +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()] @@ -22,27 +22,26 @@ 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 +print("\nIt contained the following %d merges:\n" % len(merges)) for (merge, message) in merges: if merge.startswith('Merge pull request #'): PR = ' (%s)' % merge.split()[3] else: PR = '' - print '- ' + message + PR + print('- ' + message + PR) -print "\nMade by the following committers [alphabetical by last name]:\n" +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] + author = [v for v in author.split() if v[0] in string.ascii_letters] return author[-1] authors = sorted(set(authors), key=key) for a in authors: - print '-', a + print('- ' + a) diff --git a/doc/source/_static/docversions.js b/doc/source/_static/docversions.js index b4fd531f..cb0d3339 100644 --- a/doc/source/_static/docversions.js +++ b/doc/source/_static/docversions.js @@ -1,4 +1,4 @@ -var versions = ['dev', '0.9.x', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; +var versions = ['dev', '0.10.x', '0.9.x', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; function insert_version_links() { for (i = 0; i < versions.length; i++){ diff --git a/doc/source/api_changes.txt b/doc/source/api_changes.txt index 1f755635..0271bec0 100644 --- a/doc/source/api_changes.txt +++ b/doc/source/api_changes.txt @@ -1,3 +1,7 @@ +Version 0.10 +------------ +- Removed ``skimage.io.video`` functionality due to broken gstreamer bindings + Version 0.9 ----------- - No longer wrap ``imread`` output in an ``Image`` class @@ -20,4 +24,3 @@ Version 0.3 - Remove ``as_grey``, ``dtype`` keyword from ImageCollection - Remove ``dtype`` from imread - Generalise ImageCollection to accept a load_func - diff --git a/doc/source/install.txt b/doc/source/install.txt index b78c1269..81cb7340 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -5,9 +5,9 @@ Pre-built installation `__ are kindly provided by Christoph Gohlke. -The latest stable release is also included as part of the `Enthought Python -Distribution (EPD) `__, `Python(x,y) -`__ and +The latest stable release is also included as part of +`Enthought Canopy `__, +`Python(x,y) `__ and `Anaconda `__. On Debian and Ubuntu, a Debian package ``python-skimage`` can be found in diff --git a/doc/source/user_guide/data_types.txt b/doc/source/user_guide/data_types.txt index 43ea8e9b..258314af 100644 --- a/doc/source/user_guide/data_types.txt +++ b/doc/source/user_guide/data_types.txt @@ -134,7 +134,11 @@ dtype range:: Here, the ``in_range`` argument is set to the maximum range for a 10-bit image. By default, ``rescale_intensity`` stretches the values of ``in_range`` to match -the range of the dtype. +the range of the dtype. ``rescale_intensity`` also accepts strings as inputs +to ``in_range`` and ``out_range``, so the example above could also be written +as:: + + >>> image = exposure.rescale_intensity(img10bit, in_range='uint10') Note about negative values diff --git a/doc/source/user_guide/viewer.txt b/doc/source/user_guide/viewer.txt index 82bb5949..3145effd 100644 --- a/doc/source/user_guide/viewer.txt +++ b/doc/source/user_guide/viewer.txt @@ -24,26 +24,37 @@ alternatively, ``skimage.io.imshow`` which adds support for multiple io-plugins) to display images. The advantage of ``ImageViewer`` is that you can easily add plugins for manipulating images. Currently, only a few plugins are implemented, but it is easy to write your own. Before going into the details, -let's see an example of how a plugin is added to the viewer: +let's see an example of how a pre-defined plugin is added to the viewer: .. code-block:: python - from skimage.viewer.plugins import Canny + from skimage.viewer.plugins.lineprofile import LineProfile viewer = ImageViewer(image) - viewer += Canny(view) - viewer.show() + viewer += LineProfile(viewer) + overlay, data = viewer.show()[0] -At the moment, there aren't very many plugins pre-defined, but there's a really -simple interface for creating your own plugin. First, let's create a plugin to -call the total-variation denoising function, ``tv_denoise``: +The viewer's ``show()`` method returns a list of tuples, one for each attached +plugin. Each tuple contains two elements: an overlay of the same shape as the +input image, and a data field (which may be ``None``). A plugin class documents +its return value in its ``output`` method. + +In this example, only one plugin is attached, so the list returned by ``show`` +will have length 1. We extract the single tuple and bind its ``overlay`` and +``data`` elements to individual variables. Here, ``overlay`` contains an image +of the line drawn on the viewer, and ``data`` contains the 1-dimensional +intensity profile along that line. + +At the moment, there are not many plugins pre-defined, but there is a really +simple interface for creating your own plugin. First, let us create a plugin to +call the total-variation denoising function, ``denoise_tv_bregman``: .. code-block:: python - from skimage.filter import tv_denoise + from skimage.filter import denoise_tv_bregman from skimage.viewer.plugins.base import Plugin - denoise_plugin = Plugin(image_filter=tv_denoise) + denoise_plugin = Plugin(image_filter=denoise_tv_bregman) .. note:: @@ -74,8 +85,10 @@ All that's left is to create an image viewer and add the plugin to that viewer. viewer = ImageViewer(image) viewer += denoise_plugin - viewer.show() + denoised = viewer.show()[0][0] +Here, we access only the overlay returned by the plugin, which contains the +filtered image for the last used setting of ``weight``. .. image:: data/denoise_viewer_window.png .. image:: data/denoise_plugin_window.png diff --git a/doc/tools/LICENSE.txt b/doc/tools/LICENSE.txt index 408ba99c..bb5fd095 100644 --- a/doc/tools/LICENSE.txt +++ b/doc/tools/LICENSE.txt @@ -1,6 +1,6 @@ These files were obtained from -http://groups.google.com/group/sphinx-dev/browse_thread/thread/595ef2eff60084c5/ +https://www.mail-archive.com/sphinx-dev@googlegroups.com/msg02472.html and were released under a BSD/MIT license by Fernando Perez, Matthew Brett and the PyMVPA guys. diff --git a/requirements.txt b/requirements.txt index 4a4aaa53..48e03b8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ cython>=0.17 matplotlib>=1.0 numpy>=1.6 +six>=1.3.0 diff --git a/setup.py b/setup.py index 2420c33c..68cbec22 100755 --- a/setup.py +++ b/setup.py @@ -17,13 +17,18 @@ 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.9.3' +VERSION = '0.10.0' PYTHON_VERSION = (2, 5) DEPENDENCIES = { 'numpy': (1, 6), - 'Cython': (0, 17), + 'six': (1, 3), } +# Only require Cython if we have a developer checkout +if VERSION.endswith('dev'): + DEPENDENCIES['Cython'] = (0, 17) + + import os import sys diff --git a/skimage/__init__.py b/skimage/__init__.py index c73a79b7..59c80ed2 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -14,27 +14,34 @@ color data Test images and example data. draw - Image drawing primitives (lines, text, etc.). + Drawing primitives (lines, text, etc.) that operate on NumPy arrays. exposure - Image intensity adjustment (e.g., histogram equalization). + Image intensity adjustment, e.g., histogram equalization, etc. feature - Feature detection (e.g. texture analysis, corners, etc.). + Feature detection and extraction, e.g., texture analysis corners, etc. filter - Sharpening, edge finding, denoising, etc. + Sharpening, edge finding, rank filters, thresholding, etc. graph - Graph-theoretic operations, e.g. dynamic programming (shortest paths). + Graph-theoretic operations, e.g., shortest paths. io Reading, saving, and displaying images and video. measure Measurement of image properties, e.g., similarity and contours. morphology - Morphological operations, e.g. opening or skeletonization. + Morphological operations, e.g., opening or skeletonization. +novice + Simplified interface for teaching purposes. +restoration + Restoration algorithms, e.g., deconvolution algorithms, denoising, etc. segmentation - Splitting an image into self-similar regions. + Partitioning an image into multiple regions. transform - Geometric and other transforms, e.g. rotation or the Radon transform. + Geometric and other transforms, e.g., rotation or the Radon transform. util Generic utilities. +viewer + A simple graphical user interface for visualizing results and exploring + parameters. Utility Functions ----------------- @@ -52,6 +59,7 @@ img_as_ubyte import os.path as _osp import imp as _imp import functools as _functools +import warnings as _warnings from skimage._shared.utils import deprecated as _deprecated pkg_dir = _osp.abspath(_osp.dirname(__file__)) @@ -68,24 +76,48 @@ try: _imp.find_module('nose') except ImportError: def _test(verbose=False): - """This would invoke the skimage test suite, but nose couldn't be + """This would run all unit tests, but nose couldn't be imported so the test suite can not run. """ raise ImportError("Could not load nose. Unit tests not available.") + + def _doctest(verbose=False): + """This would run all doc tests, but nose couldn't be + imported so the test suite can not run. + """ + raise ImportError("Could not load nose. Doctests not available.") else: - def _test(verbose=False): - """Invoke the skimage test suite.""" + def _test(doctest=False, verbose=False): + """Run all unit tests.""" import nose - args = ['', pkg_dir, '--exe'] + args = ['', pkg_dir, '--exe', '--ignore-files=^_test'] if verbose: args.extend(['-v', '-s']) - nose.run('skimage', argv=args) + if doctest: + args.extend(['--with-doctest', '--ignore-files=^\.', + '--ignore-files=^setup\.py$$', '--ignore-files=test']) + # Make sure warnings do not break the doc tests + with _warnings.catch_warnings(): + _warnings.simplefilter("ignore") + success = nose.run('skimage', argv=args) + else: + success = nose.run('skimage', argv=args) + # Return sys.exit code + if success: + return 0 + else: + return 1 + # do not use `test` as function name as this leads to a recursion problem with # the nose test suite test = _test test_verbose = _functools.partial(test, verbose=True) test_verbose.__doc__ = test.__doc__ +doctest = _functools.partial(test, doctest=True) +doctest.__doc__ = doctest.__doc__ +doctest_verbose = _functools.partial(test, doctest=True, verbose=True) +doctest_verbose.__doc__ = doctest.__doc__ class _Log(Warning): @@ -105,10 +137,9 @@ class _FakeLog(object): """ self._name = name - import warnings warnings.simplefilter("always", _Log) - self._warnings = warnings + self._warnings = _warnings def _warn(self, msg, wtype): self._warnings.warn('%s: %s' % (wtype, msg), _Log) @@ -137,30 +168,4 @@ class _FakeLog(object): pass -@_deprecated() -def get_log(name=None): - """Return a console logger. - - Output may be sent to the logger using the `debug`, `info`, `warning`, - `error` and `critical` methods. - - Parameters - ---------- - name : str - Name of the log. - - References - ---------- - .. [1] Logging facility for Python, - http://docs.python.org/library/logging.html - - """ - if name is None: - name = 'skimage' - else: - name = 'skimage.' + name - - return _FakeLog(name) - - from .util.dtype import * diff --git a/skimage/_build.py b/skimage/_build.py index 38239e4b..b2ea94fc 100644 --- a/skimage/_build.py +++ b/skimage/_build.py @@ -3,7 +3,6 @@ import os import hashlib import subprocess - # WindowsError is not defined on unix systems try: WindowsError @@ -26,7 +25,7 @@ def cython(pyx_files, working_path=''): return try: - import Cython + from Cython.Build import cythonize except ImportError: # If cython is not found, we do nothing -- the build will make use of # the distributed .c files @@ -39,24 +38,7 @@ def cython(pyx_files, working_path=''): if not _changed(pyxfile): continue - c_file = pyxfile[:-4] + '.c' - - # run cython compiler - cmd = 'cython -o %s %s' % (c_file, pyxfile) - print(cmd) - - try: - subprocess.call(['cython', '-o', c_file, pyxfile]) - except WindowsError: - # On Windows cython.exe may be missing if Cython was installed - # via distutils. Run the cython.py script instead. - subprocess.call( - [sys.executable, - os.path.join(os.path.dirname(sys.executable), - 'Scripts', 'cython.py'), - '-o', c_file, pyxfile], - shell=True) - + cythonize(pyxfile) def _md5sum(f): m = hashlib.new('md5') @@ -86,4 +68,4 @@ def _changed(filename): with open(filename_cache, 'wb') as cf: cf.write(md5_new.encode('utf-8')) - return md5_cached != md5_new + return md5_cached != md5_new.encode('utf-8') diff --git a/skimage/_shared/_warnings.py b/skimage/_shared/_warnings.py new file mode 100644 index 00000000..c4cd0a68 --- /dev/null +++ b/skimage/_shared/_warnings.py @@ -0,0 +1,63 @@ +__all__ = ['all_warnings'] + +from contextlib import contextmanager +import sys +import warnings +import inspect + + +@contextmanager +def all_warnings(): + """ + Context for use in testing to ensure that all warnings are raised. + + Examples + -------- + >>> import warnings + >>> def foo(): + ... warnings.warn(RuntimeWarning("bar")) + + We raise the warning once, while the warning filter is set to "once". + Hereafter, the warning is invisible, even with custom filters: + + >>> with warnings.catch_warnings(): + ... warnings.simplefilter('once') + ... foo() + + We can now run ``foo()`` without a warning being raised: + + >>> from numpy.testing import assert_warns + >>> foo() + + To catch the warning, we call in the help of ``all_warnings``: + + >>> with all_warnings(): + ... assert_warns(RuntimeWarning, foo) + """ + + # Whenever a warning is triggered, Python adds a __warningregistry__ + # member to the *calling* module. The exercize here is to find + # and eradicate all those breadcrumbs that were left lying around. + # + # We proceed by first searching all parent calling frames and explicitly + # clearing their warning registries (necessary for the doctests above to + # pass). Then, we search for all submodules of skimage and clear theirs + # as well (necessary for the skimage test suite to pass). + + frame = inspect.currentframe() + if frame: + for f in inspect.getouterframes(frame): + f[0].f_locals['__warningregistry__'] = {} + del frame + + for mod_name, mod in list(sys.modules.items()): + if 'six.moves' in mod_name: + continue + try: + mod.__warningregistry__.clear() + except AttributeError: + pass + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + yield w diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd index c5f32b6a..7d212969 100644 --- a/skimage/_shared/interpolation.pxd +++ b/skimage/_shared/interpolation.pxd @@ -1,27 +1,333 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +from libc.math cimport ceil, floor -cdef double nearest_neighbour_interpolation(double* image, Py_ssize_t rows, - Py_ssize_t cols, double r, - double c, char mode, - double cval) -cdef double bilinear_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, - double r, double c, char mode, - double cval) +cdef inline Py_ssize_t round(double r): + return ((r + 0.5) if (r > 0.0) else (r - 0.5)) -cdef double quadratic_interpolation(double x, double[3] f) -cdef double biquadratic_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, - double r, double c, char mode, - double cval) -cdef double cubic_interpolation(double x, double[4] f) -cdef double bicubic_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, - double r, double c, char mode, - double cval) +cdef inline double nearest_neighbour_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, + double c, char mode, + double cval): + """Nearest neighbour interpolation at a given position in the image. -cdef double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, Py_ssize_t r, - Py_ssize_t c, char mode, double cval) + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. -cdef double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, Py_ssize_t dims, - Py_ssize_t r, Py_ssize_t c, Py_ssize_t d, char mode, double cval) + Returns + ------- + value : double + Interpolated value. -cdef Py_ssize_t coord_map(Py_ssize_t dim, Py_ssize_t coord, char mode) + """ + + return get_pixel2d(image, rows, cols, round(r), round(c), mode, cval) + + +cdef inline double bilinear_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, + char mode, double cval): + """Bilinear interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + cdef double dr, dc + cdef Py_ssize_t minr, minc, maxr, maxc + + minr = floor(r) + minc = floor(c) + maxr = ceil(r) + maxc = ceil(c) + dr = r - minr + dc = c - minc + top = (1 - dc) * get_pixel2d(image, rows, cols, minr, minc, mode, cval) \ + + dc * get_pixel2d(image, rows, cols, minr, maxc, mode, cval) + bottom = (1 - dc) * get_pixel2d(image, rows, cols, maxr, minc, mode, cval) \ + + dc * get_pixel2d(image, rows, cols, maxr, maxc, mode, cval) + return (1 - dr) * top + dr * bottom + + +cdef inline double quadratic_interpolation(double x, double[3] f): + """Quadratic interpolation. + + Parameters + ---------- + x : double + Position in the interval [-1, 1]. + f : double[4] + Function values at positions [-1, 0, 1]. + + Returns + ------- + value : double + Interpolated value. + + """ + return f[1] - 0.25 * (f[0] - f[2]) * x + + +cdef inline double biquadratic_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, + char mode, double cval): + """Biquadratic interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + + cdef Py_ssize_t r0 = round(r) + cdef Py_ssize_t c0 = round(c) + if r < 0: + r0 -= 1 + if c < 0: + c0 -= 1 + # scale position to range [-1, 1] + cdef double xr = (r - r0) - 1 + cdef double xc = (c - c0) - 1 + if r == r0: + xr += 1 + if c == c0: + xc += 1 + + cdef double fc[3] + cdef double fr[3] + + cdef Py_ssize_t pr, pc + + # row-wise cubic interpolation + for pr in range(r0, r0 + 3): + for pc in range(c0, c0 + 3): + fc[pc - c0] = get_pixel2d(image, rows, cols, pr, pc, mode, cval) + fr[pr - r0] = quadratic_interpolation(xc, fc) + + # cubic interpolation for interpolated values of each row + return quadratic_interpolation(xr, fr) + + +cdef inline double cubic_interpolation(double x, double[4] f): + """Cubic interpolation. + + Parameters + ---------- + x : double + Position in the interval [0, 1]. + f : double[4] + Function values at positions [0, 1/3, 2/3, 1]. + + Returns + ------- + value : double + Interpolated value. + + """ + return \ + f[1] + 0.5 * x * \ + (f[2] - f[0] + x * \ + (2.0 * f[0] - 5.0 * f[1] + 4.0 * f[2] - f[3] + x * \ + (3.0 * (f[1] - f[2]) + f[3] - f[0]))) + + +cdef inline double bicubic_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, + char mode, double cval): + """Bicubic interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + + cdef Py_ssize_t r0 = r - 1 + cdef Py_ssize_t c0 = c - 1 + if r < 0: + r0 -= 1 + if c < 0: + c0 -= 1 + # scale position to range [0, 1] + cdef double xr = (r - r0) / 3 + cdef double xc = (c - c0) / 3 + + cdef double fc[4] + cdef double fr[4] + + cdef Py_ssize_t pr, pc + + # row-wise cubic interpolation + for pr in range(r0, r0 + 4): + for pc in range(c0, c0 + 4): + fc[pc - c0] = get_pixel2d(image, rows, cols, pr, pc, mode, cval) + fr[pr - r0] = cubic_interpolation(xc, fc) + + # cubic interpolation for interpolated values of each row + return cubic_interpolation(xr, fr) + + +cdef inline double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t c, char mode, double cval): + """Get a pixel from the image, taking wrapping mode into consideration. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : int + Position at which to get the pixel. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Pixel value at given position. + + """ + if mode == 'C': + if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): + return cval + else: + return image[r * cols + c] + else: + return image[coord_map(rows, r, mode) * cols + coord_map(cols, c, mode)] + + +cdef inline double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t dims, Py_ssize_t r, Py_ssize_t c, Py_ssize_t d, + char mode, double cval): + """Get a pixel from the image, taking wrapping mode into consideration. + + Parameters + ---------- + image : double array + Input image. + rows, cols, dims : int + Shape of image. + r, c, d : int + Position at which to get the pixel. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Pixel value at given position. + + """ + if mode == 'C': + if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): + return cval + else: + return image[r * cols * dims + c * dims + d] + else: + return image[coord_map(rows, r, mode) * cols * dims + + coord_map(cols, c, mode) * dims + + d] + + +cdef inline Py_ssize_t coord_map(Py_ssize_t dim, Py_ssize_t coord, char mode): + """ + Wrap a coordinate, according to a given mode. + + Parameters + ---------- + dim : int + Maximum coordinate. + coord : int + Coord provided by user. May be < 0 or > dim. + mode : {'W', 'R', 'N'} + Whether to wrap or reflect the coordinate if it + falls outside [0, dim). + + """ + dim = dim - 1 + if mode == 'R': # reflect + if coord < 0: + # How many times times does the coordinate wrap? + if (-coord / dim) % 2 != 0: + return dim - (-coord % dim) + else: + return (-coord % dim) + elif coord > dim: + if (coord / dim) % 2 != 0: + return (dim - (coord % dim)) + else: + return (coord % dim) + elif mode == 'W': # wrap + if coord < 0: + return (dim - (-coord % dim)) + elif coord > dim: + return (coord % dim) + elif mode == 'N': # nearest + if coord < 0: + return 0 + elif coord > dim: + return dim + + return coord diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx deleted file mode 100644 index a8b96014..00000000 --- a/skimage/_shared/interpolation.pyx +++ /dev/null @@ -1,331 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False -from libc.math cimport ceil, floor - - -cdef inline Py_ssize_t round(double r): - return ((r + 0.5) if (r > 0.0) else (r - 0.5)) - - -cdef inline double nearest_neighbour_interpolation(double* image, Py_ssize_t rows, - Py_ssize_t cols, double r, - double c, char mode, - double cval): - """Nearest neighbour interpolation at a given position in the image. - - Parameters - ---------- - image : double array - Input image. - rows, cols : int - Shape of image. - r, c : double - Position at which to interpolate. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Interpolated value. - - """ - - return get_pixel2d(image, rows, cols, round(r), round(c), mode, cval) - - -cdef inline double bilinear_interpolation(double* image, Py_ssize_t rows, - Py_ssize_t cols, double r, double c, - char mode, double cval): - """Bilinear interpolation at a given position in the image. - - Parameters - ---------- - image : double array - Input image. - rows, cols : int - Shape of image. - r, c : double - Position at which to interpolate. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Interpolated value. - - """ - cdef double dr, dc - cdef Py_ssize_t minr, minc, maxr, maxc - - minr = floor(r) - minc = floor(c) - maxr = ceil(r) - maxc = ceil(c) - dr = r - minr - dc = c - minc - top = (1 - dc) * get_pixel2d(image, rows, cols, minr, minc, mode, cval) \ - + dc * get_pixel2d(image, rows, cols, minr, maxc, mode, cval) - bottom = (1 - dc) * get_pixel2d(image, rows, cols, maxr, minc, mode, cval) \ - + dc * get_pixel2d(image, rows, cols, maxr, maxc, mode, cval) - return (1 - dr) * top + dr * bottom - - -cdef inline double quadratic_interpolation(double x, double[3] f): - """Quadratic interpolation. - - Parameters - ---------- - x : double - Position in the interval [-1, 1]. - f : double[4] - Function values at positions [-1, 0, 1]. - - Returns - ------- - value : double - Interpolated value. - - """ - return f[1] - 0.25 * (f[0] - f[2]) * x - - -cdef inline double biquadratic_interpolation(double* image, Py_ssize_t rows, - Py_ssize_t cols, double r, double c, - char mode, double cval): - """Biquadratic interpolation at a given position in the image. - - Parameters - ---------- - image : double array - Input image. - rows, cols : int - Shape of image. - r, c : double - Position at which to interpolate. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Interpolated value. - - """ - - cdef Py_ssize_t r0 = round(r) - cdef Py_ssize_t c0 = round(c) - if r < 0: - r0 -= 1 - if c < 0: - c0 -= 1 - # scale position to range [-1, 1] - cdef double xr = (r - r0) - 1 - cdef double xc = (c - c0) - 1 - if r == r0: - xr += 1 - if c == c0: - xc += 1 - - cdef double fc[3], fr[3] - - cdef Py_ssize_t pr, pc - - # row-wise cubic interpolation - for pr in range(r0, r0 + 3): - for pc in range(c0, c0 + 3): - fc[pc - c0] = get_pixel2d(image, rows, cols, pr, pc, mode, cval) - fr[pr - r0] = quadratic_interpolation(xc, fc) - - # cubic interpolation for interpolated values of each row - return quadratic_interpolation(xr, fr) - - -cdef inline double cubic_interpolation(double x, double[4] f): - """Cubic interpolation. - - Parameters - ---------- - x : double - Position in the interval [0, 1]. - f : double[4] - Function values at positions [0, 1/3, 2/3, 1]. - - Returns - ------- - value : double - Interpolated value. - - """ - return \ - f[1] + 0.5 * x * \ - (f[2] - f[0] + x * \ - (2.0 * f[0] - 5.0 * f[1] + 4.0 * f[2] - f[3] + x * \ - (3.0 * (f[1] - f[2]) + f[3] - f[0]))) - - -cdef inline double bicubic_interpolation(double* image, Py_ssize_t rows, - Py_ssize_t cols, double r, double c, - char mode, double cval): - """Bicubic interpolation at a given position in the image. - - Parameters - ---------- - image : double array - Input image. - rows, cols : int - Shape of image. - r, c : double - Position at which to interpolate. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Interpolated value. - - """ - - cdef Py_ssize_t r0 = r - 1 - cdef Py_ssize_t c0 = c - 1 - if r < 0: - r0 -= 1 - if c < 0: - c0 -= 1 - # scale position to range [0, 1] - cdef double xr = (r - r0) / 3 - cdef double xc = (c - c0) / 3 - - cdef double fc[4], fr[4] - - cdef Py_ssize_t pr, pc - - # row-wise cubic interpolation - for pr in range(r0, r0 + 4): - for pc in range(c0, c0 + 4): - fc[pc - c0] = get_pixel2d(image, rows, cols, pr, pc, mode, cval) - fr[pr - r0] = cubic_interpolation(xc, fc) - - # cubic interpolation for interpolated values of each row - return cubic_interpolation(xr, fr) - - -cdef inline double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, - Py_ssize_t r, Py_ssize_t c, char mode, double cval): - """Get a pixel from the image, taking wrapping mode into consideration. - - Parameters - ---------- - image : double array - Input image. - rows, cols : int - Shape of image. - r, c : int - Position at which to get the pixel. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Pixel value at given position. - - """ - if mode == 'C': - if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): - return cval - else: - return image[r * cols + c] - else: - return image[coord_map(rows, r, mode) * cols + coord_map(cols, c, mode)] - - -cdef inline double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, - Py_ssize_t dims, Py_ssize_t r, Py_ssize_t c, Py_ssize_t d, - char mode, double cval): - """Get a pixel from the image, taking wrapping mode into consideration. - - Parameters - ---------- - image : double array - Input image. - rows, cols, dims : int - Shape of image. - r, c, d : int - Position at which to get the pixel. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Pixel value at given position. - - """ - if mode == 'C': - if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): - return cval - else: - return image[r * cols * dims + c * dims + d] - else: - return image[coord_map(rows, r, mode) * cols * dims - + coord_map(cols, c, mode) * dims - + d] - - -cdef inline Py_ssize_t coord_map(Py_ssize_t dim, Py_ssize_t coord, char mode): - """ - Wrap a coordinate, according to a given mode. - - Parameters - ---------- - dim : int - Maximum coordinate. - coord : int - Coord provided by user. May be < 0 or > dim. - mode : {'W', 'R', 'N'} - Whether to wrap or reflect the coordinate if it - falls outside [0, dim). - - """ - dim = dim - 1 - if mode == 'R': # reflect - if coord < 0: - # How many times times does the coordinate wrap? - if (-coord / dim) % 2 != 0: - return dim - (-coord % dim) - else: - return (-coord % dim) - elif coord > dim: - if (coord / dim) % 2 != 0: - return (dim - (coord % dim)) - else: - return (coord % dim) - elif mode == 'W': # wrap - if coord < 0: - return (dim - (-coord % dim)) - elif coord > dim: - return (coord % dim) - elif mode == 'N': # nearest - if coord < 0: - return 0 - elif coord > dim: - return dim - - return coord diff --git a/skimage/_shared/setup.py b/skimage/_shared/setup.py index 765c30de..4c36b97e 100644 --- a/skimage/_shared/setup.py +++ b/skimage/_shared/setup.py @@ -14,12 +14,9 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['geometry.pyx'], working_path=base_path) - cython(['interpolation.pyx'], working_path=base_path) cython(['transform.pyx'], working_path=base_path) config.add_extension('geometry', sources=['geometry.c']) - config.add_extension('interpolation', sources=['interpolation.c'], - include_dirs=[get_numpy_include_dirs()]) config.add_extension('transform', sources=['transform.c'], include_dirs=[get_numpy_include_dirs()]) diff --git a/skimage/_shared/six.py b/skimage/_shared/six.py deleted file mode 100644 index 8a877b17..00000000 --- a/skimage/_shared/six.py +++ /dev/null @@ -1,423 +0,0 @@ -"""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/testing.py b/skimage/_shared/testing.py index eab83a56..9b42c582 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -1,6 +1,12 @@ """Testing utilities.""" +import re + + +SKIP_RE = re.compile("(\s*>>>.*?)(\s*)#\s*skip\s+if\s+(.*)$") + + def _assert_less(a, b, msg=None): message = "%r is not lower than %r" % (a, b) if msg is not None: @@ -24,3 +30,48 @@ try: from nose.tools import assert_greater except ImportError: assert_greater = _assert_greater + + +def doctest_skip_parser(func): + """ Decorator replaces custom skip test markup in doctests + + Say a function has a docstring:: + + >>> something # skip if not HAVE_AMODULE + >>> something + else + >>> something # skip if HAVE_BMODULE + + This decorator will evaluate the expresssion after ``skip if``. If this + evaluates to True, then the comment is replaced by ``# doctest: +SKIP``. If + False, then the comment is just removed. The expression is evaluated in the + ``globals`` scope of `func`. + + For example, if the module global ``HAVE_AMODULE`` is False, and module + global ``HAVE_BMODULE`` is False, the returned function will have docstring:: + + >>> something # doctest: +SKIP + >>> something + else + >>> something + + """ + lines = func.__doc__.split('\n') + new_lines = [] + for line in lines: + match = SKIP_RE.match(line) + if match is None: + new_lines.append(line) + continue + code, space, expr = match.groups() + + try: + # Works as a function decorator + if eval(expr, func.__globals__): + code = code + space + "# doctest: +SKIP" + except AttributeError: + # Works as a class decorator + if eval(expr, func.__init__.__globals__): + code = code + space + "# doctest: +SKIP" + + new_lines.append(code) + func.__doc__ = "\n".join(new_lines) + return func diff --git a/skimage/_shared/tests/test_safe_as_int.py b/skimage/_shared/tests/test_safe_as_int.py new file mode 100644 index 00000000..009fa9a4 --- /dev/null +++ b/skimage/_shared/tests/test_safe_as_int.py @@ -0,0 +1,36 @@ +import numpy as np +from skimage._shared.utils import safe_as_int + + +def test_int_cast_not_possible(): + np.testing.assert_raises(ValueError, safe_as_int, 7.1) + np.testing.assert_raises(ValueError, safe_as_int, [7.1, 0.9]) + np.testing.assert_raises(ValueError, safe_as_int, np.r_[7.1, 0.9]) + np.testing.assert_raises(ValueError, safe_as_int, (7.1, 0.9)) + np.testing.assert_raises(ValueError, safe_as_int, ((3, 4, 1), + (2, 7.6, 289))) + + np.testing.assert_raises(ValueError, safe_as_int, 7.1, 0.09) + np.testing.assert_raises(ValueError, safe_as_int, [7.1, 0.9], 0.09) + np.testing.assert_raises(ValueError, safe_as_int, np.r_[7.1, 0.9], 0.09) + np.testing.assert_raises(ValueError, safe_as_int, (7.1, 0.9), 0.09) + np.testing.assert_raises(ValueError, safe_as_int, ((3, 4, 1), + (2, 7.6, 289)), 0.25) + + +def test_int_cast_possible(): + np.testing.assert_equal(safe_as_int(7.1, atol=0.11), 7) + np.testing.assert_equal(safe_as_int(-7.1, atol=0.11), -7) + np.testing.assert_equal(safe_as_int(41.9, atol=0.11), 42) + np.testing.assert_array_equal(safe_as_int([2, 42, 5789234.0, 87, 4]), + np.r_[2, 42, 5789234, 87, 4]) + np.testing.assert_array_equal(safe_as_int(np.r_[[[3, 4, 1.000000001], + [7, 2, -8.999999999], + [6, 9, -4234918347.]]]), + np.r_[[[3, 4, 1], + [7, 2, -9], + [6, 9, -4234918347]]]) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/_shared/tests/test_testing.py b/skimage/_shared/tests/test_testing.py new file mode 100644 index 00000000..f563caad --- /dev/null +++ b/skimage/_shared/tests/test_testing.py @@ -0,0 +1,87 @@ +""" Testing decorators module +""" + +import numpy as np +from nose.tools import (assert_true, assert_raises, assert_equal) +from skimage._shared.testing import doctest_skip_parser + + +def test_skipper(): + def f(): + pass + + class c(): + def __init__(self): + self.me = "I think, therefore..." + + docstring = \ + """ Header + + >>> something # skip if not HAVE_AMODULE + >>> something + else + >>> a = 1 # skip if not HAVE_BMODULE + >>> something2 # skip if HAVE_AMODULE + """ + f.__doc__ = docstring + c.__doc__ = docstring + + global HAVE_AMODULE, HAVE_BMODULE + HAVE_AMODULE = False + HAVE_BMODULE = True + + f2 = doctest_skip_parser(f) + c2 = doctest_skip_parser(c) + assert_true(f is f2) + assert_true(c is c2) + + assert_equal(f2.__doc__, + """ Header + + >>> something # doctest: +SKIP + >>> something + else + >>> a = 1 + >>> something2 + """) + assert_equal(c2.__doc__, + """ Header + + >>> something # doctest: +SKIP + >>> something + else + >>> a = 1 + >>> something2 + """) + + HAVE_AMODULE = True + HAVE_BMODULE = False + f.__doc__ = docstring + c.__doc__ = docstring + f2 = doctest_skip_parser(f) + c2 = doctest_skip_parser(c) + + assert_true(f is f2) + assert_equal(f2.__doc__, + """ Header + + >>> something + >>> something + else + >>> a = 1 # doctest: +SKIP + >>> something2 # doctest: +SKIP + """) + assert_equal(c2.__doc__, + """ Header + + >>> something + >>> something + else + >>> a = 1 # doctest: +SKIP + >>> something2 # doctest: +SKIP + """) + + del HAVE_AMODULE + f.__doc__ = docstring + c.__doc__ = docstring + assert_raises(NameError, doctest_skip_parser, f) + assert_raises(NameError, doctest_skip_parser, c) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/_shared/transform.pyx b/skimage/_shared/transform.pyx index 9bdc6824..d77ec583 100644 --- a/skimage/_shared/transform.pyx +++ b/skimage/_shared/transform.pyx @@ -41,4 +41,5 @@ cdef float integrate(float[:, ::1] sat, Py_ssize_t r0, Py_ssize_t c0, if (c0 - 1 >= 0): S -= sat[r1, c0 - 1] + return S diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index b7071521..612a2b63 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -1,11 +1,14 @@ import warnings import functools import sys +import numpy as np -from . import six +import six +from ._warnings import all_warnings -__all__ = ['deprecated', 'get_bound_method_class'] +__all__ = ['deprecated', 'get_bound_method_class', 'all_warnings', + 'safe_as_int'] class skimage_deprecation(Warning): @@ -71,3 +74,70 @@ def get_bound_method_class(m): """ return m.im_class if sys.version < '3' else m.__self__.__class__ + + +def safe_as_int(val, atol=1e-3): + """ + Attempt to safely cast values to integer format. + + Parameters + ---------- + val : scalar or iterable of scalars + Number or container of numbers which are intended to be interpreted as + integers, e.g., for indexing purposes, but which may not carry integer + type. + atol : float + Absolute tolerance away from nearest integer to consider values in + ``val`` functionally integers. + + Returns + ------- + val_int : NumPy scalar or ndarray of dtype `np.int64` + Returns the input value(s) coerced to dtype `np.int64` assuming all + were within ``atol`` of the nearest integer. + + Notes + ----- + This operation calculates ``val`` modulo 1, which returns the mantissa of + all values. Then all mantissas greater than 0.5 are subtracted from one. + Finally, the absolute tolerance from zero is calculated. If it is less + than ``atol`` for all value(s) in ``val``, they are rounded and returned + in an integer array. Or, if ``val`` was a scalar, a NumPy scalar type is + returned. + + If any value(s) are outside the specified tolerance, an informative error + is raised. + + Examples + -------- + >>> _safe_as_int(7.0) + 7 + + >>> _safe_as_int([9, 4, 2.9999999999]) + array([9, 4, 3], dtype=int32) + + >>> _safe_as_int(53.01) + Traceback (most recent call last): + ... + ValueError: Integer argument required but received 53.1, check inputs. + + >>> _safe_as_int(53.01, atol=0.01) + 53 + + """ + mod = np.asarray(val) % 1 # Extract mantissa + + # Check for and subtract any mod values > 0.5 from 1 + if mod.ndim == 0: # Scalar input, cannot be indexed + if mod > 0.5: + mod = 1 - mod + else: # Iterable input, now ndarray + mod[mod > 0.5] = 1 - mod[mod > 0.5] # Test on each side of nearest int + + try: + np.testing.assert_allclose(mod, 0, atol=atol) + except AssertionError: + raise ValueError("Integer argument required but received " + "{0}, check inputs.".format(val)) + + return np.round(val).astype(np.int64) diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index 1c61020d..6b589480 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -13,6 +13,10 @@ from .colorconv import (convert_colorspace, lab2xyz, lab2rgb, rgb2lab, + xyz2luv, + luv2xyz, + luv2rgb, + rgb2luv, rgb2hed, hed2rgb, lab2lch, @@ -40,9 +44,7 @@ from .colorconv import (convert_colorspace, rgb_from_ahx, ahx_from_rgb, rgb_from_hpx, - hpx_from_rgb, - is_rgb, - is_gray) + hpx_from_rgb) from .colorlabel import color_dict, label2rgb @@ -96,8 +98,6 @@ __all__ = ['convert_colorspace', 'ahx_from_rgb', 'rgb_from_hpx', 'hpx_from_rgb', - 'is_rgb', - 'is_gray', 'color_dict', 'label2rgb', 'deltaE_cie76', diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 7562b650..80f115ee 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -29,9 +29,12 @@ Supported color spaces * LAB CIE : Lightness, a, b Colorspace derived from XYZ CIE that is intended to be more perceptually uniform +* LUV CIE : Lightness, u, v + 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) + a and b. The polar angle C is defined to be on ``(0, 2*pi)`` :author: Nicolas Pinto (rgb2hsv) :author: Ralf Gommers (hsv2rgb) @@ -68,7 +71,7 @@ def guess_spatial_dimensions(image): ------- spatial_dims : int or None The number of spatial dimensions of `image`. If ambiguous, the value - is `None`. + is ``None``. Raises ------ @@ -87,32 +90,6 @@ def guess_spatial_dimensions(image): 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. - - Parameters - ---------- - image : ndarray - Input 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). - - Parameters - ---------- - image : ndarray - Input image. - - """ - return image.ndim in (2, 3) and not is_rgb(image) - - def convert_colorspace(arr, fromspace, tospace): """Convert an image array to a new color space. @@ -122,12 +99,12 @@ def convert_colorspace(arr, fromspace, tospace): The image to convert. fromspace : str The color space to convert from. Valid color space strings are - ['RGB', 'HSV', 'RGB CIE', 'XYZ']. Value may also be specified as lower - case. + ``['RGB', 'HSV', 'RGB CIE', 'XYZ']``. Value may also be specified as + lower case. tospace : str The color space to convert to. Valid color space strings are - ['RGB', 'HSV', 'RGB CIE', 'XYZ']. Value may also be specified as lower - case. + ``['RGB', 'HSV', 'RGB CIE', 'XYZ']``. Value may also be specified as + lower case. Returns ------- @@ -137,7 +114,7 @@ def convert_colorspace(arr, fromspace, tospace): Notes ----- Conversion occurs through the "central" RGB color space, i.e. conversion - from XYZ to HSV is implemented as XYZ -> RGB -> HSV instead of directly. + from XYZ to HSV is implemented as ``XYZ -> RGB -> HSV`` instead of directly. Examples -------- @@ -181,17 +158,17 @@ def rgb2hsv(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-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in HSV format, in a 3-D array of shape (.., .., 3). + The image in HSV format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- @@ -259,21 +236,21 @@ def hsv2rgb(hsv): Parameters ---------- hsv : array_like - The image in HSV format, in a 3-D array of shape (.., .., 3). + The image in HSV format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `hsv` is not a 3-D array of shape (.., .., 3). + If `hsv` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- - The conversion assumes an input data range of [0, 1] for all + The conversion assumes an input data range of ``[0, 1]`` for all color components. Conversion between RGB and HSV color spaces results in some loss of @@ -468,17 +445,17 @@ def xyz2rgb(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-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-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 ----- @@ -513,18 +490,18 @@ def rgb2xyz(rgb): ---------- rgb : array_like The image in RGB format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Returns ------- out : ndarray The image in XYZ format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Raises ------ ValueError - If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). + If `rgb` is not a 3- or 4-D array of shape ``(.., ..,[ ..,] 3)``. Notes ----- @@ -556,17 +533,17 @@ def rgb2rgbcie(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-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB CIE format, in a 3-D array of shape (.., .., 3). + The image in RGB CIE format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3-D array of shape ``(.., .., 3)``. References ---------- @@ -588,17 +565,17 @@ def rgbcie2rgb(rgbcie): Parameters ---------- rgbcie : array_like - The image in RGB CIE format, in a 3-D array of shape (.., .., 3). + The image in RGB CIE format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `rgbcie` is not a 3-D array of shape (.., .., 3). + If `rgbcie` is not a 3-D array of shape ``(.., .., 3)``. References ---------- @@ -621,8 +598,8 @@ def rgb2gray(rgb): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3), - or in RGBA format with shape (.., .., 4). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``, + or in RGBA format with shape ``(.., .., 4)``. Returns ------- @@ -632,8 +609,8 @@ def rgb2gray(rgb): Raises ------ ValueError - If `rgb2gray` is not a 3-D array of shape (.., .., 3) or - (.., .., 4). + If `rgb2gray` is not a 3-D array of shape ``(.., .., 3)`` or + ``(.., .., 4)``. References ---------- @@ -698,18 +675,18 @@ def xyz2lab(xyz): ---------- xyz : array_like The image in XYZ format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Returns ------- out : ndarray The image in CIE-LAB format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 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 ----- @@ -755,21 +732,21 @@ def lab2xyz(lab): Parameters ---------- lab : array_like - The image in lab format, in a 3-D array of shape (.., .., 3). + The image in lab format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in XYZ format, in a 3-D array of shape (.., .., 3). + The image in XYZ format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `lab` is not a 3-D array of shape (.., .., 3). + If `lab` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- - Observer= 2A, Illuminant= D65 + Observer = 2A, Illuminant = D65 CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 References @@ -804,18 +781,18 @@ def rgb2lab(rgb): ---------- rgb : array_like The image in RGB format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Returns ------- out : ndarray The image in Lab format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Raises ------ ValueError - If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). + If `rgb` is not a 3- or 4-D array of shape ``(.., ..,[ ..,] 3)``. Notes ----- @@ -829,18 +806,18 @@ def lab2rgb(lab): Parameters ---------- - rgb : array_like - The image in Lab format, in a 3-D array of shape (.., .., 3). + lab : array_like + The image in Lab format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `lab` is not a 3-D array of shape (.., .., 3). + If `lab` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- @@ -849,23 +826,203 @@ def lab2rgb(lab): return xyz2rgb(lab2xyz(lab)) +def xyz2luv(xyz): + """XYZ to CIE-Luv color space conversion. + + Parameters + ---------- + xyz : (M, N, [P,] 3) array_like + The 3 or 4 dimensional image in XYZ format. Final dimension denotes + channels. + + Returns + ------- + out : (M, N, [P,] 3) ndarray + The image in CIE-Luv format. Same dimensions as input. + + Raises + ------ + ValueError + If `xyz` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + XYZ conversion weights use Observer = 2A. Reference whitepoint for D65 + Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. + + References + ---------- + .. [1] http://www.easyrgb.com/index.php?X=MATH&H=16#text16 + .. [2] http://en.wikipedia.org/wiki/CIELUV + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2xyz, xyz2luv + >>> lena = data.lena() + >>> lena_xyz = rgb2xyz(lena) + >>> lena_luv = xyz2luv(lena_xyz) + """ + arr = _prepare_colorarray(xyz) + + # extract channels + x, y, z = arr[..., 0], arr[..., 1], arr[..., 2] + + eps = np.finfo(np.float).eps + + # compute y_r and L + L = y / lab_ref_white[1] + mask = L > 0.008856 + L[mask] = 116. * np.power(L[mask], 1. / 3.) - 16. + L[~mask] = 903.3 * L[~mask] + + u0 = 4*lab_ref_white[0] / np.dot([1, 15, 3], lab_ref_white) + v0 = 9*lab_ref_white[1] / np.dot([1, 15, 3], lab_ref_white) + + # u' and v' helper functions + def fu(X, Y, Z): + return (4.*X) / (X + 15.*Y + 3.*Z + eps) + + def fv(X, Y, Z): + return (9.*Y) / (X + 15.*Y + 3.*Z + eps) + + # compute u and v using helper functions + u = 13.*L * (fu(x, y, z) - u0) + v = 13.*L * (fv(x, y, z) - v0) + + return np.concatenate([q[..., np.newaxis] for q in [L, u, v]], axis=-1) + + +def luv2xyz(luv): + """CIE-Luv to XYZ color space conversion. + + Parameters + ---------- + luv : (M, N, [P,] 3) array_like + The 3 or 4 dimensional image in CIE-Luv format. Final dimension denotes + channels. + + Returns + ------- + out : (M, N, [P,] 3) ndarray + The image in XYZ format. Same dimensions as input. + + Raises + ------ + ValueError + If `luv` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + XYZ conversion weights use Observer = 2A. Reference whitepoint for D65 + Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. + + References + ---------- + .. [1] http://www.easyrgb.com/index.php?X=MATH&H=16#text16 + .. [2] http://en.wikipedia.org/wiki/CIELUV + + """ + + arr = _prepare_colorarray(luv).copy() + + L, u, v = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + + eps = np.finfo(np.float).eps + + # compute y + y = L.copy() + mask = y > 7.999625 + y[mask] = np.power((y[mask]+16.) / 116., 3.) + y[~mask] = y[~mask] / 903.3 + y *= lab_ref_white[1] + + # reference white x,z + uv_weights = [1, 15, 3] + u0 = 4*lab_ref_white[0] / np.dot(uv_weights, lab_ref_white) + v0 = 9*lab_ref_white[1] / np.dot(uv_weights, lab_ref_white) + + # compute intermediate values + a = u0 + u / (13.*L + eps) + b = v0 + v / (13.*L + eps) + c = 3*y * (5*b-3) + + # compute x and z + z = ((a-4)*c - 15*a*b*y) / (12*b) + x = -(c/b + 3.*z) + + return np.concatenate([q[..., np.newaxis] for q in [x, y, z]], axis=-1) + + +def rgb2luv(rgb): + """RGB to CIE-Luv color space conversion. + + Parameters + ---------- + rgb : (M, N, [P,] 3) array_like + The 3 or 4 dimensional image in RGB format. Final dimension denotes + channels. + + Returns + ------- + out : (M, N, [P,] 3) ndarray + The image in CIE Luv format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + This function uses rgb2xyz and xyz2luv. + """ + return xyz2luv(rgb2xyz(rgb)) + + +def luv2rgb(luv): + """Luv to RGB color space conversion. + + Parameters + ---------- + luv : (M, N, [P,] 3) array_like + The 3 or 4 dimensional image in CIE Luv format. Final dimension denotes + channels. + + Returns + ------- + out : (M, N, [P,] 3) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `luv` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + This function uses luv2xyz and xyz2rgb. + """ + return xyz2rgb(luv2xyz(luv)) + + 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). + 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). + 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). + If `rgb` is not a 3-D array of shape ``(.., .., 3)``. References @@ -891,17 +1048,17 @@ def hed2rgb(hed): Parameters ---------- hed : array_like - The image in the HED color space, in a 3-D array of shape (.., .., 3). + 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). + 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). + If `hed` is not a 3-D array of shape ``(.., .., 3)``. References ---------- @@ -927,19 +1084,19 @@ def separate_stains(rgb, conv_matrix): 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-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). + 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). + If `rgb` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- @@ -981,19 +1138,19 @@ def combine_stains(stains, conv_matrix): Parameters ---------- stains : array_like - The image in stain color space, in a 3-D array of shape (.., .., 3). + 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). + 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). + If `stains` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- @@ -1043,9 +1200,9 @@ def lab2lch(lab): 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. + 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 ------- @@ -1059,7 +1216,7 @@ def lab2lch(lab): Notes ----- - The Hue is expressed as an angle between (0, 2*pi) + The Hue is expressed as an angle between ``(0, 2*pi)`` Examples -------- @@ -1079,7 +1236,7 @@ def lab2lch(lab): 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) + 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) @@ -1094,9 +1251,9 @@ def lch2lab(lch): 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. + 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 ------- diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index 8d7787aa..ed74f3a8 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -4,11 +4,12 @@ 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 +import six +from six.moves import zip + __all__ = ['color_dict', 'label2rgb', 'DEFAULT_COLORS'] @@ -17,7 +18,8 @@ DEFAULT_COLORS = ('red', 'blue', 'yellow', 'magenta', 'green', 'indigo', 'darkorange', 'cyan', 'pink', 'yellowgreen') -color_dict = rgb_colors.__dict__ +color_dict = dict((k, v) for k, v in six.iteritems(rgb_colors.__dict__) + if isinstance(v, tuple)) def _rgb_vector(color): @@ -110,7 +112,7 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, label = label - offset # Make sure you don't modify the input array. bg_label -= offset - new_type = np.min_scalar_type(label.max()) + new_type = np.min_scalar_type(int(label.max())) if new_type == np.bool: new_type = np.uint8 label = label.astype(new_type) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index fbec9ba6..4bc01e78 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -33,7 +33,8 @@ from skimage.color import (rgb2hsv, hsv2rgb, rgb2grey, gray2rgb, xyz2lab, lab2xyz, lab2rgb, rgb2lab, - is_rgb, is_gray, + xyz2luv, luv2xyz, + luv2rgb, rgb2luv, lab2lch, lch2lab, guess_spatial_dimensions ) @@ -69,17 +70,24 @@ class TestColorconv(TestCase): colbars_point75_array = np.swapaxes(colbars_point75.reshape(3, 4, 2), 0, 2) xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], # red - [[0, 0, 0]], # black - [[.9505, 1., 1.089]], # white - [[.1805, .0722, .9505]], # blue - [[.07719, .15438, .02573]], # green - ]) + [[0, 0, 0]], # black + [[.9505, 1., 1.089]], # white + [[.1805, .0722, .9505]], # blue + [[.07719, .15438, .02573]], # green + ]) lab_array = np.array([[[53.233, 80.109, 67.220]], # red - [[0., 0., 0.]], # black - [[100.0, 0.005, -0.010]], # white - [[32.303, 79.197, -107.864]], # blue - [[46.229, -51.7, 49.898]], # green - ]) + [[0., 0., 0.]], # black + [[100.0, 0.005, -0.010]], # white + [[32.303, 79.197, -107.864]], # blue + [[46.229, -51.7, 49.898]], # green + ]) + + luv_array = np.array([[[53.233, 175.053, 37.751]], # red + [[0., 0., 0.]], # black + [[100., 0.001, -0.017]], # white + [[32.303, -9.400, -130.358]], # blue + [[46.228, -43.774, 56.589]], # green + ]) # RGB to HSV def test_rgb2hsv_conversion(self): @@ -250,6 +258,41 @@ class TestColorconv(TestCase): img_rgb = img_as_float(self.img_rgb) assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb) + # test matrices for xyz2luv and luv2xyz generated using + # http://www.easyrgb.com/index.php?X=CALC + # Note: easyrgb website displays xyz*100 + def test_xyz2luv(self): + assert_array_almost_equal(xyz2luv(self.xyz_array), + self.luv_array, decimal=3) + + def test_luv2xyz(self): + assert_array_almost_equal(luv2xyz(self.luv_array), + self.xyz_array, decimal=3) + + def test_rgb2luv_brucelindbloom(self): + """ + Test the RGB->Lab conversion by comparing to the calculator on the + authoritative Bruce Lindbloom + [website](http://brucelindbloom.com/index.html?ColorCalculator.html). + """ + # Obtained with D65 white point, sRGB model and gamma + gt_for_colbars = np.array([ + [100, 0, 0], + [97.1393, 7.7056, 106.7866], + [91.1132, -70.4773, -15.2042], + [87.7347, -83.0776, 107.3985], + [60.3242, 84.0714, -108.6834], + [53.2408, 175.0151, 37.7564], + [32.2970, -9.4054, -130.3423], + [0, 0, 0]]).T + gt_array = np.swapaxes(gt_for_colbars.reshape(3, 4, 2), 0, 2) + assert_array_almost_equal(rgb2luv(self.colbars_array), + gt_array, decimal=2) + + def test_luv_rgb_roundtrip(self): + img_rgb = img_as_float(self.img_rgb) + assert_array_almost_equal(luv2rgb(rgb2luv(img_rgb)), img_rgb) + def test_lab_lch_roundtrip(self): rgb = img_as_float(self.img_rgb) lab = rgb2lab(rgb) @@ -311,17 +354,6 @@ def test_gray2rgb_rgb(): assert_equal(x, y) -def test_is_rgb(): - color = data.lena() - gray = data.camera() - - assert is_rgb(color) - assert not is_gray(color) - - assert is_gray(gray) - assert not is_gray(color) - - if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index ecd261f7..b06c1f83 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -24,7 +24,8 @@ __all__ = ['load', 'clock', 'immunohistochemistry', 'chelsea', - 'coffee'] + 'coffee', + 'hubble_deep_field'] def load(f): @@ -200,3 +201,23 @@ def coffee(): """ return load("coffee.png") + + +def hubble_deep_field(): + """Hubble eXtreme Deep Field. + + This photograph contains the Hubble Telescope's farthest ever view of + the universe. It can be useful as an example for multi-scale + detection. + + Notes + ----- + This image was downloaded from + `HubbleSite + `__. + + The image was captured by NASA and `may be freely used in the + public domain `_. + + """ + return load("hubble_deep_field.jpg") diff --git a/skimage/data/block.png b/skimage/data/block.png new file mode 100644 index 00000000..cd981597 Binary files /dev/null and b/skimage/data/block.png differ diff --git a/skimage/data/hubble_deep_field.jpg b/skimage/data/hubble_deep_field.jpg new file mode 100644 index 00000000..50c1b81a Binary files /dev/null and b/skimage/data/hubble_deep_field.jpg differ diff --git a/skimage/data/orb_descriptor_positions.txt b/skimage/data/orb_descriptor_positions.txt new file mode 100644 index 00000000..541f972f --- /dev/null +++ b/skimage/data/orb_descriptor_positions.txt @@ -0,0 +1,256 @@ +8.000000000000000000e+00 -3.000000000000000000e+00 9.000000000000000000e+00 5.000000000000000000e+00 +4.000000000000000000e+00 2.000000000000000000e+00 7.000000000000000000e+00 -1.200000000000000000e+01 +-1.100000000000000000e+01 9.000000000000000000e+00 -8.000000000000000000e+00 2.000000000000000000e+00 +7.000000000000000000e+00 -1.200000000000000000e+01 1.200000000000000000e+01 -1.300000000000000000e+01 +2.000000000000000000e+00 -1.300000000000000000e+01 2.000000000000000000e+00 1.200000000000000000e+01 +1.000000000000000000e+00 -7.000000000000000000e+00 1.000000000000000000e+00 6.000000000000000000e+00 +-2.000000000000000000e+00 -1.000000000000000000e+01 -2.000000000000000000e+00 -4.000000000000000000e+00 +-1.300000000000000000e+01 -1.300000000000000000e+01 -1.100000000000000000e+01 -8.000000000000000000e+00 +-1.300000000000000000e+01 -3.000000000000000000e+00 -1.200000000000000000e+01 -9.000000000000000000e+00 +1.000000000000000000e+01 4.000000000000000000e+00 1.100000000000000000e+01 9.000000000000000000e+00 +-1.300000000000000000e+01 -8.000000000000000000e+00 -8.000000000000000000e+00 -9.000000000000000000e+00 +-1.100000000000000000e+01 7.000000000000000000e+00 -9.000000000000000000e+00 1.200000000000000000e+01 +7.000000000000000000e+00 7.000000000000000000e+00 1.200000000000000000e+01 6.000000000000000000e+00 +-4.000000000000000000e+00 -5.000000000000000000e+00 -3.000000000000000000e+00 0.000000000000000000e+00 +-1.300000000000000000e+01 2.000000000000000000e+00 -1.200000000000000000e+01 -3.000000000000000000e+00 +-9.000000000000000000e+00 0.000000000000000000e+00 -7.000000000000000000e+00 5.000000000000000000e+00 +1.200000000000000000e+01 -6.000000000000000000e+00 1.200000000000000000e+01 -1.000000000000000000e+00 +-3.000000000000000000e+00 6.000000000000000000e+00 -2.000000000000000000e+00 1.200000000000000000e+01 +-6.000000000000000000e+00 -1.300000000000000000e+01 -4.000000000000000000e+00 -8.000000000000000000e+00 +1.100000000000000000e+01 -1.300000000000000000e+01 1.200000000000000000e+01 -8.000000000000000000e+00 +4.000000000000000000e+00 7.000000000000000000e+00 5.000000000000000000e+00 1.000000000000000000e+00 +5.000000000000000000e+00 -3.000000000000000000e+00 1.000000000000000000e+01 -3.000000000000000000e+00 +3.000000000000000000e+00 -7.000000000000000000e+00 6.000000000000000000e+00 1.200000000000000000e+01 +-8.000000000000000000e+00 -7.000000000000000000e+00 -6.000000000000000000e+00 -2.000000000000000000e+00 +-2.000000000000000000e+00 1.100000000000000000e+01 -1.000000000000000000e+00 -1.000000000000000000e+01 +-1.300000000000000000e+01 1.200000000000000000e+01 -8.000000000000000000e+00 1.000000000000000000e+01 +-7.000000000000000000e+00 3.000000000000000000e+00 -5.000000000000000000e+00 -3.000000000000000000e+00 +-4.000000000000000000e+00 2.000000000000000000e+00 -3.000000000000000000e+00 7.000000000000000000e+00 +-1.000000000000000000e+01 -1.200000000000000000e+01 -6.000000000000000000e+00 1.100000000000000000e+01 +5.000000000000000000e+00 -1.200000000000000000e+01 6.000000000000000000e+00 -7.000000000000000000e+00 +5.000000000000000000e+00 -6.000000000000000000e+00 7.000000000000000000e+00 -1.000000000000000000e+00 +1.000000000000000000e+00 0.000000000000000000e+00 4.000000000000000000e+00 -5.000000000000000000e+00 +9.000000000000000000e+00 1.100000000000000000e+01 1.100000000000000000e+01 -1.300000000000000000e+01 +4.000000000000000000e+00 7.000000000000000000e+00 4.000000000000000000e+00 1.200000000000000000e+01 +2.000000000000000000e+00 -1.000000000000000000e+00 4.000000000000000000e+00 4.000000000000000000e+00 +-4.000000000000000000e+00 -1.200000000000000000e+01 -2.000000000000000000e+00 7.000000000000000000e+00 +-8.000000000000000000e+00 -5.000000000000000000e+00 -7.000000000000000000e+00 -1.000000000000000000e+01 +4.000000000000000000e+00 1.100000000000000000e+01 9.000000000000000000e+00 1.200000000000000000e+01 +0.000000000000000000e+00 -8.000000000000000000e+00 1.000000000000000000e+00 -1.300000000000000000e+01 +-1.300000000000000000e+01 -2.000000000000000000e+00 -8.000000000000000000e+00 2.000000000000000000e+00 +-3.000000000000000000e+00 -2.000000000000000000e+00 -2.000000000000000000e+00 3.000000000000000000e+00 +-6.000000000000000000e+00 9.000000000000000000e+00 -4.000000000000000000e+00 -9.000000000000000000e+00 +8.000000000000000000e+00 1.200000000000000000e+01 1.000000000000000000e+01 7.000000000000000000e+00 +0.000000000000000000e+00 9.000000000000000000e+00 1.000000000000000000e+00 3.000000000000000000e+00 +7.000000000000000000e+00 -5.000000000000000000e+00 1.100000000000000000e+01 -1.000000000000000000e+01 +-1.300000000000000000e+01 -6.000000000000000000e+00 -1.100000000000000000e+01 0.000000000000000000e+00 +1.000000000000000000e+01 7.000000000000000000e+00 1.200000000000000000e+01 1.000000000000000000e+00 +-6.000000000000000000e+00 -3.000000000000000000e+00 -6.000000000000000000e+00 1.200000000000000000e+01 +1.000000000000000000e+01 -9.000000000000000000e+00 1.200000000000000000e+01 -4.000000000000000000e+00 +-1.300000000000000000e+01 8.000000000000000000e+00 -8.000000000000000000e+00 -1.200000000000000000e+01 +-1.300000000000000000e+01 0.000000000000000000e+00 -8.000000000000000000e+00 -4.000000000000000000e+00 +3.000000000000000000e+00 3.000000000000000000e+00 7.000000000000000000e+00 8.000000000000000000e+00 +5.000000000000000000e+00 7.000000000000000000e+00 1.000000000000000000e+01 -7.000000000000000000e+00 +-1.000000000000000000e+00 7.000000000000000000e+00 1.000000000000000000e+00 -1.200000000000000000e+01 +3.000000000000000000e+00 -1.000000000000000000e+01 5.000000000000000000e+00 6.000000000000000000e+00 +2.000000000000000000e+00 -4.000000000000000000e+00 3.000000000000000000e+00 -1.000000000000000000e+01 +-1.300000000000000000e+01 0.000000000000000000e+00 -1.300000000000000000e+01 5.000000000000000000e+00 +-1.300000000000000000e+01 -7.000000000000000000e+00 -1.200000000000000000e+01 1.200000000000000000e+01 +-1.300000000000000000e+01 3.000000000000000000e+00 -1.100000000000000000e+01 8.000000000000000000e+00 +-7.000000000000000000e+00 1.200000000000000000e+01 -4.000000000000000000e+00 7.000000000000000000e+00 +6.000000000000000000e+00 -1.000000000000000000e+01 1.200000000000000000e+01 8.000000000000000000e+00 +-9.000000000000000000e+00 -1.000000000000000000e+00 -7.000000000000000000e+00 -6.000000000000000000e+00 +-2.000000000000000000e+00 -5.000000000000000000e+00 0.000000000000000000e+00 1.200000000000000000e+01 +-1.200000000000000000e+01 5.000000000000000000e+00 -7.000000000000000000e+00 5.000000000000000000e+00 +3.000000000000000000e+00 -1.000000000000000000e+01 8.000000000000000000e+00 -1.300000000000000000e+01 +-7.000000000000000000e+00 -7.000000000000000000e+00 -4.000000000000000000e+00 5.000000000000000000e+00 +-3.000000000000000000e+00 -2.000000000000000000e+00 -1.000000000000000000e+00 -7.000000000000000000e+00 +2.000000000000000000e+00 9.000000000000000000e+00 5.000000000000000000e+00 -1.100000000000000000e+01 +-1.100000000000000000e+01 -1.300000000000000000e+01 -5.000000000000000000e+00 -1.300000000000000000e+01 +-1.000000000000000000e+00 6.000000000000000000e+00 0.000000000000000000e+00 -1.000000000000000000e+00 +5.000000000000000000e+00 -3.000000000000000000e+00 5.000000000000000000e+00 2.000000000000000000e+00 +-4.000000000000000000e+00 -1.300000000000000000e+01 -4.000000000000000000e+00 1.200000000000000000e+01 +-9.000000000000000000e+00 -6.000000000000000000e+00 -9.000000000000000000e+00 6.000000000000000000e+00 +-1.200000000000000000e+01 -1.000000000000000000e+01 -8.000000000000000000e+00 -4.000000000000000000e+00 +1.000000000000000000e+01 2.000000000000000000e+00 1.200000000000000000e+01 -3.000000000000000000e+00 +7.000000000000000000e+00 1.200000000000000000e+01 1.200000000000000000e+01 1.200000000000000000e+01 +-7.000000000000000000e+00 -1.300000000000000000e+01 -6.000000000000000000e+00 5.000000000000000000e+00 +-4.000000000000000000e+00 9.000000000000000000e+00 -3.000000000000000000e+00 4.000000000000000000e+00 +7.000000000000000000e+00 -1.000000000000000000e+00 1.200000000000000000e+01 2.000000000000000000e+00 +-7.000000000000000000e+00 6.000000000000000000e+00 -5.000000000000000000e+00 1.000000000000000000e+00 +-1.300000000000000000e+01 1.100000000000000000e+01 -1.200000000000000000e+01 5.000000000000000000e+00 +-3.000000000000000000e+00 7.000000000000000000e+00 -2.000000000000000000e+00 -6.000000000000000000e+00 +7.000000000000000000e+00 -8.000000000000000000e+00 1.200000000000000000e+01 -7.000000000000000000e+00 +-1.300000000000000000e+01 -7.000000000000000000e+00 -1.100000000000000000e+01 -1.200000000000000000e+01 +1.000000000000000000e+00 -3.000000000000000000e+00 1.200000000000000000e+01 1.200000000000000000e+01 +2.000000000000000000e+00 -6.000000000000000000e+00 3.000000000000000000e+00 0.000000000000000000e+00 +-4.000000000000000000e+00 3.000000000000000000e+00 -2.000000000000000000e+00 -1.300000000000000000e+01 +-1.000000000000000000e+00 -1.300000000000000000e+01 1.000000000000000000e+00 9.000000000000000000e+00 +7.000000000000000000e+00 1.000000000000000000e+00 8.000000000000000000e+00 -6.000000000000000000e+00 +1.000000000000000000e+00 -1.000000000000000000e+00 3.000000000000000000e+00 1.200000000000000000e+01 +9.000000000000000000e+00 1.000000000000000000e+00 1.200000000000000000e+01 6.000000000000000000e+00 +-1.000000000000000000e+00 -9.000000000000000000e+00 -1.000000000000000000e+00 3.000000000000000000e+00 +-1.300000000000000000e+01 -1.300000000000000000e+01 -1.000000000000000000e+01 5.000000000000000000e+00 +7.000000000000000000e+00 7.000000000000000000e+00 1.000000000000000000e+01 1.200000000000000000e+01 +1.200000000000000000e+01 -5.000000000000000000e+00 1.200000000000000000e+01 9.000000000000000000e+00 +6.000000000000000000e+00 3.000000000000000000e+00 7.000000000000000000e+00 1.100000000000000000e+01 +5.000000000000000000e+00 -1.300000000000000000e+01 6.000000000000000000e+00 1.000000000000000000e+01 +2.000000000000000000e+00 -1.200000000000000000e+01 2.000000000000000000e+00 3.000000000000000000e+00 +3.000000000000000000e+00 8.000000000000000000e+00 4.000000000000000000e+00 -6.000000000000000000e+00 +2.000000000000000000e+00 6.000000000000000000e+00 1.200000000000000000e+01 -1.300000000000000000e+01 +9.000000000000000000e+00 -1.200000000000000000e+01 1.000000000000000000e+01 3.000000000000000000e+00 +-8.000000000000000000e+00 4.000000000000000000e+00 -7.000000000000000000e+00 9.000000000000000000e+00 +-1.100000000000000000e+01 1.200000000000000000e+01 -4.000000000000000000e+00 -6.000000000000000000e+00 +1.000000000000000000e+00 1.200000000000000000e+01 2.000000000000000000e+00 -8.000000000000000000e+00 +6.000000000000000000e+00 -9.000000000000000000e+00 7.000000000000000000e+00 -4.000000000000000000e+00 +2.000000000000000000e+00 3.000000000000000000e+00 3.000000000000000000e+00 -2.000000000000000000e+00 +6.000000000000000000e+00 3.000000000000000000e+00 1.100000000000000000e+01 0.000000000000000000e+00 +3.000000000000000000e+00 -3.000000000000000000e+00 8.000000000000000000e+00 -8.000000000000000000e+00 +7.000000000000000000e+00 8.000000000000000000e+00 9.000000000000000000e+00 3.000000000000000000e+00 +-1.100000000000000000e+01 -5.000000000000000000e+00 -6.000000000000000000e+00 -4.000000000000000000e+00 +-1.000000000000000000e+01 1.100000000000000000e+01 -5.000000000000000000e+00 1.000000000000000000e+01 +-5.000000000000000000e+00 -8.000000000000000000e+00 -3.000000000000000000e+00 1.200000000000000000e+01 +-1.000000000000000000e+01 5.000000000000000000e+00 -9.000000000000000000e+00 0.000000000000000000e+00 +8.000000000000000000e+00 -1.000000000000000000e+00 1.200000000000000000e+01 -6.000000000000000000e+00 +4.000000000000000000e+00 -6.000000000000000000e+00 6.000000000000000000e+00 -1.100000000000000000e+01 +-1.000000000000000000e+01 1.200000000000000000e+01 -8.000000000000000000e+00 7.000000000000000000e+00 +4.000000000000000000e+00 -2.000000000000000000e+00 6.000000000000000000e+00 7.000000000000000000e+00 +-2.000000000000000000e+00 0.000000000000000000e+00 -2.000000000000000000e+00 1.200000000000000000e+01 +-5.000000000000000000e+00 -8.000000000000000000e+00 -5.000000000000000000e+00 2.000000000000000000e+00 +7.000000000000000000e+00 -6.000000000000000000e+00 1.000000000000000000e+01 1.200000000000000000e+01 +-9.000000000000000000e+00 -1.300000000000000000e+01 -8.000000000000000000e+00 -8.000000000000000000e+00 +-5.000000000000000000e+00 -1.300000000000000000e+01 -5.000000000000000000e+00 -2.000000000000000000e+00 +8.000000000000000000e+00 -8.000000000000000000e+00 9.000000000000000000e+00 -1.300000000000000000e+01 +-9.000000000000000000e+00 -1.100000000000000000e+01 -9.000000000000000000e+00 0.000000000000000000e+00 +1.000000000000000000e+00 -8.000000000000000000e+00 1.000000000000000000e+00 -2.000000000000000000e+00 +7.000000000000000000e+00 -4.000000000000000000e+00 9.000000000000000000e+00 1.000000000000000000e+00 +-2.000000000000000000e+00 1.000000000000000000e+00 -1.000000000000000000e+00 -4.000000000000000000e+00 +1.100000000000000000e+01 -6.000000000000000000e+00 1.200000000000000000e+01 -1.100000000000000000e+01 +-1.200000000000000000e+01 -9.000000000000000000e+00 -6.000000000000000000e+00 4.000000000000000000e+00 +3.000000000000000000e+00 7.000000000000000000e+00 7.000000000000000000e+00 1.200000000000000000e+01 +5.000000000000000000e+00 5.000000000000000000e+00 1.000000000000000000e+01 8.000000000000000000e+00 +0.000000000000000000e+00 -4.000000000000000000e+00 2.000000000000000000e+00 8.000000000000000000e+00 +-9.000000000000000000e+00 1.200000000000000000e+01 -5.000000000000000000e+00 -1.300000000000000000e+01 +0.000000000000000000e+00 7.000000000000000000e+00 2.000000000000000000e+00 1.200000000000000000e+01 +-1.000000000000000000e+00 2.000000000000000000e+00 1.000000000000000000e+00 7.000000000000000000e+00 +5.000000000000000000e+00 1.100000000000000000e+01 7.000000000000000000e+00 -9.000000000000000000e+00 +3.000000000000000000e+00 5.000000000000000000e+00 6.000000000000000000e+00 -8.000000000000000000e+00 +-1.300000000000000000e+01 -4.000000000000000000e+00 -8.000000000000000000e+00 9.000000000000000000e+00 +-5.000000000000000000e+00 9.000000000000000000e+00 -3.000000000000000000e+00 -3.000000000000000000e+00 +-4.000000000000000000e+00 -7.000000000000000000e+00 -3.000000000000000000e+00 -1.200000000000000000e+01 +6.000000000000000000e+00 5.000000000000000000e+00 8.000000000000000000e+00 0.000000000000000000e+00 +-7.000000000000000000e+00 6.000000000000000000e+00 -6.000000000000000000e+00 1.200000000000000000e+01 +-1.300000000000000000e+01 6.000000000000000000e+00 -5.000000000000000000e+00 -2.000000000000000000e+00 +1.000000000000000000e+00 -1.000000000000000000e+01 3.000000000000000000e+00 1.000000000000000000e+01 +4.000000000000000000e+00 1.000000000000000000e+00 8.000000000000000000e+00 -4.000000000000000000e+00 +-2.000000000000000000e+00 -2.000000000000000000e+00 2.000000000000000000e+00 -1.300000000000000000e+01 +2.000000000000000000e+00 -1.200000000000000000e+01 1.200000000000000000e+01 1.200000000000000000e+01 +-2.000000000000000000e+00 -1.300000000000000000e+01 0.000000000000000000e+00 -6.000000000000000000e+00 +4.000000000000000000e+00 1.000000000000000000e+00 9.000000000000000000e+00 3.000000000000000000e+00 +-6.000000000000000000e+00 -1.000000000000000000e+01 -3.000000000000000000e+00 -5.000000000000000000e+00 +-3.000000000000000000e+00 -1.300000000000000000e+01 -1.000000000000000000e+00 1.000000000000000000e+00 +7.000000000000000000e+00 5.000000000000000000e+00 1.200000000000000000e+01 -1.100000000000000000e+01 +4.000000000000000000e+00 -2.000000000000000000e+00 5.000000000000000000e+00 -7.000000000000000000e+00 +-1.300000000000000000e+01 9.000000000000000000e+00 -9.000000000000000000e+00 -5.000000000000000000e+00 +7.000000000000000000e+00 1.000000000000000000e+00 8.000000000000000000e+00 6.000000000000000000e+00 +7.000000000000000000e+00 -8.000000000000000000e+00 7.000000000000000000e+00 6.000000000000000000e+00 +-7.000000000000000000e+00 -4.000000000000000000e+00 -7.000000000000000000e+00 1.000000000000000000e+00 +-8.000000000000000000e+00 1.100000000000000000e+01 -7.000000000000000000e+00 -8.000000000000000000e+00 +-1.300000000000000000e+01 6.000000000000000000e+00 -1.200000000000000000e+01 -8.000000000000000000e+00 +2.000000000000000000e+00 4.000000000000000000e+00 3.000000000000000000e+00 9.000000000000000000e+00 +1.000000000000000000e+01 -5.000000000000000000e+00 1.200000000000000000e+01 3.000000000000000000e+00 +-6.000000000000000000e+00 -5.000000000000000000e+00 -6.000000000000000000e+00 7.000000000000000000e+00 +8.000000000000000000e+00 -3.000000000000000000e+00 9.000000000000000000e+00 -8.000000000000000000e+00 +2.000000000000000000e+00 -1.200000000000000000e+01 2.000000000000000000e+00 8.000000000000000000e+00 +-1.100000000000000000e+01 -2.000000000000000000e+00 -1.000000000000000000e+01 3.000000000000000000e+00 +-1.200000000000000000e+01 -1.300000000000000000e+01 -7.000000000000000000e+00 -9.000000000000000000e+00 +-1.100000000000000000e+01 0.000000000000000000e+00 -1.000000000000000000e+01 -5.000000000000000000e+00 +5.000000000000000000e+00 -3.000000000000000000e+00 1.100000000000000000e+01 8.000000000000000000e+00 +-2.000000000000000000e+00 -1.300000000000000000e+01 -1.000000000000000000e+00 1.200000000000000000e+01 +-1.000000000000000000e+00 -8.000000000000000000e+00 0.000000000000000000e+00 9.000000000000000000e+00 +-1.300000000000000000e+01 -1.100000000000000000e+01 -1.200000000000000000e+01 -5.000000000000000000e+00 +-1.000000000000000000e+01 -2.000000000000000000e+00 -1.000000000000000000e+01 1.100000000000000000e+01 +-3.000000000000000000e+00 9.000000000000000000e+00 -2.000000000000000000e+00 -1.300000000000000000e+01 +2.000000000000000000e+00 -3.000000000000000000e+00 3.000000000000000000e+00 2.000000000000000000e+00 +-9.000000000000000000e+00 -1.300000000000000000e+01 -4.000000000000000000e+00 0.000000000000000000e+00 +-4.000000000000000000e+00 6.000000000000000000e+00 -3.000000000000000000e+00 -1.000000000000000000e+01 +-4.000000000000000000e+00 1.200000000000000000e+01 -2.000000000000000000e+00 -7.000000000000000000e+00 +-6.000000000000000000e+00 -1.100000000000000000e+01 -4.000000000000000000e+00 9.000000000000000000e+00 +6.000000000000000000e+00 -3.000000000000000000e+00 6.000000000000000000e+00 1.100000000000000000e+01 +-1.300000000000000000e+01 1.100000000000000000e+01 -5.000000000000000000e+00 5.000000000000000000e+00 +1.100000000000000000e+01 1.100000000000000000e+01 1.200000000000000000e+01 6.000000000000000000e+00 +7.000000000000000000e+00 -5.000000000000000000e+00 1.200000000000000000e+01 -2.000000000000000000e+00 +-1.000000000000000000e+00 1.200000000000000000e+01 0.000000000000000000e+00 7.000000000000000000e+00 +-4.000000000000000000e+00 -8.000000000000000000e+00 -3.000000000000000000e+00 -2.000000000000000000e+00 +-7.000000000000000000e+00 1.000000000000000000e+00 -6.000000000000000000e+00 7.000000000000000000e+00 +-1.300000000000000000e+01 -1.200000000000000000e+01 -8.000000000000000000e+00 -1.300000000000000000e+01 +-7.000000000000000000e+00 -2.000000000000000000e+00 -6.000000000000000000e+00 -8.000000000000000000e+00 +-8.000000000000000000e+00 5.000000000000000000e+00 -6.000000000000000000e+00 -9.000000000000000000e+00 +-5.000000000000000000e+00 -1.000000000000000000e+00 -4.000000000000000000e+00 5.000000000000000000e+00 +-1.300000000000000000e+01 7.000000000000000000e+00 -8.000000000000000000e+00 1.000000000000000000e+01 +1.000000000000000000e+00 5.000000000000000000e+00 5.000000000000000000e+00 -1.300000000000000000e+01 +1.000000000000000000e+00 0.000000000000000000e+00 1.000000000000000000e+01 -1.300000000000000000e+01 +9.000000000000000000e+00 1.200000000000000000e+01 1.000000000000000000e+01 -1.000000000000000000e+00 +5.000000000000000000e+00 -8.000000000000000000e+00 1.000000000000000000e+01 -9.000000000000000000e+00 +-1.000000000000000000e+00 1.100000000000000000e+01 1.000000000000000000e+00 -1.300000000000000000e+01 +-9.000000000000000000e+00 -3.000000000000000000e+00 -6.000000000000000000e+00 2.000000000000000000e+00 +-1.000000000000000000e+00 -1.000000000000000000e+01 1.000000000000000000e+00 1.200000000000000000e+01 +-1.300000000000000000e+01 1.000000000000000000e+00 -8.000000000000000000e+00 -1.000000000000000000e+01 +8.000000000000000000e+00 -1.100000000000000000e+01 1.000000000000000000e+01 -6.000000000000000000e+00 +2.000000000000000000e+00 -1.300000000000000000e+01 3.000000000000000000e+00 -6.000000000000000000e+00 +7.000000000000000000e+00 -1.300000000000000000e+01 1.200000000000000000e+01 -9.000000000000000000e+00 +-1.000000000000000000e+01 -1.000000000000000000e+01 -5.000000000000000000e+00 -7.000000000000000000e+00 +-1.000000000000000000e+01 -8.000000000000000000e+00 -8.000000000000000000e+00 -1.300000000000000000e+01 +4.000000000000000000e+00 -6.000000000000000000e+00 8.000000000000000000e+00 5.000000000000000000e+00 +3.000000000000000000e+00 1.200000000000000000e+01 8.000000000000000000e+00 -1.300000000000000000e+01 +-4.000000000000000000e+00 2.000000000000000000e+00 -3.000000000000000000e+00 -3.000000000000000000e+00 +5.000000000000000000e+00 -1.300000000000000000e+01 1.000000000000000000e+01 -1.200000000000000000e+01 +4.000000000000000000e+00 -1.300000000000000000e+01 5.000000000000000000e+00 -1.000000000000000000e+00 +-9.000000000000000000e+00 9.000000000000000000e+00 -4.000000000000000000e+00 3.000000000000000000e+00 +0.000000000000000000e+00 3.000000000000000000e+00 3.000000000000000000e+00 -9.000000000000000000e+00 +-1.200000000000000000e+01 1.000000000000000000e+00 -6.000000000000000000e+00 1.000000000000000000e+00 +3.000000000000000000e+00 2.000000000000000000e+00 4.000000000000000000e+00 -8.000000000000000000e+00 +-1.000000000000000000e+01 -1.000000000000000000e+01 -1.000000000000000000e+01 9.000000000000000000e+00 +8.000000000000000000e+00 -1.300000000000000000e+01 1.200000000000000000e+01 1.200000000000000000e+01 +-8.000000000000000000e+00 -1.200000000000000000e+01 -6.000000000000000000e+00 -5.000000000000000000e+00 +2.000000000000000000e+00 2.000000000000000000e+00 3.000000000000000000e+00 7.000000000000000000e+00 +1.000000000000000000e+01 6.000000000000000000e+00 1.100000000000000000e+01 -8.000000000000000000e+00 +6.000000000000000000e+00 8.000000000000000000e+00 8.000000000000000000e+00 -1.200000000000000000e+01 +-7.000000000000000000e+00 1.000000000000000000e+01 -6.000000000000000000e+00 5.000000000000000000e+00 +-3.000000000000000000e+00 -9.000000000000000000e+00 -3.000000000000000000e+00 9.000000000000000000e+00 +-1.000000000000000000e+00 -1.300000000000000000e+01 -1.000000000000000000e+00 5.000000000000000000e+00 +-3.000000000000000000e+00 -7.000000000000000000e+00 -3.000000000000000000e+00 4.000000000000000000e+00 +-8.000000000000000000e+00 -2.000000000000000000e+00 -8.000000000000000000e+00 3.000000000000000000e+00 +4.000000000000000000e+00 2.000000000000000000e+00 1.200000000000000000e+01 1.200000000000000000e+01 +2.000000000000000000e+00 -5.000000000000000000e+00 3.000000000000000000e+00 1.100000000000000000e+01 +6.000000000000000000e+00 -9.000000000000000000e+00 1.100000000000000000e+01 -1.300000000000000000e+01 +3.000000000000000000e+00 -1.000000000000000000e+00 7.000000000000000000e+00 1.200000000000000000e+01 +1.100000000000000000e+01 -1.000000000000000000e+00 1.200000000000000000e+01 4.000000000000000000e+00 +-3.000000000000000000e+00 0.000000000000000000e+00 -3.000000000000000000e+00 6.000000000000000000e+00 +4.000000000000000000e+00 -1.100000000000000000e+01 4.000000000000000000e+00 1.200000000000000000e+01 +2.000000000000000000e+00 -4.000000000000000000e+00 2.000000000000000000e+00 1.000000000000000000e+00 +-1.000000000000000000e+01 -6.000000000000000000e+00 -8.000000000000000000e+00 1.000000000000000000e+00 +-1.300000000000000000e+01 7.000000000000000000e+00 -1.100000000000000000e+01 1.000000000000000000e+00 +-1.300000000000000000e+01 1.200000000000000000e+01 -1.100000000000000000e+01 -1.300000000000000000e+01 +6.000000000000000000e+00 0.000000000000000000e+00 1.100000000000000000e+01 -1.300000000000000000e+01 +0.000000000000000000e+00 -1.000000000000000000e+00 1.000000000000000000e+00 4.000000000000000000e+00 +-1.300000000000000000e+01 3.000000000000000000e+00 -9.000000000000000000e+00 -2.000000000000000000e+00 +-9.000000000000000000e+00 8.000000000000000000e+00 -6.000000000000000000e+00 -3.000000000000000000e+00 +-1.300000000000000000e+01 -6.000000000000000000e+00 -8.000000000000000000e+00 -2.000000000000000000e+00 +5.000000000000000000e+00 -9.000000000000000000e+00 8.000000000000000000e+00 1.000000000000000000e+01 +2.000000000000000000e+00 7.000000000000000000e+00 3.000000000000000000e+00 -9.000000000000000000e+00 +-1.000000000000000000e+00 -6.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 +9.000000000000000000e+00 5.000000000000000000e+00 1.100000000000000000e+01 -2.000000000000000000e+00 +1.100000000000000000e+01 -3.000000000000000000e+00 1.200000000000000000e+01 -8.000000000000000000e+00 +3.000000000000000000e+00 0.000000000000000000e+00 3.000000000000000000e+00 5.000000000000000000e+00 +-1.000000000000000000e+00 4.000000000000000000e+00 0.000000000000000000e+00 1.000000000000000000e+01 +3.000000000000000000e+00 -6.000000000000000000e+00 4.000000000000000000e+00 5.000000000000000000e+00 +-1.300000000000000000e+01 0.000000000000000000e+00 -1.000000000000000000e+01 5.000000000000000000e+00 +5.000000000000000000e+00 8.000000000000000000e+00 1.200000000000000000e+01 1.100000000000000000e+01 +8.000000000000000000e+00 9.000000000000000000e+00 9.000000000000000000e+00 -6.000000000000000000e+00 +7.000000000000000000e+00 -4.000000000000000000e+00 8.000000000000000000e+00 -1.200000000000000000e+01 +-1.000000000000000000e+01 4.000000000000000000e+00 -1.000000000000000000e+01 9.000000000000000000e+00 +7.000000000000000000e+00 3.000000000000000000e+00 1.200000000000000000e+01 4.000000000000000000e+00 +9.000000000000000000e+00 -7.000000000000000000e+00 1.000000000000000000e+01 -2.000000000000000000e+00 +7.000000000000000000e+00 0.000000000000000000e+00 1.200000000000000000e+01 -2.000000000000000000e+00 +-1.000000000000000000e+00 -6.000000000000000000e+00 0.000000000000000000e+00 -1.100000000000000000e+01 diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py index cbf3ced2..e61df40c 100644 --- a/skimage/draw/draw.py +++ b/skimage/draw/draw.py @@ -60,7 +60,7 @@ def ellipse(cy, cx, yradius, xradius, shape=None): cc += cx - xradius if shape is not None: - _coords_inside_image(rr, cc, shape) + return _coords_inside_image(rr, cc, shape) return rr, cc diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 2d739f0f..3cb8c2a6 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -325,6 +325,33 @@ def test_ellipse(): assert_array_equal(img, img_) +def test_ellipse_with_shape(): + img = np.zeros((15, 15), 'uint8') + + rr, cc = ellipse(7, 7, 3, 10, shape=img.shape) + 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], + [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, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] + ) + + assert_array_equal(img, img_) + + def test_ellipse_perimeter_dot_zeroangle(): # dot, angle == 0 img = np.zeros((30, 15), 'uint8') diff --git a/skimage/draw/tests/test_draw3d.py b/skimage/draw/tests/test_draw3d.py index 2e1198eb..48caf1a0 100644 --- a/skimage/draw/tests/test_draw3d.py +++ b/skimage/draw/tests/test_draw3d.py @@ -1,9 +1,25 @@ import numpy as np from numpy.testing import assert_array_equal, assert_allclose +from nose.tools import raises from skimage.draw import ellipsoid, ellipsoid_stats +@raises(ValueError) +def test_ellipsoid_sign_parameters1(): + ellipsoid(-1, 2, 2) + + +@raises(ValueError) +def test_ellipsoid_sign_parameters2(): + ellipsoid(0, 2, 2) + + +@raises(ValueError) +def test_ellipsoid_sign_parameters3(): + ellipsoid(-3, -2, 2) + + 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.)) @@ -86,18 +102,18 @@ def test_ellipsoid_levelset(): 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)) + assert_allclose(1280 * np.pi, vol, atol=1e-4) + assert_allclose(1383.28, surf, atol=1e-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)) + assert_allclose(1280 * np.pi, vol, atol=1e-4) + assert_allclose(1383.28, surf, atol=1e-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)) + assert_allclose(103428 * np.pi, vol, atol=1e-4) + assert_allclose(37426.3, surf, atol=1e-1) if __name__ == "__main__": diff --git a/skimage/exposure/__init__.py b/skimage/exposure/__init__.py index b873c339..d78cf927 100644 --- a/skimage/exposure/__init__.py +++ b/skimage/exposure/__init__.py @@ -1,11 +1,11 @@ -from .exposure import histogram, equalize, equalize_hist, \ +from .exposure import histogram, 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', diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 8a825435..699e9acf 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -126,8 +126,8 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): x_res = image.shape[1] - image.shape[1] % ntiles_x image = image[: y_res, : x_res] - x_size = image.shape[1] / ntiles_x # Actual size of contextual regions - y_size = image.shape[0] / ntiles_y + x_size = image.shape[1] // ntiles_x # Actual size of contextual regions + y_size = image.shape[0] // ntiles_y n_pixels = x_size * y_size if clip_limit > 0.0: # Calculate actual cliplimit @@ -139,7 +139,7 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): bin_size = 1 + NR_OF_GREY / nbins aLUT = np.arange(NR_OF_GREY) - aLUT /= bin_size + aLUT //= bin_size img_blocks = view_as_blocks(image, (y_size, x_size)) # Calculate greylevel mappings for each contextual region @@ -315,7 +315,8 @@ def interpolate(image, xslice, yslice, np.arange(yslice.size)) x_inv_coef, y_inv_coef = x_coef[:, ::-1] + 1, y_coef[::-1] + 1 - view = image[yslice[0]: yslice[-1] + 1, xslice[0]: xslice[-1] + 1] + view = image[int(yslice[0]):int(yslice[-1] + 1), + int(xslice[0]):int(xslice[-1] + 1)] im_slice = aLUT[view] new = ((y_inv_coef * (x_inv_coef * mapLU[im_slice] + x_coef * mapRU[im_slice]) diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index fd5d53dd..14379f6b 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -7,14 +7,21 @@ from skimage._shared.utils import deprecated __all__ = ['histogram', 'cumulative_distribution', 'equalize', - 'rescale_intensity', 'adjust_gamma', - 'adjust_log', 'adjust_sigmoid'] + 'rescale_intensity', 'adjust_gamma', 'adjust_log', 'adjust_sigmoid'] + + +DTYPE_RANGE = dtype_range.copy() +DTYPE_RANGE.update((d.__name__, limits) for d, limits in dtype_range.items()) +DTYPE_RANGE.update({'uint10': (0, 2**10 - 1), + 'uint12': (0, 2**12 - 1), + 'uint14': (0, 2**14 - 1), + 'bool': dtype_range[np.bool_], + 'float': dtype_range[np.float64]}) 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. @@ -40,11 +47,12 @@ def histogram(image, nbins=256): Examples -------- - >>> from skimage import data - >>> hist = histogram(data.camera()) - >>> import matplotlib.pyplot as plt - >>> plt.plot(hist[1], hist[0]) # doctest: +ELLIPSIS - [...] + >>> from skimage import data, exposure, util + >>> image = util.img_as_float(data.camera()) + >>> np.histogram(image, bins=2) + (array([107432, 154712]), array([ 0. , 0.5, 1. ])) + >>> exposure.histogram(image, nbins=2) + (array([107432, 154712]), array([ 0.25, 0.75])) """ sh = image.shape if len(sh) == 3 and sh[-1] < 4: @@ -97,11 +105,6 @@ def cumulative_distribution(image, nbins=256): return img_cdf, bin_centers -@deprecated('equalize_hist') -def equalize(image, nbins=256): - return equalize_hist(image, nbins) - - def equalize_hist(image, nbins=256): """Return image after histogram equalization. @@ -143,14 +146,15 @@ def rescale_intensity(image, in_range=None, out_range=None): ---------- image : array Image array. - in_range : 2-tuple (float, float) + in_range : 2-tuple (float, float) or str Min and max *allowed* intensity values of input image. If None, the *allowed* min/max values are set to the *actual* min/max values in the - input image. - out_range : 2-tuple (float, float) + input image. Intensity values outside this range are clipped. + If string, use data limits of dtype specified by the string. + out_range : 2-tuple (float, float) or str Min and max intensity values of output image. If None, use the min/max intensities of the image data type. See `skimage.util.dtype` for - details. + details. If string, use data limits of dtype specified by the string. Returns ------- @@ -201,11 +205,14 @@ def rescale_intensity(image, in_range=None, out_range=None): if in_range is None: imin = np.min(image) imax = np.max(image) + elif in_range in DTYPE_RANGE: + imin, imax = DTYPE_RANGE[in_range] else: imin, imax = in_range - if out_range is None: - omin, omax = dtype_range[dtype] + if out_range is None or out_range in DTYPE_RANGE: + out_range = dtype if out_range is None else out_range + omin, omax = DTYPE_RANGE[out_range] if imin >= 0: omin = 0 else: @@ -263,7 +270,7 @@ def adjust_gamma(image, gamma=1, gain=1): dtype = image.dtype.type if gamma < 0: - return "Gamma should be a non-negative real number" + raise ValueError("Gamma should be a non-negative real number.") scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0]) @@ -339,7 +346,8 @@ def adjust_sigmoid(image, cutoff=0.5, gain=10, inv=False): References ---------- .. [1] Gustav J. Braun, "Image Lightness Rescaling Using Sigmoidal Contrast - Enhancement Functions" http://www.cis.rit.edu/fairchild/PDFs/PAP07.pdf + Enhancement Functions", + http://www.cis.rit.edu/fairchild/PDFs/PAP07.pdf """ _assert_non_negative(image) diff --git a/skimage/exposure/setup.py b/skimage/exposure/setup.py new file mode 100644 index 00000000..d0c534dd --- /dev/null +++ b/skimage/exposure/setup.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +import os + +from skimage._build import cython + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs + + config = Configuration('exposure', parent_package, top_path) + config.add_data_dir('tests') + + return config + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='scikit-image Developers', + author='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Exposure corrections', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 2b696b32..6471ff59 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -2,7 +2,7 @@ import warnings import numpy as np from numpy.testing import assert_array_almost_equal as assert_close -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_equal, assert_raises import skimage from skimage import data from skimage import exposure @@ -44,6 +44,13 @@ def check_cdf_slope(cdf): # Test rescale intensity # ====================== + +uint10_max = 2**10 - 1 +uint12_max = 2**12 - 1 +uint14_max = 2**14 - 1 +uint16_max = 2**16 - 1 + + def test_rescale_stretch(): image = np.array([51, 102, 153], dtype=np.uint8) out = exposure.rescale_intensity(image) @@ -76,6 +83,30 @@ def test_rescale_out_range(): assert_close(out, [0, 63, 127]) +def test_rescale_named_in_range(): + image = np.array([0, uint10_max, uint10_max + 100], dtype=np.uint16) + out = exposure.rescale_intensity(image, in_range='uint10') + assert_close(out, [0, uint16_max, uint16_max]) + + +def test_rescale_named_out_range(): + image = np.array([0, uint16_max], dtype=np.uint16) + out = exposure.rescale_intensity(image, out_range='uint10') + assert_close(out, [0, uint10_max]) + + +def test_rescale_uint12_limits(): + image = np.array([0, uint16_max], dtype=np.uint16) + out = exposure.rescale_intensity(image, out_range='uint12') + assert_close(out, [0, uint12_max]) + + +def test_rescale_uint14_limits(): + image = np.array([0, uint16_max], dtype=np.uint16) + out = exposure.rescale_intensity(image, out_range='uint14') + assert_close(out, [0, uint14_max]) + + # Test adaptive histogram equalization # ==================================== @@ -230,6 +261,11 @@ def test_adjust_gamma_greater_one(): assert_array_equal(result, expected) +def test_adjust_gamma_neggative(): + image = np.arange(0, 255, 4, np.uint8).reshape(8,8) + assert_raises(ValueError, exposure.adjust_gamma, image, -1) + + # Test Logarithmic Correction # =========================== @@ -336,3 +372,8 @@ def test_adjust_inv_sigmoid_cutoff_half(): result = exposure.adjust_sigmoid(image, 0.5, 10, True) assert_array_equal(result, expected) + + +def test_neggative(): + image = np.arange(-10, 245, 4).reshape(8, 8).astype(np.double) + assert_raises(ValueError, exposure.adjust_gamma, image) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 4a6518d6..c46fde01 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -4,9 +4,17 @@ 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_cy import corner_moravec + corner_peaks, corner_fast, structure_tensor, + structure_tensor_eigvals, hessian_matrix, + hessian_matrix_eigvals, hessian_matrix_det) +from .corner_cy import corner_moravec, corner_orientations from .template import match_template +from .brief import BRIEF +from .censure import CENSURE +from .orb import ORB +from .match import match_descriptors +from .util import plot_matches +from .blob import blob_dog, blob_log, blob_doh __all__ = ['daisy', @@ -15,6 +23,11 @@ __all__ = ['daisy', 'greycoprops', 'local_binary_pattern', 'peak_local_max', + 'structure_tensor', + 'structure_tensor_eigvals', + 'hessian_matrix', + 'hessian_matrix_det', + 'hessian_matrix_eigvals', 'corner_kitchen_rosenfeld', 'corner_harris', 'corner_shi_tomasi', @@ -22,4 +35,14 @@ __all__ = ['daisy', 'corner_subpix', 'corner_peaks', 'corner_moravec', - 'match_template'] + 'corner_fast', + 'corner_orientations', + 'match_template', + 'BRIEF', + 'CENSURE', + 'ORB', + 'match_descriptors', + 'plot_matches', + 'blob_dog', + 'blob_doh', + 'blob_log'] diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py deleted file mode 100644 index ecc2ec11..00000000 --- a/skimage/feature/_brief.py +++ /dev/null @@ -1,228 +0,0 @@ -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/_daisy.py b/skimage/feature/_daisy.py index 1a97de8f..3a55faf1 100644 --- a/skimage/feature/_daisy.py +++ b/skimage/feature/_daisy.py @@ -94,15 +94,15 @@ def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8, ''' # Validate image format. - if img.ndim > 2: + if img.ndim != 2: raise ValueError('Only grey-level images are supported.') - if img.dtype.kind != 'f': - img = img_as_float(img) + + img = img_as_float(img) # Validate parameters. if sigmas is not None and ring_radii is not None \ and len(sigmas) - 1 != len(ring_radii): - raise ValueError('len(sigmas)-1 != len(ring_radii)') + raise ValueError('`len(sigmas)-1 != len(ring_radii)`') if ring_radii is not None: rings = len(ring_radii) radius = ring_radii[-1] diff --git a/skimage/feature/_greycomatrix.py b/skimage/feature/_greycomatrix.py deleted file mode 100644 index 45476d33..00000000 --- a/skimage/feature/_greycomatrix.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -Compute grey level co-occurrence matrices (GLCMs) and associated -properties to characterize image textures. -""" - -import numpy as np - -from ._texture import _glcm_loop - - -def greycomatrix(image, distances, angles, levels=256, symmetric=False, - normed=False): - """Calculate the grey-level co-occurrence matrix. - - A grey level co-occurence matrix is a histogram of co-occuring - greyscale values at a given offset over an image. - - Parameters - ---------- - image : array_like of uint8 - Integer typed input image. The image will be cast to uint8, so - the maximum value must be less than 256. - distances : array_like - List of pixel pair distance offsets. - angles : array_like - List of pixel pair angles in radians. - levels : int, optional - The input image should contain integers in [0, levels-1], - where levels indicate the number of grey-levels counted - (typically 256 for an 8-bit image). The maximum value is - 256. - symmetric : bool, optional - If True, the output matrix `P[:, :, d, theta]` is symmetric. This - is accomplished by ignoring the order of value pairs, so both - (i, j) and (j, i) are accumulated when (i, j) is encountered - for a given offset. The default is False. - normed : bool, optional - If True, normalize each matrix `P[:, :, d, theta]` by dividing - by the total number of accumulated co-occurrences for the given - offset. The elements of the resulting matrix sum to 1. The - default is False. - - Returns - ------- - P : 4-D ndarray - The grey-level co-occurrence histogram. The value - `P[i,j,d,theta]` is the number of times that grey-level `j` - occurs at a distance `d` and at an angle `theta` from - grey-level `i`. If `normed` is `False`, the output is of - type uint32, otherwise it is float64. - - References - ---------- - .. [1] The GLCM Tutorial Home Page, - http://www.fp.ucalgary.ca/mhallbey/tutorial.htm - .. [2] Pattern Recognition Engineering, Morton Nadler & Eric P. - Smith - .. [3] Wikipedia, http://en.wikipedia.org/wiki/Co-occurrence_matrix - - - Examples - -------- - Compute 2 GLCMs: One for a 1-pixel offset to the right, and one - for a 1-pixel offset upwards. - - >>> image = np.array([[0, 0, 1, 1], - ... [0, 0, 1, 1], - ... [0, 2, 2, 2], - ... [2, 2, 3, 3]], dtype=np.uint8) - >>> result = greycomatrix(image, [1], [0, np.pi/2], levels=4) - >>> result[:, :, 0, 0] - array([[2, 2, 1, 0], - [0, 2, 0, 0], - [0, 0, 3, 1], - [0, 0, 0, 1]], dtype=uint32) - >>> result[:, :, 0, 1] - array([[3, 0, 2, 0], - [0, 2, 2, 0], - [0, 0, 1, 2], - [0, 0, 0, 0]], dtype=uint32) - - """ - - assert levels <= 256 - image = np.ascontiguousarray(image) - assert image.ndim == 2 - assert image.min() >= 0 - assert image.max() < levels - image = image.astype(np.uint8) - distances = np.ascontiguousarray(distances, dtype=np.float64) - angles = np.ascontiguousarray(angles, dtype=np.float64) - assert distances.ndim == 1 - assert angles.ndim == 1 - - P = np.zeros((levels, levels, len(distances), len(angles)), - dtype=np.uint32, order='C') - - # count co-occurences - _glcm_loop(image, distances, angles, levels, P) - - # make each GLMC symmetric - if symmetric: - Pt = np.transpose(P, (1, 0, 2, 3)) - P = P + Pt - - # normalize each GLMC - if normed: - P = P.astype(np.float64) - glcm_sums = np.apply_over_axes(np.sum, P, axes=(0, 1)) - glcm_sums[glcm_sums == 0] = 1 - P /= glcm_sums - - return P - - -def greycoprops(P, prop='contrast'): - """Calculate texture properties of a GLCM. - - Compute a feature of a grey level co-occurrence matrix to serve as - a compact summary of the matrix. The properties are computed as - follows: - - - 'contrast': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}(i-j)^2` - - 'dissimilarity': :math:`\\sum_{i,j=0}^{levels-1}P_{i,j}|i-j|` - - 'homogeneity': :math:`\\sum_{i,j=0}^{levels-1}\\frac{P_{i,j}}{1+(i-j)^2}` - - 'ASM': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}^2` - - 'energy': :math:`\\sqrt{ASM}` - - 'correlation': - .. math:: \\sum_{i,j=0}^{levels-1} P_{i,j}\\left[\\frac{(i-\\mu_i) \\ - (j-\\mu_j)}{\\sqrt{(\\sigma_i^2)(\\sigma_j^2)}}\\right] - - - Parameters - ---------- - P : ndarray - Input array. `P` is the grey-level co-occurrence histogram - for which to compute the specified property. The value - `P[i,j,d,theta]` is the number of times that grey-level j - occurs at a distance d and at an angle theta from - grey-level i. - - prop : {'contrast', 'dissimilarity', 'homogeneity', 'energy', \ - 'correlation', 'ASM'}, optional - The property of the GLCM to compute. The default is 'contrast'. - - Returns - ------- - results : 2-D ndarray - 2-dimensional array. `results[d, a]` is the property 'prop' for - the d'th distance and the a'th angle. - - References - ---------- - .. [1] The GLCM Tutorial Home Page, - http://www.fp.ucalgary.ca/mhallbey/tutorial.htm - - Examples - -------- - Compute the contrast for GLCMs with distances [1, 2] and angles - [0 degrees, 90 degrees] - - >>> image = np.array([[0, 0, 1, 1], - ... [0, 0, 1, 1], - ... [0, 2, 2, 2], - ... [2, 2, 3, 3]], dtype=np.uint8) - >>> g = greycomatrix(image, [1, 2], [0, np.pi/2], levels=4, - ... normed=True, symmetric=True) - >>> contrast = greycoprops(g, 'contrast') - >>> contrast - array([[ 0.58333333, 1. ], - [ 1.25 , 2.75 ]]) - - """ - - assert P.ndim == 4 - (num_level, num_level2, num_dist, num_angle) = P.shape - assert num_level == num_level2 - assert num_dist > 0 - assert num_angle > 0 - - # create weights for specified property - I, J = np.ogrid[0:num_level, 0:num_level] - if prop == 'contrast': - weights = (I - J)**2 - elif prop == 'dissimilarity': - weights = np.abs(I - J) - elif prop == 'homogeneity': - weights = 1. / (1. + (I - J)**2) - elif prop in ['ASM', 'energy', 'correlation']: - pass - else: - raise ValueError('%s is an invalid property' % (prop)) - - # compute property for each GLCM - if prop == 'energy': - asm = np.apply_over_axes(np.sum, (P**2), axes=(0, 1))[0, 0] - results = np.sqrt(asm) - elif prop == 'ASM': - results = np.apply_over_axes(np.sum, (P**2), axes=(0, 1))[0, 0] - elif prop == 'correlation': - results = np.zeros((num_dist, num_angle), dtype=np.float64) - I = np.array(range(num_level)).reshape((num_level, 1, 1, 1)) - J = np.array(range(num_level)).reshape((1, num_level, 1, 1)) - diff_i = I - np.apply_over_axes(np.sum, (I * P), axes=(0, 1))[0, 0] - diff_j = J - np.apply_over_axes(np.sum, (J * P), axes=(0, 1))[0, 0] - - std_i = np.sqrt(np.apply_over_axes(np.sum, (P * (diff_i)**2), - axes=(0, 1))[0, 0]) - std_j = np.sqrt(np.apply_over_axes(np.sum, (P * (diff_j)**2), - axes=(0, 1))[0, 0]) - cov = np.apply_over_axes(np.sum, (P * (diff_i * diff_j)), - axes=(0, 1))[0, 0] - - # handle the special case of standard deviations near zero - mask_0 = std_i < 1e-15 - mask_0[std_j < 1e-15] = True - results[mask_0] = 1 - - # handle the standard case - mask_1 = mask_0 == False - results[mask_1] = cov[mask_1] / (std_i[mask_1] * std_j[mask_1]) - elif prop in ['contrast', 'dissimilarity', 'homogeneity']: - weights = weights.reshape((num_level, num_level, 1, 1)) - results = np.apply_over_axes(np.sum, (P * weights), axes=(0, 1))[0, 0] - - return results diff --git a/skimage/feature/_hessian_det_appx.pyx b/skimage/feature/_hessian_det_appx.pyx new file mode 100644 index 00000000..0c54cba7 --- /dev/null +++ b/skimage/feature/_hessian_det_appx.pyx @@ -0,0 +1,153 @@ +# cython: cdivision=True +# cython: boundscheck=False +# cython: nonecheck=False +# cython: wraparound=False +import numpy as np +cimport numpy as cnp + + +cdef inline Py_ssize_t _clip(Py_ssize_t x, Py_ssize_t low, Py_ssize_t high): + """Clips coordinate between high and low. + + This method was created so that `hessian_det_appx` does not have to make + a Python call. + + Parameters + ---------- + x : int + Coordinate to be clipped. + low : int + The lower bound. + high : int + The higher bound. + + Returns + ------- + x : int + `x` clipped between `high` and `low`. + """ + + if(x > high): + return high + if(x < low): + return low + return x + + +cdef inline cnp.double_t _integ( + cnp.double_t[:, ::1] img, Py_ssize_t r, Py_ssize_t c, + Py_ssize_t rl, Py_ssize_t cl): + """Integrate over the integral image in the given window + + This method was created so that `hessian_det_appx` does not have to make + a Python call. + + Parameters + ---------- + img : array + The integral image over which to integrate. + r : int + The row number of the top left corner. + c : int + The column number of the top left corner. + rl : int + The number of rows over which to integrate. + cl : int + The number of columns over which to integrate. + + Returns + ------- + ans : int + The integral over the given window. + """ + + r = _clip(r, 0, img.shape[0] - 1) + c = _clip(c, 0, img.shape[1] - 1) + + r2 = _clip(r + rl, 0, img.shape[0] - 1) + c2 = _clip(c + cl, 0, img.shape[1] - 1) + + cdef cnp.double_t ans = img[r, c] + img[r2, c2] - img[r, c2] - img[r2, c] + + if (ans < 0): + return 0 + return ans + + +def _hessian_matrix_det(cnp.double_t[:, ::1] img, double sigma): + """Computes the approximate Hessian Determinant over an image. + + This method uses box filters over integral images to compute the + approximate Hessian Determinant as described in [1]_. + + Parameters + ---------- + img : array + The integral image over which to compute Hessian Determinant. + sigma : float + Standard deviation used for the Gaussian kernel, used for the Hessian + matrix + + Returns + ------- + out : array + The array of the Determinant of Hessians. + + References + ---------- + .. [1] Herbert Bay, Andreas Ess, Tinne Tuytelaars, Luc Van Gool, + "SURF: Speeded Up Robust Features" + ftp://ftp.vision.ee.ethz.ch/publications/articles/eth_biwi_00517.pdf + + Notes + ----- + The running time of this method only depends on size of the image. It is + independent of `sigma` as one would expect. The downside is that the + result for `sigma` less than `3` is not accurate, i.e., not similar to + the result obtained if someone computed the Hessian and took it's + determinant. + """ + + cdef Py_ssize_t size = int(3 * sigma) + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] + cdef Py_ssize_t r, c + cdef Py_ssize_t s2 = (size - 1) / 2 + cdef Py_ssize_t s3 = size / 3 + cdef Py_ssize_t l = size / 3 + cdef Py_ssize_t w = size + cdef Py_ssize_t b = (size - 1) / 2 + cdef cnp.double_t mid, side, tl, tr, bl, br + cdef cnp.double_t[:, ::1] out = np.zeros_like(img, dtype=np.double) + cdef cnp.double_t w_i = 1.0 / size / size + + cdef float dxx, dyy, dxy + + if not size % 2: + size += 1 + + for r in range(height): + for c in range(width): + tl = _integ(img, r - s3, c - s3, s3, s3) # top left + br = _integ(img, r + 1, c + 1, s3, s3) # bottom right + bl = _integ(img, r - s3, c + 1, s3, s3) # bottom left + tr = _integ(img, r + 1, c - s3, s3, s3) # top right + + dxy = bl + tr - tl - br + dxy = -dxy * w_i + + mid = _integ(img, r - s3 + 1, c - s2, 2 * s3 - 1, w) # middle box + side = _integ(img, r - s3 + 1, c - s3 / 2, 2 * s3 - 1, s3) # sides + + dxx = mid - 3 * side + dxx = -dxx * w_i + + mid = _integ(img, r - s2, c - s3 + 1, w, 2 * s3 - 1) + side = _integ(img, r - s3 / 2, c - s3 + 1, s3, 2 * s3 - 1) + + dyy = mid - 3 * side + dyy = -dyy * w_i + + out[r, c] = (dxx * dyy - 0.81 * (dxy * dxy)) + + return out diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 431a5986..67922b92 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -112,7 +112,8 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # compute orientations integral images orientation_histogram = np.zeros((n_cellsy, n_cellsx, orientations)) - subsample = np.index_exp[cy / 2:cy * n_cellsy:cy, cx / 2:cx * n_cellsx:cx] + subsample = np.index_exp[cy // 2:cy * n_cellsy:cy, + cx // 2:cx * n_cellsx:cx] for i in range(orientations): #create new integral image for this orientation # isolate orientations in this range diff --git a/skimage/feature/_template.pyx b/skimage/feature/_template.pyx deleted file mode 100644 index 855ece23..00000000 --- a/skimage/feature/_template.pyx +++ /dev/null @@ -1,97 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -""" -Template matching using normalized cross-correlation. - -We use fast normalized cross-correlation algorithm (see [1]_ and [2]_) to -compute match probability. This algorithm calculates the normalized -cross-correlation of an image, `I`, with a template `T` according to the -following equation:: - - sum{ I(x, y) [T(x, y) - ] } - ------------------------------------------------------- - sqrt(sum{ [I(x, y) - ]^2 } sum{ [T(x, y) - ]^2 }) - -where `` is the average of the template, and `` is the average of the -image *coincident with the template*, and sums are over the template and the -image window coincident with the template. Note that the numerator is simply -the cross-correlation of the image and the zero-mean template. - -To speed up calculations, we use summed-area tables (a.k.a. integral images) to -quickly calculate sums of image windows inside the loop. This step relies on -the following relation (see Eq. 10 of [1]):: - - sum{ [I(x, y) - ]^2 } = - sum{ I^2(x, y) } - [sum{ I(x, y) }]^2 / N_x N_y - -(Without this relation, you would need to subtract each image-window mean from -the image window *before* squaring.) - -.. [1] Briechle and Hanebeck, "Template Matching using Fast Normalized - Cross Correlation", Proceedings of the SPIE (2001). -.. [2] J. P. Lewis, "Fast Normalized Cross-Correlation", Industrial Light and - Magic. -""" - -import numpy as np -from scipy.signal import fftconvolve - -cimport numpy as cnp -from libc.math cimport sqrt, fabs -from skimage._shared.transform cimport integrate - - -from skimage.transform import integral - - -def match_template(cnp.ndarray[float, ndim=2, mode="c"] image, - cnp.ndarray[float, ndim=2, mode="c"] template): - - 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 - cdef Py_ssize_t r, c, r_end, c_end - cdef Py_ssize_t template_rows = template.shape[0] - cdef Py_ssize_t template_cols = template.shape[1] - cdef float den, window_sqr_sum, window_mean_sqr, window_sum - - image_sat = integral.integral_image(image) - image_sqr_sat = integral.integral_image(image**2) - - template -= template_mean - template_ssd = np.sum(template**2) - # use inversed area for accuracy - inv_area = 1.0 / (template.shape[0] * template.shape[1]) - - # when `dtype=float` is used, ascontiguousarray returns ``double``. - corr = np.ascontiguousarray(fftconvolve(image, - template[::-1, ::-1], - mode="valid"), - dtype=np.float32) - - - # move window through convolution results, normalizing in the process - for r in range(corr.shape[0]): - for c in range(corr.shape[1]): - # subtract 1 because `i_end` and `c_end` are used for indexing into - # summed-area table, instead of slicing windows of the image. - r_end = r + template_rows - 1 - c_end = c + template_cols - 1 - - window_sum = integrate(image_sat, r, c, r_end, c_end) - window_mean_sqr = window_sum * window_sum * inv_area - window_sqr_sum = integrate(image_sqr_sat, r, c, r_end, c_end) - if window_sqr_sum <= window_mean_sqr: - corr[r, c] = 0 - continue - - den = sqrt((window_sqr_sum - window_mean_sqr) * template_ssd) - corr[r, c] /= den - - return np.asarray(corr) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index ec83fa65..8d49b377 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -5,7 +5,7 @@ import numpy as np cimport numpy as cnp from libc.math cimport sin, cos, abs -from skimage._shared.interpolation cimport bilinear_interpolation +from skimage._shared.interpolation cimport bilinear_interpolation, round def _glcm_loop(cnp.uint8_t[:, ::1] image, double[:] distances, @@ -48,8 +48,8 @@ def _glcm_loop(cnp.uint8_t[:, ::1] image, double[:] distances, i = image[r, c] # compute the location of the offset pixel - row = r + (sin(angle) * distance + 0.5) - col = c + (cos(angle) * distance + 0.5) + row = r + round(sin(angle) * distance) + col = c + round(cos(angle) * distance) # make sure the offset is within bounds if row >= 0 and row < rows and \ @@ -129,6 +129,9 @@ def _local_binary_pattern(double[:, ::1] image, cdef Py_ssize_t rot_index, n_ones cdef cnp.int8_t first_zero, first_one + # To compute the variance features + cdef double sum_, var_, texture_i + for r in range(image.shape[0]): for c in range(image.shape[1]): for i in range(P): @@ -144,8 +147,24 @@ def _local_binary_pattern(double[:, ::1] image, lbp = 0 - # if method == 'uniform' or method == 'var': - if method == 'U' or method == 'N' or method == 'V': + # if method == 'var': + if method == 'V': + # Compute the variance without passing from numpy. + # Following the LBP paper, we're taking a biased estimate + # of the variance (ddof=0) + sum_ = 0.0 + var_ = 0.0 + for i in range(P): + texture_i = texture[i] + sum_ += texture_i + var_ += texture_i * texture_i + var_ = (var_ - (sum_ * sum_) / P) / P + if var_ != 0: + lbp = var_ + else: + lbp = np.nan + # if method == 'uniform': + elif method == 'U' or method == 'N': # determine number of 0 - 1 changes changes = 0 for i in range(P - 1): @@ -186,7 +205,7 @@ def _local_binary_pattern(double[:, ::1] image, if changes <= 2: # We have a uniform pattern - n_ones = 0 # determies the number of ones + n_ones = 0 # determines 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): @@ -215,13 +234,6 @@ def _local_binary_pattern(double[:, ::1] image, lbp += signed_texture[i] else: lbp = P + 1 - - if method == 'V': - var = np.var(texture) - if var != 0: - lbp /= var - else: - lbp = np.nan else: # method == 'default' for i in range(P): diff --git a/skimage/feature/blob.py b/skimage/feature/blob.py new file mode 100644 index 00000000..134e7026 --- /dev/null +++ b/skimage/feature/blob.py @@ -0,0 +1,410 @@ + +import numpy as np +from scipy.ndimage.filters import gaussian_filter, gaussian_laplace +import itertools as itt +import math +from math import sqrt, hypot, log +from numpy import arccos +from skimage.util import img_as_float +from .peak import peak_local_max +from ._hessian_det_appx import _hessian_matrix_det +from skimage.transform import integral_image + + +# This basic blob detection algorithm is based on: +# http://www.cs.utah.edu/~jfishbau/advimproc/project1/ (04.04.2013) +# Theory behind: http://en.wikipedia.org/wiki/Blob_detection (04.04.2013) + + +def _blob_overlap(blob1, blob2): + """Finds the overlapping area fraction between two blobs. + + Returns a float representing fraction of overlapped area. + + Parameters + ---------- + blob1 : sequence + A sequence of ``(y,x,sigma)``, where ``x,y`` are coordinates of blob + and sigma is the standard deviation of the Gaussian kernel which + detected the blob. + blob2 : sequence + A sequence of ``(y,x,sigma)``, where ``x,y`` are coordinates of blob + and sigma is the standard deviation of the Gaussian kernel which + detected the blob. + + Returns + ------- + f : float + Fraction of overlapped area. + """ + root2 = sqrt(2) + + # extent of the blob is given by sqrt(2)*scale + r1 = blob1[2] * root2 + r2 = blob2[2] * root2 + + d = hypot(blob1[0] - blob2[0], blob1[1] - blob2[1]) + + if d > r1 + r2: + return 0 + + # one blob is inside the other, the smaller blob must die + if d <= abs(r1 - r2): + return 1 + + acos1 = arccos((d ** 2 + r1 ** 2 - r2 ** 2) / (2 * d * r1)) + acos2 = arccos((d ** 2 + r2 ** 2 - r1 ** 2) / (2 * d * r2)) + a = -d + r2 + r1 + b = d - r2 + r1 + c = d + r2 - r1 + d = d + r2 + r1 + area = r1 ** 2 * acos1 + r2 ** 2 * acos2 - 0.5 * sqrt(abs(a * b * c * d)) + + return area / (math.pi * (min(r1, r2) ** 2)) + + +def _prune_blobs(blobs_array, overlap): + """Eliminated blobs with area overlap. + + Parameters + ---------- + blobs_array : ndarray + A 2d array with each row representing 3 values, ``(y,x,sigma)`` + where ``(y,x)`` are coordinates of the blob and ``sigma`` is the + standard deviation of the Gaussian kernel which detected the blob. + overlap : float + A value between 0 and 1. If the fraction of area overlapping for 2 + blobs is greater than `overlap` the smaller blob is eliminated. + + Returns + ------- + A : ndarray + `array` with overlapping blobs removed. + """ + + # iterating again might eliminate more blobs, but one iteration suffices + # for most cases + for blob1, blob2 in itt.combinations(blobs_array, 2): + if _blob_overlap(blob1, blob2) > overlap: + if blob1[2] > blob2[2]: + blob2[2] = -1 + else: + blob1[2] = -1 + + # return blobs_array[blobs_array[:, 2] > 0] + return np.array([b for b in blobs_array if b[2] > 0]) + + +def blob_dog(image, min_sigma=1, max_sigma=50, sigma_ratio=1.6, threshold=2.0, + overlap=.5,): + """Finds blobs in the given grayscale image. + + Blobs are found using the Difference of Gaussian (DoG) method [1]_. + For each blob found, the method returns its coordinates and the standard + deviation of the Gaussian kernel that detected the blob. + + Parameters + ---------- + image : ndarray + Input grayscale image, blobs are assumed to be light on dark + background (white on black). + min_sigma : float, optional + The minimum standard deviation for Gaussian Kernel. Keep this low to + detect smaller blobs. + max_sigma : float, optional + The maximum standard deviation for Gaussian Kernel. Keep this high to + detect larger blobs. + sigma_ratio : float, optional + The ratio between the standard deviation of Gaussian Kernels used for + computing the Difference of Gaussians + threshold : float, optional. + The absolute lower bound for scale space maxima. Local maxima smaller + than thresh are ignored. Reduce this to detect blobs with less + intensities. + overlap : float, optional + A value between 0 and 1. If the area of two blobs overlaps by a + fraction greater than `threshold`, the smaller blob is eliminated. + + Returns + ------- + A : (n, 3) ndarray + A 2d array with each row representing 3 values, ``(y,x,sigma)`` + where ``(y,x)`` are coordinates of the blob and ``sigma`` is the + standard deviation of the Gaussian kernel which detected the blob. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Blob_detection#The_difference_of_Gaussians_approach + + Examples + -------- + >>> from skimage import data, feature + >>> feature.blob_dog(data.coins(), threshold=.5, max_sigma=40) + array([[ 45, 336, 16], + [ 52, 155, 16], + [ 52, 216, 16], + [ 54, 42, 16], + [ 54, 276, 10], + [ 58, 100, 10], + [120, 272, 16], + [124, 337, 10], + [125, 45, 16], + [125, 208, 10], + [127, 102, 10], + [128, 154, 10], + [185, 347, 16], + [193, 213, 16], + [194, 277, 16], + [195, 102, 16], + [196, 43, 10], + [198, 155, 10], + [260, 46, 16], + [261, 173, 16], + [263, 245, 16], + [263, 302, 16], + [267, 115, 10], + [267, 359, 16]]) + + Notes + ----- + The radius of each blob is approximately :math:`\sqrt{2}sigma`. + """ + + if image.ndim != 2: + raise ValueError("'image' must be a grayscale ") + + image = img_as_float(image) + + # k such that min_sigma*(sigma_ratio**k) > max_sigma + k = int(log(float(max_sigma) / min_sigma, sigma_ratio)) + 1 + + # a geometric progression of standard deviations for gaussian kernels + sigma_list = np.array([min_sigma * (sigma_ratio ** i) + for i in range(k + 1)]) + + gaussian_images = [gaussian_filter(image, s) for s in sigma_list] + + # computing difference between two successive Gaussian blurred images + # multiplying with standard deviation provides scale invariance + dog_images = [(gaussian_images[i] - gaussian_images[i + 1]) + * sigma_list[i] for i in range(k)] + image_cube = np.dstack(dog_images) + + # local_maxima = get_local_maxima(image_cube, threshold) + local_maxima = peak_local_max(image_cube, threshold_abs=threshold, + footprint=np.ones((3, 3, 3)), + threshold_rel=0.0, + exclude_border=False) + + # Convert the last index to its corresponding scale value + local_maxima[:, 2] = sigma_list[local_maxima[:, 2]] + return _prune_blobs(local_maxima, overlap) + + +def blob_log(image, min_sigma=1, max_sigma=50, num_sigma=10, threshold=.2, + overlap=.5, log_scale=False): + """Finds blobs in the given grayscale image. + + Blobs are found using the Laplacian of Gaussian (LoG) method [1]_. + For each blob found, the method returns its coordinates and the standard + deviation of the Gaussian kernel that detected the blob. + + Parameters + ---------- + image : ndarray + Input grayscale image, blobs are assumed to be light on dark + background (white on black). + min_sigma : float, optional + The minimum standard deviation for Gaussian Kernel. Keep this low to + detect smaller blobs. + max_sigma : float, optional + The maximum standard deviation for Gaussian Kernel. Keep this high to + detect larger blobs. + num_sigma : int, optional + The number of intermediate values of standard deviations to consider + between `min_sigma` and `max_sigma`. + threshold : float, optional. + The absolute lower bound for scale space maxima. Local maxima smaller + than thresh are ignored. Reduce this to detect blobs with less + intensities. + overlap : float, optional + A value between 0 and 1. If the area of two blobs overlaps by a + fraction greater than `threshold`, the smaller blob is eliminated. + log_scale : bool, optional + If set intermediate values of standard deviations are interpolated + using a logarithmic scale to the base `10`. If not, linear + interpolation is used. + + Returns + ------- + A : (n, 3) ndarray + A 2d array with each row representing 3 values, ``(y,x,sigma)`` + where ``(y,x)`` are coordinates of the blob and ``sigma`` is the + standard deviation of the Gaussian kernel which detected the blob. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Blob_detection#The_Laplacian_of_Gaussian + + Examples + -------- + >>> from skimage import data, feature, exposure + >>> img = data.coins() + >>> img = exposure.equalize_hist(img) # improves detection + >>> feature.blob_log(img, threshold = .3) + array([[113, 323, 1], + [121, 272, 17], + [124, 336, 11], + [126, 46, 11], + [126, 208, 11], + [127, 102, 11], + [128, 154, 11], + [185, 344, 17], + [194, 213, 17], + [194, 276, 17], + [197, 44, 11], + [198, 103, 11], + [198, 155, 11], + [260, 174, 17], + [263, 244, 17], + [263, 302, 17], + [266, 115, 11]]) + + Notes + ----- + The radius of each blob is approximately :math:`\sqrt{2}sigma`. + """ + + if image.ndim != 2: + raise ValueError("'image' must be a grayscale ") + + image = img_as_float(image) + + if log_scale: + start, stop = log(min_sigma, 10), log(max_sigma, 10) + sigma_list = np.logspace(start, stop, num_sigma) + else: + sigma_list = np.linspace(min_sigma, max_sigma, num_sigma) + + # computing gaussian laplace + # s**2 provides scale invariance + gl_images = [-gaussian_laplace(image, s) * s ** 2 for s in sigma_list] + image_cube = np.dstack(gl_images) + + local_maxima = peak_local_max(image_cube, threshold_abs=threshold, + footprint=np.ones((3, 3, 3)), + threshold_rel=0.0, + exclude_border=False) + + # Convert the last index to its corresponding scale value + local_maxima[:, 2] = sigma_list[local_maxima[:, 2]] + return _prune_blobs(local_maxima, overlap) + + +def blob_doh(image, min_sigma=1, max_sigma=30, num_sigma=10, threshold=0.01, + overlap=.5, log_scale=False): + """Finds blobs in the given grayscale image. + + Blobs are found using the Determinant of Hessian method [1]_. For each blob + found, the method returns its coordinates and the standard deviation + of the Gaussian Kernel used for the Hessian matrix whose determinant + detected the blob. Determinant of Hessians is approximated using [2]_. + + Parameters + ---------- + image : ndarray + Input grayscale image.Blobs can either be light on dark or vice versa. + min_sigma : float, optional + The minimum standard deviation for Gaussian Kernel used to compute + Hessian matrix. Keep this low to detect smaller blobs. + max_sigma : float, optional + The maximum standard deviation for Gaussian Kernel used to compute + Hessian matrix. Keep this high to detect larger blobs. + num_sigma : int, optional + The number of intermediate values of standard deviations to consider + between `min_sigma` and `max_sigma`. + threshold : float, optional. + The absolute lower bound for scale space maxima. Local maxima smaller + than thresh are ignored. Reduce this to detect less prominent blobs. + overlap : float, optional + A value between 0 and 1. If the area of two blobs overlaps by a + fraction greater than `threshold`, the smaller blob is eliminated. + log_scale : bool, optional + If set intermediate values of standard deviations are interpolated + using a logarithmic scale to the base `10`. If not, linear + interpolation is used. + + Returns + ------- + A : (n, 3) ndarray + A 2d array with each row representing 3 values, ``(y,x,sigma)`` + where ``(y,x)`` are coordinates of the blob and ``sigma`` is the + standard deviation of the Gaussian kernel of the Hessian Matrix whose + determinant detected the blob. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Blob_detection#The_determinant_of_the_Hessian + + .. [2] Herbert Bay, Andreas Ess, Tinne Tuytelaars, Luc Van Gool, + "SURF: Speeded Up Robust Features" + ftp://ftp.vision.ee.ethz.ch/publications/articles/eth_biwi_00517.pdf + + Examples + -------- + >>> from skimage import data, feature + >>> img = data.coins() + >>> feature.blob_doh(img) + array([[121, 271, 30], + [123, 44, 23], + [123, 205, 20], + [124, 336, 20], + [126, 101, 20], + [126, 153, 20], + [156, 302, 30], + [185, 348, 30], + [192, 212, 23], + [193, 275, 23], + [195, 100, 23], + [197, 44, 20], + [197, 153, 20], + [260, 173, 30], + [262, 243, 23], + [265, 113, 23], + [270, 363, 30]]) + + + Notes + ----- + The radius of each blob is approximately `sigma`. + Computation of Determinant of Hessians is independent of the standard + deviation. Therefore detecting larger blobs won't take more time. In + methods line :py:meth:`blob_dog` and :py:meth:`blob_log` the computation + of Gaussians for larger `sigma` takes more time. The downside is that + this method can't be used for detecting blobs of radius less than `3px` + due to the box filters used in the approximation of Hessian Determinant. + """ + + if image.ndim != 2: + raise ValueError("'image' must be grayscale ") + + image = img_as_float(image) + image = integral_image(image) + + if log_scale: + start, stop = log(min_sigma, 10), log(max_sigma, 10) + sigma_list = np.logspace(start, stop, num_sigma) + else: + sigma_list = np.linspace(min_sigma, max_sigma, num_sigma) + + hessian_images = [_hessian_matrix_det(image, s) for s in sigma_list] + image_cube = np.dstack(hessian_images) + + local_maxima = peak_local_max(image_cube, threshold_abs=threshold, + footprint=np.ones((3, 3, 3)), + threshold_rel=0.0, + exclude_border=False) + + # Convert the last index to its corresponding scale value + local_maxima[:, 2] = sigma_list[local_maxima[:, 2]] + return _prune_blobs(local_maxima, overlap) diff --git a/skimage/feature/brief.py b/skimage/feature/brief.py new file mode 100644 index 00000000..d1626f17 --- /dev/null +++ b/skimage/feature/brief.py @@ -0,0 +1,181 @@ +import numpy as np +from scipy.ndimage.filters import gaussian_filter + +from .util import (DescriptorExtractor, _mask_border_keypoints, + _prepare_grayscale_input_2D) + +from .brief_cy import _brief_loop + + +class BRIEF(DescriptorExtractor): + + """BRIEF binary descriptor extractor. + + BRIEF (Binary Robust Independent Elementary Features) is an efficient + feature point descriptor. It is highly discriminative even when using + relatively few bits and is computed using simple intensity difference + tests. + + For each keypoint, intensity comparisons are carried out for a specifically + distributed number N of pixel-pairs resulting in a binary descriptor of + length N. For binary descriptors the Hamming distance can be used for + feature matching, which leads to lower computational cost in comparison to + the L2 norm. + + Parameters + ---------- + descriptor_size : int, optional + Size of BRIEF descriptor for each keypoint. Sizes 128, 256 and 512 + recommended by the authors. Default is 256. + patch_size : int, optional + Length of the two dimensional square patch sampling region around + the keypoints. Default is 49. + mode : {'normal', 'uniform'}, optional + Probability distribution for sampling location of decision pixel-pairs + around keypoints. + sample_seed : int, optional + Seed for the random sampling of 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` must be the same for the images to be + matched while building the descriptors. + sigma : float, optional + Standard deviation of the Gaussian low-pass filter applied to the image + to alleviate noise sensitivity, which is strongly recommended to obtain + discriminative and good descriptors. + + Attributes + ---------- + descriptors : (Q, `descriptor_size`) array of dtype bool + 2D ndarray of binary descriptors of size `descriptor_size` for Q + keypoints after filtering out border keypoints with value at an + index ``(i, j)`` either being ``True`` or ``False`` representing + the outcome of the intensity comparison for i-th keypoint on j-th + decision pixel-pair. It is ``Q == np.sum(mask)``. + mask : (N, ) array of dtype bool + Mask indicating whether a keypoint has been filtered out + (``False``) or is described in the `descriptors` array (``True``). + + Examples + -------- + >>> from skimage.feature import (corner_harris, corner_peaks, BRIEF, + ... match_descriptors) + >>> import numpy as np + >>> 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) + >>> 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) + >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) + >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) + >>> extractor = BRIEF(patch_size=5) + >>> extractor.extract(square1, keypoints1) + >>> descriptors1 = extractor.descriptors + >>> extractor.extract(square2, keypoints2) + >>> descriptors2 = extractor.descriptors + >>> matches = match_descriptors(descriptors1, descriptors2) + >>> matches + array([[0, 0], + [1, 1], + [2, 2], + [3, 3]]) + >>> keypoints1[matches[:, 0]] + array([[2, 2], + [2, 5], + [5, 2], + [5, 5]]) + >>> keypoints2[matches[:, 1]] + array([[2, 2], + [2, 6], + [6, 2], + [6, 6]]) + + """ + + def __init__(self, descriptor_size=256, patch_size=49, + mode='normal', sigma=1, sample_seed=1): + + mode = mode.lower() + if mode not in ('normal', 'uniform'): + raise ValueError("`mode` must be 'normal' or 'uniform'.") + + self.descriptor_size = descriptor_size + self.patch_size = patch_size + self.mode = mode + self.sigma = sigma + self.sample_seed = sample_seed + + self.descriptors = None + self.mask = None + + def extract(self, image, keypoints): + """Extract BRIEF binary descriptors for given keypoints in image. + + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + + """ + + np.random.seed(self.sample_seed) + + image = _prepare_grayscale_input_2D(image) + + # Gaussian low-pass filtering to alleviate noise sensitivity + image = np.ascontiguousarray(gaussian_filter(image, self.sigma)) + + # Sampling pairs of decision pixels in patch_size x patch_size window + desc_size = self.descriptor_size + patch_size = self.patch_size + if self.mode == 'normal': + samples = (patch_size / 5.0) * np.random.randn(desc_size * 8) + samples = np.array(samples, dtype=np.int32) + samples = samples[(samples < (patch_size // 2)) + & (samples > - (patch_size - 2) // 2)] + + pos1 = samples[:desc_size * 2].reshape(desc_size, 2) + pos2 = samples[desc_size * 2:desc_size * 4].reshape(desc_size, 2) + elif self.mode == 'uniform': + samples = np.random.randint(-(patch_size - 2) // 2, + (patch_size // 2) + 1, + (desc_size * 2, 2)) + samples = np.array(samples, dtype=np.int32) + pos1, pos2 = np.split(samples, 2) + + pos1 = np.ascontiguousarray(pos1) + pos2 = np.ascontiguousarray(pos2) + + # Removing keypoints that are within (patch_size / 2) distance from the + # image border + self.mask = _mask_border_keypoints(image.shape, keypoints, + patch_size // 2) + + keypoints = np.array(keypoints[self.mask, :], dtype=np.intp, + order='C', copy=False) + + self.descriptors = np.zeros((keypoints.shape[0], desc_size), + dtype=bool, order='C') + + _brief_loop(image, self.descriptors.view(np.uint8), keypoints, + pos1, pos2) diff --git a/skimage/feature/_brief_cy.pyx b/skimage/feature/brief_cy.pyx similarity index 89% rename from skimage/feature/_brief_cy.pyx rename to skimage/feature/brief_cy.pyx index c53d85fc..8cd1afa7 100644 --- a/skimage/feature/_brief_cy.pyx +++ b/skimage/feature/brief_cy.pyx @@ -6,7 +6,7 @@ cimport numpy as cnp -def _brief_loop(double[:, ::1] image, char[:, ::1] descriptors, +def _brief_loop(double[:, ::1] image, unsigned char[:, ::1] descriptors, Py_ssize_t[:, ::1] keypoints, int[:, ::1] pos0, int[:, ::1] pos1): diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 4bb7fdda..eb69f115 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -1,9 +1,10 @@ import numpy as np from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve +from skimage.feature.util import FeatureDetector, _prepare_grayscale_input_2D + from skimage.transform import integral_image -from skimage.feature.corner import _compute_auto_correlation -from skimage.util import img_as_float +from skimage.feature import structure_tensor from skimage.morphology import octagon, star from skimage.feature.util import _mask_border_keypoints @@ -36,7 +37,7 @@ def _filter_image(image, min_scale, max_scale, mode): # make response[:, :, i] contiguous memory block item_size = response.itemsize - response.strides = (item_size * response.shape[0], item_size, + response.strides = (item_size * response.shape[1], item_size, item_size * response.shape[0] * response.shape[1]) integral_img = integral_image(image) @@ -65,19 +66,19 @@ def _filter_image(image, min_scale, max_scale, mode): 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)) + _octagon_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)) + response[:, :, i] = convolve(image, _star_kernel(m, n)) return response -def _octagon_filter_kernel(mo, no, mi, ni): +def _octagon_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) @@ -91,7 +92,7 @@ def _octagon_filter_kernel(mo, no, mi, ni): return bfilter -def _star_filter_kernel(m, n): +def _star_kernel(m, n): c = m + m // 2 - n - n // 2 outer_star = star(m) inner_star = np.zeros_like(outer_star) @@ -104,29 +105,25 @@ def _star_filter_kernel(m, n): 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 + Axx, Axy, Ayy = structure_tensor(image, sigma) + feature_mask[(Axx + Ayy) ** 2 + > line_threshold * (Axx * Ayy - Axy ** 2)] = 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. +class CENSURE(FeatureDetector): - Parameters - ---------- - image : 2D ndarray - Input image. - min_scale : int + """CENSURE keypoint detector. + + min_scale : int, optional Minimum scale to extract keypoints from. - max_scale : int + max_scale : int, optional 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'} + from the scales in the range [min_scale + 1, max_scale - 1]. The filter + sizes for different scales is such that the two adjacent scales + comprise of an octave. + mode : {'DoB', 'Octagon', 'STAR'}, optional 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 @@ -135,24 +132,24 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', 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 + non_max_threshold : float, optional Threshold value used to suppress maximas and minimas with a weak magnitude response obtained after Non-Maximal Suppression. - line_threshold : float + line_threshold : float, optional Threshold for rejecting interest points which have ratio of principal curvatures greater than this value. - Returns - ------- + Attributes + ---------- 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. + Keypoint coordinates as ``(row, col)``. + scales : (N, ) array + Corresponding scales. References ---------- .. [1] Motilal Agrawal, Kurt Konolige and Morten Rufus Blas - "CenSurE: Center Surround Extremas for Realtime Feature + "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 @@ -161,74 +158,129 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', Descriptors in the Context of Robot Navigation" http://www.jamris.org/01_2013/saveas.php?QUEST=JAMRIS_No01_2013_P_11-20.pdf + Examples + -------- + >>> from skimage.data import lena + >>> from skimage.color import rgb2gray + >>> from skimage.feature import CENSURE + >>> img = rgb2gray(lena()[100:300, 100:300]) + >>> censure = CENSURE() + >>> censure.detect(img) + >>> censure.keypoints + array([[ 71, 148], + [ 77, 186], + [ 78, 189], + [ 89, 174], + [127, 134], + [131, 133], + [134, 125], + [137, 125], + [149, 36], + [162, 165], + [168, 167], + [170, 5], + [171, 29], + [179, 20], + [194, 65]]) + >>> censure.scales + array([2, 4, 2, 3, 4, 2, 2, 3, 4, 6, 3, 2, 3, 4, 2]) + """ - # (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. + def __init__(self, min_scale=1, max_scale=7, mode='DoB', + non_max_threshold=0.15, line_threshold=10): - 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'.") - 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.') - 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.') + self.min_scale = min_scale + self.max_scale = max_scale + self.mode = mode + self.non_max_threshold = non_max_threshold + self.line_threshold = line_threshold - image = img_as_float(image) - image = np.ascontiguousarray(image) + self.keypoints = None + self.scales = None - # Generating all the scales - filter_response = _filter_image(image, min_scale, max_scale, mode) + def detect(self, image): + """Detect CENSURE keypoints along with the corresponding scale. - # 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 + Parameters + ---------- + image : 2D ndarray + Input image. - 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) + # (1) First we generate the required scales on the input grayscale + # image using a bi-level filter and stack them up in `filter_response`. - rows, cols, scales = np.nonzero(feature_mask[..., 1:max_scale - min_scale]) - keypoints = np.column_stack([rows, cols]) - scales = scales + min_scale + 1 + # (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. - if mode == 'dob': - return keypoints, scales + num_scales = self.max_scale - self.min_scale - cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool) + image = np.ascontiguousarray(_prepare_grayscale_input_2D(image)) - 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) + # Generating all the scales + filter_response = _filter_image(image, self.min_scale, self.max_scale, + self.mode) - return keypoints[cumulative_mask], scales[cumulative_mask] + # Suppressing points that are neither minima or maxima in their + # 3 x 3 x 3 neighborhood 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 < self.non_max_threshold] = False + + for i in range(1, num_scales): + # 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 + (self.min_scale + i - 1) / 3.0), + self.line_threshold) + + rows, cols, scales = np.nonzero(feature_mask[..., 1:num_scales]) + keypoints = np.column_stack([rows, cols]) + scales = scales + self.min_scale + 1 + + if self.mode == 'dob': + self.keypoints = keypoints + self.scales = scales + return + + cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool) + + if self.mode == 'octagon': + for i in range(self.min_scale + 1, self.max_scale): + c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 \ + + OCTAGON_OUTER_SHAPE[i - 1][1] + cumulative_mask |= ( + _mask_border_keypoints(image.shape, keypoints, c) + & (scales == i)) + elif self.mode == 'star': + for i in range(self.min_scale + 1, self.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.shape, keypoints, c) + & (scales == i)) + + self.keypoints = keypoints[cumulative_mask] + self.scales = scales[cumulative_mask] diff --git a/skimage/feature/corner.py b/skimage/feature/corner.py index 53b2241e..fefff40d 100644 --- a/skimage/feature/corner.py +++ b/skimage/feature/corner.py @@ -1,18 +1,28 @@ import numpy as np from scipy import ndimage from scipy import stats -from skimage.color import rgb2grey -from skimage.util import img_as_float + +from skimage.util import img_as_float, pad from skimage.feature import peak_local_max +from skimage.feature.util import _prepare_grayscale_input_2D +from skimage.feature.corner_cy import _corner_fast +from ._hessian_det_appx import _hessian_matrix_det +from ..transform import integral_image +from .._shared.utils import safe_as_int -def _compute_derivatives(image): +def _compute_derivatives(image, mode='constant', cval=0): """Compute derivatives in x and y direction using the Sobel operator. Parameters ---------- image : ndarray Input image. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. Returns ------- @@ -23,14 +33,82 @@ def _compute_derivatives(image): """ - imy = ndimage.sobel(image, axis=0, mode='constant', cval=0) - imx = ndimage.sobel(image, axis=1, mode='constant', cval=0) + imy = ndimage.sobel(image, axis=0, mode=mode, cval=cval) + imx = ndimage.sobel(image, axis=1, mode=mode, cval=cval) return imx, imy -def _compute_auto_correlation(image, sigma): - """Compute auto-correlation matrix using sum of squared differences. +def structure_tensor(image, sigma=1, mode='constant', cval=0): + """Compute structure tensor using sum of squared differences. + + The structure tensor A is defined as:: + + A = [Axx Axy] + [Axy Ayy] + + which is approximated by the weighted sum of squared differences in a local + window around each pixel in the image. + + Parameters + ---------- + image : ndarray + Input image. + sigma : float + Standard deviation used for the Gaussian kernel, which is used as a + weighting function for the local summation of squared differences. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + Axx : ndarray + Element of the structure tensor for each pixel in the input image. + Axy : ndarray + Element of the structure tensor for each pixel in the input image. + Ayy : ndarray + Element of the structure tensor for each pixel in the input image. + + Examples + -------- + >>> from skimage.feature import structure_tensor + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 1 + >>> Axx, Axy, Ayy = structure_tensor(square, sigma=0.1) + >>> Axx + array([[ 0., 0., 0., 0., 0.], + [ 0., 1., 0., 1., 0.], + [ 0., 4., 0., 4., 0.], + [ 0., 1., 0., 1., 0.], + [ 0., 0., 0., 0., 0.]]) + + """ + + image = _prepare_grayscale_input_2D(image) + + imx, imy = _compute_derivatives(image, mode=mode, cval=cval) + + # structure tensore + Axx = ndimage.gaussian_filter(imx * imx, sigma, mode=mode, cval=cval) + Axy = ndimage.gaussian_filter(imx * imy, sigma, mode=mode, cval=cval) + Ayy = ndimage.gaussian_filter(imy * imy, sigma, mode=mode, cval=cval) + + return Axx, Axy, Ayy + + +def hessian_matrix(image, sigma=1, mode='constant', cval=0): + """Compute Hessian matrix. + + The Hessian matrix is defined as:: + + H = [Hxx Hxy] + [Hxy Hyy] + + which is computed by convolving the image with the second derivatives + of the Gaussian kernel in the respective x- and y-directions. Parameters ---------- @@ -39,46 +117,200 @@ def _compute_auto_correlation(image, sigma): sigma : float Standard deviation used for the Gaussian kernel, which is used as weighting function for the auto-correlation matrix. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. Returns ------- - Axx : ndarray - Element of the auto-correlation matrix for each pixel in input image. - Axy : ndarray - Element of the auto-correlation matrix for each pixel in input image. - Ayy : ndarray - Element of the auto-correlation matrix for each pixel in input image. + Hxx : ndarray + Element of the Hessian matrix for each pixel in the input image. + Hxy : ndarray + Element of the Hessian matrix for each pixel in the input image. + Hyy : ndarray + Element of the Hessian matrix for each pixel in the input image. + + Examples + -------- + >>> from skimage.feature import hessian_matrix + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 1 + >>> Hxx, Hxy, Hyy = hessian_matrix(square, sigma=0.1) + >>> Hxx + 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.]]) """ - if image.ndim == 3: - image = img_as_float(rgb2grey(image)) + image = _prepare_grayscale_input_2D(image) - imx, imy = _compute_derivatives(image) + # window extent to the left and right, which covers > 99% of the normal + # distribution + window_ext = max(1, np.ceil(3 * sigma)) - # structure tensore - Axx = ndimage.gaussian_filter(imx * imx, sigma, mode='constant', cval=0) - Axy = ndimage.gaussian_filter(imx * imy, sigma, mode='constant', cval=0) - Ayy = ndimage.gaussian_filter(imy * imy, sigma, mode='constant', cval=0) + ky, kx = np.mgrid[-window_ext:window_ext + 1, -window_ext:window_ext + 1] - return Axx, Axy, Ayy + # second derivative Gaussian kernels + gaussian_exp = np.exp(-(kx ** 2 + ky ** 2) / (2 * sigma ** 2)) + kernel_xx = 1 / (2 * np.pi * sigma ** 4) * (kx ** 2 / sigma ** 2 - 1) + kernel_xx *= gaussian_exp + kernel_xx /= kernel_xx.sum() + kernel_xy = 1 / (2 * np.pi * sigma ** 6) * (kx * ky) + kernel_xy *= gaussian_exp + kernel_xy /= kernel_xx.sum() + kernel_yy = kernel_xx.transpose() + + Hxx = ndimage.convolve(image, kernel_xx, mode=mode, cval=cval) + Hxy = ndimage.convolve(image, kernel_xy, mode=mode, cval=cval) + Hyy = ndimage.convolve(image, kernel_yy, mode=mode, cval=cval) + + return Hxx, Hxy, Hyy -def corner_kitchen_rosenfeld(image): +def hessian_matrix_det(image, sigma): + """Computes the approximate Hessian Determinant over an image. + + This method uses box filters over integral images to compute the + approximate Hessian Determinant as described in [1]_. + + Parameters + ---------- + image : array + The image over which to compute Hessian Determinant. + sigma : float + Standard deviation used for the Gaussian kernel, used for the Hessian + matrix. + + Returns + ------- + out : array + The array of the Determinant of Hessians. + + References + ---------- + .. [1] Herbert Bay, Andreas Ess, Tinne Tuytelaars, Luc Van Gool, + "SURF: Speeded Up Robust Features" + ftp://ftp.vision.ee.ethz.ch/publications/articles/eth_biwi_00517.pdf + + Notes + ----- + The running time of this method only depends on size of the image. It is + independent of `sigma` as one would expect. The downside is that the + result for `sigma` less than `3` is not accurate, i.e., not similar to + the result obtained if someone computed the Hessian and took it's + determinant. + """ + + image = img_as_float(image) + image = integral_image(image) + return np.array(_hessian_matrix_det(image, sigma)) + + +def _image_orthogonal_matrix22_eigvals(M00, M01, M11): + l1 = (M00 + M11) / 2 + np.sqrt(4 * M01 ** 2 + (M00 - M11) ** 2) / 2 + l2 = (M00 + M11) / 2 - np.sqrt(4 * M01 ** 2 + (M00 - M11) ** 2) / 2 + return l1, l2 + + +def structure_tensor_eigvals(Axx, Axy, Ayy): + """Compute Eigen values of structure tensor. + + Parameters + ---------- + Axx : ndarray + Element of the structure tensor for each pixel in the input image. + Axy : ndarray + Element of the structure tensor for each pixel in the input image. + Ayy : ndarray + Element of the structure tensor for each pixel in the input image. + + Returns + ------- + l1 : ndarray + Larger eigen value for each input matrix. + l2 : ndarray + Smaller eigen value for each input matrix. + + Examples + -------- + >>> from skimage.feature import structure_tensor, structure_tensor_eigvals + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 1 + >>> Axx, Axy, Ayy = structure_tensor(square, sigma=0.1) + >>> structure_tensor_eigvals(Axx, Axy, Ayy)[0] + array([[ 0., 0., 0., 0., 0.], + [ 0., 2., 4., 2., 0.], + [ 0., 4., 0., 4., 0.], + [ 0., 2., 4., 2., 0.], + [ 0., 0., 0., 0., 0.]]) + + """ + + return _image_orthogonal_matrix22_eigvals(Axx, Axy, Ayy) + + +def hessian_matrix_eigvals(Hxx, Hxy, Hyy): + """Compute Eigen values of Hessian matrix. + + Parameters + ---------- + Hxx : ndarray + Element of the Hessian matrix for each pixel in the input image. + Hxy : ndarray + Element of the Hessian matrix for each pixel in the input image. + Hyy : ndarray + Element of the Hessian matrix for each pixel in the input image. + + Returns + ------- + l1 : ndarray + Larger eigen value for each input matrix. + l2 : ndarray + Smaller eigen value for each input matrix. + + Examples + -------- + >>> from skimage.feature import hessian_matrix, hessian_matrix_eigvals + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 1 + >>> Hxx, Hxy, Hyy = hessian_matrix(square, sigma=0.1) + >>> hessian_matrix_eigvals(Hxx, Hxy, Hyy)[0] + 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.]]) + + """ + + return _image_orthogonal_matrix22_eigvals(Hyy, Hxy, Hyy) + + +def corner_kitchen_rosenfeld(image, mode='constant', cval=0): """Compute Kitchen and Rosenfeld corner measure response image. The corner measure is calculated as follows:: (imxx * imy**2 + imyy * imx**2 - 2 * imxy * imx * imy) - ------------------------------------------------------ - (imx**2 + imy**2) + / (imx**2 + imy**2) - Where imx and imy are the first and imxx, imxy, imyy the second derivatives. + Where imx and imy are the first and imxx, imxy, imyy the second + derivatives. Parameters ---------- image : ndarray Input image. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. Returns ------- @@ -87,9 +319,9 @@ def corner_kitchen_rosenfeld(image): """ - imx, imy = _compute_derivatives(image) - imxx, imxy = _compute_derivatives(imx) - imyx, imyy = _compute_derivatives(imy) + imx, imy = _compute_derivatives(image, mode=mode, cval=cval) + imxx, imxy = _compute_derivatives(imx, mode=mode, cval=cval) + imyx, imyy = _compute_derivatives(imy, mode=mode, cval=cval) numerator = (imxx * imy**2 + imyy * imx**2 - 2 * imxy * imx * imy) denominator = (imx**2 + imy**2) @@ -110,7 +342,7 @@ def corner_harris(image, method='k', k=0.05, eps=1e-6, sigma=1): A = [(imx**2) (imx*imy)] = [Axx Axy] [(imx*imy) (imy**2)] [Axy Ayy] - Where imx and imy are the first derivatives averaged with a gaussian filter. + Where imx and imy are first derivatives, averaged with a gaussian filter. The corner measure is then defined as:: det(A) - k * trace(A)**2 @@ -149,17 +381,17 @@ def corner_harris(image, method='k', k=0.05, eps=1e-6, sigma=1): >>> from skimage.feature import corner_harris, corner_peaks >>> square = np.zeros([10, 10]) >>> square[2:8, 2:8] = 1 - >>> square - array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) >>> corner_peaks(corner_harris(square), min_distance=1) array([[2, 2], [2, 7], @@ -168,7 +400,7 @@ def corner_harris(image, method='k', k=0.05, eps=1e-6, sigma=1): """ - Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + Axx, Axy, Ayy = structure_tensor(image, sigma) # determinant detA = Axx * Ayy - Axy**2 @@ -191,7 +423,7 @@ def corner_shi_tomasi(image, sigma=1): A = [(imx**2) (imx*imy)] = [Axx Axy] [(imx*imy) (imy**2)] [Axy Ayy] - Where imx and imy are the first derivatives averaged with a gaussian filter. + Where imx and imy are first derivatives, averaged with a gaussian filter. The corner measure is then defined as the smaller eigenvalue of A:: ((Axx + Ayy) - sqrt((Axx - Ayy)**2 + 4 * Axy**2)) / 2 @@ -219,17 +451,17 @@ def corner_shi_tomasi(image, sigma=1): >>> from skimage.feature import corner_shi_tomasi, corner_peaks >>> square = np.zeros([10, 10]) >>> square[2:8, 2:8] = 1 - >>> square - array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) >>> corner_peaks(corner_shi_tomasi(square), min_distance=1) array([[2, 2], [2, 7], @@ -238,7 +470,7 @@ def corner_shi_tomasi(image, sigma=1): """ - Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + Axx, Axy, Ayy = structure_tensor(image, sigma) # minimum eigenvalue of A response = ((Axx + Ayy) - np.sqrt((Axx - Ayy)**2 + 4 * Axy**2)) / 2 @@ -254,7 +486,7 @@ def corner_foerstner(image, sigma=1): A = [(imx**2) (imx*imy)] = [Axx Axy] [(imx*imy) (imy**2)] [Axy Ayy] - Where imx and imy are the first derivatives averaged with a gaussian filter. + Where imx and imy are first derivatives, averaged with a gaussian filter. The corner measure is then defined as:: w = det(A) / trace(A) (size of error ellipse) @@ -285,17 +517,17 @@ def corner_foerstner(image, sigma=1): >>> from skimage.feature import corner_foerstner, corner_peaks >>> square = np.zeros([10, 10]) >>> square[2:8, 2:8] = 1 - >>> square - array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) >>> w, q = corner_foerstner(square) >>> accuracy_thresh = 0.5 >>> roundness_thresh = 0.3 @@ -308,7 +540,7 @@ def corner_foerstner(image, sigma=1): """ - Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + Axx, Axy, Ayy = structure_tensor(image, sigma) # determinant detA = Axx * Ayy - Axy**2 @@ -326,9 +558,79 @@ def corner_foerstner(image, sigma=1): return w, q +def corner_fast(image, n=12, threshold=0.15): + """Extract FAST corners for a given image. + + Parameters + ---------- + image : 2D ndarray + Input image. + n : int + Minimum number of consecutive pixels out of 16 pixels on the circle + that should all be either brighter or darker w.r.t testpixel. + A point c on the circle is darker w.r.t test pixel p if + `Ic < Ip - threshold` and brighter if `Ic > Ip + threshold`. Also + stands for the n in `FAST-n` corner detector. + threshold : float + Threshold used in deciding whether the pixels on the circle are + brighter, darker or similar w.r.t. the test pixel. Decrease the + threshold when more corners are desired and vice-versa. + + Returns + ------- + response : ndarray + FAST corner response image. + + References + ---------- + .. [1] Edward Rosten and Tom Drummond + "Machine Learning for high-speed corner detection", + http://www.edwardrosten.com/work/rosten_2006_machine.pdf + .. [2] Wikipedia, "Features from accelerated segment test", + https://en.wikipedia.org/wiki/Features_from_accelerated_segment_test + + Examples + -------- + >>> from skimage.feature import corner_fast, corner_peaks + >>> square = np.zeros((12, 12)) + >>> square[3:9, 3:9] = 1 + >>> square.astype(int) + 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, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 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]]) + >>> corner_peaks(corner_fast(square, 9), min_distance=1) + array([[3, 3], + [3, 8], + [8, 3], + [8, 8]]) + + """ + image = _prepare_grayscale_input_2D(image) + + image = np.ascontiguousarray(image) + response = _corner_fast(image, n, threshold) + return response + + def corner_subpix(image, corners, window_size=11, alpha=0.99): """Determine subpixel position of corners. + A statistical test decides whether the corner is defined as the + intersection of two edges or a single peak. Depending on the classification + result, the subpixel corner location is determined based on the local + covariance of the grey-values. If the significance level for either + statistical test is not sufficient, the corner cannot be classified, and + the output subpixel position is set to NaN. + Parameters ---------- image : ndarray @@ -338,7 +640,7 @@ def corner_subpix(image, corners, window_size=11, alpha=0.99): window_size : int, optional Search window size for subpixel estimation. alpha : float, optional - Significance level for point classification. + Significance level for corner classification. Returns ------- @@ -351,10 +653,37 @@ def corner_subpix(image, corners, window_size=11, alpha=0.99): foerstner87.fast.pdf .. [2] http://en.wikipedia.org/wiki/Corner_detection + Examples + -------- + >>> from skimage.feature import corner_harris, corner_peaks, corner_subpix + >>> img = np.zeros((10, 10)) + >>> img[:5, :5] = 1 + >>> img[5:, 5:] = 1 + >>> img.astype(int) + array([[1, 1, 1, 1, 1, 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], + [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 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, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]]) + >>> coords = corner_peaks(corner_harris(img), min_distance=2) + >>> coords_subpix = corner_subpix(img, coords, window_size=7) + >>> coords_subpix + array([[ 4.5, 4.5]]) + """ # window extent in one direction - wext = (window_size - 1) / 2 + wext = (window_size - 1) // 2 + + image = pad(image, pad_width=wext, mode='constant', constant_values=0) + + # add pad width, make sure to not modify the input values in-place + corners = safe_as_int(corners + wext) # normal equation arrays N_dot = np.zeros((2, 2), dtype=np.double) @@ -381,7 +710,7 @@ def corner_subpix(image, corners, window_size=11, alpha=0.99): maxx = x0 + wext + 2 window = image[miny:maxy, minx:maxx] - winx, winy = _compute_derivatives(window) + winx, winy = _compute_derivatives(window, mode='constant', cval=0) # compute gradient suares and remove border winx_winx = (winx * winx)[1:-1, 1:-1] @@ -415,8 +744,13 @@ def corner_subpix(image, corners, window_size=11, alpha=0.99): b_edge[:] = byy_y + bxy_x, bxx_x + bxy_y # estimated positions - est_dot = np.linalg.solve(N_dot, b_dot) - est_edge = np.linalg.solve(N_edge, b_edge) + try: + est_dot = np.linalg.solve(N_dot, b_dot) + est_edge = np.linalg.solve(N_edge, b_edge) + except np.linalg.LinAlgError: + # if image is constant the system is singular + corners_subpix[i, :] = np.nan, np.nan + continue # residuals ry_dot = y - est_dot[0] @@ -433,22 +767,32 @@ def corner_subpix(image, corners, window_size=11, alpha=0.99): # determine corner class (dot or edge) # variance for different models - var_dot = np.sum(winx_winx * ryy_dot - 2 * winx_winy * rxy_dot \ + var_dot = np.sum(winx_winx * ryy_dot - 2 * winx_winy * rxy_dot + winy_winy * rxx_dot) - var_edge = np.sum(winy_winy * ryy_edge + 2 * winx_winy * rxy_edge \ + var_edge = np.sum(winy_winy * ryy_edge + 2 * winx_winy * rxy_edge + winx_winx * rxx_edge) - # test value (F-distributed) - t = var_edge / var_dot - # 1 for edge, -1 for dot, 0 for "not classified" - corner_class = (t < t_crit_edge) - (t > t_crit_dot) - if corner_class == - 1: + # test value (F-distributed) + if var_dot < np.spacing(1) and var_edge < np.spacing(1): + t = np.nan + elif var_dot == 0: + t = np.inf + else: + t = var_edge / var_dot + + # 1 for edge, -1 for dot, 0 for "not classified" + corner_class = int(t < t_crit_edge) - int(t > t_crit_dot) + + if corner_class == -1: corners_subpix[i, :] = y0 + est_dot[0], x0 + est_dot[1] elif corner_class == 0: corners_subpix[i, :] = np.nan, np.nan elif corner_class == 1: corners_subpix[i, :] = y0 + est_edge[0], x0 + est_edge[1] + # subtract pad width + corners_subpix -= wext + return corners_subpix @@ -462,15 +806,12 @@ def corner_peaks(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, Parameters ---------- - See `skimage.feature.peak_local_max`. - - Returns - ------- - See `skimage.feature.peak_local_max`. + * : * + See :py:meth:`skimage.feature.peak_local_max`. Examples -------- - >>> from skimage.feature import peak_local_max, corner_peaks + >>> from skimage.feature import peak_local_max >>> response = np.zeros((5, 5)) >>> response[2:4, 2:4] = 1 >>> response diff --git a/skimage/feature/corner_cy.pyx b/skimage/feature/corner_cy.pyx index 7d558e52..86ad3c43 100644 --- a/skimage/feature/corner_cy.pyx +++ b/skimage/feature/corner_cy.pyx @@ -5,9 +5,12 @@ import numpy as np cimport numpy as cnp from libc.float cimport DBL_MAX +from libc.math cimport atan2 +from skimage.util import img_as_float, pad from skimage.color import rgb2grey -from skimage.util import img_as_float + +from .util import _prepare_grayscale_input_2D def corner_moravec(image, Py_ssize_t window_size=1): @@ -30,30 +33,30 @@ def corner_moravec(image, Py_ssize_t window_size=1): References ---------- - ..[1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/moravec.htm - ..[2] http://en.wikipedia.org/wiki/Corner_detection + .. [1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/moravec.htm + .. [2] http://en.wikipedia.org/wiki/Corner_detection Examples -------- - >>> from skimage.feature import moravec, peak_local_max + >>> from skimage.feature import corner_moravec >>> square = np.zeros([7, 7]) >>> square[3, 3] = 1 - >>> square - array([[ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 1., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.]]) - >>> moravec(square) - array([[ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 2., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.]]) + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]]) + >>> corner_moravec(square).astype(int) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 2, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]]) """ cdef Py_ssize_t rows = image.shape[0] @@ -80,3 +83,193 @@ def corner_moravec(image, Py_ssize_t window_size=1): out[r, c] = min_msum return np.asarray(out) + + +cdef inline double _corner_fast_response(double curr_pixel, + double* circle_intensities, + char* bins, char state, char n): + cdef char consecutive_count = 0 + cdef double curr_response + cdef Py_ssize_t l, m + for l in range(15 + n): + if bins[l % 16] == state: + consecutive_count += 1 + if consecutive_count == n: + curr_response = 0 + for m in range(16): + curr_response += abs(circle_intensities[m] - curr_pixel) + return curr_response + else: + consecutive_count = 0 + return 0 + + +def _corner_fast(double[:, ::1] image, char n, double threshold): + + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + + cdef Py_ssize_t i, j, k + + cdef char speed_sum_b, speed_sum_d + cdef double curr_pixel + cdef double lower_threshold, upper_threshold + cdef double[:, ::1] corner_response = np.zeros((rows, cols), + dtype=np.double) + + cdef char *rp = [0, 1, 2, 3, 3, 3, 2, 1, 0, -1, -2, -3, -3, -3, -2, -1] + cdef char *cp = [3, 3, 2, 1, 0, -1, -2, -3, -3, -3, -2, -1, 0, 1, 2, 3] + cdef char bins[16] + cdef double circle_intensities[16] + + cdef double curr_response + + for i in range(3, rows - 3): + for j in range(3, cols - 3): + + curr_pixel = image[i, j] + lower_threshold = curr_pixel - threshold + upper_threshold = curr_pixel + threshold + + for k in range(16): + circle_intensities[k] = image[i + rp[k], j + cp[k]] + if circle_intensities[k] > upper_threshold: + # Brighter pixel + bins[k] = 'b' + elif circle_intensities[k] < lower_threshold: + # Darker pixel + bins[k] = 'd' + else: + # Similar pixel + bins[k] = 's' + + # High speed test for n >= 12 + if n >= 12: + speed_sum_b = 0 + speed_sum_d = 0 + for k in range(0, 16, 4): + if bins[k] == 'b': + speed_sum_b += 1 + elif bins[k] == 'd': + speed_sum_d += 1 + if speed_sum_d < 3 and speed_sum_b < 3: + continue + + # Test for bright pixels + curr_response = \ + _corner_fast_response(curr_pixel, circle_intensities, + bins, 'b', n) + + # Test for dark pixels + if curr_response == 0: + curr_response = \ + _corner_fast_response(curr_pixel, circle_intensities, + bins, 'd', n) + + corner_response[i, j] = curr_response + + return np.asarray(corner_response) + + +def corner_orientations(image, Py_ssize_t[:, :] corners, mask): + """Compute the orientation of corners. + + The orientation of corners is computed using the first order central moment + i.e. the center of mass approach. The corner orientation is the angle of + the vector from the corner coordinate to the intensity centroid in the + local neighborhood around the corner calculated using first order central + moment. + + Parameters + ---------- + image : 2D array + Input grayscale image. + corners : (N, 2) array + Corner coordinates as ``(row, col)``. + mask : 2D array + Mask defining the local neighborhood of the corner used for the + calculation of the central moment. + + Returns + ------- + orientations : (N, 1) array + Orientations of corners in the range [-pi, pi]. + + References + ---------- + .. [1] Ethan Rublee, Vincent Rabaud, Kurt Konolige and Gary Bradski + "ORB : An efficient alternative to SIFT and SURF" + http://www.vision.cs.chubu.ac.jp/CV-R/pdf/Rublee_iccv2011.pdf + .. [2] Paul L. Rosin, "Measuring Corner Properties" + http://users.cs.cf.ac.uk/Paul.Rosin/corner2.pdf + + Examples + -------- + >>> from skimage.morphology import octagon + >>> from skimage.feature import (corner_fast, corner_peaks, + ... corner_orientations) + >>> square = np.zeros((12, 12)) + >>> square[3:9, 3:9] = 1 + >>> square.astype(int) + 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, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 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]]) + >>> corners = corner_peaks(corner_fast(square, 9), min_distance=1) + >>> corners + array([[3, 3], + [3, 8], + [8, 3], + [8, 8]]) + >>> orientations = corner_orientations(square, corners, octagon(3, 2)) + >>> np.rad2deg(orientations) + array([ 45., 135., -45., -135.]) + + """ + + image = _prepare_grayscale_input_2D(image) + + if mask.shape[0] % 2 != 1 or mask.shape[1] % 2 != 1: + raise ValueError("Size of mask must be uneven.") + + cdef unsigned char[:, ::1] cmask = np.ascontiguousarray(mask != 0, + dtype=np.uint8) + + cdef Py_ssize_t i, r, c, r0, c0 + cdef Py_ssize_t mrows = mask.shape[0] + cdef Py_ssize_t mcols = mask.shape[1] + cdef Py_ssize_t mrows2 = (mrows - 1) / 2 + cdef Py_ssize_t mcols2 = (mcols - 1) / 2 + cdef double[:, :] cimage = pad(image, (mrows2, mcols2), mode='constant', + constant_values=0) + cdef double[:] orientations = np.zeros(corners.shape[0], dtype=np.double) + cdef double curr_pixel + cdef double m01, m10, m01_tmp + + for i in range(corners.shape[0]): + r0 = corners[i, 0] + c0 = corners[i, 1] + + m01 = 0 + m10 = 0 + + for r in range(mrows): + m01_tmp = 0 + for c in range(mcols): + if cmask[r, c]: + curr_pixel = cimage[r0 + r, c0 + c] + m10 += curr_pixel * (c - mcols2) + m01_tmp += curr_pixel + m01 += m01_tmp * (r - mrows2) + + orientations[i] = atan2(m01, m10) + + return np.asarray(orientations) diff --git a/skimage/feature/match.py b/skimage/feature/match.py new file mode 100644 index 00000000..5cb1a5ed --- /dev/null +++ b/skimage/feature/match.py @@ -0,0 +1,70 @@ +import numpy as np +from scipy.spatial.distance import cdist + + +def match_descriptors(descriptors1, descriptors2, metric=None, p=2, + max_distance=np.inf, cross_check=True): + """Brute-force matching of descriptors. + + For each descriptor in the first set this matcher finds the closest + descriptor in the second set (and vice-versa in the case of enabled + cross-checking). + + Parameters + ---------- + descriptors1 : (M, P) array + Binary descriptors of size P about M keypoints in the first image. + descriptors2 : (N, P) array + Binary descriptors of size P about N keypoints in the second image. + metric : {'euclidean', 'cityblock', 'minkowski', 'hamming', ...} + The metric to compute the distance between two descriptors. See + `scipy.spatial.distance.cdist` for all possible types. The hamming + distance should be used for binary descriptors. By default the L2-norm + is used for all descriptors of dtype float or double and the Hamming + distance is used for binary descriptors automatically. + p : int + The p-norm to apply for ``metric='minkowski'``. + max_distance : float + Maximum allowed distance between descriptors of two keypoints + in separate images to be regarded as a match. + cross_check : bool + If True, the matched keypoints are returned after cross checking i.e. a + matched pair (keypoint1, keypoint2) is returned if keypoint2 is the + best match for keypoint1 in second image and keypoint1 is the best + match for keypoint2 in first image. + + Returns + ------- + matches : (Q, 2) array + Indices of corresponding matches in first and second set of + descriptors, where ``matches[:, 0]`` denote the indices in the first + and ``matches[:, 1]`` the indices in the second set of descriptors. + + """ + + if descriptors1.shape[1] != descriptors2.shape[1]: + raise ValueError("Descriptor length must equal.") + + if metric is None: + if np.issubdtype(descriptors1.dtype, np.bool): + metric = 'hamming' + else: + metric = 'euclidean' + + distances = cdist(descriptors1, descriptors2, metric=metric, p=p) + + indices1 = np.arange(descriptors1.shape[0]) + indices2 = np.argmin(distances, axis=1) + + if cross_check: + matches1 = np.argmin(distances, axis=0) + mask = indices1 == matches1[indices2] + indices1 = indices1[mask] + indices2 = indices2[mask] + + matches = np.column_stack((indices1, indices2)) + + if max_distance < np.inf: + matches = matches[distances[indices1, indices2] < max_distance] + + return matches diff --git a/skimage/feature/orb.py b/skimage/feature/orb.py new file mode 100644 index 00000000..2ddcf4f3 --- /dev/null +++ b/skimage/feature/orb.py @@ -0,0 +1,336 @@ +import numpy as np + +from skimage.feature.util import (FeatureDetector, DescriptorExtractor, + _mask_border_keypoints, + _prepare_grayscale_input_2D) + +from skimage.feature import (corner_fast, corner_orientations, corner_peaks, + corner_harris) +from skimage.transform import pyramid_gaussian + +from .orb_cy import _orb_loop + + +OFAST_MASK = np.zeros((31, 31)) +OFAST_UMAX = [15, 15, 15, 15, 14, 14, 14, 13, 13, 12, 11, 10, 9, 8, 6, 3] +for i in range(-15, 16): + for j in range(-OFAST_UMAX[abs(i)], OFAST_UMAX[abs(i)] + 1): + OFAST_MASK[15 + j, 15 + i] = 1 + + +class ORB(FeatureDetector, DescriptorExtractor): + + """Oriented FAST and rotated BRIEF feature detector and binary descriptor + extractor. + + Parameters + ---------- + n_keypoints : int, optional + Number of keypoints to be returned. The function will return the best + `n_keypoints` according to the Harris corner response if more than + `n_keypoints` are detected. If not, then all the detected keypoints + are returned. + fast_n : int, optional + The `n` parameter in `skimage.feature.corner_fast`. Minimum number of + consecutive pixels out of 16 pixels on the circle that should all be + either brighter or darker w.r.t test-pixel. A point c on the circle is + darker w.r.t test pixel p if ``Ic < Ip - threshold`` and brighter if + ``Ic > Ip + threshold``. Also stands for the n in ``FAST-n`` corner + detector. + fast_threshold : float, optional + The ``threshold`` parameter in ``feature.corner_fast``. Threshold used + to decide whether the pixels on the circle are brighter, darker or + similar w.r.t. the test pixel. Decrease the threshold when more + corners are desired and vice-versa. + harris_k : float, optional + The `k` parameter in `skimage.feature.corner_harris`. Sensitivity + factor to separate corners from edges, typically in range ``[0, 0.2]``. + Small values of `k` result in detection of sharp corners. + downscale : float, optional + Downscale factor for the image pyramid. Default value 1.2 is chosen so + that there are more dense scales which enable robust scale invariance + for a subsequent feature description. + n_scales : int, optional + Maximum number of scales from the bottom of the image pyramid to + extract the features from. + + Attributes + ---------- + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + scales : (N, ) array + Corresponding scales. + orientations : (N, ) array + Corresponding orientations in radians. + responses : (N, ) array + Corresponding Harris corner responses. + descriptors : (Q, `descriptor_size`) array of dtype bool + 2D array of binary descriptors of size `descriptor_size` for Q + keypoints after filtering out border keypoints with value at an + index ``(i, j)`` either being ``True`` or ``False`` representing + the outcome of the intensity comparison for i-th keypoint on j-th + decision pixel-pair. It is ``Q == np.sum(mask)``. + + References + ---------- + .. [1] Ethan Rublee, Vincent Rabaud, Kurt Konolige and Gary Bradski + "ORB: An efficient alternative to SIFT and SURF" + http://www.vision.cs.chubu.ac.jp/CV-R/pdf/Rublee_iccv2011.pdf + + Examples + -------- + >>> from skimage.feature import ORB, match_descriptors + >>> img1 = np.zeros((100, 100)) + >>> img2 = np.zeros_like(img1) + >>> np.random.seed(1) + >>> square = np.random.rand(20, 20) + >>> img1[40:60, 40:60] = square + >>> img2[53:73, 53:73] = square + >>> detector_extractor1 = ORB(n_keypoints=5) + >>> detector_extractor2 = ORB(n_keypoints=5) + >>> detector_extractor1.detect_and_extract(img1) + >>> detector_extractor2.detect_and_extract(img2) + >>> matches = match_descriptors(detector_extractor1.descriptors, + ... detector_extractor2.descriptors) + >>> matches + array([[0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4]]) + >>> detector_extractor1.keypoints[matches[:, 0]] + array([[ 42., 40.], + [ 47., 58.], + [ 44., 40.], + [ 59., 42.], + [ 45., 44.]]) + >>> detector_extractor2.keypoints[matches[:, 1]] + array([[ 55., 53.], + [ 60., 71.], + [ 57., 53.], + [ 72., 55.], + [ 58., 57.]]) + + """ + + def __init__(self, downscale=1.2, n_scales=8, + n_keypoints=500, fast_n=9, fast_threshold=0.08, + harris_k=0.04): + self.downscale = downscale + self.n_scales = n_scales + self.n_keypoints = n_keypoints + self.fast_n = fast_n + self.fast_threshold = fast_threshold + self.harris_k = harris_k + + self.keypoints = None + self.scales = None + self.responses = None + self.orientations = None + self.descriptors = None + + def _build_pyramid(self, image): + image = _prepare_grayscale_input_2D(image) + return list(pyramid_gaussian(image, self.n_scales - 1, self.downscale)) + + def _detect_octave(self, octave_image): + # Extract keypoints for current octave + fast_response = corner_fast(octave_image, self.fast_n, + self.fast_threshold) + keypoints = corner_peaks(fast_response, min_distance=1) + + if len(keypoints) == 0: + return (np.zeros((0, 2), dtype=np.double), + np.zeros((0, ), dtype=np.double), + np.zeros((0, ), dtype=np.double)) + + mask = _mask_border_keypoints(octave_image.shape, keypoints, + distance=16) + keypoints = keypoints[mask] + + orientations = corner_orientations(octave_image, keypoints, + OFAST_MASK) + + harris_response = corner_harris(octave_image, method='k', + k=self.harris_k) + responses = harris_response[keypoints[:, 0], keypoints[:, 1]] + + return keypoints, orientations, responses + + def detect(self, image): + """Detect oriented FAST keypoints along with the corresponding scale. + + Parameters + ---------- + image : 2D array + Input image. + + """ + + pyramid = self._build_pyramid(image) + + keypoints_list = [] + orientations_list = [] + scales_list = [] + responses_list = [] + + for octave in range(len(pyramid)): + + octave_image = np.ascontiguousarray(pyramid[octave]) + + keypoints, orientations, responses = \ + self._detect_octave(octave_image) + + keypoints_list.append(keypoints * self.downscale ** octave) + orientations_list.append(orientations) + scales_list.append(self.downscale ** octave + * np.ones(keypoints.shape[0], dtype=np.intp)) + responses_list.append(responses) + + keypoints = np.vstack(keypoints_list) + orientations = np.hstack(orientations_list) + scales = np.hstack(scales_list) + responses = np.hstack(responses_list) + + if keypoints.shape[0] < self.n_keypoints: + self.keypoints = keypoints + self.scales = scales + self.orientations = orientations + self.responses = responses + else: + # Choose best n_keypoints according to Harris corner response + best_indices = responses.argsort()[::-1][:self.n_keypoints] + self.keypoints = keypoints[best_indices] + self.scales = scales[best_indices] + self.orientations = orientations[best_indices] + self.responses = responses[best_indices] + + def _extract_octave(self, octave_image, keypoints, orientations): + mask = _mask_border_keypoints(octave_image.shape, keypoints, + distance=20) + keypoints = np.array(keypoints[mask], dtype=np.intp, order='C', + copy=False) + orientations = np.array(orientations[mask], dtype=np.double, order='C', + copy=False) + + descriptors = _orb_loop(octave_image, keypoints, orientations) + + return descriptors, mask + + def extract(self, image, keypoints, scales, orientations): + """Extract rBRIEF binary descriptors for given keypoints in image. + + Note that the keypoints must be extracted using the same `downscale` + and `n_scales` parameters. Additionally, if you want to extract both + keypoints and descriptors you should use the faster + `detect_and_extract`. + + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + scales : (N, ) array + Corresponding scales. + orientations : (N, ) array + Corresponding orientations in radians. + + """ + + pyramid = self._build_pyramid(image) + + descriptors_list = [] + mask_list = [] + + # Determine octaves from scales + octaves = (np.log(scales) / np.log(self.downscale)).astype(np.intp) + + for octave in range(len(pyramid)): + + # Mask for all keypoints in current octave + octave_mask = octaves == octave + + if np.sum(octave_mask) > 0: + + octave_image = np.ascontiguousarray(pyramid[octave]) + + octave_keypoints = keypoints[octave_mask] + octave_keypoints /= self.downscale ** octave + + octave_orientations = orientations[octave_mask] + + descriptors, mask = self._extract_octave(octave_image, + octave_keypoints, + octave_orientations) + + descriptors_list.append(descriptors) + mask_list.append(mask) + + self.descriptors = np.vstack(descriptors_list).view(np.bool) + self.mask_ = np.hstack(mask_list) + + def detect_and_extract(self, image): + """Detect oriented FAST keypoints and extract rBRIEF descriptors. + + Note that this is faster than first calling `detect` and then + `extract`. + + Parameters + ---------- + image : 2D array + Input image. + + """ + + pyramid = self._build_pyramid(image) + + keypoints_list = [] + responses_list = [] + scales_list = [] + orientations_list = [] + descriptors_list = [] + + for octave in range(len(pyramid)): + + octave_image = np.ascontiguousarray(pyramid[octave]) + + keypoints, orientations, responses = \ + self._detect_octave(octave_image) + + if len(keypoints) == 0: + keypoints_list.append(keypoints) + responses_list.append(responses) + descriptors_list.append(np.zeros((0, 256), dtype=np.bool)) + continue + + descriptors, mask = self._extract_octave(octave_image, keypoints, + orientations) + + keypoints_list.append(keypoints[mask] * self.downscale ** octave) + responses_list.append(responses[mask]) + orientations_list.append(orientations[mask]) + scales_list.append(self.downscale ** octave + * np.ones(keypoints.shape[0], dtype=np.intp)) + descriptors_list.append(descriptors) + + keypoints = np.vstack(keypoints_list) + responses = np.hstack(responses_list) + scales = np.hstack(scales_list) + orientations = np.hstack(orientations_list) + descriptors = np.vstack(descriptors_list).view(np.bool) + + if keypoints.shape[0] < self.n_keypoints: + self.keypoints = keypoints + self.scales = scales + self.orientations = orientations + self.responses = responses + self.descriptors = descriptors + else: + # Choose best n_keypoints according to Harris corner response + best_indices = responses.argsort()[::-1][:self.n_keypoints] + self.keypoints = keypoints[best_indices] + self.scales = scales[best_indices] + self.orientations = orientations[best_indices] + self.responses = responses[best_indices] + self.descriptors = descriptors[best_indices] diff --git a/skimage/feature/orb_cy.pyx b/skimage/feature/orb_cy.pyx new file mode 100644 index 00000000..6e0801f8 --- /dev/null +++ b/skimage/feature/orb_cy.pyx @@ -0,0 +1,56 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +import os +import numpy as np + +from skimage import data_dir + +cimport numpy as cnp +from libc.math cimport sin, cos + +from skimage._shared.interpolation cimport round + +POS = np.loadtxt(os.path.join(data_dir, "orb_descriptor_positions.txt"), + dtype=np.int8) +POS0 = np.ascontiguousarray(POS[:, :2]) +POS1 = np.ascontiguousarray(POS[:, 2:]) + + +def _orb_loop(double[:, ::1] image, Py_ssize_t[:, ::1] keypoints, + double[:] orientations): + + cdef Py_ssize_t i, d, kr, kc, pr0, pr1, pc0, pc1, spr0, spc0, spr1, spc1 + cdef int[:, ::1] steered_pos0, steered_pos1 + cdef double angle + cdef char[:, ::1] descriptors = np.zeros((keypoints.shape[0], + POS.shape[0]), dtype=np.uint8) + cdef char[:, ::1] cpos0 = POS0 + cdef char[:, ::1] cpos1 = POS1 + + for i in range(descriptors.shape[0]): + + angle = orientations[i] + sin_a = sin(angle) + cos_a = cos(angle) + + kr = keypoints[i, 0] + kc = keypoints[i, 1] + + for j in range(descriptors.shape[1]): + pr0 = cpos0[j, 0] + pc0 = cpos0[j, 1] + pr1 = cpos1[j, 0] + pc1 = cpos1[j, 1] + + spr0 = round(sin_a * pr0 + cos_a * pc0) + spc0 = round(cos_a * pr0 - sin_a * pc0) + spr1 = round(sin_a * pr1 + cos_a * pc1) + spc1 = round(cos_a * pr1 - sin_a * pc1) + + if image[kr + spr0, kc + spc0] < image[kr + spr1, kc + spc1]: + descriptors[i, j] = True + + return np.asarray(descriptors) diff --git a/skimage/feature/peak.py b/skimage/feature/peak.py index 3268831a..378d22b4 100644 --- a/skimage/feature/peak.py +++ b/skimage/feature/peak.py @@ -49,9 +49,9 @@ def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, Returns ------- - output : (N, 2) array or ndarray of bools + output : ndarray or ndarray of bools - * If `indices = True` : (row, column) coordinates of peaks. + * If `indices = True` : (row, column, ...) coordinates of peaks. * If `indices = False` : Boolean array shaped like `image`, with peaks represented by True values. @@ -65,10 +65,10 @@ def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, Examples -------- - >>> im = np.zeros((7, 7)) - >>> im[3, 4] = 1 - >>> im[3, 2] = 1.5 - >>> im + >>> img1 = np.zeros((7, 7)) + >>> img1[3, 4] = 1 + >>> img1[3, 2] = 1.5 + >>> img1 array([[ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], @@ -77,13 +77,18 @@ def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ]]) - >>> peak_local_max(im, min_distance=1) + >>> peak_local_max(img1, min_distance=1) array([[3, 2], [3, 4]]) - >>> peak_local_max(im, min_distance=2) + >>> peak_local_max(img1, min_distance=2) array([[3, 2]]) + >>> img2 = np.zeros((20, 20, 20)) + >>> img2[10, 10, 10] = 1 + >>> peak_local_max(img2, exclude_border=False) + array([[10, 10, 10]]) + """ out = np.zeros_like(image, dtype=np.bool) # In the case of labels, recursively build and return an output diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index 7df64c32..d7be3dcf 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -14,20 +14,23 @@ def configuration(parent_package='', top_path=None): 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(['orb_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) + cython(['_hessian_det_appx.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'], + config.add_extension('orb_cy', sources=['orb_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'], - include_dirs=[get_numpy_include_dirs(), '../_shared']) + config.add_extension('_hessian_det_appx', sources=['_hessian_det_appx.c'], + include_dirs=[get_numpy_include_dirs()]) return config diff --git a/skimage/feature/template.py b/skimage/feature/template.py index 5cb4d382..fbf95866 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -1,81 +1,171 @@ -"""template.py - Template matching -""" import numpy as np -from . import _template +from scipy.signal import fftconvolve + +from skimage.util import pad -def match_template(image, template, pad_input=False): - """Match a template to an image using normalized correlation. +def _window_sum_2d(image, window_shape): - The output is an array with values between -1.0 and 1.0, which correspond - to the probability that the template is found at that position. + window_sum = np.cumsum(image, axis=0) + window_sum = (window_sum[window_shape[0]:-1] + - window_sum[:-window_shape[0]-1]) + + window_sum = np.cumsum(window_sum, axis=1) + window_sum = (window_sum[:, window_shape[1]:-1] + - window_sum[:, :-window_shape[1]-1]) + + return window_sum + + +def _window_sum_3d(image, window_shape): + + window_sum = _window_sum_2d(image, window_shape) + + window_sum = np.cumsum(window_sum, axis=2) + window_sum = (window_sum[:, :, window_shape[2]:-1] + - window_sum[:, :, :-window_shape[2]-1]) + + return window_sum + + +def match_template(image, template, pad_input=False, mode='constant', + constant_values=0): + """Match a template to a 2-D or 3-D image using normalized correlation. + + The output is an array with values between -1.0 and 1.0. The value at a + given position corresponds to the correlation coefficient between the image + and the template. + + For `pad_input=True` matches correspond to the center and otherwise to the + top-left corner of the template. To find the best match you must search for + peaks in the response (output) image. Parameters ---------- - image : array_like - Image to process. - template : array_like - Template to locate. + image : (M, N[, D]) array + 2-D or 3-D input image. + template : (m, n[, d]) array + Template to locate. It must be `(m <= M, n <= N[, d <= D])`. pad_input : bool - If True, pad `image` with image mean so that output is the same size as - the image, and output values correspond to the template center. - Otherwise, the output is an array with shape `(M - m + 1, N - n + 1)` - for an `(M, N)` image and an `(m, n)` template, and matches correspond - to origin (top-left corner) of the template. + If True, pad `image` so that output is the same size as the image, and + output values correspond to the template center. Otherwise, the output + is an array with shape `(M - m + 1, N - n + 1)` for an `(M, N)` image + and an `(m, n)` template, and matches correspond to origin + (top-left corner) of the template. + mode : see `numpy.pad`, optional + Padding mode. + constant_values : see `numpy.pad`, optional + Constant values used in conjunction with ``mode='constant'``. Returns ------- - output : ndarray - Correlation results between -1.0 and 1.0. For an `(M, N)` image and an - `(m, n)` template, the `output` is `(M - m + 1, N - n + 1)` when - `pad_input = False` and `(M, N)` when `pad_input = True`. + output : array + Response image with correlation coefficients. + + References + ---------- + .. [1] Briechle and Hanebeck, "Template Matching using Fast Normalized + Cross Correlation", Proceedings of the SPIE (2001). + .. [2] J. P. Lewis, "Fast Normalized Cross-Correlation", Industrial Light + and Magic. Examples -------- >>> template = np.zeros((3, 3)) >>> template[1, 1] = 1 - >>> print(template) - [[ 0. 0. 0.] - [ 0. 1. 0.] - [ 0. 0. 0.]] + >>> template + array([[ 0., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 0.]]) >>> image = np.zeros((6, 6)) >>> image[1, 1] = 1 >>> image[4, 4] = -1 - >>> print(image) - [[ 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. -1. 0.] - [ 0. 0. 0. 0. 0. 0.]] + >>> image + array([[ 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., -1., 0.], + [ 0., 0., 0., 0., 0., 0.]]) >>> result = match_template(image, template) - >>> 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. ]] + >>> np.round(result, 3) + array([[ 1. , -0.125, 0. , 0. ], + [-0.125, -0.125, 0. , 0. ], + [ 0. , 0. , 0.125, 0.125], + [ 0. , 0. , 0.125, -1. ]], dtype=float32) >>> result = match_template(image, template, pad_input=True) - >>> 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. ] - [ 0. 0. 0. 0.125 0.125 0.125] - [ 0. 0. 0. 0.125 -1. 0.125] - [ 0. 0. 0. 0.125 0.125 0.125]] + >>> np.round(result, 3) + array([[-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. ], + [ 0. , 0. , 0. , 0.125, 0.125, 0.125], + [ 0. , 0. , 0. , 0.125, -1. , 0.125], + [ 0. , 0. , 0. , 0.125, 0.125, 0.125]], dtype=float32) """ + + if image.ndim not in (2, 3) or template.ndim not in (2, 3): + raise ValueError("Only 2- and 3-D images supported.") + if image.ndim < template.ndim: + raise ValueError("Dimensionality of template must be less than or " + "equal to the dimensionality of image.") if np.any(np.less(image.shape, template.shape)): raise ValueError("Image must be larger than template.") - image = np.ascontiguousarray(image, dtype=np.float32) - template = np.ascontiguousarray(template, dtype=np.float32) - if pad_input: - pad_size = tuple(np.array(image.shape) + np.array(template.shape) - 1) - pad_image = np.mean(image) * np.ones(pad_size, dtype=np.float32) - h, w = image.shape - i0, j0 = template.shape - i0 /= 2 - j0 /= 2 - pad_image[i0:i0 + h, j0:j0 + w] = image - image = pad_image - result = _template.match_template(image, template) - return result + image_shape = image.shape + + image = np.array(image, dtype=np.float32, copy=False) + + pad_width = tuple((width, width) for width in template.shape) + if mode == 'constant': + image = pad(image, pad_width=pad_width, mode=mode, + constant_values=constant_values) + else: + image = pad(image, pad_width=pad_width, mode=mode) + + # Use special case for 2-D images for much better performance in + # computation of integral images + if image.ndim == 2: + image_window_sum = _window_sum_2d(image, template.shape) + image_window_sum2 = _window_sum_2d(image**2, template.shape) + elif image.ndim == 3: + image_window_sum = _window_sum_3d(image, template.shape) + image_window_sum2 = _window_sum_3d(image**2, template.shape) + + template_volume = np.prod(template.shape) + template_ssd = np.sum((template - template.mean())**2) + + if image.ndim == 2: + xcorr = fftconvolve(image, template[::-1, ::-1], + mode="valid")[1:-1, 1:-1] + elif image.ndim == 3: + xcorr = fftconvolve(image, template[::-1, ::-1, ::-1], + mode="valid")[1:-1, 1:-1, 1:-1] + + nom = xcorr - image_window_sum * (template.sum() / template_volume) + + denom = image_window_sum2 + np.multiply(image_window_sum, image_window_sum, out=image_window_sum) + np.divide(image_window_sum, template_volume, out=image_window_sum) + denom -= image_window_sum + denom *= template_ssd + np.maximum(denom, 0, out=denom) # sqrt of negative number not allowed + np.sqrt(denom, out=denom) + + response = np.zeros_like(xcorr, dtype=np.float32) + + # avoid zero-division + mask = denom > np.finfo(np.float32).eps + + response[mask] = nom[mask] / denom[mask] + + slices = [] + for i in range(template.ndim): + if pad_input: + d0 = (template.shape[i] - 1) // 2 + d1 = d0 + image_shape[i] + else: + d0 = template.shape[i] - 1 + d1 = d0 + image_shape[i] - template.shape[i] + 1 + slices.append(slice(d0, d1)) + + return response[slices] diff --git a/skimage/feature/tests/_test_brief.py b/skimage/feature/tests/_test_brief.py deleted file mode 100644 index 1d26cbbd..00000000 --- a/skimage/feature/tests/_test_brief.py +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index 4cd2ad68..00000000 --- a/skimage/feature/tests/_test_censure.py +++ /dev/null @@ -1,89 +0,0 @@ -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_blob.py b/skimage/feature/tests/test_blob.py new file mode 100644 index 00000000..a46c442b --- /dev/null +++ b/skimage/feature/tests/test_blob.py @@ -0,0 +1,214 @@ +import numpy as np +from skimage.draw import circle +from skimage.feature import blob_dog, blob_log, blob_doh +import math +from numpy.testing import assert_raises + + +def test_blob_dog(): + r2 = math.sqrt(2) + img = np.ones((512, 512)) + img3 = np.ones((5, 5, 5)) + + xs, ys = circle(400, 130, 5) + img[xs, ys] = 255 + + xs, ys = circle(100, 300, 25) + img[xs, ys] = 255 + + xs, ys = circle(200, 350, 45) + img[xs, ys] = 255 + + blobs = blob_dog(img, min_sigma=5, max_sigma=50) + radius = lambda x: r2 * x[2] + s = sorted(blobs, key=radius) + thresh = 5 + + b = s[0] + assert abs(b[0] - 400) <= thresh + assert abs(b[1] - 130) <= thresh + assert abs(radius(b) - 5) <= thresh + + b = s[1] + assert abs(b[0] - 100) <= thresh + assert abs(b[1] - 300) <= thresh + assert abs(radius(b) - 25) <= thresh + + b = s[2] + assert abs(b[0] - 200) <= thresh + assert abs(b[1] - 350) <= thresh + assert abs(radius(b) - 45) <= thresh + + assert_raises(ValueError, blob_dog, img3) + + +def test_blob_log(): + r2 = math.sqrt(2) + img = np.ones((512, 512)) + img3 = np.ones((5, 5, 5)) + + xs, ys = circle(400, 130, 5) + img[xs, ys] = 255 + + xs, ys = circle(160, 50, 15) + img[xs, ys] = 255 + + xs, ys = circle(100, 300, 25) + img[xs, ys] = 255 + + xs, ys = circle(200, 350, 30) + img[xs, ys] = 255 + + blobs = blob_log(img, min_sigma=5, max_sigma=20, threshold=1) + + radius = lambda x: r2 * x[2] + s = sorted(blobs, key=radius) + thresh = 3 + + b = s[0] + assert abs(b[0] - 400) <= thresh + assert abs(b[1] - 130) <= thresh + assert abs(radius(b) - 5) <= thresh + + b = s[1] + assert abs(b[0] - 160) <= thresh + assert abs(b[1] - 50) <= thresh + assert abs(radius(b) - 15) <= thresh + + b = s[2] + assert abs(b[0] - 100) <= thresh + assert abs(b[1] - 300) <= thresh + assert abs(radius(b) - 25) <= thresh + + b = s[3] + assert abs(b[0] - 200) <= thresh + assert abs(b[1] - 350) <= thresh + assert abs(radius(b) - 30) <= thresh + + # Testing log scale + blobs = blob_log( + img, + min_sigma=5, + max_sigma=20, + threshold=1, + log_scale=True) + + b = s[0] + assert abs(b[0] - 400) <= thresh + assert abs(b[1] - 130) <= thresh + assert abs(radius(b) - 5) <= thresh + + b = s[1] + assert abs(b[0] - 160) <= thresh + assert abs(b[1] - 50) <= thresh + assert abs(radius(b) - 15) <= thresh + + b = s[2] + assert abs(b[0] - 100) <= thresh + assert abs(b[1] - 300) <= thresh + assert abs(radius(b) - 25) <= thresh + + b = s[3] + assert abs(b[0] - 200) <= thresh + assert abs(b[1] - 350) <= thresh + assert abs(radius(b) - 30) <= thresh + + assert_raises(ValueError, blob_log, img3) + + +def test_blob_doh(): + img = np.ones((512, 512), dtype=np.uint8) + img3 = np.ones((5, 5, 5)) + + xs, ys = circle(400, 130, 20) + img[xs, ys] = 255 + + xs, ys = circle(460, 50, 30) + img[xs, ys] = 255 + + xs, ys = circle(100, 300, 40) + img[xs, ys] = 255 + + xs, ys = circle(200, 350, 50) + img[xs, ys] = 255 + + blobs = blob_doh( + img, + min_sigma=1, + max_sigma=60, + num_sigma=10, + threshold=.05) + + radius = lambda x: x[2] + s = sorted(blobs, key=radius) + thresh = 3 + + b = s[0] + assert abs(b[0] - 400) <= thresh + assert abs(b[1] - 130) <= thresh + assert abs(radius(b) - 20) <= thresh + + b = s[1] + assert abs(b[0] - 460) <= thresh + assert abs(b[1] - 50) <= thresh + assert abs(radius(b) - 30) <= thresh + + b = s[2] + assert abs(b[0] - 100) <= thresh + assert abs(b[1] - 300) <= thresh + assert abs(radius(b) - 40) <= thresh + + b = s[3] + assert abs(b[0] - 200) <= thresh + assert abs(b[1] - 350) <= thresh + assert abs(radius(b) - 50) <= thresh + + # Testing log scale + blobs = blob_doh( + img, + min_sigma=1, + max_sigma=60, + num_sigma=10, + log_scale=True, + threshold=.05) + + b = s[0] + assert abs(b[0] - 400) <= thresh + assert abs(b[1] - 130) <= thresh + assert abs(radius(b) - 20) <= thresh + + b = s[1] + assert abs(b[0] - 460) <= thresh + assert abs(b[1] - 50) <= thresh + assert abs(radius(b) - 30) <= thresh + + b = s[2] + assert abs(b[0] - 100) <= thresh + assert abs(b[1] - 300) <= thresh + assert abs(radius(b) - 40) <= thresh + + b = s[3] + assert abs(b[0] - 200) <= thresh + assert abs(b[1] - 350) <= thresh + assert abs(radius(b) - 50) <= thresh + + assert_raises(ValueError, blob_doh, img3) + + +def test_blob_overlap(): + img = np.ones((512, 512), dtype=np.uint8) + + xs, ys = circle(100, 100, 20) + img[xs, ys] = 255 + + xs, ys = circle(120, 100, 30) + img[xs, ys] = 255 + + blobs = blob_doh( + img, + min_sigma=1, + max_sigma=60, + num_sigma=10, + threshold=.05) + + assert len(blobs) == 1 diff --git a/skimage/feature/tests/test_brief.py b/skimage/feature/tests/test_brief.py new file mode 100644 index 00000000..554301b2 --- /dev/null +++ b/skimage/feature/tests/test_brief.py @@ -0,0 +1,77 @@ +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, corner_peaks, corner_harris + + +def test_color_image_unsupported_error(): + """Brief descriptors can be evaluated on gray-scale images only.""" + img = np.zeros((20, 20, 3)) + keypoints = np.asarray([[7, 5], [11, 13]]) + assert_raises(ValueError, BRIEF().extract, img, keypoints) + + +def test_normal_mode(): + """Verify the computed BRIEF descriptors with expected for normal mode.""" + img = rgb2gray(data.lena()) + + keypoints = corner_peaks(corner_harris(img), min_distance=5) + + extractor = BRIEF(descriptor_size=8, sigma=2) + + extractor.extract(img, keypoints[:8]) + + expected = np.array([[ True, False, True, False, True, True, False, False], + [False, False, False, False, True, False, False, False], + [ True, True, True, True, True, True, True, True], + [ True, False, True, True, False, True, False, True], + [False, True, True, True, True, True, True, True], + [ True, False, False, False, False, True, False, True], + [False, True, True, True, False, False, True, False], + [False, False, False, False, True, False, False, False]], dtype=bool) + + assert_array_equal(extractor.descriptors, expected) + + +def test_uniform_mode(): + """Verify the computed BRIEF descriptors with expected for uniform mode.""" + img = rgb2gray(data.lena()) + + keypoints = corner_peaks(corner_harris(img), min_distance=5) + + extractor = BRIEF(descriptor_size=8, sigma=2, mode='uniform') + + extractor.extract(img, keypoints[:8]) + + expected = np.array([[ True, False, True, False, False, True, False, False], + [False, True, False, False, True, True, True, True], + [ True, False, False, False, False, False, False, False], + [False, True, True, False, False, False, True, False], + [False, False, False, False, False, False, True, False], + [False, True, False, False, True, False, False, False], + [False, False, True, True, False, False, True, True], + [ True, True, False, False, False, False, False, False]], dtype=bool) + + assert_array_equal(extractor.descriptors, expected) + + +def test_unsupported_mode(): + assert_raises(ValueError, BRIEF, mode='foobar') + + +def test_border(): + img = np.zeros((100, 100)) + keypoints = np.array([[1, 1], [20, 20], [50, 50], [80, 80]]) + + extractor = BRIEF(patch_size=41) + extractor.extract(img, keypoints) + + assert extractor.descriptors.shape[0] == 3 + assert_array_equal(extractor.mask, (False, True, True, True)) + + +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..53c1c59b --- /dev/null +++ b/skimage/feature/tests/test_censure.py @@ -0,0 +1,97 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage.data import moon +from skimage.feature import CENSURE + + +img = moon() + + +def test_censure_on_rectangular_images(): + """Censure feature detector should work on 2D image of any shape.""" + rect_image = np.random.random((300, 200)) + square_image = np.random.random((200, 200)) + CENSURE().detect((square_image)) + CENSURE().detect((rect_image)) + + +def test_keypoints_censure_color_image_unsupported_error(): + """Censure keypoints can be extracted from gray-scale images only.""" + assert_raises(ValueError, CENSURE().detect, np.zeros((20, 20, 3))) + + +def test_keypoints_censure_mode_validity_error(): + """Mode argument in keypoints_censure can be either DoB, Octagon or + STAR.""" + assert_raises(ValueError, CENSURE, 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.""" + assert_raises(ValueError, CENSURE, 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.""" + detector = CENSURE() + detector.detect(img) + expected_keypoints = np.array([[ 21, 497], + [ 36, 46], + [119, 350], + [185, 177], + [287, 250], + [357, 239], + [463, 116], + [464, 132], + [467, 260]]) + expected_scales = np.array([3, 4, 4, 2, 2, 3, 2, 2, 2]) + + assert_array_equal(expected_keypoints, detector.keypoints) + assert_array_equal(expected_scales, detector.scales) + + +def test_keypoints_censure_moon_image_octagon(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for Octagon filter.""" + + detector = CENSURE(mode='octagon') + detector.detect(img) + expected_keypoints = np.array([[ 21, 496], + [ 35, 46], + [287, 250], + [356, 239], + [463, 116]]) + + expected_scales = np.array([3, 4, 2, 2, 2]) + + assert_array_equal(expected_keypoints, detector.keypoints) + assert_array_equal(expected_scales, detector.scales) + + +def test_keypoints_censure_moon_image_star(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for STAR filter.""" + detector = CENSURE(mode='star') + detector.detect(img) + expected_keypoints = np.array([[ 21, 497], + [ 36, 46], + [117, 356], + [185, 177], + [260, 227], + [287, 250], + [357, 239], + [451, 281], + [463, 116], + [467, 260]]) + + expected_scales = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) + + assert_array_equal(expected_keypoints, detector.keypoints) + assert_array_equal(expected_scales, detector.scales) + + +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 66ef41c6..1c39dd2d 100644 --- a/skimage/feature/tests/test_corner.py +++ b/skimage/feature/tests/test_corner.py @@ -1,12 +1,102 @@ import numpy as np -from numpy.testing import assert_array_equal +from numpy.testing import (assert_array_equal, assert_raises, + assert_almost_equal) from skimage import data from skimage import img_as_float +from skimage.color import rgb2gray +from skimage.morphology import octagon from skimage.feature import (corner_moravec, corner_harris, corner_shi_tomasi, corner_subpix, peak_local_max, corner_peaks, - corner_kitchen_rosenfeld, corner_foerstner) + corner_kitchen_rosenfeld, corner_foerstner, + corner_fast, corner_orientations, + structure_tensor, structure_tensor_eigvals, + hessian_matrix, hessian_matrix_eigvals, + hessian_matrix_det) + + +def test_structure_tensor(): + square = np.zeros((5, 5)) + square[2, 2] = 1 + Axx, Axy, Ayy = structure_tensor(square, sigma=0.1) + assert_array_equal(Axx, np.array([[ 0, 0, 0, 0, 0], + [ 0, 1, 0, 1, 0], + [ 0, 4, 0, 4, 0], + [ 0, 1, 0, 1, 0], + [ 0, 0, 0, 0, 0]])) + assert_array_equal(Axy, np.array([[ 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]])) + assert_array_equal(Ayy, np.array([[ 0, 0, 0, 0, 0], + [ 0, 1, 4, 1, 0], + [ 0, 0, 0, 0, 0], + [ 0, 1, 4, 1, 0], + [ 0, 0, 0, 0, 0]])) + + +def test_hessian_matrix(): + square = np.zeros((5, 5)) + square[2, 2] = 1 + Hxx, Hxy, Hyy = hessian_matrix(square, sigma=0.1) + assert_array_equal(Hxx, 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]])) + assert_array_equal(Hxy, 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]])) + assert_array_equal(Hyy, 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]])) + + +def test_structure_tensor_eigvals(): + square = np.zeros((5, 5)) + square[2, 2] = 1 + Axx, Axy, Ayy = structure_tensor(square, sigma=0.1) + l1, l2 = structure_tensor_eigvals(Axx, Axy, Ayy) + assert_array_equal(l1, np.array([[0, 0, 0, 0, 0], + [0, 2, 4, 2, 0], + [0, 4, 0, 4, 0], + [0, 2, 4, 2, 0], + [0, 0, 0, 0, 0]])) + assert_array_equal(l2, 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]])) + + +def test_hessian_matrix_eigvals(): + square = np.zeros((5, 5)) + square[2, 2] = 1 + Hxx, Hxy, Hyy = hessian_matrix(square, sigma=0.1) + l1, l2 = hessian_matrix_eigvals(Hxx, Hxy, Hyy) + assert_array_equal(l1, 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]])) + assert_array_equal(l2, 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]])) + + +def test_hessian_matrix_det(): + image = np.zeros((5, 5)) + image[2, 2] = 1 + det = hessian_matrix_det(image, 5) + assert_almost_equal(det, 0, decimal = 3) def test_square_image(): @@ -19,7 +109,11 @@ def test_square_image(): assert len(results) == 57 # Harris - results = peak_local_max(corner_harris(im)) + results = peak_local_max(corner_harris(im, method='k')) + # interest at corner + assert len(results) == 1 + + results = peak_local_max(corner_harris(im, method='eps')) # interest at corner assert len(results) == 1 @@ -41,7 +135,9 @@ def test_noisy_square_image(): assert results.any() # Harris - results = peak_local_max(corner_harris(im, sigma=1.5)) + results = peak_local_max(corner_harris(im, sigma=1.5, method='k')) + assert len(results) == 1 + results = peak_local_max(corner_harris(im, sigma=1.5, method='eps')) assert len(results) == 1 # Shi-Tomasi @@ -92,21 +188,56 @@ def test_rotated_lena(): assert (np.sort(results[:, 1]) == np.sort(results_rotated[:, 0])).all() -def test_subpix(): +def test_subpix_edge(): img = np.zeros((50, 50)) - img[:25,:25] = 255 - img[25:,25:] = 255 + img[:25, :25] = 255 + img[25:, 25:] = 255 corner = peak_local_max(corner_harris(img), num_peaks=1) subpix = corner_subpix(img, corner) assert_array_equal(subpix[0], (24.5, 24.5)) +def test_subpix_dot(): + img = np.zeros((50, 50)) + img[25, 25] = 255 + corner = peak_local_max(corner_harris(img), num_peaks=1) + subpix = corner_subpix(img, corner) + assert_array_equal(subpix[0], (25, 25)) + + +def test_subpix_no_class(): + img = np.zeros((50, 50)) + subpix = corner_subpix(img, np.array([[25, 25]])) + assert_array_equal(subpix[0], (np.nan, np.nan)) + + img[25, 25] = 1e-10 + corner = peak_local_max(corner_harris(img), num_peaks=1) + subpix = corner_subpix(img, np.array([[25, 25]])) + assert_array_equal(subpix[0], (np.nan, np.nan)) + + +def test_subpix_border(): + img = np.zeros((50, 50)) + img[1:25,1:25] = 255 + img[25:-1,25:-1] = 255 + corner = corner_peaks(corner_harris(img), min_distance=1) + subpix = corner_subpix(img, corner, window_size=11) + ref = np.array([[ 0.52040816, 0.52040816], + [ 0.52040816, 24.47959184], + [24.47959184, 0.52040816], + [24.5 , 24.5 ], + [24.52040816, 48.47959184], + [48.47959184, 24.52040816], + [48.47959184, 48.47959184]]) + assert_almost_equal(subpix, ref) + + 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()) + lena_corners = corner_harris(rgb2gray(data.lena())) for i in range(20): n = np.random.random_integers(20) @@ -124,6 +255,10 @@ def test_corner_peaks(): corners = corner_peaks(response, exclude_border=False, min_distance=0) assert len(corners) == 4 + corners = corner_peaks(response, exclude_border=False, min_distance=0, + indices=False) + assert np.sum(corners) == 4 + def test_blank_image_nans(): """Some of the corner detectors had a weakness in terms of returning @@ -140,6 +275,59 @@ def test_blank_image_nans(): assert np.all(np.isfinite(response)) +def test_corner_fast_image_unsupported_error(): + img = np.zeros((20, 20, 3)) + assert_raises(ValueError, corner_fast, img) + + +def test_corner_fast_lena(): + img = rgb2gray(data.lena()) + expected = np.array([[ 67, 157], + [204, 261], + [247, 146], + [269, 111], + [318, 158], + [386, 73], + [413, 70], + [435, 180], + [455, 177], + [461, 160]]) + actual = corner_peaks(corner_fast(img, 12, 0.3)) + assert_array_equal(actual, expected) + + +def test_corner_orientations_image_unsupported_error(): + img = np.zeros((20, 20, 3)) + assert_raises(ValueError, corner_orientations, img, + np.asarray([[7, 7]]), np.ones((3, 3))) + + +def test_corner_orientations_even_shape_error(): + img = np.zeros((20, 20)) + assert_raises(ValueError, corner_orientations, img, + np.asarray([[7, 7]]), np.ones((4, 4))) + + +def test_corner_orientations_lena(): + img = rgb2gray(data.lena()) + corners = corner_peaks(corner_fast(img, 11, 0.35)) + expected = np.array([-1.9195897 , -3.03159624, -1.05991162, -2.89573739, + -2.61607644, 2.98660159]) + actual = corner_orientations(img, corners, octagon(3, 2)) + assert_almost_equal(actual, expected) + + +def test_corner_orientations_square(): + square = np.zeros((12, 12)) + square[3:9, 3:9] = 1 + corners = corner_peaks(corner_fast(square, 9), min_distance=1) + actual_orientations = corner_orientations(square, corners, octagon(3, 2)) + actual_orientations_degrees = np.rad2deg(actual_orientations) + expected_orientations_degree = np.array([ 45., 135., -45., -135.]) + assert_array_equal(actual_orientations_degrees, + expected_orientations_degree) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/feature/tests/test_daisy.py b/skimage/feature/tests/test_daisy.py index 40781a64..32a7a5df 100644 --- a/skimage/feature/tests/test_daisy.py +++ b/skimage/feature/tests/test_daisy.py @@ -45,8 +45,15 @@ def test_descs_shape(): assert(descs.shape[1] == ceil((img.shape[1] - radius * 2) / float(step))) +def test_daisy_sigmas_and_radii(): + img = img_as_float(data.lena()[:64, :64].mean(axis=2)) + sigmas = [1, 2, 3] + radii = [1, 2] + daisy(img, sigmas=sigmas, ring_radii=radii) + + def test_daisy_incompatible_sigmas_and_radii(): - img = img_as_float(data.lena()[:128, :128].mean(axis=2)) + img = img_as_float(data.lena()[:64, :64].mean(axis=2)) sigmas = [1, 2] radii = [1, 2] assert_raises(ValueError, daisy, img, sigmas=sigmas, ring_radii=radii) @@ -86,9 +93,10 @@ def test_daisy_normalization(): def test_daisy_visualization(): - img = img_as_float(data.lena()[:128, :128].mean(axis=2)) + img = img_as_float(data.lena()[:32, :32].mean(axis=2)) descs, descs_img = daisy(img, visualize=True) - assert(descs_img.shape == (128, 128, 3)) + assert(descs_img.shape == (32, 32, 3)) + if __name__ == '__main__': from numpy import testing diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index b37513a5..8cab52f7 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -32,9 +32,11 @@ def test_hog_color_image_unsupported_error(): 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 + # 1) create image (with float values) where upper half is filled by + # zeros, bottom half by 100 # 2) create unsigned integer version of this image - # 3) calculate feature.hog() for both images, both with 'normalise' option enabled and disabled + # 3) calculate feature.hog() for both images, both with 'normalise' + # option enabled and disabled # 4) verify that all results are equal where expected # 5) verify that computed feature vector is as expected # 6) repeat the scenario for 90, 180 and 270 degrees rotated images @@ -43,7 +45,7 @@ def test_hog_basic_orientations_and_data_types(): width = height = 35 image0 = np.zeros((height, width), dtype='float') - image0[height / 2:] = 100 + image0[height // 2:] = 100 for rot in range(4): # rotate by 0, 90, 180 and 270 degrees @@ -52,13 +54,17 @@ def test_hog_basic_orientations_and_data_types(): # create uint8 image from image_float image_uint8 = image_float.astype('uint8') - (hog_float, hog_img_float) = feature.hog(image_float, orientations=4, pixels_per_cell=(8, 8), + (hog_float, hog_img_float) = feature.hog( + image_float, orientations=4, pixels_per_cell=(8, 8), cells_per_block=(1, 1), visualise=True, normalise=False) - (hog_uint8, hog_img_uint8) = feature.hog(image_uint8, orientations=4, pixels_per_cell=(8, 8), + (hog_uint8, hog_img_uint8) = feature.hog( + image_uint8, orientations=4, pixels_per_cell=(8, 8), cells_per_block=(1, 1), visualise=True, normalise=False) - (hog_float_norm, hog_img_float_norm) = feature.hog(image_float, orientations=4, pixels_per_cell=(8, 8), + (hog_float_norm, hog_img_float_norm) = feature.hog( + image_float, orientations=4, pixels_per_cell=(8, 8), cells_per_block=(1, 1), visualise=True, normalise=True) - (hog_uint8_norm, hog_img_uint8_norm) = feature.hog(image_uint8, orientations=4, pixels_per_cell=(8, 8), + (hog_uint8_norm, hog_img_uint8_norm) = feature.hog( + image_uint8, orientations=4, pixels_per_cell=(8, 8), cells_per_block=(1, 1), visualise=True, normalise=True) # set to True to enable manual debugging with graphical output, @@ -66,23 +72,42 @@ def test_hog_basic_orientations_and_data_types(): if False: import matplotlib.pyplot as plt plt.figure() - plt.subplot(2, 3, 1); plt.imshow(image_float); plt.colorbar(); plt.title('image') - plt.subplot(2, 3, 2); plt.imshow(hog_img_float); plt.colorbar(); plt.title('HOG result visualisation (float img)') - plt.subplot(2, 3, 5); plt.imshow(hog_img_uint8); plt.colorbar(); plt.title('HOG result visualisation (uint8 img)') - plt.subplot(2, 3, 3); plt.imshow(hog_img_float_norm); plt.colorbar(); plt.title('HOG result (normalise) visualisation (float img)') - plt.subplot(2, 3, 6); plt.imshow(hog_img_uint8_norm); plt.colorbar(); plt.title('HOG result (normalise) visualisation (uint8 img)') + plt.subplot(2, 3, 1) + plt.imshow(image_float) + plt.colorbar() + plt.title('image') + plt.subplot(2, 3, 2) + plt.imshow(hog_img_float) + plt.colorbar() + plt.title('HOG result visualisation (float img)') + plt.subplot(2, 3, 5) + plt.imshow(hog_img_uint8) + plt.colorbar() + plt.title('HOG result visualisation (uint8 img)') + plt.subplot(2, 3, 3) + plt.imshow(hog_img_float_norm) + plt.colorbar() + plt.title('HOG result (normalise) visualisation (float img)') + plt.subplot(2, 3, 6) + plt.imshow(hog_img_uint8_norm) + plt.colorbar() + plt.title('HOG result (normalise) visualisation (uint8 img)') plt.show() - # results (features and visualisation) for float and uint8 images must be almost equal + # results (features and visualisation) for float and uint8 images must + # be almost equal assert_almost_equal(hog_float, hog_uint8) assert_almost_equal(hog_img_float, hog_img_uint8) - # resulting features should be almost equal when 'normalise' is enabled or disabled (for current simple testing image) + # resulting features should be almost equal when 'normalise' is enabled + # or disabled (for current simple testing image) assert_almost_equal(hog_float, hog_float_norm, decimal=4) assert_almost_equal(hog_float, hog_uint8_norm, decimal=4) - # reshape resulting feature vector to matrix with 4 columns (each corresponding to one of 4 directions), - # only one direction should contain nonzero values (this is manually determined for testing image) + # reshape resulting feature vector to matrix with 4 columns (each + # corresponding to one of 4 directions); only one direction should + # contain nonzero values (this is manually determined for testing + # image) actual = np.max(hog_float.reshape(-1, 4), axis=0) if rot in [0, 2]: @@ -101,8 +126,9 @@ def test_hog_orientations_circle(): # scenario: # 1) create image with blurred circle in the middle # 2) calculate feature.hog() - # 3) verify that the resulting feature vector contains uniformly distributed values for all orientations, - # i.e. no orientation is lost or emphasized + # 3) verify that the resulting feature vector contains uniformly + # distributed values for all orientations, i.e. no orientation is + # lost or emphasized # 4) repeat the scenario for other 'orientations' option # size of testing image @@ -114,29 +140,39 @@ def test_hog_orientations_circle(): image = ndimage.gaussian_filter(image, 2) for orientations in range(2, 15): - (hog, hog_img) = feature.hog(image, orientations=orientations, pixels_per_cell=(8, 8), - cells_per_block=(1, 1), visualise=True, normalise=False) + (hog, hog_img) = feature.hog(image, orientations=orientations, + pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, + normalise=False) # set to True to enable manual debugging with graphical output, # must be False for automatic testing if False: import matplotlib.pyplot as plt plt.figure() - plt.subplot(1, 2, 1); plt.imshow(image); plt.colorbar(); plt.title('image_float') - plt.subplot(1, 2, 2); plt.imshow(hog_img); plt.colorbar(); plt.title('HOG result visualisation, orientations=%d' % (orientations)) + plt.subplot(1, 2, 1) + plt.imshow(image) + plt.colorbar() + plt.title('image_float') + plt.subplot(1, 2, 2) + plt.imshow(hog_img) + plt.colorbar() + plt.title('HOG result visualisation, ' + 'orientations=%d' % (orientations)) plt.show() - # reshape resulting feature vector to matrix with N columns (each column corresponds to one direction), + # reshape resulting feature vector to matrix with N columns (each + # column corresponds to one direction), hog_matrix = hog.reshape(-1, orientations) - # compute mean values in the resulting feature vector for each direction, - # these values should be almost equal to the global mean value (since the image contains a circle), - # i.e. all directions have same contribution to the result + # compute mean values in the resulting feature vector for each + # direction, these values should be almost equal to the global mean + # value (since the image contains a circle), i.e., all directions have + # same contribution to the result actual = np.mean(hog_matrix, axis=0) desired = np.mean(hog_matrix) assert_almost_equal(actual, desired, decimal=1) if __name__ == '__main__': - from numpy.testing import run_module_suite - run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/feature/tests/test_match.py b/skimage/feature/tests/test_match.py new file mode 100644 index 00000000..1b0a622f --- /dev/null +++ b/skimage/feature/tests/test_match.py @@ -0,0 +1,120 @@ +import numpy as np +from numpy.testing import assert_equal, assert_raises +from skimage import data +from skimage import transform as tf +from skimage.color import rgb2gray +from skimage.feature import (BRIEF, match_descriptors, + corner_peaks, corner_harris) + + +def test_binary_descriptors_unequal_descriptor_sizes_error(): + """Sizes of descriptors of keypoints to be matched should be equal.""" + descs1 = np.array([[True, True, False, True], + [False, True, False, True]]) + descs2 = np.array([[True, False, False, True, False], + [False, True, True, True, False]]) + assert_raises(ValueError, match_descriptors, descs1, descs2) + + +def test_binary_descriptors(): + descs1 = np.array([[True, True, False, True, True], + [False, True, False, True, True]]) + descs2 = np.array([[True, False, False, True, False], + [False, False, True, True, True]]) + matches = match_descriptors(descs1, descs2) + assert_equal(matches, [[0, 0], [1, 1]]) + + +def test_binary_descriptors_lena_rotation_crosscheck_false(): + """Verify matched keypoints and their corresponding masks results between + lena image and its rotated version with the expected keypoint pairs with + cross_check disabled.""" + img = data.lena() + img = rgb2gray(img) + tform = tf.SimilarityTransform(scale=1, rotation=0.15, translation=(0, 0)) + rotated_img = tf.warp(img, tform) + + extractor = BRIEF(descriptor_size=512) + + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + extractor.extract(img, keypoints1) + descriptors1 = extractor.descriptors + + keypoints2 = corner_peaks(corner_harris(rotated_img), min_distance=5) + extractor.extract(rotated_img, keypoints2) + descriptors2 = extractor.descriptors + + matches = match_descriptors(descriptors1, descriptors2, cross_check=False) + + exp_matches1 = np.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, + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46]) + exp_matches2 = np.array([33, 0, 35, 7, 1, 35, 3, 2, 3, 6, 4, 9, + 11, 10, 28, 7, 8, 5, 31, 14, 13, 15, 21, 16, + 16, 13, 17, 18, 19, 21, 22, 23, 0, 24, 1, 24, + 23, 0, 26, 27, 25, 34, 28, 14, 29, 30, 21]) + assert_equal(matches[:, 0], exp_matches1) + assert_equal(matches[:, 1], exp_matches2) + + +def test_binary_descriptors_lena_rotation_crosscheck_true(): + """Verify matched keypoints and their corresponding masks results between + lena image and its rotated version with the expected keypoint pairs with + cross_check enabled.""" + img = data.lena() + img = rgb2gray(img) + tform = tf.SimilarityTransform(scale=1, rotation=0.15, translation=(0, 0)) + rotated_img = tf.warp(img, tform) + + extractor = BRIEF(descriptor_size=512) + + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + extractor.extract(img, keypoints1) + descriptors1 = extractor.descriptors + + keypoints2 = corner_peaks(corner_harris(rotated_img), min_distance=5) + extractor.extract(rotated_img, keypoints2) + descriptors2 = extractor.descriptors + + matches = match_descriptors(descriptors1, descriptors2, cross_check=True) + + exp_matches1 = np.array([ 0, 1, 2, 4, 6, 7, 9, 10, 11, 12, 13, 15, + 16, 17, 19, 20, 21, 24, 26, 27, 28, 29, 30, 35, + 36, 38, 39, 40, 42, 44, 45]) + exp_matches2 = np.array([33, 0, 35, 1, 3, 2, 6, 4, 9, 11, 10, 7, + 8, 5, 14, 13, 15, 16, 17, 18, 19, 21, 22, 24, + 23, 26, 27, 25, 28, 29, 30]) + assert_equal(matches[:, 0], exp_matches1) + assert_equal(matches[:, 1], exp_matches2) + + +def test_max_distance(): + descs1 = np.zeros((10, 128)) + descs2 = np.zeros((15, 128)) + + descs1[0, :] = 1 + + matches = match_descriptors(descs1, descs2, metric='euclidean', + max_distance=0.1, cross_check=False) + assert len(matches) == 9 + + matches = match_descriptors(descs1, descs2, metric='euclidean', + max_distance=np.sqrt(128.1), + cross_check=False) + assert len(matches) == 10 + + matches = match_descriptors(descs1, descs2, metric='euclidean', + max_distance=0.1, + cross_check=True) + assert_equal(matches, [[1, 0]]) + + matches = match_descriptors(descs1, descs2, metric='euclidean', + max_distance=np.sqrt(128.1), + cross_check=True) + assert_equal(matches, [[1, 0]]) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_orb.py b/skimage/feature/tests/test_orb.py new file mode 100644 index 00000000..30394d07 --- /dev/null +++ b/skimage/feature/tests/test_orb.py @@ -0,0 +1,115 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_almost_equal +from skimage.feature import ORB +from skimage.data import lena +from skimage.color import rgb2gray + + +img = rgb2gray(lena()) + + +def test_keypoints_orb_desired_no_of_keypoints(): + detector_extractor = ORB(n_keypoints=10, fast_n=12, fast_threshold=0.20) + detector_extractor.detect(img) + + exp_rows = np.array([ 435. , 435.6 , 376. , 455. , 434.88, 269. , + 375.6 , 310.8 , 413. , 311.04]) + exp_cols = np.array([ 180. , 180. , 156. , 176. , 180. , 111. , + 156. , 172.8, 70. , 172.8]) + + exp_scales = np.array([ 1. , 1.2 , 1. , 1. , 1.44 , 1. , + 1.2 , 1.2 , 1. , 1.728]) + + exp_orientations = np.array([-175.64733392, -167.94842949, -148.98350192, + -142.03599837, -176.08535837, -53.08162354, + -150.89208271, 97.7693776 , -173.4479964 , + 38.66312042]) + exp_response = np.array([ 0.96770745, 0.81027306, 0.72376257, + 0.5626413 , 0.5097993 , 0.44351774, + 0.39154173, 0.39084861, 0.39063076, + 0.37602487]) + + assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0]) + assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1]) + assert_almost_equal(exp_scales, detector_extractor.scales) + assert_almost_equal(exp_response, detector_extractor.responses) + assert_almost_equal(exp_orientations, + np.rad2deg(detector_extractor.orientations), 5) + + detector_extractor.detect_and_extract(img) + assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0]) + assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1]) + + +def test_keypoints_orb_less_than_desired_no_of_keypoints(): + img = rgb2gray(lena()) + detector_extractor = ORB(n_keypoints=15, fast_n=12, + fast_threshold=0.33, downscale=2, n_scales=2) + detector_extractor.detect(img) + + exp_rows = np.array([ 67., 247., 269., 413., 435., 230., 264., + 330., 372.]) + exp_cols = np.array([ 157., 146., 111., 70., 180., 136., 336., + 148., 156.]) + + exp_scales = np.array([ 1., 1., 1., 1., 1., 2., 2., 2., 2.]) + + exp_orientations = np.array([-105.76503839, -96.28973044, -53.08162354, + -173.4479964 , -175.64733392, -106.07927215, + -163.40016243, 75.80865813, -154.73195911]) + + exp_response = np.array([ 0.13197835, 0.24931321, 0.44351774, + 0.39063076, 0.96770745, 0.04935129, + 0.21431068, 0.15826555, 0.42403573]) + + assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0]) + assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1]) + assert_almost_equal(exp_scales, detector_extractor.scales) + assert_almost_equal(exp_response, detector_extractor.responses) + assert_almost_equal(exp_orientations, + np.rad2deg(detector_extractor.orientations), 5) + + detector_extractor.detect_and_extract(img) + assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0]) + assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1]) + + +def test_descriptor_orb(): + detector_extractor = ORB(fast_n=12, fast_threshold=0.20) + + exp_descriptors = np.array([[ True, False, True, True, False, False, False, False, False, False], + [False, False, True, True, False, True, True, False, True, True], + [ True, False, False, False, True, False, True, True, True, False], + [ True, False, False, True, False, True, True, False, False, False], + [False, True, True, True, False, False, False, True, True, False], + [False, False, False, False, False, True, False, True, True, True], + [False, True, True, True, True, False, False, True, False, True], + [ True, True, True, False, True, True, True, True, False, False], + [ True, True, False, True, True, True, True, False, False, False], + [ True, False, False, False, False, True, False, False, True, True], + [ True, False, False, False, True, True, True, False, False, False], + [False, False, True, False, True, False, False, True, False, False], + [False, False, True, True, False, False, False, False, False, True], + [ True, True, False, False, False, True, True, True, True, True], + [ True, True, True, False, False, True, False, True, True, False], + [False, True, True, False, False, True, True, True, True, True], + [ True, True, True, False, False, False, False, True, True, True], + [False, False, False, False, True, False, False, True, True, False], + [False, True, False, False, True, False, False, False, True, True], + [ True, False, True, False, False, False, True, True, False, False]], dtype=bool) + + detector_extractor.detect(img) + detector_extractor.extract(img, detector_extractor.keypoints, + detector_extractor.scales, + detector_extractor.orientations) + assert_array_equal(exp_descriptors, + detector_extractor.descriptors[100:120, 10:20]) + + detector_extractor.detect_and_extract(img) + assert_array_equal(exp_descriptors, + detector_extractor.descriptors[100:120, 10:20]) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_peak.py b/skimage/feature/tests/test_peak.py index 1a3e91f2..98242a4d 100644 --- a/skimage/feature/tests/test_peak.py +++ b/skimage/feature/tests/test_peak.py @@ -1,5 +1,6 @@ import numpy as np -from numpy.testing import assert_array_almost_equal as assert_close +from numpy.testing import (assert_array_almost_equal as assert_close, + assert_equal) import scipy.ndimage from skimage.feature import peak @@ -266,6 +267,30 @@ def test_disk(): assert np.all(result) +def test_3D(): + image = np.zeros((30, 30, 30)) + image[15, 15, 15] = 1 + image[5, 5, 5] = 1 + assert_equal(peak.peak_local_max(image), [[15, 15, 15]]) + assert_equal(peak.peak_local_max(image, min_distance=6), [[15, 15, 15]]) + assert_equal(peak.peak_local_max(image, exclude_border=False), + [[5, 5, 5], [15, 15, 15]]) + assert_equal(peak.peak_local_max(image, min_distance=5), + [[5, 5, 5], [15, 15, 15]]) + + +def test_4D(): + image = np.zeros((30, 30, 30, 30)) + image[15, 15, 15, 15] = 1 + image[5, 5, 5, 5] = 1 + assert_equal(peak.peak_local_max(image), [[15, 15, 15, 15]]) + assert_equal(peak.peak_local_max(image, min_distance=6), [[15, 15, 15, 15]]) + assert_equal(peak.peak_local_max(image, exclude_border=False), + [[5, 5, 5, 5], [15, 15, 15, 15]]) + assert_equal(peak.peak_local_max(image, min_distance=5), + [[5, 5, 5, 5], [15, 15, 15, 15]]) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/feature/tests/test_template.py b/skimage/feature/tests/test_template.py index 1b9ff213..10e6f677 100644 --- a/skimage/feature/tests/test_template.py +++ b/skimage/feature/tests/test_template.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import assert_array_almost_equal as assert_close +from numpy.testing import assert_almost_equal, assert_equal, assert_raises from skimage.morphology import diamond from skimage.feature import match_template, peak_local_max @@ -31,7 +31,7 @@ def test_template(): positions = positions[np.argsort(positions[:, 0])] for xy_target, xy in zip(target_positions, positions): - yield assert_close, xy, xy_target + yield assert_almost_equal, xy, xy_target def test_normalization(): @@ -88,7 +88,7 @@ def test_no_nans(): def test_switched_arguments(): image = np.ones((5, 5)) template = np.ones((3, 3)) - np.testing.assert_raises(ValueError, match_template, template, image) + assert_raises(ValueError, match_template, template, image) def test_pad_input(): @@ -108,14 +108,66 @@ def test_pad_input(): image[mid, -9:-4] -= template # full min template centered at 12 image[mid, -3:] += template[:, :3] # half max template centered at 18 - result = match_template(image, template, pad_input=True) + result = match_template(image, template, pad_input=True, + constant_values=image.mean()) # get the max and min results. sorted_result = np.argsort(result.flat) i, j = np.unravel_index(sorted_result[:2], result.shape) - assert_close(j, (12, 0)) + assert_equal(j, (12, 0)) i, j = np.unravel_index(sorted_result[-2:], result.shape) - assert_close(j, (18, 6)) + assert_equal(j, (18, 6)) + + +def test_3d(): + np.random.seed(1) + template = np.random.rand(3, 3, 3) + image = np.zeros((12, 12, 12)) + + image[3:6, 5:8, 4:7] = template + + result = match_template(image, template) + + assert_equal(result.shape, (10, 10, 10)) + assert_equal(np.unravel_index(result.argmax(), result.shape), (3, 5, 4)) + + +def test_3d_pad_input(): + np.random.seed(1) + template = np.random.rand(3, 3, 3) + image = np.zeros((12, 12, 12)) + + image[3:6, 5:8, 4:7] = template + + result = match_template(image, template, pad_input=True) + + assert_equal(result.shape, (12, 12, 12)) + assert_equal(np.unravel_index(result.argmax(), result.shape), (4, 6, 5)) + + +def test_padding_reflect(): + template = diamond(2) + image = np.zeros((10, 10)) + image[2:7, :3] = template[:, -3:] + + result = match_template(image, template, pad_input=True, + mode='reflect') + + assert_equal(np.unravel_index(result.argmax(), result.shape), (4, 0)) + + +def test_wrong_input(): + image = np.ones((5, 5, 1)) + template = np.ones((3, 3)) + assert_raises(ValueError, match_template, template, image) + + image = np.ones((5, 5)) + template = np.ones((3, 3, 2)) + assert_raises(ValueError, match_template, template, image) + + image = np.ones((5, 5, 3, 3)) + template = np.ones((3, 3, 2)) + assert_raises(ValueError, match_template, template, image) if __name__ == "__main__": diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py index e4fb6acb..dac511d4 100644 --- a/skimage/feature/tests/test_texture.py +++ b/skimage/feature/tests/test_texture.py @@ -11,18 +11,28 @@ class TestGLCM(): [2, 2, 3, 3]], dtype=np.uint8) def test_output_angles(self): - result = greycomatrix(self.image, [1], [0, np.pi / 2], 4) - assert result.shape == (4, 4, 1, 2) + result = greycomatrix(self.image, [1], [0, np.pi / 4, np.pi / 2, 3 * np.pi / 4], 4) + assert result.shape == (4, 4, 1, 4) expected1 = np.array([[2, 2, 1, 0], [0, 2, 0, 0], [0, 0, 3, 1], [0, 0, 0, 1]], dtype=np.uint32) np.testing.assert_array_equal(result[:, :, 0, 0], expected1) - expected2 = np.array([[3, 0, 2, 0], + expected2 = np.array([[1, 1, 3, 0], + [0, 1, 1, 0], + [0, 0, 0, 2], + [0, 0, 0, 0]], dtype=np.uint32) + np.testing.assert_array_equal(result[:, :, 0, 1], expected2) + expected3 = np.array([[3, 0, 2, 0], [0, 2, 2, 0], [0, 0, 1, 2], [0, 0, 0, 0]], dtype=np.uint32) - np.testing.assert_array_equal(result[:, :, 0, 1], expected2) + np.testing.assert_array_equal(result[:, :, 0, 2], expected3) + expected4 = np.array([[2, 0, 0, 0], + [1, 1, 2, 0], + [0, 0, 2, 1], + [0, 0, 0, 0]], dtype=np.uint32) + np.testing.assert_array_equal(result[:, :, 0, 3], expected4) def test_output_symmetric_1(self): result = greycomatrix(self.image, [1], [np.pi / 2], 4, @@ -183,21 +193,27 @@ class TestLBP(): np.testing.assert_array_equal(lbp, ref) def test_var(self): - lbp = local_binary_pattern(self.image, 8, 1, 'var') - ref = np.array([[0. , 0.00072786, 0. , 0.00115377, - 0.00032355, 0.00224467], - [0.00051758, 0. , 0.0026383 , 0.00163246, - 0.00027414, 0.00041124], - [0.00192834, 0.00130368, 0.00042095, 0.00171894, - 0. , 0.00063726], - [0.00023048, 0.00019464 , 0.00082291, 0.00225386, - 0.00076696, 0. ], - [0.00097253, 0.00013236, 0.0009134 , 0.0014467 , - 0. , 0.00082472], - [0.00024701, 0.0012277 , 0. , 0.00109869, - 0.00015445, 0.00035881]]) - np.testing.assert_array_almost_equal(lbp, ref) + # Test idea: mean of variance is estimate of overall variance. + # Fix random seed for test stability. + np.random.seed(13141516) + + # Create random image with known variance. + image = np.random.random((500, 500)) + target_std = 0.3 + image = image / image.std() * target_std + + # Use P=4 to avoid interpolation effects + P, R = 4, 1 + lbp = local_binary_pattern(image, P, R, 'var') + + # Take central part to avoid border effect. + lbp = lbp[5:-5,5:-5] + + # The LBP variance is biased (ddof=0), correct for that. + expected = target_std**2 * (P-1)/P + + np.testing.assert_almost_equal(lbp.mean(), expected, 4) def test_nri_uniform(self): lbp = local_binary_pattern(self.image, 8, 1, 'nri_uniform') diff --git a/skimage/feature/tests/test_util.py b/skimage/feature/tests/test_util.py index 6e25f51a..2b601b94 100644 --- a/skimage/feature/tests/test_util.py +++ b/skimage/feature/tests/test_util.py @@ -1,30 +1,71 @@ import numpy as np -from numpy.testing import assert_array_equal -from skimage.feature.util import pairwise_hamming_distance +import matplotlib.pyplot as plt +from numpy.testing import assert_equal, assert_raises + +from skimage.feature.util import (FeatureDetector, DescriptorExtractor, + _prepare_grayscale_input_2D, + _mask_border_keypoints, plot_matches) -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_feature_detector(): + assert_raises(NotImplementedError, FeatureDetector().detect, None) -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) +def test_descriptor_extractor(): + assert_raises(NotImplementedError, DescriptorExtractor().extract, + None, None) + + +def test_prepare_grayscale_input_2D(): + assert_raises(ValueError, _prepare_grayscale_input_2D, np.zeros((3, 3, 3))) + assert_raises(ValueError, _prepare_grayscale_input_2D, np.zeros((3, 1))) + assert_raises(ValueError, _prepare_grayscale_input_2D, np.zeros((3, 1, 1))) + img = _prepare_grayscale_input_2D(np.zeros((3, 3))) + img = _prepare_grayscale_input_2D(np.zeros((3, 3, 1))) + img = _prepare_grayscale_input_2D(np.zeros((1, 3, 3))) + + +def test_mask_border_keypoints(): + keypoints = np.array([[0, 0], [1, 1], [2, 2], [3, 3], [4, 4]]) + assert_equal(_mask_border_keypoints((10, 10), keypoints, 0), + [1, 1, 1, 1, 1]) + assert_equal(_mask_border_keypoints((10, 10), keypoints, 2), + [0, 0, 1, 1, 1]) + assert_equal(_mask_border_keypoints((4, 4), keypoints, 2), + [0, 0, 1, 0, 0]) + assert_equal(_mask_border_keypoints((10, 10), keypoints, 5), + [0, 0, 0, 0, 0]) + assert_equal(_mask_border_keypoints((10, 10), keypoints, 4), + [0, 0, 0, 0, 1]) + + +def test_plot_matches(): + fig, ax = plt.subplots(nrows=1, ncols=1) + + shapes = (((10, 10), (10, 10)), + ((10, 10), (12, 10)), + ((10, 10), (10, 12)), + ((10, 10), (12, 12)), + ((12, 10), (10, 10)), + ((10, 12), (10, 10)), + ((12, 12), (10, 10))) + + keypoints1 = 10 * np.random.rand(10, 2) + keypoints2 = 10 * np.random.rand(10, 2) + idxs1 = np.random.randint(10, size=10) + idxs2 = np.random.randint(10, size=10) + matches = np.column_stack((idxs1, idxs2)) + + for shape1, shape2 in shapes: + img1 = np.zeros(shape1) + img2 = np.zeros(shape2) + plot_matches(ax, img1, img2, keypoints1, keypoints2, matches) + plot_matches(ax, img1, img2, keypoints1, keypoints2, matches, + only_matches=True) + plot_matches(ax, img1, img2, keypoints1, keypoints2, matches, + keypoints_color='r') + plot_matches(ax, img1, img2, keypoints1, keypoints2, matches, + matches_color='r') if __name__ == '__main__': diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index f9bf6c9f..a4e69518 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -66,17 +66,27 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, ... [0, 0, 1, 1], ... [0, 2, 2, 2], ... [2, 2, 3, 3]], dtype=np.uint8) - >>> result = greycomatrix(image, [1], [0, np.pi/2], levels=4) + >>> result = greycomatrix(image, [1], [0, np.pi/4, np.pi/2, 3*np.pi/4], levels=4) >>> result[:, :, 0, 0] array([[2, 2, 1, 0], [0, 2, 0, 0], [0, 0, 3, 1], [0, 0, 0, 1]], dtype=uint32) >>> result[:, :, 0, 1] + array([[1, 1, 3, 0], + [0, 1, 1, 0], + [0, 0, 0, 2], + [0, 0, 0, 0]], dtype=uint32) + >>> result[:, :, 0, 2] array([[3, 0, 2, 0], [0, 2, 2, 0], [0, 0, 1, 2], [0, 0, 0, 0]], dtype=uint32) + >>> result[:, :, 0, 3] + array([[2, 0, 0, 0], + [1, 1, 2, 0], + [0, 0, 2, 1], + [0, 0, 0, 0]], dtype=uint32) """ diff --git a/skimage/feature/util.py b/skimage/feature/util.py index a5267d44..5a3e5687 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -1,38 +1,161 @@ +import numpy as np + +from skimage.util import img_as_float -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] +class FeatureDetector(object): - keypoints_filtering_mask = ((dist - 1 < keypoints[:, 0]) & - (keypoints[:, 0] < width - dist + 1) & - (dist - 1 < keypoints[:, 1]) & - (keypoints[:, 1] < height - dist + 1)) + def __init__(self): + self.keypoints_ = np.array([]) - return keypoints_filtering_mask + def detect(self, image): + """Detect keypoints in image. + + Parameters + ---------- + image : 2D array + Input image. + + """ + raise NotImplementedError() -def pairwise_hamming_distance(array1, array2): - """**Experimental function**. +class DescriptorExtractor(object): - Calculate hamming dissimilarity measure between two sets of - vectors. + def __init__(self): + self.descriptors_ = np.array([]) + + def extract(self, image, keypoints): + """Extract feature descriptors in image for given keypoints. + + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint locations as ``(row, col)``. + + """ + raise NotImplementedError() + + +def plot_matches(ax, image1, image2, keypoints1, keypoints2, matches, + keypoints_color='k', matches_color=None, only_matches=False): + """Plot matched features. Parameters ---------- - array1 : (P1, D) array - P1 vectors of size D. - array2 : (P2, D) array - P2 vectors of size D. + ax : matplotlib.axes.Axes + Matches and image are drawn in this ax. + image1 : (N, M [, 3]) array + First grayscale or color image. + image2 : (N, M [, 3]) array + Second grayscale or color image. + keypoints1 : (K1, 2) array + First keypoint coordinates as ``(row, col)``. + keypoints2 : (K2, 2) array + Second keypoint coordinates as ``(row, col)``. + matches : (Q, 2) array + Indices of corresponding matches in first and second set of + descriptors, where ``matches[:, 0]`` denote the indices in the first + and ``matches[:, 1]`` the indices in the second set of descriptors. + keypoints_color : matplotlib color, optional + Color for keypoint locations. + matches_color : matplotlib color, optional + Color for lines which connect keypoint matches. By default the + color is chosen randomly. + only_matches : bool, optional + Whether to only plot matches and not plot the keypoint locations. + + """ + + image1 = img_as_float(image1) + image2 = img_as_float(image2) + + new_shape1 = list(image1.shape) + new_shape2 = list(image2.shape) + + if image1.shape[0] < image2.shape[0]: + new_shape1[0] = image2.shape[0] + elif image1.shape[0] > image2.shape[0]: + new_shape2[0] = image1.shape[0] + + if image1.shape[1] < image2.shape[1]: + new_shape1[1] = image2.shape[1] + elif image1.shape[1] > image2.shape[1]: + new_shape2[1] = image1.shape[1] + + if new_shape1 != image1.shape: + new_image1 = np.zeros(new_shape1, dtype=image1.dtype) + new_image1[:image1.shape[0], :image1.shape[1]] = image1 + image1 = new_image1 + + if new_shape2 != image2.shape: + new_image2 = np.zeros(new_shape2, dtype=image2.dtype) + new_image2[:image2.shape[0], :image2.shape[1]] = image2 + image2 = new_image2 + + image = np.concatenate([image1, image2], axis=1) + + offset = image1.shape + + if not only_matches: + ax.scatter(keypoints1[:, 1], keypoints1[:, 0], + facecolors='none', edgecolors=keypoints_color) + ax.scatter(keypoints2[:, 1] + offset[1], keypoints2[:, 0], + facecolors='none', edgecolors=keypoints_color) + + ax.imshow(image, interpolation='nearest', cmap='gray') + ax.axis((0, 2 * offset[1], offset[0], 0)) + + for i in range(matches.shape[0]): + idx1 = matches[i, 0] + idx2 = matches[i, 1] + + if matches_color is None: + color = np.random.rand(3, 1) + else: + color = matches_color + + ax.plot((keypoints1[idx1, 1], keypoints2[idx2, 1] + offset[1]), + (keypoints1[idx1, 0], keypoints2[idx2, 0]), + '-', color=color) + + +def _prepare_grayscale_input_2D(image): + image = np.squeeze(image) + if image.ndim != 2: + raise ValueError("Only 2-D gray-scale images supported.") + + return img_as_float(image) + + +def _mask_border_keypoints(image_shape, keypoints, distance): + """Mask coordinates that are within certain distance from the image border. + + Parameters + ---------- + image_shape : (2, ) array_like + Shape of the image as ``(rows, cols)``. + keypoints : (N, 2) array + Keypoint coordinates as ``(rows, cols)``. + distance : int + Image border distance. 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. + mask : (N, ) bool array + Mask indicating if pixels are within the image (``True``) or in the + border region of the image (``False``). """ - distance = (array1[:, None] != array2[None]).mean(axis=2) - return distance + + rows = image_shape[0] + cols = image_shape[1] + + mask = (((distance - 1) < keypoints[:, 0]) + & (keypoints[:, 0] < (rows - distance + 1)) + & ((distance - 1) < keypoints[:, 1]) + & (keypoints[:, 1] < (cols - distance + 1))) + + return mask diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index cef9b2e2..649eba6b 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,22 +1,29 @@ 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, 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 .thresholding import (threshold_adaptive, threshold_otsu, threshold_yen, + threshold_isodata) from . import rank +from skimage._shared.utils import deprecated +from skimage import restoration +denoise_bilateral = deprecated('skimage.restoration.denoise_bilateral')\ + (restoration.denoise_bilateral) +denoise_tv_bregman = deprecated('skimage.restoration.denoise_tv_bregman')\ + (restoration.denoise_tv_bregman) +denoise_tv_chambolle = deprecated('skimage.restoration.denoise_tv_chambolle')\ + (restoration.denoise_tv_chambolle) + + __all__ = ['inverse', 'wiener', 'LPIFilter2D', - 'median_filter', 'gaussian_filter', 'canny', 'sobel', @@ -37,6 +44,8 @@ __all__ = ['inverse', 'rank_order', 'gabor_kernel', 'gabor_filter', - 'threshold_otsu', 'threshold_adaptive', + 'threshold_otsu', + 'threshold_yen', + 'threshold_isodata', 'rank'] diff --git a/skimage/filter/_rank_order.py b/skimage/filter/_rank_order.py index cdd992ff..c7a5ce60 100644 --- a/skimage/filter/_rank_order.py +++ b/skimage/filter/_rank_order.py @@ -36,12 +36,12 @@ def rank_order(image): >>> a = np.array([[1, 4, 5], [4, 4, 1], [5, 1, 1]]) >>> a array([[1, 4, 5], - [4, 4, 1], - [5, 1, 1]]) + [4, 4, 1], + [5, 1, 1]]) >>> rank_order(a) (array([[0, 1, 2], - [1, 1, 0], - [2, 0, 0]], dtype=uint32), array([1, 4, 5])) + [1, 1, 0], + [2, 0, 0]], dtype=uint32), array([1, 4, 5])) >>> b = np.array([-1., 2.5, 3.1, 2.5]) >>> rank_order(b) (array([0, 1, 2, 1], dtype=uint32), array([-1. , 2.5, 3.1])) diff --git a/skimage/filter/ctmf.py b/skimage/filter/ctmf.py deleted file mode 100644 index e39aa8bc..00000000 --- a/skimage/filter/ctmf.py +++ /dev/null @@ -1,109 +0,0 @@ -"""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. - -Originally part of CellProfiler, code licensed under both GPL and BSD licenses. -Website: http://www.cellprofiler.org -Copyright (c) 2003-2009 Massachusetts Institute of Technology -Copyright (c) 2009-2011 Broad Institute -All rights reserved. -Original author: Lee Kamentsky -""" - -import 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. - - Parameters - ---------- - image : (M, N) ndarray - Input image. - 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 `percent` percent of the index range is chosen. - Default value of 50 gives the median pixel. - - Returns - ------- - 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("Input 'image' must be a two-dimensional array.") - - if radius < 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() - - if (not np.issubdtype(image.dtype, np.int) or - np.min(image) < 0 or np.max(image) > 255): - 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_values = image[mask] - was_ranked = False - 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(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_values > 255: - result = translation[output.astype(np.uint32) * - max_ranked_values // 255] - else: - result = translation[output] - else: - result = output - return result - diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index 5b5705f2..ef85c5cf 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -18,7 +18,7 @@ def _centre(x, oshape): """Return an array of oshape from the centre of x. """ - start = (np.array(x.shape) - np.array(oshape)) / 2. + 1 + start = (np.array(x.shape) - np.array(oshape)) // 2 + 1 out = x[[slice(s, s + n) for s, n in zip(start, oshape)]] return out @@ -62,12 +62,12 @@ class LPIFilter2D(object): >>> filter_params = {'kw1': 1, 'kw2': 2, 'kw3': 3} >>> impulse_response(r, c, **filter_params) + Examples -------- - Gaussian filter: - Use a 1-D gaussian in each direction without normalization - coefficients. + Gaussian filter: 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/__init__.py b/skimage/filter/rank/__init__.py index cfd034f1..8641b984 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -1,34 +1,11 @@ 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) + pop, threshold, tophat, noise_filter, entropy, otsu, sum) +from ._percentile import (autolevel_percentile, gradient_percentile, + mean_percentile, subtract_mean_percentile, + enhance_contrast_percentile, percentile, + pop_percentile, sum_percentile, threshold_percentile) +from .bilateral import mean_bilateral, pop_bilateral, sum_bilateral __all__ = ['autolevel', @@ -51,20 +28,13 @@ __all__ = ['autolevel', 'pop', 'pop_percentile', 'pop_bilateral', + 'sum', + 'sum_bilateral', + 'sum_percentile', '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'] + 'otsu', + 'percentile'] diff --git a/skimage/filter/rank/percentile.py b/skimage/filter/rank/_percentile.py similarity index 65% rename from skimage/filter/rank/percentile.py rename to skimage/filter/rank/_percentile.py index ff3b1559..01dd0b49 100644 --- a/skimage/filter/rank/percentile.py +++ b/skimage/filter/rank/_percentile.py @@ -50,17 +50,19 @@ 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 - percentiles [p0, p1] are used. + This filter locally stretches the histogram of greyvalues to cover the + entire range of values from "white" to "black". + + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -74,7 +76,7 @@ def autolevel_percentile(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -86,19 +88,18 @@ def autolevel_percentile(image, selem, out=None, mask=None, shift_x=False, 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. + """Return local gradient of an image (i.e. local maximum - local minimum). - gradient is computed on the given structuring element. Only - levels between percentiles [p0, p1] are used. + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -112,7 +113,7 @@ def gradient_percentile(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -124,19 +125,18 @@ def gradient_percentile(image, selem, out=None, mask=None, shift_x=False, 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. + """Return local mean of an image. - Mean is computed on the given structuring element. Only levels between - percentiles [p0, p1] are used. + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -150,7 +150,7 @@ def mean_percentile(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -162,19 +162,18 @@ def mean_percentile(image, selem, out=None, mask=None, shift_x=False, 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. + """Return image subtracted from its local mean. - subtract_mean is computed on the given structuring element. Only levels - between percentiles [p0, p1] are used. + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -188,7 +187,7 @@ def subtract_mean_percentile(image, selem, out=None, mask=None, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -200,19 +199,22 @@ def subtract_mean_percentile(image, selem, out=None, mask=None, 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. + """Enhance contrast of an image. - enhance_contrast is computed on the given structuring element. Only levels - between percentiles [p0, p1] are used. + This replaces each pixel by the local maximum if the pixel greyvalue is + closer to the local maximum than the local minimum. Otherwise it is + replaced by the local minimum. + + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -226,7 +228,7 @@ def enhance_contrast_percentile(image, selem, out=None, mask=None, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -238,19 +240,21 @@ def enhance_contrast_percentile(image, selem, out=None, mask=None, def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0): - """Return greyscale local percentile of an image. + """Return local percentile of an image. - percentile is computed on the given structuring element. Returns the value - of the p0 lower percentile of the neighborhood value distribution. + Returns the value of the p0 lower percentile of the local greyvalue + distribution. + + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -263,7 +267,7 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -275,19 +279,21 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, 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. + """Return the local number (population) of pixels. - pop is computed on the given structuring element. Only levels between - percentiles [p0, p1] are used. + The number of pixels is defined as the number of pixels which are included + in the structuring element and the mask. + + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -301,7 +307,7 @@ def pop_percentile(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -311,23 +317,63 @@ def pop_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=shift_y, p0=p0, p1=p1) -def threshold_percentile(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=0): - """Return greyscale local threshold of an image. +def sum_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0, p1=1): + """Return the local sum of pixels. - 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). + Only greyvalues between percentiles [p0, p1] are considered in the filter. + + Note that the sum may overflow depending on the data type of the input + array. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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 : 2-D array (same dtype as input image) + Output image. + + """ + + return _apply(percentile_cy._sum, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def threshold_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0): + """Local threshold of an image. + + The resulting binary mask is True if the greyvalue of the center pixel is + greater than the local mean. + + Only greyvalues between percentiles [p0, p1] are considered in the filter. + + Parameters + ---------- + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array + The neighborhood expressed as a 2-D array of 1's and 0's. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -338,10 +384,10 @@ def threshold_percentile(image, selem, out=None, mask=None, shift_x=False, p0 : float in [0, ..., 1] Set the percentile value. - out : ndarray (same dtype as input image) + Returns + ------- + out : 2-D array (same dtype as input image) Output image. - local threshold : ndarray (same dtype as input) - The result of the local threshold. """ diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index f1b10fec..d01680db 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -30,7 +30,7 @@ from . import bilateral_cy from .generic import _handle_input -__all__ = ['mean_bilateral', 'pop_bilateral'] +__all__ = ['mean_bilateral', 'pop_bilateral', 'sum_bilateral'] def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1, @@ -53,21 +53,22 @@ def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, pixels based on their spatial closeness and radiometric similarity. Spatial closeness is measured by considering only the local pixel - neighborhood given by a structuring element (selem). + neighborhood given by a structuring element. 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. + where g is the current pixel greylevel. + + Only pixels belonging to the structuring element and having a greylevel + inside this interval are averaged. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -81,22 +82,20 @@ def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. See also -------- - skimage.filter.denoise_bilateral for a gaussian bilateral filter. + skimage.filter.denoise_bilateral for a Gaussian bilateral filter. Examples -------- >>> from skimage import data >>> from skimage.morphology import disk - >>> from skimage.filter.rank import bilateral_mean - >>> # Load test image / cast to uint16 - >>> ima = data.camera().astype(np.uint16) - >>> # bilateral filtering of cameraman image using a flat kernel - >>> bilat_ima = bilateral_mean(ima, disk(20), s0=10,s1=10) + >>> from skimage.filter.rank import mean_bilateral + >>> img = data.camera().astype(np.uint16) + >>> bilat_img = mean_bilateral(img, disk(20), s0=10,s1=10) """ @@ -106,18 +105,22 @@ def mean_bilateral(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 - level inside the interval [g-s0, g+s1]. + """Return the local number (population) of pixels. + + + The number of pixels is defined as the number of pixels which are included + in the structuring element and the mask. Additionally the must have a + greylevel inside the interval [g-s0, g+s1] where g is the greyvalue of the + center pixel. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -131,20 +134,19 @@ def pop_bilateral(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples -------- - >>> # Local mean >>> from skimage.morphology import square >>> import skimage.filter.rank as rank - >>> ima16 = 255 * np.array([[0, 0, 0, 0, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 0, 0, 0, 0]], dtype=np.uint16) - >>> rank.bilateral_pop(ima16, square(3), s0=10,s1=10) + >>> img = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint16) + >>> rank.pop_bilateral(img, square(3), s0=10, s1=10) array([[3, 4, 3, 4, 3], [4, 4, 6, 4, 4], [3, 6, 9, 6, 3], @@ -155,3 +157,63 @@ def pop_bilateral(image, selem, out=None, mask=None, shift_x=False, return _apply(bilateral_cy._pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) + +def sum_bilateral(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, s0=10, s1=10): + """Apply a flat kernel bilateral filter. + + This is an edge-preserving and noise reducing denoising filter. It averages + pixels based on their spatial closeness and radiometric similarity. + + Spatial closeness is measured by considering only the local pixel + neighborhood given by a structuring element (selem). + + Radiometric similarity is defined by the greylevel interval [g-s0, g+s1] + where g is the current pixel greylevel. + + Only pixels belonging to the structuring element AND having a greylevel + inside this interval are summed. + + Note that the sum may overflow depending on the data type of the input + array. + + Parameters + ---------- + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array + The neighborhood expressed as a 2-D array of 1's and 0's. + out : 2-D array (same dtype as input) + If None, a new array is 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). + s0, s1 : int + Define the [s0, s1] interval around the greyvalue of the center pixel + to be considered for computing the value. + + Returns + ------- + out : 2-D array (same dtype as input image) + Output image. + + See also + -------- + skimage.filter.denoise_bilateral for a Gaussian bilateral filter. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import sum_bilateral + >>> img = data.camera().astype(np.uint16) + >>> bilat_img = sum_bilateral(img, disk(10), s0=10, s1=10) + + """ + + return _apply(bilateral_cy._sum, 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 index de2b3e53..25a81766 100644 --- a/skimage/filter/rank/bilateral_cy.pyx +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -47,6 +47,27 @@ cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, else: return 0 +cdef inline double _kernel_sum(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 sum = 0 + + if pop: + for i in range(max_bin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + sum += histo[i] * i + if bilat_pop: + return sum + else: + return 0 + else: + return 0 + def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, @@ -68,3 +89,13 @@ def _pop(dtype_t[:, ::1] image, _core(_kernel_pop[dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, s0, s1, max_bin) + +def _sum(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_sum[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 50c9a370..ccc1166e 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -77,16 +77,19 @@ def _apply(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Autolevel image using local histogram. + """Auto-level image using local histogram. + + This filter locally stretches the histogram of greyvalues to cover the + entire range of values from "white" to "black". Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -97,7 +100,7 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples @@ -105,10 +108,8 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): >>> 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)) + >>> img = data.camera() + >>> auto = autolevel(img, disk(5)) """ @@ -117,17 +118,20 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns greyscale local bottomhat of an image. + """Local bottom-hat of an image. + + This filter computes the morphological closing of the image and then + subtracts the result from the original image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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 + out : 2-D array (same dtype as input) + If None, a new array is allocated. + mask : 2-D array 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 @@ -137,8 +141,16 @@ def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - bottomhat : ndarray (same dtype as input image) - The result of the local bottomhat. + out : 2-D array (same dtype as input image) + Output image. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import bottomhat + >>> img = data.camera() + >>> out = bottomhat(img, disk(5)) """ @@ -151,12 +163,12 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -167,7 +179,7 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples @@ -175,10 +187,8 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): >>> 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)) + >>> img = data.camera() + >>> equ = equalize(img, disk(5)) """ @@ -187,18 +197,16 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): 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). - + """Return local gradient of an image (i.e. local maximum - local minimum). Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -209,9 +217,17 @@ def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import gradient + >>> img = data.camera() + >>> out = gradient(img, disk(5)) + """ return _apply(generic_cy._gradient, image, selem, @@ -219,17 +235,16 @@ def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local maximum of an image. - + """Return local maximum of an image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -240,17 +255,25 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (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 + Notes + ----- + The lower algorithm complexity makes the `skimage.filter.rank.maximum` + more efficient for larger images and structuring elements. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import maximum + >>> img = data.camera() + >>> out = maximum(img, disk(5)) """ @@ -259,16 +282,16 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local mean of an image. + """Return local mean of an image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -279,7 +302,7 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples @@ -287,10 +310,8 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): >>> 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)) + >>> img = data.camera() + >>> avg = mean(img, disk(5)) """ @@ -304,12 +325,12 @@ def subtract_mean(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -320,9 +341,17 @@ def subtract_mean(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import subtract_mean + >>> img = data.camera() + >>> out = subtract_mean(img, disk(5)) + """ return _apply(generic_cy._subtract_mean, image, selem, @@ -330,16 +359,16 @@ def subtract_mean(image, selem, out=None, mask=None, shift_x=False, def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local median of an image. + """Return local median of an image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -350,7 +379,7 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples @@ -358,10 +387,8 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): >>> 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)) + >>> img = data.camera() + >>> med = median(img, disk(5)) """ @@ -370,16 +397,16 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local minimum of an image. + """Return local minimum of an image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -390,17 +417,25 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (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 + Notes + ----- + The lower algorithm complexity makes the `skimage.filter.rank.minimum` more + efficient for larger images and structuring elements. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import minimum + >>> img = data.camera() + >>> out = minimum(img, disk(5)) """ @@ -409,16 +444,18 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local mode of an image. + """Return local mode of an image. + + The mode is the value that appears most often in the local histogram. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -429,9 +466,17 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import modal + >>> img = data.camera() + >>> out = modal(img, disk(5)) + """ return _apply(generic_cy._modal, image, selem, @@ -440,18 +485,20 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): 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. + """Enhance contrast of an image. + + This replaces each pixel by the local maximum if the pixel greyvalue is + closer to the local maximum than the local minimum. Otherwise it is + replaced by the local minimum. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -462,7 +509,7 @@ def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, Returns Output image. - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) The result of the local enhance_contrast. Examples @@ -470,10 +517,8 @@ def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, >>> 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)) + >>> img = data.camera() + >>> out = enhance_contrast(img, disk(5)) """ @@ -482,17 +527,19 @@ def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return the number (population) of pixels actually inside the - neighborhood. + """Return the local number (population) of pixels. + + The number of pixels is defined as the number of pixels which are included + in the structuring element and the mask. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -503,20 +550,19 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (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], + >>> img = 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)) + >>> rank.pop(img, square(3)) array([[4, 6, 6, 6, 4], [6, 9, 9, 9, 6], [6, 9, 9, 9, 6], @@ -529,17 +575,20 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): 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. +def sum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return the local sum of pixels. + + Note that the sum may overflow depending on the data type of the input + array. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -550,20 +599,68 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) + Output image. + + Examples + -------- + >>> from skimage.morphology import square + >>> import skimage.filter.rank as rank + >>> img = 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.sum(img, square(3)) + array([[1, 2, 3, 2, 1], + [2, 4, 6, 4, 2], + [3, 6, 9, 6, 3], + [2, 4, 6, 4, 2], + [1, 2, 3, 2, 1]], dtype=uint8) + + """ + + return _apply(generic_cy._sum, 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): + """Local threshold of an image. + + The resulting binary mask is True if the greyvalue of the center pixel is + greater than the local mean. + + Parameters + ---------- + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array + The neighborhood expressed as a 2-D array of 1's and 0's. + out : 2-D array (same dtype as input) + If None, a new array is 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 : 2-D array (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], + >>> img = 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)) + >>> threshold(img, square(3)) array([[0, 0, 0, 0, 0], [0, 1, 1, 1, 0], [0, 1, 0, 1, 0], @@ -577,16 +674,19 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local tophat of an image. + """Local top-hat of an image. + + This filter computes the morphological opening of the image and then + subtracts the result from the original image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -597,9 +697,17 @@ def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import tophat + >>> img = data.camera() + >>> out = tophat(img, disk(5)) + """ return _apply(generic_cy._tophat, image, selem, @@ -608,16 +716,16 @@ def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def noise_filter(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns the noise feature as described in [Hashimoto12]_ + """Noise feature as described in [Hashimoto12]_. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -633,9 +741,17 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import noise_filter + >>> img = data.camera() + >>> out = noise_filter(img, disk(5)) + """ # ensure that the central pixel in the structuring element is empty @@ -650,18 +766,19 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, 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. + """Local entropy [1]_. + + The entropy is computed using base 2 logarithm i.e. the filter returns the + minimum number of bits needed to encode the local greylevel distribution. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array 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. + out : 2-D array (same dtype as input) + If None, a new array is 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). @@ -677,16 +794,15 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): References ---------- - .. [1] http://en.wikipedia.org/wiki/Entropy_(information_theory)> + .. [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)) + >>> img = data.camera() + >>> ent = entropy(img, disk(5)) """ @@ -696,13 +812,13 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns the Otsu's threshold value for each pixel. + """Local Otsu's threshold value for each pixel. Parameters ---------- image : ndarray Image array (uint8 array). - selem : ndarray + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray If None, a new array will be allocated. @@ -716,7 +832,7 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. References @@ -725,14 +841,12 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): 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 + >>> img = data.camera() + >>> local_otsu = otsu(img, disk(5)) + >>> thresh_image = img >= local_otsu """ diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index dcf6e361..1c26cf53 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -222,6 +222,22 @@ cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, return pop +cdef inline double _kernel_sum(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] * i + return sum + 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, @@ -455,6 +471,15 @@ def _pop(dtype_t[:, ::1] image, _core(_kernel_pop[dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) +def _sum(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_sum[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, diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index e951a76e..38d04b33 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -90,6 +90,29 @@ cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, else: return 0 +cdef inline double _kernel_sum(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, sum_g, n + + if pop: + sum = 0 + sum_g = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + sum_g += histo[i] * i + + if n > 0: + return sum_g + else: + return 0 + else: + return 0 cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, dtype_t g, @@ -245,6 +268,15 @@ def _mean(dtype_t[:, ::1] image, _core(_kernel_mean[dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) +def _sum(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_sum[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, diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index 1d573b81..72f9afe6 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -437,7 +437,7 @@ def test_16bit(): 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 + assert rank.mean(image, selem)[10, 10] == int(value / selem.size) def test_bilateral(): @@ -498,6 +498,48 @@ def test_percentile_median(): img_max = rank.median(img16, selem=selem) assert_array_equal(img_p0, img_max) +def test_sum(): + # check the number of valid pixels in the neighborhood + + image8 = 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) + image16 = 400*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) + elem = np.ones((3, 3), dtype=np.uint8) + out8 = np.empty_like(image8) + out16 = np.empty_like(image16) + mask = np.ones(image8.shape, dtype=np.uint8) + + r = np.array([[1, 2, 3, 2, 1], + [2, 4, 6, 4, 2], + [3, 6, 9, 6, 3], + [2, 4, 6, 4, 2], + [1, 2, 3, 2, 1]], dtype=np.uint8) + rank.sum(image=image8, selem=elem, out=out8, mask=mask) + assert_array_equal(r, out8) + rank.sum_percentile(image=image8, selem=elem, out=out8, mask=mask,p0=.0,p1=1.) + assert_array_equal(r, out8) + rank.sum_bilateral(image=image8, selem=elem, out=out8, mask=mask,s0=255,s1=255) + assert_array_equal(r, out8) + + r = 400* np.array([[1, 2, 3, 2, 1], + [2, 4, 6, 4, 2], + [3, 6, 9, 6, 3], + [2, 4, 6, 4, 2], + [1, 2, 3, 2, 1]], dtype=np.uint16) + rank.sum(image=image16, selem=elem, out=out16, mask=mask) + assert_array_equal(r, out16) + rank.sum_percentile(image=image16, selem=elem, out=out16, mask=mask,p0=.0,p1=1.) + assert_array_equal(r, out16) + rank.sum_bilateral(image=image16, selem=elem, out=out16, mask=mask,s0=1000,s1=1000) + assert_array_equal(r, out16) + if __name__ == "__main__": run_module_suite() diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index 33ad97df..2f5a72e6 100644 --- a/skimage/filter/setup.py +++ b/skimage/filter/setup.py @@ -11,9 +11,9 @@ def configuration(parent_package='', top_path=None): config = Configuration('filter', parent_package, top_path) config.add_data_dir('tests') + config.add_data_dir('rank/tests') cython(['_ctmf.pyx'], working_path=base_path) - cython(['_denoise_cy.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) @@ -21,8 +21,6 @@ def configuration(parent_package='', top_path=None): 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.core_cy', sources=['rank/core_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('rank.generic_cy', sources=['rank/generic_cy.c'], diff --git a/skimage/filter/tests/test_ctmf.py b/skimage/filter/tests/test_ctmf.py deleted file mode 100644 index c4f6d10e..00000000 --- a/skimage/filter/tests/test_ctmf.py +++ /dev/null @@ -1,127 +0,0 @@ -import numpy as np -from nose.tools import raises - -from skimage.filter import median_filter - - -def test_00_00_zeros(): - '''The median filter on an array of all zeros should be zero''' - result = median_filter(np.zeros((10, 10)), 3, np.ones((10, 10), bool)) - assert np.all(result == 0) - - -def test_00_01_all_masked(): - '''Test a completely masked image - - Regression test of IMG-1029''' - result = median_filter(np.zeros((10, 10)), 3, np.zeros((10, 10), bool)) - assert (np.all(result == 0)) - - -def test_00_02_all_but_one_masked(): - mask = np.zeros((10, 10), bool) - mask[5, 5] = True - median_filter(np.zeros((10, 10)), 3, mask) - - -def test_01_01_mask(): - '''The median filter, masking a single value''' - img = np.zeros((10, 10)) - img[5, 5] = 1 - mask = np.ones((10, 10), bool) - mask[5, 5] = False - result = median_filter(img, 3, mask) - assert (np.all(result[mask] == 0)) - np.testing.assert_equal(result[5, 5], 1) - - -def test_02_01_median(): - '''A median filter larger than the image = median of image''' - np.random.seed(0) - img = np.random.uniform(size=(9, 9)) - result = median_filter(img, 20, np.ones((9, 9), bool)) - np.testing.assert_equal(result[0, 0], np.median(img)) - assert (np.all(result == np.median(img))) - - -def test_02_02_median_bigger(): - '''Use an image of more than 255 values to test approximation''' - np.random.seed(0) - img = np.random.uniform(size=(20, 20)) - result = median_filter(img, 40, np.ones((20, 20), bool)) - sorted = np.ravel(img) - sorted.sort() - min_acceptable = sorted[198] - max_acceptable = sorted[202] - assert (np.all(result >= min_acceptable)) - assert (np.all(result <= max_acceptable)) - - -def test_03_01_shape(): - '''Make sure the median filter is the expected octagonal shape''' - - radius = 5 - a_2 = int(radius / 2.414213) - i, j = np.mgrid[-10:11, -10:11] - octagon = np.ones((21, 21), bool) - # - # constrain the octagon mask to be the points that are on - # the correct side of the 8 edges - # - octagon[i < -radius] = False - octagon[i > radius] = False - octagon[j < -radius] = False - octagon[j > radius] = False - octagon[i + j < -radius - a_2] = False - octagon[j - i > radius + a_2] = False - octagon[i + j > radius + a_2] = False - octagon[i - j > radius + a_2] = False - np.random.seed(0) - img = np.random.uniform(size=(21, 21)) - result = median_filter(img, radius, np.ones((21, 21), bool)) - sorted = img[octagon] - sorted.sort() - min_acceptable = sorted[len(sorted) / 2 - 1] - max_acceptable = sorted[len(sorted) / 2 + 1] - assert (result[10, 10] >= min_acceptable) - assert (result[10, 10] <= max_acceptable) - - -def test_04_01_half_masked(): - '''Make sure that the median filter can handle large masked areas.''' - img = np.ones((20, 20)) - mask = np.ones((20, 20), bool) - mask[10:, :] = False - img[~ mask] = 2 - img[1, 1] = 0 # to prevent short circuit for uniform data. - result = median_filter(img, 5, mask) - # in partial coverage areas, the result should be only - # from the masked pixels - assert (np.all(result[:14, :] == 1)) - # in zero coverage areas, the result should be the lowest - # value in the valid area - assert (np.all(result[15:, :] == np.min(img[mask]))) - - -def test_default_values(): - img = (np.random.random((20, 20)) * 255).astype(np.uint8) - mask = np.ones((20, 20), dtype=np.uint8) - result1 = median_filter(img, radius=2, mask=mask, percent=50) - result2 = median_filter(img) - np.testing.assert_array_equal(result1, result2) - - -@raises(ValueError) -def test_insufficient_size(): - img = (np.random.random((20, 20)) * 255).astype(np.uint8) - median_filter(img, radius=1) - - -@raises(TypeError) -def test_wrong_shape(): - img = np.empty((10, 10, 3)) - median_filter(img) - - -if __name__ == "__main__": - np.testing.run_module_suite() diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 0edfe4e7..f0a68f7d 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -5,7 +5,8 @@ import skimage from skimage import data from skimage.filter.thresholding import (threshold_adaptive, threshold_otsu, - threshold_yen) + threshold_yen, + threshold_isodata) class TestSimpleImage(): @@ -43,10 +44,29 @@ class TestSimpleImage(): assert threshold_yen(image) == 127 def test_yen_binary(self): - image = np.zeros([2,256], dtype='uint8') + image = np.zeros([2,256], dtype=np.uint8) image[0] = 255 assert threshold_yen(image) < 1 + def test_yen_blank_zero(self): + image = np.zeros((5, 5), dtype=np.uint8) + assert threshold_yen(image) == 0 + + def test_yen_blank_max(self): + image = np.empty((5, 5), dtype=np.uint8) + image.fill(255) + assert threshold_yen(image) == 255 + + def test_isodata(self): + assert threshold_isodata(self.image) == 2 + + def test_isodata_blank_zero(self): + image = np.zeros((5, 5), dtype=np.uint8) + assert threshold_isodata(image) == 0 + + def test_isodata_linspace(self): + assert -63.8 < threshold_isodata(np.linspace(-127, 0, 256)) < -63.6 + def test_threshold_adaptive_generic(self): def func(arr): return arr.sum() / arr.shape[0] @@ -114,6 +134,11 @@ def test_otsu_lena_image(): assert 140 < threshold_otsu(lena) < 142 +def test_yen_camera_image(): + camera = skimage.img_as_ubyte(data.camera()) + assert 197 < threshold_yen(camera) < 199 + + def test_yen_coins_image(): coins = skimage.img_as_ubyte(data.coins()) assert 109 < threshold_yen(coins) < 111 @@ -124,5 +149,32 @@ def test_yen_coins_image_as_float(): assert 0.43 < threshold_yen(coins) < 0.44 +def test_isodata_camera_image(): + camera = skimage.img_as_ubyte(data.camera()) + assert threshold_isodata(camera) == 88 + + +def test_isodata_coins_image(): + coins = skimage.img_as_ubyte(data.coins()) + assert threshold_isodata(coins) == 107 + + +def test_isodata_moon_image(): + moon = skimage.img_as_ubyte(data.moon()) + assert threshold_isodata(moon) == 87 + + +def test_isodata_moon_image_negative_int(): + moon = skimage.img_as_ubyte(data.moon()).astype(np.int32) + moon -= 100 + assert threshold_isodata(moon) == -13 + + +def test_isodata_moon_image_negative_float(): + moon = skimage.img_as_ubyte(data.moon()).astype(np.float64) + moon -= 100 + assert -13 < threshold_isodata(moon) < -12 + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 7f980387..6ccf8f6b 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -1,4 +1,7 @@ -__all__ = ['threshold_adaptive', 'threshold_otsu', 'threshold_yen'] +__all__ = ['threshold_adaptive', + 'threshold_otsu', + 'threshold_yen', + 'threshold_isodata'] import numpy as np import scipy.ndimage @@ -57,7 +60,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, Examples -------- >>> from skimage.data import camera - >>> image = camera() + >>> image = camera()[:50, :50] >>> binary_image1 = threshold_adaptive(image, 15, 'mean') >>> func = lambda arr: arr.mean() >>> binary_image2 = threshold_adaptive(image, 15, 'generic', param=func) @@ -129,7 +132,7 @@ def threshold_otsu(image, nbins=256): # Clip ends to align class 1 and class 2 variables: # The last value of `weight1`/`mean1` should pair with zero values in # `weight2`/`mean2`, which do not exist. - variance12 = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:])**2 + variance12 = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:]) ** 2 idx = np.argmax(variance12) threshold = bin_centers[:-1][idx] @@ -172,15 +175,84 @@ def threshold_yen(image, nbins=256): >>> 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) + # On blank images (e.g. filled with 0) with int dtype, `histogram()` + # returns `bin_centers` containing only one value. Speed up with it. + if bin_centers.size == 1: + return bin_centers[0] + + # Calculate probability mass function + pmf = hist.astype(np.float32) / hist.sum() + P1 = np.cumsum(pmf) # Cumulative normalized histogram + P1_sq = np.cumsum(pmf ** 2) # Get cumsum calculated from end of squared array: - P2_sq = np.cumsum(norm_histo[::-1] ** 2)[::-1] + P2_sq = np.cumsum(pmf[::-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) * \ + 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 bin_centers[crit.argmax()] + + +def threshold_isodata(image, nbins=256): + """Return threshold value based on ISODATA method. + + Histogram-based threshold, known as Ridler-Calvard method or intermeans. + + 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 or int, corresponding input array dtype. + Upper threshold value. All pixels intensities that less or equal of + this value assumed as background. + + References + ---------- + .. [1] Ridler, TW & Calvard, S (1978), "Picture thresholding using an + iterative selection method" + .. [2] IEEE Transactions on Systems, Man and Cybernetics 8: 630-632, + http://ieeexplore.ieee.org/xpls/abs_all.jsp?arnumber=4310039 + .. [3] 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 + .. [4] ImageJ AutoThresholder code, + http://fiji.sc/wiki/index.php/Auto_Threshold + + Examples + -------- + >>> from skimage.data import coins + >>> image = coins() + >>> thresh = threshold_isodata(image) + >>> binary = image > thresh + """ + hist, bin_centers = histogram(image, nbins) + # On blank images (e.g. filled with 0) with int dtype, `histogram()` + # returns `bin_centers` containing only one value. Speed up with it. + if bin_centers.size == 1: + return bin_centers[0] + # It is not necessary to calculate the probability mass function here, + # because the l and h fractions already include the normalization. + pmf = hist.astype(np.float32) # / hist.sum() + cpmfl = np.cumsum(pmf, dtype=np.float32) + cpmfh = np.cumsum(pmf[::-1], dtype=np.float32)[::-1] + + binnums = np.arange(pmf.size, dtype=np.uint8) + # l and h contain average value of pixels in sum of bins, calculated + # from lower to higher and from higher to lower respectively. + l = np.ma.divide(np.cumsum(pmf * binnums, dtype=np.float32), cpmfl) + h = np.ma.divide( + np.cumsum((pmf[::-1] * binnums[::-1]), dtype=np.float32)[::-1], + cpmfh) + + allmean = (l + h) / 2.0 + threshold = bin_centers[np.nonzero(allmean.round() == binnums)[0][0]] + # This implementation returns threshold where + # `background <= threshold < foreground`. return threshold diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index eb817c77..a335971d 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -1,7 +1,9 @@ from .spath import shortest_path -from .mcp import MCP, MCP_Geometric, route_through_array +from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array __all__ = ['shortest_path', 'MCP', 'MCP_Geometric', + 'MCP_Connect', + 'MCP_Flexible', 'route_through_array'] \ No newline at end of file diff --git a/skimage/graph/_mcp.pxd b/skimage/graph/_mcp.pxd index b2e2a548..1cfdbc9a 100644 --- a/skimage/graph/_mcp.pxd +++ b/skimage/graph/_mcp.pxd @@ -9,22 +9,36 @@ cimport numpy as cnp ctypedef heap.BOOL_T BOOL_T ctypedef unsigned char DIM_T ctypedef cnp.float64_t FLOAT_T +ctypedef cnp.intp_t INDEX_T +ctypedef cnp.int8_t EDGE_T +ctypedef cnp.int8_t OFFSET_T +ctypedef cnp.int16_t OFFSETS_INDEX_T + cdef class MCP: cdef heap.FastUpdateBinaryHeap costs_heap cdef object costs_shape + cdef object _starts + cdef object _ends cdef DIM_T dim - cdef object flat_costs - cdef object flat_cumulative_costs - cdef object traceback_offsets - cdef object flat_pos_edge_map - cdef object flat_neg_edge_map - cdef readonly object offsets - cdef object flat_offsets - cdef object offset_lengths cdef BOOL_T dirty cdef BOOL_T use_start_cost # if use_start_cost is true, the cost of the starting element is added to # the cost of the path. Set to true by default in the base class... - + + # Arrays used during front propagation + cdef FLOAT_T [:] flat_costs + cdef FLOAT_T [:] flat_cumulative_costs + cdef OFFSETS_INDEX_T [:] traceback_offsets + cdef EDGE_T [:,:] flat_pos_edge_map + cdef EDGE_T [:,:] flat_neg_edge_map + cdef OFFSET_T [:,:] offsets + cdef INDEX_T [:] flat_offsets + cdef FLOAT_T [:] offset_lengths + + # Methods + cpdef int goal_reached(self, INDEX_T index, FLOAT_T cumcost) cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, FLOAT_T offset_length) + cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index, FLOAT_T offset_length) + cdef void _update_node(self, INDEX_T index, INDEX_T new_index, FLOAT_T offset_length) + \ No newline at end of file diff --git a/skimage/graph/_mcp.pyx b/skimage/graph/_mcp.pyx index 34904dcc..1223f3bc 100644 --- a/skimage/graph/_mcp.pyx +++ b/skimage/graph/_mcp.pyx @@ -5,6 +5,7 @@ for use with data on a n-dimensional lattice. Original author: Zachary Pincus Inspired by code from Almar Klein +Later modifications by Almar Klein (Dec 2013) License: BSD @@ -39,13 +40,9 @@ import heap cimport numpy as cnp cimport heap -ctypedef cnp.int8_t OFFSET_T OFFSET_D = np.int8 -ctypedef cnp.int16_t OFFSETS_INDEX_T OFFSETS_INDEX_D = np.int16 -ctypedef cnp.int8_t EDGE_T EDGE_D = np.int8 -ctypedef cnp.intp_t INDEX_T INDEX_D = np.intp FLOAT_D = np.float64 @@ -106,7 +103,7 @@ def _offset_edge_map(shape, offsets): [0, 0, 2, 1]], dtype=int8) """ - indices = np.indices(shape) # indices.shape = (n,)+shape + indices = np.indices(shape) # indices.shape = (n,)+shape #get the distance from each index to the upper or lower edge in each dim pos_edges = (shape - indices.T).T @@ -172,11 +169,12 @@ def make_offsets(d, fully_connected): def _unravel_index_fortran(flat_indices, shape): """_unravel_index_fortran(flat_indices, shape) - Given a flat index into an n-d fortran-strided array, return an index tuple. + Given a flat index into an n-d fortran-strided array, return an + index tuple. """ strides = np.multiply.accumulate([1] + list(shape[:-1])) - indices = [tuple(idx/strides % shape) for idx in flat_indices] + indices = [tuple((idx // strides) % shape) for idx in flat_indices] return indices @@ -185,7 +183,8 @@ def _unravel_index_fortran(flat_indices, shape): def _ravel_index_fortran(indices, shape): """_ravel_index_fortran(flat_indices, shape) - Given an index tuple into an n-d fortran-strided array, return a flat index. + Given an index tuple into an n-d fortran-strided array, return a + flat index. """ strides = np.multiply.accumulate([1] + list(shape[:-1])) @@ -209,7 +208,7 @@ def _normalize_indices(indices, shape): for i, s in zip(index, shape): i = int(i) if i < 0: - i = s+i + i = s + i if not (0 <= i < s): return None new_index.append(i) @@ -220,14 +219,15 @@ def _normalize_indices(indices, shape): @cython.boundscheck(True) @cython.wraparound(True) def _reverse(arr): - """Reverse index an array safely, with bounds/wraparound checks on.""" + """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) + """MCP(costs, offsets=None, fully_connected=True, sampling=None) A class for finding the minimum cost path through a given n-d costs array. @@ -261,6 +261,9 @@ cdef class MCP: generated neighborhood. If true, the path may go along diagonals between elements of the `costs` array; otherwise only axial moves are permitted. + sampling : tuple, optional + For each dimension, specifies the distance between two cells/voxels. + If not given or None, the distance is assumed unit. Attributes ---------- @@ -271,8 +274,10 @@ cdef class MCP: returned by the find_costs() method. """ - def __init__(self, costs, offsets=None, fully_connected=True): - """__init__(costs, offsets=None, fully_connected=True) + + def __init__(self, costs, offsets=None, fully_connected=True, + sampling=None): + """__init__(costs, offsets=None, fully_connected=True, sampling=None) See class documentation. """ @@ -280,6 +285,16 @@ cdef class MCP: if not np.can_cast(costs.dtype, FLOAT_D): raise TypeError('cannot cast costs array to ' + str(FLOAT_D)) + # Check sampling + if sampling is None: + sampling = np.array([1.0 for s in costs.shape], FLOAT_D) + elif isinstance(sampling, (list, tuple)): + sampling = np.array(sampling, FLOAT_D) + if sampling.ndim != 1 or len(sampling) != costs.ndim: + raise ValueError('Need one sampling element per dimension.') + else: + raise ValueError('Invalid type for sampling: %r.' % type(sampling)) + # We use flat, fortran-style indexing here (could use C-style, # but this is my code and I like fortran-style! Also, it's # faster when working with image arrays, which are often @@ -287,23 +302,21 @@ cdef class MCP: self.flat_costs = costs.astype(FLOAT_D).flatten('F') size = self.flat_costs.shape[0] self.flat_cumulative_costs = np.empty(size, dtype=FLOAT_D) - self.flat_cumulative_costs.fill(np.inf) self.dim = len(costs.shape) self.costs_shape = costs.shape - self.costs_heap = heap.FastUpdateBinaryHeap(initial_capacity=size, + self.costs_heap = heap.FastUpdateBinaryHeap(initial_capacity=128, max_reference=size-1) # This array stores, for each point, the index into the offset # array (see below) that leads to that point from the # predecessor point. self.traceback_offsets = np.empty(size, dtype=OFFSETS_INDEX_D) - self.traceback_offsets.fill(-1) # The offsets are a list of relative offsets from a central # point to each point in the relevant neighborhood. (e.g. (-1, # 0) might be a 2d offset). - # These offsets are raveled to provide flat, 1d offsets that can be used - # in the same way for flat indices to move to neighboring points. + # These offsets are raveled to provide flat, 1d offsets that can be + # used in the same way for flat indices to move to neighboring points. if offsets is None: offsets = make_offsets(self.dim, fully_connected) self.offsets = np.array(offsets, dtype=OFFSET_D) @@ -313,9 +326,9 @@ cdef class MCP: # Instead of unraveling each index during the pathfinding algorithm, we # will use a pre-computed "edge map" that specifies for each dimension - # whether a given index is on a lower or upper boundary (or none at all) - # Flatten this map to get something that can be indexed as by the same - # flat indices as elsewhere. + # whether a given index is on a lower or upper boundary (or none at + # all). Flatten this map to get something that can be indexed as by the + # same flat indices as elsewhere. # The edge map stores more than a boolean "on some edge" flag so as to # allow us to examine the non-out-of-bounds neighbors for a given edge # point while excluding the neighbors which are outside the array. @@ -325,26 +338,83 @@ cdef class MCP: # The offset lengths are the distances traveled along each offset - self.offset_lengths = np.sqrt( - np.sum(self.offsets**2, axis=1)).astype(FLOAT_D) + self.offset_lengths = np.sqrt(np.sum((sampling * self.offsets)**2, + axis=1)).astype(FLOAT_D) self.dirty = 0 self.use_start_cost = 1 + def _reset(self): """_reset() - Clears paths found by find_costs(). """ + + cdef INDEX_T start + self.costs_heap.reset() - self.traceback_offsets.fill(-1) - self.flat_cumulative_costs.fill(np.inf) + self.traceback_offsets[...] = -2 # -2 is not reached, -1 is start + self.flat_cumulative_costs[...] = np.inf self.dirty = 0 + # Get starts and ends + # We do not pass them in as arguments for backwards compat + starts, ends = self._starts, self._ends + + # push each start point into the heap. Note that we use flat indexing! + for start in _ravel_index_fortran(starts, self.costs_shape): + self.traceback_offsets[start] = -1 + if self.use_start_cost: + self.costs_heap.push_fast(self.flat_costs[start], start) + else: + self.costs_heap.push_fast(0, start) + + cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, FLOAT_T offset_length): + """ float _travel_cost(float old_cost, float new_cost, + float offset_length) + The travel cost for going from the current node to the next. + Default is simply the cost of the next node. + """ return new_cost - def find_costs(self, starts, ends=None, find_all_ends=True): + + cpdef int goal_reached(self, INDEX_T index, FLOAT_T cumcost): + """ int goal_reached(int index, float cumcost) + This method is called each iteration after popping an index + from the heap, before examining the neighbours. + + This method can be overloaded to modify the behavior of the MCP + algorithm. An example might be to stop the algorithm when a + certain cumulative cost is reached, or when the front is a + certain distance away from the seed point. + + This method should return 1 if the algorithm should not check + the current point's neighbours and 2 if the algorithm is now + done. + """ + return 0 + + + cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ _examine_neighbor(int index, int new_index, float offset_length) + This method is called once for every pair of neighboring nodes, + as soon as both nodes become frozen. + """ + pass + + + cdef void _update_node(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ _update_node(int index, int new_index, float offset_length) + This method is called when a node is updated. + """ + pass + + + def find_costs(self, starts, ends=None, find_all_ends=True, + max_coverage=1.0, max_cumulative_cost=None, max_cost=None): """ Find the minimum-cost path from the given starting points. @@ -392,10 +462,13 @@ cdef class MCP: cdef BOOL_T use_ends = 0 cdef INDEX_T num_ends cdef BOOL_T all_ends = find_all_ends - cdef cnp.ndarray[INDEX_T, ndim=1] flat_ends + cdef INDEX_T[:] flat_ends starts = _normalize_indices(starts, self.costs_shape) if starts is None: raise ValueError('start points must all be within the costs array') + elif not starts: + raise ValueError('no valid start points to start front' + + 'propagation') if ends is not None: ends = _normalize_indices(ends, self.costs_shape) if ends is None: @@ -406,44 +479,44 @@ cdef class MCP: flat_ends = np.array(_ravel_index_fortran( ends, self.costs_shape), dtype=INDEX_D) - if self.dirty: - self._reset() + # Always perform a reset to (re)initialize our arrays and start + # positions + self._starts, self._ends = starts, ends + self._reset() - # lookup and array-ify object attributes for fast use + # Get shorter names for arrays + cdef FLOAT_T[:] flat_costs = self.flat_costs + cdef FLOAT_T[:] flat_cumulative_costs = self.flat_cumulative_costs + cdef OFFSETS_INDEX_T[:] traceback_offsets = self.traceback_offsets + cdef EDGE_T[:, :] flat_pos_edge_map = self.flat_pos_edge_map + cdef EDGE_T[:, :] flat_neg_edge_map = self.flat_neg_edge_map + cdef OFFSET_T[:, :] offsets = self.offsets + cdef INDEX_T[:] flat_offsets = self.flat_offsets + cdef FLOAT_T[:] offset_lengths = self.offset_lengths + + # Short names for other attributes cdef heap.FastUpdateBinaryHeap costs_heap = self.costs_heap - cdef cnp.ndarray[FLOAT_T, ndim=1] flat_costs = self.flat_costs - cdef cnp.ndarray[FLOAT_T, ndim=1] flat_cumulative_costs = \ - self.flat_cumulative_costs - cdef cnp.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ - self.traceback_offsets - cdef cnp.ndarray[EDGE_T, ndim=2] flat_pos_edge_map = \ - self.flat_pos_edge_map - cdef cnp.ndarray[EDGE_T, ndim=2] flat_neg_edge_map = \ - self.flat_neg_edge_map - cdef cnp.ndarray[OFFSET_T, ndim=2] offsets = self.offsets - cdef cnp.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets - cdef cnp.ndarray[FLOAT_T, ndim=1] offset_lengths = self.offset_lengths - cdef DIM_T dim = self.dim cdef int num_offsets = len(flat_offsets) - # push each start point into the heap. Note that we use flat indexing! - for start in _ravel_index_fortran(starts, self.costs_shape): - if self.use_start_cost: - costs_heap.push_fast(flat_costs[start], start) - else: - costs_heap.push_fast(0, start) - - cdef FLOAT_T cost, new_cost + # Variables used during front propagation + cdef FLOAT_T cost, new_cost, cumcost, new_cumcost, offset_length cdef INDEX_T index, new_index cdef BOOL_T is_at_edge, use_offset - cdef INDEX_T d, i + cdef INDEX_T d, i, iter cdef OFFSET_T offset cdef EDGE_T pos_edge_val, neg_edge_val cdef int num_ends_found = 0 cdef FLOAT_T inf = np.inf - cdef FLOAT_T travel_cost - while 1: + cdef int goal_reached + + cdef INDEX_T maxiter = int(max_coverage * flat_costs.size) + + for iter in range(maxiter): + + # This is rather like a while loop, except we are guaranteed to + # exit, which is nice during developing to prevent eternal loops. + # Find the point with the minimum cost in the heap. Once # popped, this point's minimum cost path has been found. if costs_heap.count == 0: @@ -451,11 +524,20 @@ cdef class MCP: # point in the array break - cost = costs_heap.pop_fast() + # Get current cumulative cost and index from the heap + cumcost = costs_heap.pop_fast() index = costs_heap._popped_ref # Record the cost we found to this point - flat_cumulative_costs[index] = cost + flat_cumulative_costs[index] = cumcost + + # Check if goal is reached + goal_reached = self.goal_reached(index, cumcost) + if goal_reached > 0: + if goal_reached == 1: + continue # Skip neighbours + else: + break # Done completely if use_ends: # If we're only tracing out a path to one or more @@ -508,38 +590,55 @@ cdef class MCP: # using the flat offsets, calculate the new flat index new_index = index + flat_offsets[i] + # Get offset length + offset_length = offset_lengths[i] + # If we have already found the best path here then # ignore this point if flat_cumulative_costs[new_index] != inf: + # Give subclass the oportunity to examine these two nodes + # Note that only when both nodes are "frozen" their + # cumulative cost is set. By doing the check here, each + # pair of nodes is checked exactly once. + self._examine_neighbor(index, new_index, offset_length) continue - # If the cost at this point is negative or infinite, ignore it + + # Get cost and new cost + cost = flat_costs[index] new_cost = flat_costs[new_index] + + # If the cost at this point is negative or infinite, ignore it if new_cost < 0 or new_cost == inf: continue + # Calculate new cumulative cost + new_cumcost = cumcost + self._travel_cost(cost, new_cost, + offset_length) + # Now we ask the heap to append or update the cost to # this new point, but only if that point isn't already # in the heap, or it is but the new cost is lower. - travel_cost = self._travel_cost(flat_costs[index], - new_cost, - offset_lengths[i]) # don't push infs into the heap though! - new_cost = cost + travel_cost - if new_cost != inf: - costs_heap.push_if_lower_fast(new_cost, new_index) + if new_cumcost != inf: + costs_heap.push_if_lower_fast(new_cumcost, new_index) # If we did perform an append or update, we should # record the offset from the predecessor to this new # point if costs_heap._pushed: traceback_offsets[new_index] = i + self._update_node(index, new_index, offset_length) + # Un-flatten the costs and traceback arrays for human consumption. - cumulative_costs = flat_cumulative_costs.reshape(self.costs_shape, - order='F') - traceback = traceback_offsets.reshape(self.costs_shape, order='F') + cumulative_costs = np.asarray(flat_cumulative_costs) + cumulative_costs = cumulative_costs.reshape(self.costs_shape, + order='F') + traceback = np.asarray(traceback_offsets) + traceback = traceback.reshape(self.costs_shape, order='F') self.dirty = 1 return cumulative_costs, traceback + def traceback(self, end): """traceback(end) @@ -580,12 +679,12 @@ cdef class MCP: raise ValueError('no minimum-cost path was found ' 'to the specified end point') - cdef cnp.ndarray[INDEX_T, ndim=1] position = \ - np.array(ends[0], dtype=INDEX_D) - cdef cnp.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ - self.traceback_offsets - cdef cnp.ndarray[OFFSET_T, ndim=2] offsets = self.offsets - cdef cnp.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets + # Short names for arrays + cdef OFFSETS_INDEX_T [:] traceback_offsets = self.traceback_offsets + cdef OFFSET_T [:,:] offsets = self.offsets + cdef INDEX_T [:] flat_offsets = self.flat_offsets + # New array + cdef INDEX_T [:] position = np.array(ends[0], dtype=INDEX_D) cdef OFFSETS_INDEX_T offset cdef DIM_T d @@ -602,6 +701,7 @@ cdef class MCP: return _reverse(traceback) + @cython.boundscheck(False) @cython.wraparound(False) cdef class MCP_Geometric(MCP): @@ -626,15 +726,17 @@ cdef class MCP_Geometric(MCP): `(sqrt(2)/2)*costs[1,1] + (sqrt(2)/2)*costs[2,2]`. These calculations don't make a lot of sense with offsets of magnitude - greater than 1. + greater than 1. Use the `sampling` argument in order to deal with + anisotropic data. """ - def __init__(self, costs, offsets=None, fully_connected=True): - """__init__(costs, offsets=None, fully_connected=True) + def __init__(self, costs, offsets=None, fully_connected=True, + sampling=None): + """__init__(costs, offsets=None, fully_connected=True, sampling=None) See class documentation. """ - MCP.__init__(self, costs, offsets, fully_connected) + MCP.__init__(self, costs, offsets, fully_connected, sampling) if np.absolute(self.offsets).max() > 1: raise ValueError('all offset components must be 0, 1, or -1') self.use_start_cost = 0 @@ -642,3 +744,187 @@ cdef class MCP_Geometric(MCP): cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, FLOAT_T offset_length): return offset_length * 0.5 * (old_cost + new_cost) + + + +@cython.boundscheck(True) +@cython.wraparound(True) +cdef class MCP_Connect(MCP): + """MCP_Connect(costs, offsets=None, fully_connected=True) + + Connect source points using the distance-weighted minimum cost function. + + A front is grown from each seed point simultaneously, while the + origin of the front is tracked as well. When two fronts meet, + create_connection() is called. This method must be overloaded to + deal with the found edges in a way that is appropriate for the + application. + """ + + cdef INDEX_T [:] flat_idmap + + + def __init__(self, costs, offsets=None, fully_connected=True, + sampling=None): + MCP.__init__(self, costs, offsets, fully_connected, sampling) + + # Create id map to keep track of origin of nodes + self.flat_idmap = np.zeros(self.costs_shape, INDEX_D).ravel('F') + + + def _reset(self): + """ Reset the id map. + """ + cdef INDEX_T start + + MCP._reset(self) + starts, ends = self._starts, self._ends + + # Reset idmap + self.flat_idmap[...] = -1 + id = 0 + for start in _ravel_index_fortran(starts, self.costs_shape): + self.flat_idmap[start] = id + id += 1 + + + cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, + FLOAT_T offset_length): + """ Equivalent to MCP_Geometric. + """ + return offset_length * 0.5 * (old_cost + new_cost) + + + cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ Check whether two fronts are meeting. If so, the flat_traceback + is obtained and a connection is created. + """ + + # Short names + cdef INDEX_T [:] flat_idmap = self.flat_idmap + cdef FLOAT_T [:] flat_cumulative_costs = self.flat_cumulative_costs + + # Get ids + cdef INDEX_T id1 = flat_idmap[index] + cdef INDEX_T id2 = flat_idmap[new_index] + + if id2 < 0 or id1 < 0: + pass + elif id2 != id1: + # We reached the 'front' of another seed point! + # Get position/coordinates + pos1, pos2 = _unravel_index_fortran([index, new_index], + self.costs_shape) + # Also get the costs, so we can keep the path with the least cost + cost1 = flat_cumulative_costs[index] + cost2 = flat_cumulative_costs[new_index] + # Create connection + self.create_connection(id1, id2, pos1, pos2, cost1, cost2) + + + def create_connection(self, id1, id2, tb1, tb2, cost1, cost2): + """ create_connection id1, id2, pos1, pos2, cost1, cost2) + + Overload this method to keep track of the connections that are + found during MCP processing. Note that a connection with the + same ids can be found multiple times (but with different + positions and costs). + + At the time that this method is called, both points are "frozen" + and will not be visited again by the MCP algorithm. + + Parameters + ---------- + id1 : int + The seed point id where the first neighbor originated from. + id2 : int + The seed point id where the second neighbor originated from. + pos1 : tuple + The index of of the first neighbour in the connection. + pos2 : tuple + The index of of the second neighbour in the connection. + cost1 : float + The cumulative cost at `pos1`. + cost2 : float + The cumulative costs at `pos2`. + """ + pass + + + cdef void _update_node(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ Keep track of the id map so that we know which seed point + a certain front originates from. + """ + self.flat_idmap[new_index] = self.flat_idmap[index] + + + +@cython.boundscheck(False) +@cython.wraparound(False) +cdef class MCP_Flexible(MCP): + """MCP_Flexible(costs, offsets=None, fully_connected=True) + + Find minimum cost paths through an N-d costs array. + + See the documentation for MCP for full details. This class differs from + MCP in that several methods can be overloaded (from pure Python) to + modify the behavior of the algorithm and/or create custom algorithms + based on MCP. Note that goal_reached can also be overloaded in the + MCP class. + + """ + + def travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, + FLOAT_T offset_length): + """ travel_cost(old_cost, new_cost, offset_length) + This method calculates the travel cost for going from the + current node to the next. The default implementation returns + new_cost. Overload this method to adapt the behaviour of the + algorithm. + """ + return new_cost + + + def examine_neighbor(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ examine_neighbor(index, new_index, offset_length) + This method is called once for every pair of neighboring nodes, + as soon as both nodes are frozen. + + This method can be overloaded to obtain information about + neightboring nodes, and/or to modify the behavior of the MCP + algorithm. One example is the MCP_Connect class, which checks + for meeting fronts using this hook. + """ + pass + + + def update_node(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ update_node(index, new_index, offset_length) + This method is called when a node is updated, right after + new_index is pushed onto the heap and the traceback map is + updated. + + This method can be overloaded to keep track of other arrays + that are used by a specific implementation of the algorithm. + For instance the MCP_Connect class uses it to update an id map. + """ + pass + + + cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, + FLOAT_T offset_length): + return self.travel_cost(old_cost, new_cost, offset_length) + + + cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + self.examine_neighbor(index, new_index, offset_length) + + + cdef void _update_node(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + self.update_node(index, new_index, offset_length) diff --git a/skimage/graph/mcp.py b/skimage/graph/mcp.py index dc584226..bf693a45 100644 --- a/skimage/graph/mcp.py +++ b/skimage/graph/mcp.py @@ -1,4 +1,4 @@ -from ._mcp import MCP, MCP_Geometric +from ._mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible def route_through_array(array, start, end, fully_connected=True, diff --git a/skimage/graph/tests/test_anisotropy.py b/skimage/graph/tests/test_anisotropy.py new file mode 100644 index 00000000..199d2e73 --- /dev/null +++ b/skimage/graph/tests/test_anisotropy.py @@ -0,0 +1,53 @@ +import skimage.graph.mcp as mcp +from numpy.testing import (assert_array_equal, + assert_almost_equal, + ) + +import numpy as np + +a = np.ones((8, 8), dtype=np.float32) + + +horizontal_ramp = np.array([[ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,]]) + +vertical_ramp = np.array( [[ 0., 0., 0., 0., 0., 0., 0., 0.,], + [ 1., 1., 1., 1., 1., 1., 1., 1.,], + [ 2., 2., 2., 2., 2., 2., 2., 2.,], + [ 3., 3., 3., 3., 3., 3., 3., 3.,], + [ 4., 4., 4., 4., 4., 4., 4., 4.,], + [ 5., 5., 5., 5., 5., 5., 5., 5.,], + [ 6., 6., 6., 6., 6., 6., 6., 6.,], + [ 7., 7., 7., 7., 7., 7., 7., 7.,]]) + + +def test_anisotropy(): + + # Create seeds; vertical seeds create a horizonral ramp + seeds_for_horizontal = [(i, 0) for i in range(8) ] + seeds_for_vertcal = [(0, i) for i in range(8) ] + + + for sy in range(1, 5): + for sx in range(1,5): + sampling = sy, sx + # Trace horizontally + m1 = mcp.MCP_Geometric(a, sampling=sampling, fully_connected=True) + costs1, traceback = m1.find_costs(seeds_for_horizontal) + # Trace vertically + m2 = mcp.MCP_Geometric(a, sampling=sampling, fully_connected=True) + costs2, traceback = m2.find_costs(seeds_for_vertcal) + + # Check + assert_array_equal(costs1, horizontal_ramp * sx) + assert_array_equal(costs2, vertical_ramp * sy) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/graph/tests/test_connect.py b/skimage/graph/tests/test_connect.py new file mode 100644 index 00000000..619ac3b4 --- /dev/null +++ b/skimage/graph/tests/test_connect.py @@ -0,0 +1,83 @@ +import skimage.graph.mcp as mcp +# import stentseg.graph._mcp as mcp +from numpy.testing import (assert_array_equal, + assert_almost_equal, + ) + +import numpy as np + + +a = np.ones((8, 8), dtype=np.float32) + +count = 0 +class MCP(mcp.MCP_Connect): + + def _reset(self): + """ Reset the id map. + """ + mcp.MCP_Connect._reset(self) + self._conn = {} + self._bestconn = {} + + + def create_connection(self, id1, id2, pos1, pos2, cost1, cost2): + # Process data + hash = min(id1, id2), max(id1, id2) + val = min(pos1, pos2), max(pos1, pos2) + cost = min(cost1, cost2) + # Add to total list + self._conn.setdefault(hash, []).append(val) + # Keep track of connection with lowest cost + curcost = self._bestconn.get(hash, (np.inf,))[0] + if cost < curcost: + self._bestconn[hash] = (cost,) + val + + +def test_connections(): + + # Create MCP object with three seed points + mcp = MCP(a) + costs, traceback = mcp.find_costs([ (1,1), (7,7), (1,7) ]) + + # Test that all three seed points are connected + connections = set(mcp._conn.keys()) + assert (0, 1) in connections + assert (1, 2) in connections + assert (0, 2) in connections + + # Test that any two neighbors have only been connected once + for position_tuples in mcp._conn.values(): + n1 = len(position_tuples) + n2 = len(set(position_tuples)) + assert n1 == n2 + + # For seed 0 and 1 + cost, pos1, pos2 = mcp._bestconn[(0,1)] + # Test meeting points + assert (pos1, pos2) == ( (3,3), (4,4) ) + # Test the whole path + path = mcp.traceback(pos1) + list(reversed(mcp.traceback(pos2))) + assert_array_equal(path, + [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7)]) + + # For seed 1 and 2 + cost, pos1, pos2 = mcp._bestconn[(1,2)] + # Test meeting points + assert (pos1, pos2) == ( (3,7), (4,7) ) + # Test the whole path + path = mcp.traceback(pos1) + list(reversed(mcp.traceback(pos2))) + assert_array_equal(path, + [(1, 7), (2, 7), (3, 7), (4, 7), (5, 7), (6, 7), (7, 7)]) + + # For seed 0 and 2 + cost, pos1, pos2 = mcp._bestconn[(0,2)] + # Test meeting points + assert (pos1, pos2) == ( (1,3), (1,4) ) + # Test the whole path + path = mcp.traceback(pos1) + list(reversed(mcp.traceback(pos2))) + assert_array_equal(path, + [(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7)]) + + +if __name__ == "__main__": + np.testing.run_module_suite() \ No newline at end of file diff --git a/skimage/graph/tests/test_flexible.py b/skimage/graph/tests/test_flexible.py new file mode 100644 index 00000000..8fc40d1b --- /dev/null +++ b/skimage/graph/tests/test_flexible.py @@ -0,0 +1,61 @@ +import skimage.graph.mcp as mcp +from numpy.testing import (assert_array_equal, + assert_almost_equal, + ) + +import numpy as np + +a = np.ones((8, 8), dtype=np.float32) +a[1::2] *= 2.0 + + +class FlexibleMCP(mcp.MCP_Flexible): + """ Simple MCP subclass that allows the front to travel + a certain distance from the seed point, and uses a constant + cost factor that is independant of the cost array. + """ + + def _reset(self): + mcp.MCP_Flexible._reset(self) + self._distance = np.zeros((8, 8), dtype=np.float32).ravel() + + def goal_reached(self, index, cumcost): + if self._distance[index] > 4: + return 2 + else: + return 0 + + def travel_cost(self, index, new_index, offset_length): + return 1.0 # fixed cost + + def examine_neighbor(self, index, new_index, offset_length): + pass # We do not test this + + def update_node(self, index, new_index, offset_length): + self._distance[new_index] = self._distance[index] + 1 + + +def test_flexible(): + + # Create MCP and do a traceback + mcp = FlexibleMCP(a) + costs, traceback = mcp.find_costs([(0, 0)]) + + # Check that inner part is correct. This basically + # tests whether travel_cost works. + assert_array_equal(costs[:4,:4], [[1, 2, 3, 4], + [2, 2, 3, 4], + [3, 3, 3, 4], + [4, 4, 4, 4]]) + + # Test that the algorithm stopped at the right distance. + # Note that some of the costs are filled in but not yet frozen, + # so we take a bit of margin + assert np.all( costs[-2:,:] == np.inf ) + assert np.all( costs[:,-2:] == np.inf ) + + #print(costs) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index 560f19d0..5c44abad 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -116,8 +116,8 @@ def test_offsets(): m = mcp.MCP(a, offsets=offsets) costs, traceback = m.find_costs([(1, 6)]) assert_array_equal(traceback, - [[-1, -1, -1, -1, -1, -1, -1, -1], - [-1, -1, -1, -1, -1, -1, -1, -1], + [[-2, -2, -2, -2, -2, -2, -2, -2], + [-2, -2, -2, -2, -2, -2, -1, -2], [15, 14, 13, 12, 11, 10, 0, 1], [10, 0, 1, 2, 3, 4, 5, 6], [10, 0, 1, 2, 3, 4, 5, 6], @@ -151,5 +151,6 @@ def _test_random(shape): return a, costs, offsets + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/io/__init__.py b/skimage/io/__init__.py index 5e701a51..144fef57 100644 --- a/skimage/io/__init__.py +++ b/skimage/io/__init__.py @@ -1,49 +1,33 @@ -__doc__ = """Utilities to read and write images in various formats. +"""Utilities to read and write images in various formats. The following plug-ins are available: """ -from ._plugins import use as use_plugin -from ._plugins import available as plugins -from ._plugins import info as plugin_info -from ._plugins import configuration as plugin_order -from ._plugins import reset_plugins as _reset_plugins - +from .manage_plugins import * from .sift import * from .collection import * from ._io import * -from .video import * +from ._image_stack import * -available_plugins = plugins() +reset_plugins() + +WRAP_LEN = 73 -def _load_preferred_plugins(): - # Load preferred plugin for each io function. - io_funcs = ['imsave', 'imshow', 'imread_collection', 'imread'] - preferred_plugins = ['matplotlib', 'pil', 'qt', 'freeimage', 'null'] - for func in io_funcs: - for plugin in preferred_plugins: - if plugin not in available_plugins: - continue - try: - use_plugin(plugin, kind=func) - break - except (ImportError, RuntimeError, OSError): - pass +def _separator(char, lengths): + return [char * separator_length for separator_length in lengths] - # Use PIL as the default imread plugin, since matplotlib (1.2.x) - # is buggy (flips PNGs around, returns bytes as floats, etc.) - try: - use_plugin('pil', 'imread') - except ImportError: - pass -def reset_plugins(): - _reset_plugins() - _load_preferred_plugins() +def _format_plugin_info_table(info_table, column_lengths): + """Add separators and column titles to plugin info table.""" + info_table.insert(0, _separator('=', column_lengths)) + info_table.insert(1, ('Plugin', 'Description')) + info_table.insert(2, _separator('-', column_lengths)) + info_table.append(_separator('-', column_lengths)) + def _update_doc(doc): """Add a list of plugins to the module docstring, formatted as @@ -52,27 +36,24 @@ def _update_doc(doc): """ from textwrap import wrap - info = [(p, plugin_info(p)) for p in plugins() if not p == 'test'] - col_1_len = max([len(n) for (n, _) in info]) - wrap_len = 73 - col_2_len = wrap_len - 1 - col_1_len + info_table = [(p, plugin_info(p).get('description', 'no description')) + for p in available_plugins if not p == 'test'] - # Insert table header - info.insert(0, ('=' * col_1_len, {'description': '=' * col_2_len})) - info.insert(1, ('Plugin', {'description': 'Description'})) - info.insert(2, ('-' * col_1_len, {'description': '-' * col_2_len})) - info.append(('=' * col_1_len, {'description': '=' * col_2_len})) + name_length = max([len(n) for (n, _) in info_table]) + description_length = WRAP_LEN - 1 - name_length + column_lengths = [name_length, description_length] + _format_plugin_info_table(info_table, column_lengths) - for (name, meta_data) in info: - wrapped_descr = wrap(meta_data.get('description', ''), - col_2_len) - doc += "%s %s\n" % (name.ljust(col_1_len), - '\n'.join(wrapped_descr)) + for (name, plugin_description) in info_table: + description_lines = wrap(plugin_description, description_length) + name_column = [name] + name_column.extend(['' for _ in range(len(description_lines) - 1)]) + for name, description in zip(name_column, description_lines): + doc += "%s %s\n" % (name.ljust(name_length), description) doc = doc.strip() return doc -__doc__ = _update_doc(__doc__) -reset_plugins() +__doc__ = _update_doc(__doc__) diff --git a/skimage/io/_image_stack.py b/skimage/io/_image_stack.py new file mode 100644 index 00000000..ca9896d5 --- /dev/null +++ b/skimage/io/_image_stack.py @@ -0,0 +1,35 @@ +import numpy as np + + +__all__ = ['image_stack', 'push', 'pop'] + + +# Shared image queue +image_stack = [] + + +def push(img): + """Push an image onto the shared image stack. + + Parameters + ---------- + img : ndarray + Image to push. + + """ + if not isinstance(img, np.ndarray): + raise ValueError("Can only push ndarrays to the image stack.") + + image_stack.append(img) + + +def pop(): + """Pop an image from the shared image stack. + + Returns + ------- + img : ndarray + Image popped from the stack. + + """ + return image_stack.pop() diff --git a/skimage/io/_io.py b/skimage/io/_io.py index a7df4694..97f8718e 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,33 +1,14 @@ -__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 tempfile from io import BytesIO import numpy as np +import six -from skimage.io._plugins import call as call_plugin +from skimage.io.manage_plugins import call_plugin from skimage.color import rgb2grey -from skimage._shared import six +from .util import file_or_url_context -# Shared image queue -_image_stack = [] - -URL_REGEX = re.compile(r'http://|https://|ftp://|file://|file:\\') - - -def is_url(filename): - """Return True if string is an http or ftp path.""" - return (isinstance(filename, six.string_types) and - URL_REGEX.match(filename) is not None) +__all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show'] class Image(np.ndarray): @@ -76,33 +57,6 @@ class Image(np.ndarray): return return_str -def push(img): - """Push an image onto the shared image stack. - - Parameters - ---------- - img : ndarray - Image to push. - - """ - if not isinstance(img, np.ndarray): - raise ValueError("Can only push ndarrays to the image stack.") - - _image_stack.append(img) - - -def pop(): - """Pop an image from the shared image stack. - - Returns - ------- - img : ndarray - Image popped from the stack. - - """ - return _image_stack.pop() - - def imread(fname, as_grey=False, plugin=None, flatten=None, **plugin_args): """Load an image from file. @@ -139,14 +93,7 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, if flatten is not None: as_grey = flatten - if is_url(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) - else: + with file_or_url_context(fname) as fname: img = call_plugin('imread', fname, plugin=plugin, **plugin_args) if as_grey and getattr(img, 'ndim', 0) >= 3: @@ -247,7 +194,7 @@ def show(): >>> for i in range(4): ... io.imshow(np.random.random((50, 50))) - >>> io.show() + >>> io.show() # doctest: +SKIP ''' return call_plugin('_app_show') diff --git a/skimage/io/_plugins/__init__.py b/skimage/io/_plugins/__init__.py index 48aad58e..e69de29b 100644 --- a/skimage/io/_plugins/__init__.py +++ b/skimage/io/_plugins/__init__.py @@ -1 +0,0 @@ -from .plugin import * diff --git a/skimage/io/_plugins/fits_plugin.py b/skimage/io/_plugins/fits_plugin.py index 52785814..cf714ea6 100644 --- a/skimage/io/_plugins/fits_plugin.py +++ b/skimage/io/_plugins/fits_plugin.py @@ -3,11 +3,14 @@ __all__ = ['imread', 'imread_collection'] import skimage.io as io try: - import pyfits + from astropy.io import fits as pyfits except ImportError: - raise ImportError("PyFITS could not be found. Please refer to\n" - "http://www.stsci.edu/resources/software_hardware/pyfits\n" - "for further instructions.") + try: + import pyfits + except ImportError: + raise ImportError("PyFITS could not be found. Please refer to\n" + "http://www.stsci.edu/resources/software_hardware/pyfits\n" + "for further instructions.") def imread(fname, dtype=None): diff --git a/skimage/io/_plugins/null_plugin.ini b/skimage/io/_plugins/null_plugin.ini index ddc45f33..9703aab0 100644 --- a/skimage/io/_plugins/null_plugin.ini +++ b/skimage/io/_plugins/null_plugin.ini @@ -1,3 +1,3 @@ [null] description = Default plugin that does nothing -provides = imshow, imread, _app_show +provides = imshow, imread, imsave, _app_show diff --git a/skimage/io/_plugins/null_plugin.py b/skimage/io/_plugins/null_plugin.py index 70bfd5d8..4eb7adf3 100644 --- a/skimage/io/_plugins/null_plugin.py +++ b/skimage/io/_plugins/null_plugin.py @@ -1,4 +1,4 @@ -__all__ = ['imshow', 'imread', '_app_show'] +__all__ = ['imshow', 'imread', 'imsave', '_app_show'] import warnings @@ -17,4 +17,9 @@ def imshow(*args, **kwargs): def imread(*args, **kwargs): warnings.warn(RuntimeWarning(message)) + +def imsave(*args, **kwargs): + warnings.warn(RuntimeWarning(message)) + + _app_show = imshow diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 2ddbfe26..88e7f04b 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -6,12 +6,14 @@ try: from PIL import Image except ImportError: raise ImportError("The Python Image Library could not be found. " - "Please refer to http://pypi.python.org/pypi/PIL/ " + "Please refer to " + "https://pypi.python.org/pypi/Pillow/ (or " + "http://pypi.python.org/pypi/PIL/) " "for further instructions.") from skimage.util import img_as_ubyte -from skimage._shared import six +from six import string_types def imread(fname, dtype=None): @@ -106,10 +108,16 @@ def imsave(fname, arr, format_str=None): arr = arr.astype(np.uint8) # default to PNG if file-like object - if not isinstance(fname, six.string_types) and format_str is None: + if not isinstance(fname, string_types) and format_str is None: format_str = "PNG" - img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) + try: + img = Image.frombytes(mode, (arr.shape[1], arr.shape[0]), + arr.tostring()) + except AttributeError: + img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), + arr.tostring()) + img.save(fname, format=format_str) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 96855dbd..83811fc0 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -2,15 +2,17 @@ from __future__ import with_statement -__all__ = ['MultiImage', 'ImageCollection', 'imread', 'concatenate_images'] - +import os from glob import glob import re from copy import copy import numpy as np -from ._io import imread -from .._shared import six +import six + + +__all__ = ['MultiImage', 'ImageCollection', 'concatenate_images', + 'imread_collection_wrapper'] def concatenate_images(ic): @@ -95,18 +97,14 @@ class MultiImage(object): -------- >>> from skimage import data_dir - >>> img = MultiImage(data_dir + '/multipage.tif') - >>> len(img) + >>> img = MultiImage(data_dir + '/multipage.tif') # doctest: +SKIP + >>> len(img) # doctest: +SKIP 2 - >>> for frame in img: - ... print(frame.shape) + >>> for frame in img: # doctest: +SKIP + ... print(frame.shape) # doctest: +SKIP (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.""" @@ -232,7 +230,7 @@ class ImageCollection(object): ---------- load_pattern : str or list Pattern glob or filenames to load. The path can be absolute or - relative. Multiple patterns should be separated by a colon, + relative. Multiple patterns should be separated by os.pathsep, e.g. '/tmp/work/*.png:/tmp/other/*.jpg'. Also see implementation notes below. conserve_memory : bool, optional @@ -298,7 +296,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, six.string_types): - load_pattern = load_pattern.split(':') + load_pattern = load_pattern.split(os.pathsep) self._files = [] for pattern in load_pattern: self._files.extend(glob(pattern)) @@ -315,6 +313,7 @@ class ImageCollection(object): self._cached = None if load_func is None: + from ._io import imread self.load_func = imread else: self.load_func = load_func @@ -433,3 +432,29 @@ class ImageCollection(object): If images in the `ImageCollection` don't have identical shapes. """ return concatenate_images(self) + + +def imread_collection_wrapper(imread): + def imread_collection(load_pattern, conserve_memory=True): + """Return an `ImageCollection` from files matching the given pattern. + + Note that files are always stored in alphabetical order. Also note that + slicing returns a new ImageCollection, *not* a view into the data. + + See `skimage.io.ImageCollection` for details. + + Parameters + ---------- + load_pattern : str or list + Pattern glob or filenames to load. The path can be absolute or + relative. Multiple patterns should be separated by a colon, + e.g. '/tmp/work/*.png:/tmp/other/*.jpg'. Also see + implementation notes below. + conserve_memory : bool, optional + If True, never keep more than one in memory at a specific + time. Otherwise, images will be cached once they are loaded. + + """ + return ImageCollection(load_pattern, conserve_memory=conserve_memory, + load_func=imread) + return imread_collection diff --git a/skimage/io/_plugins/plugin.py b/skimage/io/manage_plugins.py similarity index 50% rename from skimage/io/_plugins/plugin.py rename to skimage/io/manage_plugins.py index 5e04f320..d31e6dbe 100644 --- a/skimage/io/_plugins/plugin.py +++ b/skimage/io/manage_plugins.py @@ -1,26 +1,57 @@ """Handle image reading, writing and plotting plugins. +To improve performance, plugins are only loaded as needed. As a result, there +can be multiple states for a given plugin: + + available: Defined in an *ini file located in `skimage.io._plugins`. + See also `skimage.io.available_plugins`. + partial definition: Specified in an *ini file, but not defined in the + corresponding plugin module. This will raise an error when loaded. + available but not on this system: Defined in `skimage.io._plugins`, but + a dependent library (e.g. Qt, PIL) is not available on your system. + This will raise an error when loaded. + loaded: The real availability is determined when it's explicitly loaded, + either because it's one of the default plugins, or because it's + loaded explicitly by the user. + """ -__all__ = ['use', 'available', 'call', 'info', 'configuration', 'reset_plugins'] - try: - from configparser import ConfigParser + from configparser import ConfigParser # Python 3 except ImportError: - from ConfigParser import ConfigParser + from ConfigParser import ConfigParser # Python 2 import os.path from glob import glob +from .collection import imread_collection_wrapper + +__all__ = ['use_plugin', 'call_plugin', 'plugin_info', 'plugin_order', + 'reset_plugins', 'find_available_plugins', 'available_plugins'] + + +# The plugin store will save a list of *loaded* io functions for each io type +# (e.g. 'imread', 'imsave', etc.). Plugins are loaded as requested. plugin_store = None - +# Dictionary mapping plugin names to a list of functions they provide. plugin_provides = {} +# The module names for the plugins in `skimage.io._plugins`. plugin_module_name = {} +# Meta-data about plugins provided by *.ini files. plugin_meta_data = {} +# For each plugin type, default to the first available plugin as defined by +# the following preferences. +preferred_plugins = { + # Default plugins for all types (overridden by specific types below). + 'all': ['matplotlib', 'pil', 'qt', 'freeimage', 'null'], + # Use PIL as the default imread plugin, since matplotlib (1.2.x) + # is buggy (flips PNGs around, returns bytes as floats, etc.) + 'imread': ['pil'], +} -def reset_plugins(): +def _clear_plugins(): """Clear the plugin state to the default, i.e., where no plugins are loaded """ @@ -30,8 +61,47 @@ def reset_plugins(): 'imshow': [], 'imread_collection': [], '_app_show': []} +_clear_plugins() -reset_plugins() + +def _load_preferred_plugins(): + # Load preferred plugin for each io function. + io_types = ['imsave', 'imshow', 'imread_collection', 'imread'] + for p_type in io_types: + _set_plugin(p_type, preferred_plugins['all']) + + plugin_types = (p for p in preferred_plugins.keys() if p != 'all') + for p_type in plugin_types: + _set_plugin(p_type, preferred_plugins[p_type]) + + +def _set_plugin(plugin_type, plugin_list): + for plugin in plugin_list: + if plugin not in available_plugins: + continue + try: + use_plugin(plugin, kind=plugin_type) + break + except (ImportError, RuntimeError, OSError): + pass + + +def reset_plugins(): + _clear_plugins() + _load_preferred_plugins() + + +def _parse_config_file(filename): + """Return plugin name and meta-data dict from plugin config file.""" + parser = ConfigParser() + parser.read(filename) + name = parser.sections()[0] + + meta_data = {} + for opt in parser.options(name): + meta_data[opt] = parser.get(name, opt) + + return name, meta_data def _scan_plugins(): @@ -40,19 +110,13 @@ def _scan_plugins(): """ pd = os.path.dirname(__file__) - ini = glob(os.path.join(pd, '*.ini')) + config_files = glob(os.path.join(pd, '_plugins', '*.ini')) - for f in ini: - cp = ConfigParser() - cp.read(f) - name = cp.sections()[0] - - meta_data = {} - for opt in cp.options(name): - meta_data[opt] = cp.get(name, opt) + for filename in config_files: + name, meta_data = _parse_config_file(filename) plugin_meta_data[name] = meta_data - provides = [s.strip() for s in cp.get(name, 'provides').split(',')] + provides = [s.strip() for s in meta_data['provides'].split(',')] valid_provides = [p for p in provides if p in plugin_store] for p in provides: @@ -60,13 +124,53 @@ def _scan_plugins(): print("Plugin `%s` wants to provide non-existent `%s`." \ " Ignoring." % (name, p)) + # Add plugins that provide 'imread' as provider of 'imread_collection'. + need_to_add_collection = ('imread_collection' not in valid_provides and + 'imread' in valid_provides) + if need_to_add_collection: + valid_provides.append('imread_collection') + plugin_provides[name] = valid_provides - plugin_module_name[name] = os.path.basename(f)[:-4] + + plugin_module_name[name] = os.path.basename(filename)[:-4] _scan_plugins() -def call(kind, *args, **kwargs): +def find_available_plugins(loaded=False): + """List available plugins. + + Parameters + ---------- + loaded : bool + If True, show only those plugins currently loaded. By default, + all plugins are shown. + + Returns + ------- + p : dict + Dictionary with plugin names as keys and exposed functions as + values. + + """ + active_plugins = set() + for plugin_func in plugin_store.values(): + for plugin, func in plugin_func: + active_plugins.add(plugin) + + d = {} + for plugin in plugin_provides: + if not loaded or plugin in active_plugins: + d[plugin] = [f for f in plugin_provides[plugin] + if not f.startswith('_')] + + return d + + +available_plugins = find_available_plugins() + + +def call_plugin(kind, *args, **kwargs): """Find the appropriate plugin of 'kind' and execute it. Parameters @@ -85,11 +189,11 @@ def call(kind, *args, **kwargs): plugin_funcs = plugin_store[kind] if len(plugin_funcs) == 0: - raise RuntimeError('''No suitable plugin registered for %s. - -You may load I/O plugins with the `skimage.io.use_plugin` -command. A list of all available plugins can be found using -`skimage.io.plugins()`.''' % kind) + msg = ("No suitable plugin registered for %s.\n\n" + "You may load I/O plugins with the `skimage.io.use_plugin` " + "command. A list of all available plugins can be found using " + "`skimage.io.plugins()`.") + raise RuntimeError(msg % kind) plugin = kwargs.pop('plugin', None) if plugin is None: @@ -105,7 +209,7 @@ command. A list of all available plugins can be found using return func(*args, **kwargs) -def use(name, kind=None): +def use_plugin(name, kind=None): """Set the default plugin for a specified operation. The plugin will be loaded if it hasn't been already. @@ -119,15 +223,19 @@ def use(name, kind=None): See Also -------- - plugins : List of available plugins + available_plugins : List of available plugins Examples -------- - Use the Python Imaging Library to read images: + To use Matplotlib as the default image reader, you would write: - >>> from skimage.io import use_plugin - >>> use_plugin('pil', 'imread') + >>> from skimage import io + >>> io.use_plugin('matplotlib', 'imread') + + To see a list of available plugins run ``io.available_plugins``. Note that + this lists plugins that are defined, but the full list may not be usable + if your system does not have the required libraries installed. """ if kind is None: @@ -158,34 +266,12 @@ def use(name, kind=None): plugin_store[k] = funcs -def available(loaded=False): - """List available plugins. - - Parameters - ---------- - loaded : bool - If True, show only those plugins currently loaded. By default, - all plugins are shown. - - Returns - ------- - p : dict - Dictionary with plugin names as keys and exposed functions as - values. - - """ - active_plugins = set() - for plugin_func in plugin_store.values(): - for plugin, func in plugin_func: - active_plugins.add(plugin) - - d = {} - for plugin in plugin_provides: - if not loaded or plugin in active_plugins: - d[plugin] = [f for f in plugin_provides[plugin] \ - if not f.startswith('_')] - - return d +def _inject_imread_collection_if_needed(module): + """Add `imread_collection` to module if not already present.""" + if not hasattr(module, 'imread_collection') and hasattr(module, 'imread'): + imread = getattr(module, 'imread') + func = imread_collection_wrapper(imread) + setattr(module, 'imread_collection', func) def _load(plugin): @@ -201,7 +287,7 @@ def _load(plugin): plugins : List of available plugins """ - if plugin in available(loaded=True): + if plugin in find_available_plugins(loaded=True): return if not plugin in plugin_module_name: raise ValueError("Plugin %s not found." % plugin) @@ -212,17 +298,20 @@ def _load(plugin): provides = plugin_provides[plugin] for p in provides: - if not hasattr(plugin_module, p): + if p == 'imread_collection': + _inject_imread_collection_if_needed(plugin_module) + elif not hasattr(plugin_module, p): print("Plugin %s does not provide %s as advertised. Ignoring." % \ (plugin, p)) - else: - store = plugin_store[p] - func = getattr(plugin_module, p) - if not (plugin, func) in store: - store.append((plugin, func)) + continue + + store = plugin_store[p] + func = getattr(plugin_module, p) + if not (plugin, func) in store: + store.append((plugin, func)) -def info(plugin): +def plugin_info(plugin): """Return plugin meta-data. Parameters @@ -242,7 +331,7 @@ def info(plugin): raise ValueError('No information on plugin "%s"' % plugin) -def configuration(): +def plugin_order(): """Return the currently preferred plugin order. Returns diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index f56d753b..1e9fea73 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -1,87 +1,72 @@ -import sys import os.path import numpy as np -from numpy.testing import (assert_raises, - assert_equal, - assert_array_almost_equal, - ) -from numpy.testing.decorators import skipif +from numpy.testing import assert_raises, assert_equal, assert_allclose 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 +from skimage.io.collection import ImageCollection, alphanumeric_key -try: - from PIL import Image -except ImportError: - PIL_available = False -else: - PIL_available = True +def test_string_split(): + test_string = 'z23a' + test_str_result = ['z', 23, 'a'] + assert_equal(alphanumeric_key(test_string), test_str_result) -class TestAlphanumericKey(): - def setUp(self): - self.test_string = 'z23a' - self.test_str_result = ['z', 23, 'a'] - self.filenames = ['f9.10.png', 'f9.9.png', 'f10.10.png', 'f10.9.png', - 'e9.png', 'e10.png', 'em.png'] - self.sorted_filenames = \ - ['e9.png', 'e10.png', 'em.png', 'f9.9.png', 'f9.10.png', - 'f10.9.png', 'f10.10.png'] - - def test_string_split(self): - assert_equal(alphanumeric_key(self.test_string), self.test_str_result) - - def test_string_sort(self): - sorted_filenames = sorted(self.filenames, key=alphanumeric_key) - assert_equal(sorted_filenames, self.sorted_filenames) +def test_string_sort(): + filenames = ['f9.10.png', 'f9.9.png', 'f10.10.png', 'f10.9.png', + 'e9.png', 'e10.png', 'em.png'] + sorted_filenames = ['e9.png', 'e10.png', 'em.png', 'f9.9.png', + 'f9.10.png', 'f10.9.png', 'f10.10.png'] + sorted_filenames = sorted(filenames, key=alphanumeric_key) + assert_equal(sorted_filenames, sorted_filenames) class TestImageCollection(): - pattern = [os.path.join(data_dir, pic) for pic in ['camera.png', - 'color.png']] - pattern_matched = [os.path.join(data_dir, pic) for pic in - ['camera.png', 'moon.png']] + + pattern = [os.path.join(data_dir, pic) + for pic in ['camera.png', 'color.png']] + + pattern_matched = [os.path.join(data_dir, pic) + for pic in ['camera.png', 'moon.png']] def setUp(self): - self.collection = ImageCollection(self.pattern) - self.collection_matched = ImageCollection(self.pattern_matched) + # Generic image collection with images of different shapes. + self.images = ImageCollection(self.pattern) + # Image collection with images having shapes that match. + self.images_matched = ImageCollection(self.pattern_matched) def test_len(self): - assert len(self.collection) == 2 + assert len(self.images) == 2 def test_getitem(self): - num = len(self.collection) + num = len(self.images) for i in range(-num, num): - assert type(self.collection[i]) is np.ndarray - assert_array_almost_equal(self.collection[0], - self.collection[-num]) + assert type(self.images[i]) is np.ndarray + assert_allclose(self.images[0], + self.images[-num]) - #assert_raises expects a callable, hence this do-very-little func + # assert_raises expects a callable, hence this thin wrapper function. def return_img(n): - return self.collection[n] + return self.images[n] assert_raises(IndexError, return_img, num) assert_raises(IndexError, return_img, -num - 1) def test_slicing(self): - assert type(self.collection[:]) is ImageCollection - assert len(self.collection[:]) == 2 - assert len(self.collection[:1]) == 1 - assert len(self.collection[1:]) == 1 - assert_array_almost_equal(self.collection[0], self.collection[:1][0]) - assert_array_almost_equal(self.collection[1], self.collection[1:][0]) - assert_array_almost_equal(self.collection[1], self.collection[::-1][0]) - assert_array_almost_equal(self.collection[0], self.collection[::-1][1]) + assert type(self.images[:]) is ImageCollection + assert len(self.images[:]) == 2 + assert len(self.images[:1]) == 1 + assert len(self.images[1:]) == 1 + assert_allclose(self.images[0], self.images[:1][0]) + assert_allclose(self.images[1], self.images[1:][0]) + assert_allclose(self.images[1], self.images[::-1][0]) + assert_allclose(self.images[0], self.images[::-1][1]) def test_files_property(self): - assert isinstance(self.collection.files, list) + assert isinstance(self.images.files, list) def set_files(f): - self.collection.files = f + self.images.files = f assert_raises(AttributeError, set_files, 'newfiles') def test_custom_load(self): @@ -94,59 +79,12 @@ class TestImageCollection(): assert_equal(ic[1], (2, 'two')) def test_concatenate(self): - ar = self.collection_matched.concatenate() - assert_equal(ar.shape, (len(self.collection_matched),) + - self.collection[0].shape) - assert_raises(ValueError, self.collection.concatenate) + array = self.images_matched.concatenate() + expected_shape = (len(self.images_matched),) + self.images[0].shape + assert_equal(array.shape, expected_shape) - -class TestMultiImage(): - - def setUp(self): - # This multipage TIF file was created with imagemagick: - # convert im1.tif im2.tif -adjoin multipage.tif - if PIL_available: - self.img = MultiImage(os.path.join(data_dir, 'multipage.tif')) - - @skipif(not PIL_available) - def test_len(self): - assert len(self.img) == 2 - - @skipif(not PIL_available) - def test_getitem(self): - num = len(self.img) - for i in range(-num, num): - assert type(self.img[i]) is np.ndarray - assert_array_almost_equal(self.img[0], - self.img[-num]) - - #assert_raises expects a callable, hence this do-very-little func - def return_img(n): - return self.img[n] - assert_raises(IndexError, return_img, num) - assert_raises(IndexError, return_img, -num - 1) - - @skipif(not PIL_available) - def test_files_property(self): - assert isinstance(self.img.filename, six.string_types) - - def set_filename(f): - self.img.filename = f - assert_raises(AttributeError, set_filename, 'newfile') - - @skipif(not PIL_available) - def test_conserve_memory_property(self): - assert isinstance(self.img.conserve_memory, bool) - - def set_mem(val): - self.img.conserve_memory = val - assert_raises(AttributeError, set_mem, True) - - @skipif(not PIL_available) - def test_concatenate(self): - ar = self.img.concatenate() - assert_equal(ar.shape, (len(self.img),) + - self.img[0].shape) + def test_concatentate_mismatched_image_shapes(self): + assert_raises(ValueError, self.images.concatenate) if __name__ == "__main__": diff --git a/skimage/io/tests/test_fits.py b/skimage/io/tests/test_fits.py index d432b611..4fa1b467 100644 --- a/skimage/io/tests/test_fits.py +++ b/skimage/io/tests/test_fits.py @@ -9,10 +9,14 @@ from skimage import data_dir pyfits_available = True try: - import pyfits + from astropy.io import fits as pyfits except ImportError: - pyfits_available = False -else: + try: + import pyfits + except ImportError: + pyfits_available = False + +if pyfits_available: import skimage.io._plugins.fits_plugin as fplug diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index 550b39a2..32e01bd7 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -53,9 +53,10 @@ 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) + 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') @@ -64,7 +65,8 @@ def test_write_multipage(): 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) @@ -94,13 +96,13 @@ class TestSave: def test_metadata(): meta = fi.read_metadata(os.path.join(si.data_dir, 'multipage.tif')) assert meta[('EXIF_MAIN', 'Orientation')] == 1 - assert meta[('EXIF_MAIN', 'Software')].startswith('ImageMagick') + assert meta[('EXIF_MAIN', 'Software')].startswith('I') meta = fi.read_multipage_metadata(os.path.join(si.data_dir, 'multipage.tif')) assert len(meta) == 2 assert meta[0][('EXIF_MAIN', 'Orientation')] == 1 - assert meta[1][('EXIF_MAIN', 'Software')].startswith('ImageMagick') + assert meta[1][('EXIF_MAIN', 'Software')].startswith('I') if __name__ == "__main__": diff --git a/skimage/io/tests/test_image.py b/skimage/io/tests/test_image.py index 6c54695b..b74c973e 100644 --- a/skimage/io/tests/test_image.py +++ b/skimage/io/tests/test_image.py @@ -1,7 +1,12 @@ -from skimage.io import Image +from io import BytesIO + +import numpy as np +from skimage import img_as_ubyte +from skimage.io import Image, imread 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') @@ -11,7 +16,18 @@ def test_tags(): assert_array_equal((g + 2).tags['sun'], 'moon') assert_equal(h.tags, {}) + +def test_repr_png_roundtrip(): + # Use RGB-like shape since some backends convert grayscale to RGB + original_array = 255 * np.ones((5, 5, 3), dtype=np.uint8) + image = Image(original_array) + array = imread(BytesIO(image._repr_png_())) + # Force output to ubyte range for plugin compatibility. + # For example, Matplotlib will return floats even if the image is uint8. + assert_array_equal(img_as_ubyte(array), original_array) + # Note that PIL breaks with `_repr_jpeg_`. + + 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 8049860a..14879e64 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -4,6 +4,7 @@ from numpy.testing import assert_array_equal, raises, run_module_suite import numpy as np import skimage.io as io +from skimage.io.manage_plugins import plugin_store from skimage import data_dir @@ -28,5 +29,17 @@ def test_imread_url(): assert image.shape == (512, 512) +@raises(RuntimeError) +def test_imread_no_plugin(): + # tweak data path so that file URI works on both unix and windows. + image_path = os.path.join(data_dir, 'lena.png') + plugins = plugin_store['imread'] + plugin_store['imread'] = [] + try: + io.imread(image_path) + finally: + plugin_store['imread'] = plugins + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/test_multi_image.py b/skimage/io/tests/test_multi_image.py new file mode 100644 index 00000000..ebaa71dc --- /dev/null +++ b/skimage/io/tests/test_multi_image.py @@ -0,0 +1,69 @@ +import os + +import numpy as np +from numpy.testing.decorators import skipif +from numpy.testing import assert_raises, assert_equal, assert_allclose + +from skimage import data_dir +from skimage.io.collection import MultiImage + +try: + from PIL import Image +except ImportError: + PIL_available = False +else: + PIL_available = True + +import six + + +class TestMultiImage(): + + def setUp(self): + # This multipage TIF file was created with imagemagick: + # convert im1.tif im2.tif -adjoin multipage.tif + if PIL_available: + self.img = MultiImage(os.path.join(data_dir, 'multipage.tif')) + + @skipif(not PIL_available) + def test_len(self): + assert len(self.img) == 2 + + @skipif(not PIL_available) + def test_getitem(self): + num = len(self.img) + for i in range(-num, num): + assert type(self.img[i]) is np.ndarray + assert_allclose(self.img[0], self.img[-num]) + + # assert_raises expects a callable, hence this thin wrapper function. + def return_img(n): + return self.img[n] + assert_raises(IndexError, return_img, num) + assert_raises(IndexError, return_img, -num - 1) + + @skipif(not PIL_available) + def test_files_property(self): + assert isinstance(self.img.filename, six.string_types) + + def set_filename(f): + self.img.filename = f + assert_raises(AttributeError, set_filename, 'newfile') + + @skipif(not PIL_available) + def test_conserve_memory_property(self): + assert isinstance(self.img.conserve_memory, bool) + + def set_mem(val): + self.img.conserve_memory = val + assert_raises(AttributeError, set_mem, True) + + @skipif(not PIL_available) + def test_concatenate(self): + array = self.img.concatenate() + assert_equal(array.shape, (len(self.img),) + self.img[0].shape) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/io/tests/test_null.py b/skimage/io/tests/test_null.py new file mode 100644 index 00000000..56f5df89 --- /dev/null +++ b/skimage/io/tests/test_null.py @@ -0,0 +1,50 @@ +import os +import warnings +from contextlib import contextmanager + +import numpy as np +from numpy.testing import raises + +from skimage import io +from skimage import data_dir + + +@contextmanager +def warnings_as_errors(): + # Temporarily set warnings as errors so we can test the warning is raised. + with warnings.catch_warnings(): + warnings.filterwarnings('error') + yield + +@raises(Warning) +def test_null_imread(): + path = os.path.join(data_dir, 'color.png') + with warnings_as_errors(): + io.imread(path, plugin='null') + + +@raises(Warning) +def test_null_imsave(): + with warnings_as_errors(): + io.imsave('dummy.png', np.zeros((3, 3)), plugin='null') + + +@raises(Warning) +def test_null_imshow(): + with warnings_as_errors(): + io.imshow(np.zeros((3, 3)), plugin='null') + + +@raises(Warning) +def test_null_imread_collection(): + # Note that the null plugin doesn't define an `imread_collection` plugin + # but this function is dynamically added by the plugin manager. + path = os.path.join(data_dir, '*.png') + with warnings_as_errors(): + collection = io.imread_collection(path, plugin='null') + collection[0] + + +if __name__ == '__main__': + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index aa582ebc..243d2fd1 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -8,7 +8,8 @@ from tempfile import NamedTemporaryFile from skimage import data_dir from skimage.io import (imread, imsave, use_plugin, reset_plugins, Image as ioImage) -from skimage._shared.six import BytesIO + +from six import BytesIO try: diff --git a/skimage/io/tests/test_plugin.py b/skimage/io/tests/test_plugin.py index 5d1febe4..06f3a600 100644 --- a/skimage/io/tests/test_plugin.py +++ b/skimage/io/tests/test_plugin.py @@ -1,7 +1,9 @@ -from numpy.testing import * +from contextlib import contextmanager + +from numpy.testing import assert_equal, raises from skimage import io -from skimage.io._plugins import plugin +from skimage.io import manage_plugins from numpy.testing.decorators import skipif try: @@ -19,70 +21,115 @@ except RuntimeError: FI_available = False -def setup_module(self): - plugin.use('test') # see ../_plugins/test_plugin.py +def setup_module(): + manage_plugins.use_plugin('test') # see ../_plugins/test_plugin.py -def teardown_module(self): +def teardown_module(): io.reset_plugins() -class TestPlugin: - def test_read(self): - io.imread('test.png', as_grey=True, dtype='i4', plugin='test') +@contextmanager +def protect_preferred_plugins(): + """Contexts where `preferred_plugins` can be modified w/o side-effects.""" + preferred_plugins = manage_plugins.preferred_plugins.copy() + try: + yield + finally: + manage_plugins.preferred_plugins = preferred_plugins - def test_save(self): - io.imsave('test.png', [1, 2, 3], plugin='test') - def test_show(self): - io.imshow([1, 2, 3], plugin_arg=(1, 2), plugin='test') +def test_read(): + io.imread('test.png', as_grey=True, dtype='i4', plugin='test') - def test_collection(self): - io.imread_collection('*.png', conserve_memory=False, plugin='test') - def test_use(self): - plugin.use('test') - plugin.use('test', 'imshow') +def test_save(): + io.imsave('test.png', [1, 2, 3], plugin='test') - @raises(ValueError) - def test_failed_use(self): - plugin.use('asd') - @skipif(not PIL_available and not FI_available) - def test_use_priority(self): - plugin.use(priority_plugin) - plug, func = plugin.plugin_store['imread'][0] - assert_equal(plug, priority_plugin) +def test_show(): + io.imshow([1, 2, 3], plugin_arg=(1, 2), plugin='test') - plugin.use('test') - plug, func = plugin.plugin_store['imread'][0] - assert_equal(plug, 'test') - @skipif(not PIL_available) - def test_use_priority_with_func(self): - plugin.use('pil') - plug, func = plugin.plugin_store['imread'][0] - assert_equal(plug, 'pil') +def test_collection(): + io.imread_collection('*.png', conserve_memory=False, plugin='test') - plugin.use('test', 'imread') - plug, func = plugin.plugin_store['imread'][0] - assert_equal(plug, 'test') - plug, func = plugin.plugin_store['imsave'][0] - assert_equal(plug, 'pil') +def test_use(): + manage_plugins.use_plugin('test') + manage_plugins.use_plugin('test', 'imshow') - plugin.use('test') - plug, func = plugin.plugin_store['imsave'][0] - assert_equal(plug, 'test') - def test_plugin_order(self): - p = io.plugin_order() - assert 'imread' in p - assert 'test' in p['imread'] +@raises(ValueError) +def test_failed_use(): + manage_plugins.use_plugin('asd') + + +@skipif(not PIL_available and not FI_available) +def test_use_priority(): + manage_plugins.use_plugin(priority_plugin) + plug, func = manage_plugins.plugin_store['imread'][0] + assert_equal(plug, priority_plugin) + + manage_plugins.use_plugin('test') + plug, func = manage_plugins.plugin_store['imread'][0] + assert_equal(plug, 'test') + + +@skipif(not PIL_available) +def test_use_priority_with_func(): + manage_plugins.use_plugin('pil') + plug, func = manage_plugins.plugin_store['imread'][0] + assert_equal(plug, 'pil') + + manage_plugins.use_plugin('test', 'imread') + plug, func = manage_plugins.plugin_store['imread'][0] + assert_equal(plug, 'test') + + plug, func = manage_plugins.plugin_store['imsave'][0] + assert_equal(plug, 'pil') + + manage_plugins.use_plugin('test') + plug, func = manage_plugins.plugin_store['imsave'][0] + assert_equal(plug, 'test') + + +def test_plugin_order(): + p = io.plugin_order() + assert 'imread' in p + assert 'test' in p['imread'] + + +def test_available(): + assert 'qt' in io.available_plugins + assert 'test' in io.find_available_plugins(loaded=True) + + +def test_load_preferred_plugins_all(): + from skimage.io._plugins import null_plugin + + with protect_preferred_plugins(): + manage_plugins.preferred_plugins = {'all': ['null']} + manage_plugins.reset_plugins() + + for plugin_type in ('imread', 'imsave', 'imshow'): + plug, func = manage_plugins.plugin_store[plugin_type][0] + assert func == getattr(null_plugin, plugin_type) + + +def test_load_preferred_plugins_imread(): + from skimage.io._plugins import null_plugin + + with protect_preferred_plugins(): + manage_plugins.preferred_plugins['imread'] = ['null'] + manage_plugins.reset_plugins() + + plug, func = manage_plugins.plugin_store['imread'][0] + assert func == null_plugin.imread + plug, func = manage_plugins.plugin_store['imshow'][0] + assert func != null_plugin.imshow - def test_available(self): - assert 'qt' in io.plugins() - assert 'test' in io.plugins(loaded=True) if __name__ == "__main__": + from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/io/util.py b/skimage/io/util.py new file mode 100644 index 00000000..74158cac --- /dev/null +++ b/skimage/io/util.py @@ -0,0 +1,35 @@ +try: + from urllib.request import urlopen # Python 3 +except ImportError: + from urllib2 import urlopen # Python 2 + +import os +import re +import tempfile +from contextlib import contextmanager +import six + + +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, six.string_types) and + URL_REGEX.match(filename) is not None) + + +@contextmanager +def file_or_url_context(resource_name): + """Yield name of file from the given resource (i.e. file or url).""" + if is_url(resource_name): + _, ext = os.path.splitext(resource_name) + with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as f: + u = urlopen(resource_name) + f.write(u.read()) + try: + yield f.name + finally: + os.remove(f.name) + else: + yield resource_name diff --git a/skimage/io/video.py b/skimage/io/video.py deleted file mode 100644 index 003a1f53..00000000 --- a/skimage/io/video.py +++ /dev/null @@ -1,366 +0,0 @@ -import numpy as np -import os -from skimage.io import ImageCollection - -try: - import pygst - pygst.require("0.10") - import gst - import gobject - gobject.threads_init() - from gst.extend.discoverer import Discoverer - gstreamer_available = True -except ImportError: - gstreamer_available = False - -try: - import cv - opencv_available = True -except ImportError: - opencv_available = False - - -class CvVideo(object): - """ - Opencv-based video loader. - - Parameters - ---------- - source : str - Media location URI. Video file path or http address of IP camera. - size: tuple, optional - Size of returned array. - """ - def __init__(self, source=None, size=None, backend=None): - if not opencv_available: - raise ImportError("Opencv 2.0+ required") - self.source = source - self.capture = cv.CreateFileCapture(self.source) - self.size = size - - def get(self): - """ - Retrieve a video frame as a numpy array. - - Returns - ------- - output : array (image) - Retrieved image. - """ - img = cv.QueryFrame(self.capture) - if not self.size: - self.size = cv.GetSize(img) - img_mat = np.empty((self.size[1], self.size[0], 3), dtype=np.uint8) - if cv.GetSize(img) == self.size: - cv.Copy(img, cv.fromarray(img_mat)) - else: - cv.Resize(img, cv.fromarray(img_mat)) - # opencv stores images in BGR format - cv.CvtColor(cv.fromarray(img_mat), cv.fromarray(img_mat), - cv.CV_BGR2RGB) - return img_mat - - def seek_frame(self, frame_number): - """ - Seek to specified frame in video. - - Parameters - ---------- - frame_number : int - Frame position - """ - cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_FRAMES, - frame_number) - - def seek_time(self, milliseconds): - """ - Seek to specified time in video. - - Parameters - ---------- - milliseconds : int - Time position - """ - cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_MSEC, - milliseconds) - - def frame_count(self): - """ - Returns frame count of video. - - Returns - ------- - output : int - Frame count. - """ - return cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FRAME_COUNT) - - def duration(self): - """ - Returns time length of video in milliseconds. - - Returns - ------- - output : int - Time length [ms]. - """ - return cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FPS) * \ - cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FRAME_COUNT) - - -class GstVideo(object): - """ - GStreamer-based video loader. - - Parameters - ---------- - source : str - Media location URI. Video file path or http address of IP camera. - size: tuple, optional - Size of returned array. - sync: bool, optional (default False) - Frames are extracted per frame or per time basis. If enabled the video - time step continues onward according to the play rate. Useful for ip - cameras and other real time video feeds. - """ - def __init__(self, source=None, size=None, sync=False): - if not gstreamer_available: - raise ImportError("GStreamer Python bindings 0.10+ required") - self.source = source - self.size = size - self.video_length = 0 - self.video_rate = 0 - # extract video size - if not size: - gobject.idle_add(self._discover_one) - self.mainloop = gobject.MainLoop() - self.mainloop.run() - if not self.size: - self.size = (640, 480) - if os.path.exists(self.source): - self.source = "file://" + self.source - self._create_main_pipeline(self.source, self.size, sync) - - def _discover_one(self): - """ - Callback to start media discovery process, used to retrieve video parameters. - """ - discoverer = Discoverer(self.source) - discoverer.connect('discovered', self._discovered) - discoverer.discover() - return False - - def _discovered(self, d, is_media): - """ - Callback to on media discovery result. - """ - if is_media: - self.size = (d.videowidth, d.videoheight) - self.video_length = d.videolength / gst.MSECOND - self.video_rate = d.videorate.num - self.mainloop.quit() - return False - - def _create_main_pipeline(self, source, size, sync): - """ - Create the frame extraction pipeline. - """ - pipeline_string = "uridecodebin name=decoder uri=%s ! ffmpegcolorspace ! videoscale ! appsink name=play_sink" % self.source - self.pipeline = gst.parse_launch(pipeline_string) - caps = "video/x-raw-rgb, width=%d, height=%d, depth=24, bpp=24" % size - self.decoder = self.pipeline.get_by_name("decoder") - self.appsink = self.pipeline.get_by_name('play_sink') - self.appsink.set_property('emit-signals', True) - self.appsink.set_property('sync', sync) - self.appsink.set_property('drop', True) - self.appsink.set_property('max-buffers', 1) - self.appsink.set_property('caps', gst.caps_from_string(caps)) - if self.pipeline.set_state(gst.STATE_PLAYING) == gst.STATE_CHANGE_FAILURE: - raise NameError("Failed to load video source %s" % self.source) - self.appsink.emit('pull-preroll') - - def get(self): - """ - Retrieve a video frame as a numpy array. - - Returns - ------- - output : array (image) - Retrieved image. - """ - buff = self.appsink.emit('pull-buffer') - img_mat = np.ndarray(shape=(self.size[1], self.size[0], 3), - dtype=np.uint8, buffer=buff.data) - return img_mat - - def seek_frame(self, frame_number): - """ - Seek to specified frame in video. - - Parameters - ---------- - frame_number : int - Frame position - """ - self.pipeline.seek_simple(gst.FORMAT_DEFAULT, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, frame_number) - - def seek_time(self, milliseconds): - """ - Seek to specified time in video. - - Parameters - ---------- - milliseconds : int - Time position - """ - self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, milliseconds / 1000.0 * gst.SECOND) - - def frame_count(self): - """ - Returns frame count of video. - - Returns - ------- - output : int - Frame count. - """ - return self.video_length / 1000 * self.video_rate - - def duration(self): - """ - Returns time length of video in milliseconds. - - Returns - ------- - output : int - Time length [ms]. - """ - return self.video_length - - -class Video(object): - """ - Video loader. Supports Opencv and Gstreamer backends. - - Parameters - ---------- - source : str - Media location URI. Video file path or http address of IP camera. - size: tuple, optional - Size of returned array. - sync: bool, optional (default False) - Frames are extracted per frame or per time basis. Gstreamer only. - If enabled the video time step continues onward according to the play rate. - Useful for IP cameras and other real time video feeds. - backend: str, 'gstreamer' or 'opencv' - Backend to use. - """ - def __init__(self, source=None, size=None, sync=False, backend=None): - if backend is None: - # select backend that is available - if gstreamer_available: - self.video = GstVideo(source, size, sync) - elif opencv_available: - self.video = CvVideo(source, size) - else: - # if no backend available, raise exception - self.video = GstVideo(source, size, sync) - elif backend == "gstreamer": - self.video = GstVideo(source, size, sync) - elif backend == "opencv": - self.video = CvVideo(source, size) - else: - raise ValueError("Unknown backend: %s", backend) - - def get(self): - """ - Retrieve the next video frame as a numpy array. - - Returns - ------- - output : array (image) - Retrieved image. - """ - return self.video.get() - - def seek_frame(self, frame_number): - """ - Seek to specified frame in video. - - Parameters - ---------- - frame_number : int - Frame position - """ - self.video.seek_frame(frame_number) - - def seek_time(self, milliseconds): - """ - Seek to specified time in video. - - Parameters - ---------- - milliseconds : int - Time position - """ - self.video.seek_time(milliseconds) - - def frame_count(self): - """ - Returns frame count of video. - - Returns - ------- - output : int - Frame count. - """ - return self.video.frame_count() - - def duration(self): - """ - Returns time length of video in milliseconds. - - Returns - ------- - output : int - Time length [ms]. - """ - return self.video.duration() - - def get_index_frame(self, frame_number): - """ - Retrieve a specified video frame as a numpy array. - - Parameters - ---------- - frame_number : int - Frame position - - Returns - ------- - output : array (image) - Retrieved image. - """ - self.video.seek_frame(frame_number) - return self.video.get() - - def get_collection(self, time_range=None): - """ - Returns an ImageCollection object. - - Parameters - ---------- - time_range: range (int), optional - Time steps to extract, defaults to the entire length of video. - - Returns - ------- - output: ImageCollection - Collection of images iterator. - """ - if not time_range: - time_range = range(int(self.frame_count())) - return ImageCollection(time_range, load_func=self.get_index_frame) - - -__all__ = ["Video"] diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 108cd7d9..e07b9789 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -1,11 +1,14 @@ -from .find_contours import find_contours -from ._marching_cubes import marching_cubes, mesh_surface_area +from ._find_contours import find_contours +from ._marching_cubes import (marching_cubes, mesh_surface_area, + correct_mesh_orientation) from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon from ._moments import moments, moments_central, moments_normalized, moments_hu +from .profile import profile_line from .fit import LineModel, CircleModel, EllipseModel, ransac from .block import block_reduce +from ._label import label __all__ = ['find_contours', @@ -23,6 +26,8 @@ __all__ = ['find_contours', 'moments_central', 'moments_normalized', 'moments_hu', - 'sum_blocks', 'marching_cubes', - 'mesh_surface_area'] + 'mesh_surface_area', + 'correct_mesh_orientation', + 'profile_line', + 'label'] diff --git a/skimage/morphology/ccomp.pxd b/skimage/measure/_ccomp.pxd similarity index 100% rename from skimage/morphology/ccomp.pxd rename to skimage/measure/_ccomp.pxd diff --git a/skimage/morphology/ccomp.pyx b/skimage/measure/_ccomp.pyx similarity index 87% rename from skimage/morphology/ccomp.pyx rename to skimage/measure/_ccomp.pyx index 91e2611c..26b9645a 100644 --- a/skimage/morphology/ccomp.pyx +++ b/skimage/measure/_ccomp.pyx @@ -4,6 +4,7 @@ #cython: wraparound=False import numpy as np +import warnings cimport numpy as cnp @@ -82,7 +83,7 @@ cdef inline void link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node): # Connected components search as described in Fiorio et al. -def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): +def label(input, DTYPE_t neighbors=8, background=None, return_num=False): """Label connected regions of an integer array. Two pixels are connected when they are neighbors and have the same value. @@ -100,11 +101,14 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): ---------- input : ndarray of dtype int Image to label. - neighbors : {4, 8}, int + neighbors : {4, 8}, int, optional Whether to use 4- or 8-connectivity. - background : int + background : int, optional Consider all pixels with this value as background pixels, and label - them as -1. + them as -1. (Note: background pixels will be labeled as 0 starting with + version 0.12). + return_num : bool, optional + Whether to return the number of assigned labels. Returns ------- @@ -157,17 +161,27 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): cdef DTYPE_t i, j + cdef DTYPE_t background_val + + if background is None: + background_val = -1 + warnings.warn(DeprecationWarning( + 'The default value for `background` will change to 0 in v0.12' + )) + else: + background_val = background + cdef DTYPE_t background_node = -999 if neighbors != 4 and neighbors != 8: raise ValueError('Neighbors must be either 4 or 8.') # Initialize the first row - if data[0, 0] == background: + if data[0, 0] == background_val: link_bg(forest_p, 0, &background_node) for j in range(1, cols): - if data[0, j] == background: + if data[0, j] == background_val: link_bg(forest_p, j, &background_node) if data[0, j] == data[0, j-1]: @@ -175,7 +189,7 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): for i in range(1, rows): # Handle the first column - if data[i, 0] == background: + if data[i, 0] == background_val: link_bg(forest_p, i * cols, &background_node) if data[i, 0] == data[i-1, 0]: @@ -186,7 +200,7 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): join_trees(forest_p, i*cols, (i-1)*cols + 1) for j in range(1, cols): - if data[i, j] == background: + if data[i, j] == background_val: link_bg(forest_p, i * cols + j, &background_node) if neighbors == 8: diff --git a/skimage/measure/find_contours.py b/skimage/measure/_find_contours.py similarity index 98% rename from skimage/measure/find_contours.py rename to skimage/measure/_find_contours.py index d36c2110..0eea9126 100755 --- a/skimage/measure/find_contours.py +++ b/skimage/measure/_find_contours.py @@ -1,5 +1,5 @@ import numpy as np -from . import _find_contours +from . import _find_contours_cy from collections import deque @@ -115,8 +115,8 @@ def find_contours(array, level, positive_orientation not in _param_options): 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') + point_list = _find_contours_cy.iterate_and_store(array, level, + fully_connected == 'high') contours = _assemble_contours(_take_2(point_list)) if positive_orientation == 'high': contours = [c[::-1] for c in contours] diff --git a/skimage/measure/_find_contours.pyx b/skimage/measure/_find_contours_cy.pyx similarity index 100% rename from skimage/measure/_find_contours.pyx rename to skimage/measure/_find_contours_cy.pyx diff --git a/skimage/measure/_label.py b/skimage/measure/_label.py new file mode 100644 index 00000000..47e8af74 --- /dev/null +++ b/skimage/measure/_label.py @@ -0,0 +1,6 @@ +from ._ccomp import label as _label + +def label(input, neighbors=8, background=None, return_num=False): + return _label(input, neighbors, background, return_num) + +label.__doc__ = _label.__doc__ diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py index c3ec9070..8f9dfcbc 100644 --- a/skimage/measure/_marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -68,19 +68,25 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): lexicographical order) coordinate in the contour. This is a side-effect of how the input array is traversed, but can be relied upon. + The generated mesh does not guarantee coherent orientation because of how + symmetry is used in the algorithm. If this is required, e.g. due to a + particular visualization package or for generating 3D printing STL files, + the utility ``skimage.measure.correct_mesh_orientation`` is available to + fix this in post-processing. + 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.)) + >>> from mayavi import mlab # doctest: +SKIP + >>> verts, faces = marching_cubes(myvolume, 0.0, (1., 1., 2.)) # doctest: +SKIP >>> mlab.triangular_mesh([vert[0] for vert in verts], ... [vert[1] for vert in verts], ... [vert[2] for vert in verts], - ... tris) - >>> mlab.show() + ... faces) # doctest: +SKIP + >>> mlab.show() # doctest: +SKIP References ---------- @@ -90,6 +96,7 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): See Also -------- + skimage.measure.correct_mesh_orientation skimage.measure.mesh_surface_area """ @@ -106,17 +113,17 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): # 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) + raw_faces = _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) + verts, faces = _marching_cubes_cy.unpack_unique_verts(raw_faces) return np.asarray(verts), np.asarray(faces) -def mesh_surface_area(verts, tris): +def mesh_surface_area(verts, faces): """ Compute surface area, given vertices & triangular faces @@ -145,13 +152,123 @@ def mesh_surface_area(verts, tris): See Also -------- skimage.measure.marching_cubes + skimage.measure.correct_mesh_orientation """ # Fancy indexing to define two vector arrays from triangle vertices - actual_verts = verts[tris] + actual_verts = verts[faces] 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. + + +def correct_mesh_orientation(volume, verts, faces, spacing=(1., 1., 1.), + gradient_direction='descent'): + """ + Correct orientations of mesh faces. + + Parameters + ---------- + volume : (M, N, P) array of doubles + Input data volume to find isosurfaces. Will be cast to `np.float64`. + 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`. + spacing : length-3 tuple of floats + Voxel spacing in spatial dimensions corresponding to numpy array + indexing dimensions (M, N, P) as in `volume`. + gradient_direction : string + Controls if the mesh was generated from an isosurface with gradient + ascent toward objects of interest (the default), or the opposite. + The two options are: + * descent : Object was greater than exterior + * ascent : Exterior was greater than object + + Returns + ------- + faces_corrected (F, 3) array of ints + Corrected list of faces referencing vertex coordinates in `verts`. + + Notes + ----- + Certain applications and mesh processing algorithms require all faces + to be oriented in a consistent way. Generally, this means a normal vector + points "out" of the meshed shapes. This algorithm corrects the output from + `skimage.measure.marching_cubes` by flipping the orientation of + mis-oriented faces. + + Because marching cubes could be used to find isosurfaces either on + gradient descent (where the desired object has greater values than the + exterior) or ascent (where the desired object has lower values than the + exterior), the ``gradient_direction`` kwarg allows the user to inform this + algorithm which is correct. If the resulting mesh appears to be oriented + completely incorrectly, try changing this option. + + The arguments expected by this function are the exact outputs from + `skimage.measure.marching_cubes`. Only `faces` is corrected and returned, + as the vertices do not change; only the order in which they are + referenced. + + This algorithm assumes ``faces`` provided are all triangles. + + See Also + -------- + skimage.measure.marching_cubes + skimage.measure.mesh_surface_area + + """ + import scipy.ndimage as ndi + + # Calculate gradient of `volume`, then interpolate to vertices in `verts` + grad_x, grad_y, grad_z = np.gradient(volume, *spacing) + + # Fancy indexing to define two vector arrays from triangle vertices + actual_verts = verts[faces] + a = actual_verts[:, 0, :] - actual_verts[:, 1, :] + b = actual_verts[:, 0, :] - actual_verts[:, 2, :] + + # Find triangle centroids + centroids = (actual_verts.sum(axis=1) / 3.).T + + del actual_verts + + # Interpolate face centroids into each gradient axis + grad_centroids_x = ndi.map_coordinates(grad_x, centroids) + grad_centroids_y = ndi.map_coordinates(grad_y, centroids) + grad_centroids_z = ndi.map_coordinates(grad_z, centroids) + + # Combine and normalize interpolated gradients + grad_centroids = np.c_[grad_centroids_x, grad_centroids_y, + grad_centroids_z] + grad_centroids = (grad_centroids / + (np.sum(grad_centroids ** 2, + axis=1) ** 0.5)[:, np.newaxis]) + + # Find normal vectors for each face via cross product + crosses = np.cross(a, b) + crosses = crosses / (np.sum(crosses ** 2, axis=1) ** (0.5))[:, np.newaxis] + + # Take dot product + dotproducts = (grad_centroids * crosses).sum(axis=1) + + # Find mis-oriented faces + if 'descent' in gradient_direction: + # Faces with incorrect orientations have dot product < 0 + indices = (dotproducts < 0).nonzero()[0] + elif 'ascent' in gradient_direction: + # Faces with incorrection orientation have dot product > 0 + indices = (dotproducts > 0).nonzero()[0] + else: + raise ValueError("Incorrect input %s in `gradient_direction`, see " + "docstring." % (gradient_direction)) + + # Correct orientation and return, without modifying original data + faces_corrected = faces.copy() + faces_corrected[indices] = faces_corrected[indices, ::-1] + + return faces_corrected diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index 6b7197d2..e6ccdb75 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -12,7 +12,7 @@ def moments(double[:, :] image, Py_ssize_t order=3): * 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 + Note that raw moments are neither translation, scale nor rotation invariant. Parameters diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 798e04cc..c6e1fa03 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -4,10 +4,8 @@ from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage -from collections import MutableMapping - -from skimage.morphology import convex_hull_image, label -from skimage.measure import _moments +from ._label import label +from . import _moments __all__ = ['regionprops', 'perimeter'] @@ -56,6 +54,8 @@ PROPS = { 'WeightedNormalizedMoments': 'weighted_moments_normalized' } +PROP_VALS = PROPS.values() + class _cached_property(object): """Decorator to use a function as a cached property. @@ -105,16 +105,15 @@ class _cached_property(object): return value -class _RegionProperties(MutableMapping): +class _RegionProperties(object): def __init__(self, slice, label, label_image, intensity_image, - cache_active, properties=None): + cache_active): 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): @@ -136,6 +135,7 @@ class _RegionProperties(MutableMapping): @_cached_property def convex_image(self): + from ..morphology.convex_hull import convex_hull_image return convex_hull_image(self.image) @_cached_property @@ -304,46 +304,40 @@ class _RegionProperties(MutableMapping): 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()) + return iter(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 __eq__(self, other): + if not isinstance(other, _RegionProperties): + return False -def regionprops(label_image, properties=None, - intensity_image=None, cache=True): - """Measure properties of labelled image regions. + for key in PROP_VALS: + try: + #so that NaNs are equal + np.testing.assert_equal(getattr(self, key, None), + getattr(other, key, None)) + except AssertionError: + return False + + return True + + +def regionprops(label_image, intensity_image=None, cache=True): + """Measure properties of labeled image regions. Parameters ---------- label_image : (N, M) ndarray - Labelled input image. - properties : {'all', list} - **Deprecated parameter** - - This parameter is not needed any more since all properties are - determined dynamically. - + Labeled input image. intensity_image : (N, M) ndarray, optional - Intensity image with same size as labelled image. Default is None. + Intensity image with same size as labeled 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 @@ -351,9 +345,9 @@ def regionprops(label_image, properties=None, Returns ------- - properties : list - List containing a properties for each region. The properties of each - region can be accessed as attributes and keys. + properties : list of RegionProperties + Each item describes one labeled region, and can be accessed using the + attributes listed below. Notes ----- @@ -399,7 +393,7 @@ def regionprops(label_image, properties=None, **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 + **max_intensity** : float Value with the greatest intensity in the region. **mean_intensity** : float Value with the mean intensity in the region. @@ -479,13 +473,16 @@ def regionprops(label_image, properties=None, Examples -------- - >>> from skimage.data import coins + >>> from skimage import data, util >>> from skimage.morphology import label - >>> img = coins() > 110 + >>> img = util.img_as_ubyte(data.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 labeled object + (22.729879860483141, 81.912285234465827) + >>> props[0]['centroid'] # centroid of first labeled object + (22.729879860483141, 81.912285234465827) + """ label_image = np.squeeze(label_image) @@ -493,20 +490,17 @@ def regionprops(label_image, properties=None, 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): + if sl is None: + continue + label = i + 1 - props = _RegionProperties(sl, label, label_image, - intensity_image, cache, properties=properties) + props = _RegionProperties(sl, label, label_image, intensity_image, + cache) regions.append(props) return regions diff --git a/skimage/measure/block.py b/skimage/measure/block.py index fad5668c..aabd1a39 100644 --- a/skimage/measure/block.py +++ b/skimage/measure/block.py @@ -28,27 +28,25 @@ def block_reduce(image, block_size, func=np.sum, cval=0): -------- >>> from skimage.measure import block_reduce >>> image = np.arange(3*3*4).reshape(3, 3, 4) - >>> image + >>> image # doctest: +NORMALIZE_WHITESPACE 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) + >>> image_max1 = block_reduce(image, block_size=(1, 3, 4), func=np.max) + >>> image_max1 # doctest: +NORMALIZE_WHITESPACE array([[[11]], - [[23]], - [[35]]]) - >>> block_reduce(image, block_size=(3, 1, 4), func=np.max) + >>> image_max2 = block_reduce(image, block_size=(3, 1, 4), func=np.max) + >>> image_max2 # doctest: +NORMALIZE_WHITESPACE array([[[27], [31], [35]]]) diff --git a/skimage/measure/fit.py b/skimage/measure/fit.py index 66502ffa..8f9ceb9c 100644 --- a/skimage/measure/fit.py +++ b/skimage/measure/fit.py @@ -1,4 +1,5 @@ import math +import warnings import numpy as np from scipy import optimize @@ -11,7 +12,13 @@ def _check_data_dim(data, dim): class BaseModel(object): def __init__(self): - self._params = None + self.params = None + + @property + def _params(self): + warnings.warn('`_params` attribute is deprecated, ' + 'use `params` instead.') + return self.params class LineModel(BaseModel): @@ -30,12 +37,13 @@ class LineModel(BaseModel): 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. + Attributes + ---------- + params : tuple + Line model parameters in the following order `dist`, `theta`. + """ def estimate(self, data): @@ -68,7 +76,7 @@ class LineModel(BaseModel): # line always passes through mean dist = X0[0] * math.cos(theta) + X0[1] * math.sin(theta) - self._params = (dist, theta) + self.params = (dist, theta) def residuals(self, data): """Determine residuals of data to model. @@ -89,7 +97,7 @@ class LineModel(BaseModel): _check_data_dim(data, dim=2) - dist, theta = self._params + dist, theta = self.params x = data[:, 0] y = data[:, 1] @@ -114,7 +122,7 @@ class LineModel(BaseModel): """ if params is None: - params = self._params + params = self.params dist, theta = params return (dist - y * math.sin(theta)) / math.cos(theta) @@ -136,7 +144,7 @@ class LineModel(BaseModel): """ if params is None: - params = self._params + params = self.params dist, theta = params return (dist - x * math.cos(theta)) / math.sin(theta) @@ -154,12 +162,13 @@ class CircleModel(BaseModel): 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. + Attributes + ---------- + params : tuple + Circle model parameters in the following order `xc`, `yc`, `r`. + """ def estimate(self, data): @@ -203,7 +212,7 @@ class CircleModel(BaseModel): params0 = (xc0, yc0, r0) params, _ = optimize.leastsq(fun, params0, Dfun=Dfun, col_deriv=True) - self._params = params + self.params = params def residuals(self, data): """Determine residuals of data to model. @@ -224,7 +233,7 @@ class CircleModel(BaseModel): _check_data_dim(data, dim=2) - xc, yc, r = self._params + xc, yc, r = self.params x = data[:, 0] y = data[:, 1] @@ -249,7 +258,7 @@ class CircleModel(BaseModel): """ if params is None: - params = self._params + params = self.params xc, yc, r = params x = xc + r * np.cos(t) @@ -279,12 +288,18 @@ class EllipseModel(BaseModel): 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:: + 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. + Attributes + ---------- + params : tuple + Ellipse model parameters in the following order `xc`, `yc`, `a`, + `b`, `theta`. + """ def estimate(self, data): @@ -353,7 +368,7 @@ class EllipseModel(BaseModel): params, _ = optimize.leastsq(fun, params0, Dfun=Dfun, col_deriv=True) - self._params = params[:5] + self.params = params[:5] def residuals(self, data): """Determine residuals of data to model. @@ -374,7 +389,7 @@ class EllipseModel(BaseModel): _check_data_dim(data, dim=2) - xc, yc, a, b, theta = self._params + xc, yc, a, b, theta = self.params ctheta = math.cos(theta) stheta = math.sin(theta) @@ -436,7 +451,7 @@ class EllipseModel(BaseModel): """ if params is None: - params = self._params + params = self.params xc, yc, a, b, theta = params ct = np.cos(t) @@ -550,21 +565,23 @@ def ransac(data, model_class, min_samples, residual_threshold, >>> model = EllipseModel() >>> model.estimate(data) - >>> model._params - array([ 4.85808595e+02, 4.51492793e+02, 1.15018491e+03, - 5.52428289e+00, 7.32420126e-01]) + >>> model.params # doctest: +SKIP + array([ -3.30354146e+03, -2.87791160e+03, 5.59062118e+03, + 7.84365066e+00, 7.19203152e-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] + >>> ransac_model.params + array([ 20.12762373, 29.73563063, 4.81499637, 10.4743584 , 0.05217117]) + >>> inliers + array([False, False, False, False, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True], dtype=bool) Robustly estimate geometric transformation: @@ -578,10 +595,12 @@ def ransac(data, model_class, min_samples, residual_threshold, >>> 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]) - + array([False, False, False, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True], dtype=bool) """ diff --git a/skimage/measure/profile.py b/skimage/measure/profile.py new file mode 100644 index 00000000..fef3fac8 --- /dev/null +++ b/skimage/measure/profile.py @@ -0,0 +1,112 @@ +import numpy as np +import scipy.ndimage as nd + + +def profile_line(img, src, dst, linewidth=1, + order=1, mode='constant', cval=0.0): + """Return the intensity profile of an image measured along a scan line. + + Parameters + ---------- + img : numeric array, shape (M, N[, C]) + The image, either grayscale (2D array) or multichannel + (3D array, where the final axis contains the channel + information). + src : 2-tuple of numeric scalar (float or int) + The start point of the scan line. + dst : 2-tuple of numeric scalar (float or int) + The end point of the scan line. + linewidth : int, optional + Width of the scan, perpendicular to the line + order : int in {0, 1, 2, 3, 4, 5}, optional + The order of the spline interpolation to compute image values at + non-integer coordinates. 0 means nearest-neighbor interpolation. + mode : string, one of {'constant', 'nearest', 'reflect', 'wrap'}, optional + How to compute any values falling outside of the image. + cval : float, optional + If `mode` is 'constant', what constant value to use outside the image. + + Returns + ------- + return_value : array + The intensity profile along the scan line. The length of the profile + is the ceil of the computed length of the scan line. + + Examples + -------- + >>> x = np.array([[1, 1, 1, 2, 2, 2]]) + >>> img = np.vstack([np.zeros_like(x), x, x, x, np.zeros_like(x)]) + >>> img + array([[0, 0, 0, 0, 0, 0], + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [0, 0, 0, 0, 0, 0]]) + >>> profile_line(img, (2, 1), (2, 4)) + array([ 1., 1., 2., 2.]) + + Notes + ----- + The destination point is included in the profile, in contrast to + standard numpy indexing. + """ + perp_lines = _line_profile_coordinates(src, dst, linewidth=linewidth) + if img.ndim == 3: + pixels = [nd.map_coordinates(img[..., i], perp_lines, + order=order, mode=mode, cval=cval) + for i in range(img.shape[2])] + pixels = np.transpose(np.asarray(pixels), (1, 2, 0)) + else: + pixels = nd.map_coordinates(img, perp_lines, + order=order, mode=mode, cval=cval) + intensities = pixels.mean(axis=1) + + return intensities + + +def _line_profile_coordinates(src, dst, linewidth=1): + """Return the coordinates of the profile of an image along a scan line. + + Parameters + ---------- + src : 2-tuple of numeric scalar (float or int) + The start point of the scan line. + dst : 2-tuple of numeric scalar (float or int) + The end point of the scan line. + linewidth : int, optional + Width of the scan, perpendicular to the line + + Returns + ------- + coords : array, shape (2, N, C), float + The coordinates of the profile along the scan line. The length of the + profile is the ceil of the computed length of the scan line. + + Notes + ----- + This is a utility method meant to be used internally by skimage functions. + The destination point is included in the profile, in contrast to + standard numpy indexing. + """ + src_row, src_col = src = np.asarray(src, dtype=float) + dst_row, dst_col = dst = np.asarray(dst, dtype=float) + d_row, d_col = dst - src + theta = np.arctan2(d_row, d_col) + + length = np.ceil(np.hypot(d_row, d_col) + 1) + # we add one above because we include the last point in the profile + # (in contrast to standard numpy indexing) + line_col = np.linspace(src_col, dst_col, length) + line_row = np.linspace(src_row, dst_row, length) + + # we subtract 1 from linewidth to change from pixel-counting + # (make this line 3 pixels wide) to point distances (the + # distance between pixel centers) + col_width = (linewidth - 1) * np.sin(-theta) / 2 + row_width = (linewidth - 1) * np.cos(theta) / 2 + perp_rows = np.array([np.linspace(row_i - row_width, row_i + row_width, + linewidth) for row_i in line_row]) + perp_cols = np.array([np.linspace(col_i - col_width, col_i + col_width, + linewidth) for col_i in line_col]) + return np.array([perp_rows, perp_cols]) + diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index be57ca7b..0fab0787 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -12,11 +12,14 @@ def configuration(parent_package='', top_path=None): config = Configuration('measure', parent_package, top_path) config.add_data_dir('tests') - cython(['_find_contours.pyx'], working_path=base_path) + cython(['_ccomp.pyx'], working_path=base_path) + cython(['_find_contours_cy.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'], + config.add_extension('_ccomp', sources=['_ccomp.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_find_contours_cy', sources=['_find_contours_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_moments', sources=['_moments.c'], include_dirs=[get_numpy_include_dirs()]) diff --git a/skimage/measure/tests/test_fit.py b/skimage/measure/tests/test_fit.py index bde1fcc7..3ef7a0f0 100644 --- a/skimage/measure/tests/test_fit.py +++ b/skimage/measure/tests/test_fit.py @@ -10,7 +10,7 @@ def test_line_model_invalid_input(): def test_line_model_predict(): model = LineModel() - model._params = (10, 1) + model.params = (10, 1) x = np.arange(-10, 10) y = model.predict_y(x) assert_almost_equal(x, model.predict_x(y)) @@ -19,7 +19,7 @@ def test_line_model_predict(): def test_line_model_estimate(): # generate original data without noise model0 = LineModel() - model0._params = (10, 1) + model0.params = (10, 1) x0 = np.arange(-100, 100) y0 = model0.predict_y(x0) data0 = np.column_stack([x0, y0]) @@ -33,18 +33,18 @@ def test_line_model_estimate(): model_est.estimate(data) # test whether estimated parameters almost equal original parameters - assert_almost_equal(model0._params, model_est._params, 1) + assert_almost_equal(model0.params, model_est.params, 1) def test_line_model_residuals(): model = LineModel() - model._params = (0, 0) + 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) + 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) + assert_almost_equal(abs(model.residuals(np.array([[np.sqrt(50), 0]]))), 0) def test_line_model_under_determined(): @@ -59,7 +59,7 @@ def test_circle_model_invalid_input(): def test_circle_model_predict(): model = CircleModel() r = 5 - model._params = (0, 0, r) + 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))) @@ -69,7 +69,7 @@ def test_circle_model_predict(): def test_circle_model_estimate(): # generate original data without noise model0 = CircleModel() - model0._params = (10, 12, 3) + model0.params = (10, 12, 3) t = np.linspace(0, 2 * np.pi, 1000) data0 = model0.predict_xy(t) @@ -82,12 +82,12 @@ def test_circle_model_estimate(): model_est.estimate(data) # test whether estimated parameters almost equal original parameters - assert_almost_equal(model0._params, model_est._params, 1) + assert_almost_equal(model0.params, model_est.params, 1) def test_circle_model_residuals(): model = CircleModel() - model._params = (0, 0, 5) + 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) @@ -101,7 +101,7 @@ def test_ellipse_model_invalid_input(): def test_ellipse_model_predict(): model = EllipseModel() r = 5 - model._params = (0, 0, 5, 10, 0) + 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))) @@ -111,7 +111,7 @@ def test_ellipse_model_predict(): def test_ellipse_model_estimate(): # generate original data without noise model0 = EllipseModel() - model0._params = (10, 20, 15, 25, 0) + model0.params = (10, 20, 15, 25, 0) t = np.linspace(0, 2 * np.pi, 100) data0 = model0.predict_xy(t) @@ -124,13 +124,13 @@ def test_ellipse_model_estimate(): model_est.estimate(data) # test whether estimated parameters almost equal original parameters - assert_almost_equal(model0._params, model_est._params, 0) + assert_almost_equal(model0.params, model_est.params, 0) -def test_line_model_residuals(): +def test_ellipse_model_residuals(): model = EllipseModel() # vertical line through origin - model._params = (0, 0, 10, 5, 0) + 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) @@ -141,7 +141,7 @@ def test_ransac_shape(): # generate original data without noise model0 = CircleModel() - model0._params = (10, 12, 3) + model0.params = (10, 12, 3) t = np.linspace(0, 2 * np.pi, 1000) data0 = model0.predict_xy(t) @@ -155,7 +155,7 @@ def test_ransac_shape(): model_est, inliers = ransac(data0, CircleModel, 3, 5) # test whether estimated parameters equal original parameters - assert_equal(model0._params, model_est._params) + assert_equal(model0.params, model_est.params) for outlier in outliers: assert outlier not in inliers @@ -204,5 +204,13 @@ def test_ransac_is_model_valid(): assert_equal(inliers, None) +def test_deprecated_params_attribute(): + model = LineModel() + model.params = (10, 1) + x = np.arange(-10, 10) + y = model.predict_y(x) + assert_equal(model.params, model._params) + + 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 index b3c2ddc1..60adbb0d 100644 --- a/skimage/measure/tests/test_marching_cubes.py +++ b/skimage/measure/tests/test_marching_cubes.py @@ -2,7 +2,8 @@ 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 +from skimage.measure import (marching_cubes, mesh_surface_area, + correct_mesh_orientation) def test_marching_cubes_isotropic(): @@ -36,5 +37,25 @@ def test_invalid_input(): assert_raises(ValueError, marching_cubes, np.zeros((20, 20)), 0) +def test_correct_mesh_orientation(): + sphere_small = ellipsoid(1, 1, 1, levelset=True) + verts, faces = marching_cubes(sphere_small, 0.) + + # Correct mesh orientation - descent + corrected_faces1 = correct_mesh_orientation(sphere_small, verts, faces, + gradient_direction='descent') + corrected_faces2 = correct_mesh_orientation(sphere_small, verts, faces, + gradient_direction='ascent') + + # Ensure ascent is opposite of descent for all faces + np.testing.assert_array_equal(corrected_faces1, corrected_faces2[:, ::-1]) + + # Ensure correct faces have been reversed: 1, 4, and 5 + idx = [1, 4, 5] + expected = faces.copy() + expected[idx] = expected[idx, ::-1] + np.testing.assert_array_equal(expected, corrected_faces1) + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_profile.py b/skimage/measure/tests/test_profile.py new file mode 100644 index 00000000..e911673b --- /dev/null +++ b/skimage/measure/tests/test_profile.py @@ -0,0 +1,110 @@ +from numpy.testing import assert_equal, assert_almost_equal +import numpy as np + +from skimage.measure import profile_line + +image = np.arange(100).reshape((10, 10)).astype(np.float) + +def test_horizontal_rightward(): + prof = profile_line(image, (0, 2), (0, 8), order=0) + expected_prof = np.arange(2, 9) + assert_equal(prof, expected_prof) + + +def test_horizontal_leftward(): + prof = profile_line(image, (0, 8), (0, 2), order=0) + expected_prof = np.arange(8, 1, -1) + assert_equal(prof, expected_prof) + + +def test_vertical_downward(): + prof = profile_line(image, (2, 5), (8, 5), order=0) + expected_prof = np.arange(25, 95, 10) + assert_equal(prof, expected_prof) + + +def test_vertical_upward(): + prof = profile_line(image, (8, 5), (2, 5), order=0) + expected_prof = np.arange(85, 15, -10) + assert_equal(prof, expected_prof) + + +def test_45deg_right_downward(): + prof = profile_line(image, (2, 2), (8, 8), order=0) + expected_prof = np.array([22, 33, 33, 44, 55, 55, 66, 77, 77, 88]) + # repeats are due to aliasing using nearest neighbor interpolation. + # to see this, imagine a diagonal line with markers every unit of + # length traversing a checkerboard pattern of squares also of unit + # length. Because the line is diagonal, sometimes more than one + # marker will fall on the same checkerboard box. + assert_almost_equal(prof, expected_prof) + + +def test_45deg_right_downward_interpolated(): + prof = profile_line(image, (2, 2), (8, 8), order=1) + expected_prof = np.linspace(22, 88, 10) + assert_almost_equal(prof, expected_prof) + + +def test_45deg_right_upward(): + prof = profile_line(image, (8, 2), (2, 8), order=1) + expected_prof = np.arange(82, 27, -6) + assert_almost_equal(prof, expected_prof) + + +def test_45deg_left_upward(): + prof = profile_line(image, (8, 8), (2, 2), order=1) + expected_prof = np.arange(88, 21, -22. / 3) + assert_almost_equal(prof, expected_prof) + + +def test_45deg_left_downward(): + prof = profile_line(image, (2, 8), (8, 2), order=1) + expected_prof = np.arange(28, 83, 6) + assert_almost_equal(prof, expected_prof) + + +def test_pythagorean_triangle_right_downward(): + prof = profile_line(image, (1, 1), (7, 9), order=0) + expected_prof = np.array([11, 22, 23, 33, 34, 45, 56, 57, 67, 68, 79]) + assert_equal(prof, expected_prof) + + +def test_pythagorean_triangle_right_downward_interpolated(): + prof = profile_line(image, (1, 1), (7, 9), order=1) + expected_prof = np.linspace(11, 79, 11) + assert_almost_equal(prof, expected_prof) + +pyth_image = np.zeros((6, 7), np.float) +line = ((1, 2, 2, 3, 3, 4), (1, 2, 3, 3, 4, 5)) +below = ((2, 2, 3, 4, 4, 5), (0, 1, 2, 3, 4, 4)) +above = ((0, 1, 1, 2, 3, 3), (2, 2, 3, 4, 5, 6)) +pyth_image[line] = 1.8 +pyth_image[below] = 0.6 +pyth_image[above] = 0.6 + + +def test_pythagorean_triangle_right_downward_linewidth(): + prof = profile_line(pyth_image, (1, 1), (4, 5), linewidth=3, order=0) + expected_prof = np.ones(6) + assert_almost_equal(prof, expected_prof) + + +def test_pythagorean_triangle_right_upward_linewidth(): + prof = profile_line(pyth_image[::-1, :], (4, 1), (1, 5), + linewidth=3, order=0) + expected_prof = np.ones(6) + assert_almost_equal(prof, expected_prof) + + +def test_pythagorean_triangle_transpose_left_down_linewidth(): + prof = profile_line(pyth_image.T[:, ::-1], (1, 4), (5, 1), + linewidth=3, order=0) + expected_prof = np.ones(6) + assert_almost_equal(prof, expected_prof) + + +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 c0c6443d..934e209e 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -23,9 +23,9 @@ INTENSITY_SAMPLE[1, 9:11] = 2 def test_all_props(): - regions = regionprops(SAMPLE, 'all', INTENSITY_SAMPLE)[0] + region = regionprops(SAMPLE, INTENSITY_SAMPLE)[0] for prop in PROPS: - regions[prop] + assert_equal(region[prop], getattr(region, PROPS[prop])) def test_dtype(): @@ -336,15 +336,41 @@ def test_weighted_moments_normalized(): 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) +def test_label_sequence(): + a = np.empty((2, 2), dtype=np.int) + a[:, :] = 2 + ps = regionprops(a) + assert len(ps) == 1 + assert ps[0].label == 2 - np.array([list(props.values()) for props in feats], np.float) - assert_equal(len(feats[0]), 8) + +def test_pure_background(): + a = np.zeros((2, 2), dtype=np.int) + ps = regionprops(a) + assert len(ps) == 0 + + +def test_invalid(): + ps = regionprops(SAMPLE) + def get_intensity_image(): + ps[0].intensity_image + assert_raises(AttributeError, get_intensity_image) + + +def test_equals(): + arr = np.zeros((100, 100), dtype=np.int) + arr[0:25, 0:25] = 1 + arr[50:99, 50:99] = 2 + + regions = regionprops(arr) + r1 = regions[0] + + regions = regionprops(arr) + r2 = regions[0] + r3 = regions[1] + + assert_equal(r1 == r2, True, "Same regionprops are not equal") + assert_equal(r1 != r3, True, "Different regionprops are equal") if __name__ == "__main__": diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index 4788c308..5b659093 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -1,18 +1,19 @@ from .binary import (binary_erosion, binary_dilation, binary_opening, binary_closing) 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) + black_tophat) from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball, octagon, star) -from .ccomp import label from .watershed import watershed from ._skeletonize import skeletonize, medial_axis from .convex_hull import convex_hull_image, convex_hull_object from .greyreconstruct import reconstruction from .misc import remove_small_objects +from ..measure._label import label +from skimage._shared.utils import deprecated as _deprecated +label = _deprecated('skimage.measure.label')(label) + __all__ = ['binary_erosion', 'binary_dilation', @@ -24,12 +25,6 @@ __all__ = ['binary_erosion', 'closing', 'white_tophat', 'black_tophat', - 'greyscale_erode', - 'greyscale_dilate', - 'greyscale_open', - 'greyscale_close', - 'greyscale_white_top_hat', - 'greyscale_black_top_hat', 'square', 'rectangle', 'diamond', diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py index 11181a7b..ddc62008 100644 --- a/skimage/morphology/_skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -69,7 +69,7 @@ def skeletonize(image): [0, 0, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8) >>> skel = skeletonize(ellipse) - >>> skel + >>> skel.astype(np.uint8) 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], @@ -212,7 +212,6 @@ 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 @@ -223,7 +222,7 @@ def medial_axis(image, mask=None, return_distance=False): [0, 0, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0]], dtype=uint8) - >>> morphology.medial_axis(square).astype(np.uint8) + >>> medial_axis(square).astype(np.uint8) array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 1, 0, 0], [0, 0, 0, 1, 0, 0, 0], diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index adc63607..c5b9eb3f 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -3,7 +3,7 @@ __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 ..measure._label import label from skimage.util import unique_rows @@ -54,6 +54,10 @@ def convex_hull_image(image): raise ImportError('Could not import scipy.spatial, only available in ' 'scipy >= 0.9.') + # Subtract offset + offset = coords.mean(axis=0) + coords -= offset + # Find the convex hull chull = Delaunay(coords).convex_hull v = coords[np.unique(chull)] @@ -63,6 +67,9 @@ def convex_hull_image(image): angles = np.arctan2(v_centred[:, 0], v_centred[:, 1]) v = v[np.argsort(angles)] + # Add back offset + v += offset + # For each pixel coordinate, check whether that pixel # lies inside the convex hull mask = grid_points_inside_poly(image.shape[:2], v) @@ -88,8 +95,8 @@ def convex_hull_object(image, neighbors=8): hull : ndarray of bool Binary image with pixels in convex hull set to True. - Note - ---- + Notes + ----- 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 diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index ef7158b2..13e9dfbb 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -5,9 +5,7 @@ from . import cmorph __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', - 'black_tophat', 'greyscale_erode', 'greyscale_dilate', - 'greyscale_open', 'greyscale_close', 'greyscale_white_top_hat', - 'greyscale_black_top_hat'] + 'black_tophat'] def erosion(image, selem, out=None, shift_x=False, shift_y=False): @@ -313,33 +311,3 @@ def black_tophat(image, selem, out=None): out = closing(image, selem, out=out) out = out - image return out - - -def greyscale_erode(*args, **kwargs): - warnings.warn("`greyscale_erode` renamed `erosion`.") - return erosion(*args, **kwargs) - - -def greyscale_dilate(*args, **kwargs): - warnings.warn("`greyscale_dilate` renamed `dilation`.") - return dilation(*args, **kwargs) - - -def greyscale_open(*args, **kwargs): - warnings.warn("`greyscale_open` renamed `opening`.") - return opening(*args, **kwargs) - - -def greyscale_close(*args, **kwargs): - warnings.warn("`greyscale_close` renamed `closing`.") - return closing(*args, **kwargs) - - -def greyscale_white_top_hat(*args, **kwargs): - warnings.warn("`greyscale_white_top_hat` renamed `white_tophat`.") - return white_tophat(*args, **kwargs) - - -def greyscale_black_top_hat(*args, **kwargs): - warnings.warn("`greyscale_black_top_hat` renamed `black_tophat`.") - return black_tophat(*args, **kwargs) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 3fffd28e..cea200a4 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -131,11 +131,11 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): if selem is None: selem = np.ones([3] * seed.ndim, dtype=bool) else: - selem = selem.copy() + selem = selem.astype(bool, copy=True) if offset is None: if not all([d % 2 == 1 for d in selem.shape]): - ValueError("Footprint dimensions must all be odd") + raise ValueError("Footprint dimensions must all be odd") offset = np.array([d // 2 for d in selem.shape]) # Cross out the center of the selem selem[[slice(d, d + 1) for d in offset]] = False @@ -158,7 +158,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 + value_stride = np.array(images.strides[1:]) // images.dtype.itemsize image_stride = images.strides[0] // images.dtype.itemsize selem_mgrid = np.mgrid[[slice(-o, d - o) for d, o in zip(selem.shape, offset)]] @@ -187,7 +187,8 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): value_map = -value_map start = index_sorted[0] - reconstruction_loop(value_rank, prev, next, nb_strides, start, image_stride) + reconstruction_loop(value_rank, prev, next, nb_strides, start, + image_stride) # Reshape reconstructed image to original image shape and remove padding. rec_img = value_map[value_rank[:image_stride]] diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 5c157e7f..dc8290d2 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -37,17 +37,17 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): >>> a = np.array([[0, 0, 0, 1, 0], ... [1, 1, 1, 0, 0], ... [1, 1, 1, 0, 1]], bool) - >>> b = morphology.remove_small_connected_components(a, 6) + >>> b = morphology.remove_small_objects(a, 6) >>> b array([[False, False, False, False, False], [ True, True, True, False, False], [ True, True, True, False, False]], dtype=bool) - >>> c = morphology.remove_small_connected_components(a, 7, connectivity=2) + >>> c = morphology.remove_small_objects(a, 7, connectivity=2) >>> c array([[False, False, False, True, False], [ True, True, True, False, False], [ True, True, True, False, False]], dtype=bool) - >>> d = morphology.remove_small_connected_components(a, 6, in_place=True) + >>> d = morphology.remove_small_objects(a, 6, in_place=True) >>> d is a True """ diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index 1936377b..f0f69764 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -12,7 +12,6 @@ def configuration(parent_package='', top_path=None): config = Configuration('morphology', parent_package, top_path) config.add_data_dir('tests') - cython(['ccomp.pyx'], working_path=base_path) cython(['cmorph.pyx'], working_path=base_path) cython(['_watershed.pyx'], working_path=base_path) cython(['_skeletonize_cy.pyx'], working_path=base_path) @@ -20,8 +19,6 @@ def configuration(parent_package='', top_path=None): cython(['_convex_hull.pyx'], working_path=base_path) cython(['_greyreconstruct.pyx'], working_path=base_path) - config.add_extension('ccomp', sources=['ccomp.c'], - include_dirs=[get_numpy_include_dirs()]) config.add_extension('cmorph', sources=['cmorph.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_watershed', sources=['_watershed.c'], diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index 1169be85..e934a5b7 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -2,7 +2,8 @@ import numpy as np from numpy.testing import assert_array_equal, run_module_suite from skimage.morphology import label - +from warnings import catch_warnings +from skimage._shared.utils import skimage_deprecation class TestConnectedComponents: def setup(self): @@ -25,7 +26,9 @@ class TestConnectedComponents: def test_random(self): x = (np.random.random((20, 30)) * 5).astype(np.int) - labels = label(x) + with catch_warnings(): + labels = label(x) + n = labels.max() for i in range(n): values = x[labels == i] @@ -35,27 +38,29 @@ class TestConnectedComponents: x = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]) - assert_array_equal(label(x), - x) + with catch_warnings(): + assert_array_equal(label(x), x) def test_4_vs_8(self): x = np.array([[0, 1], [1, 0]], dtype=int) - assert_array_equal(label(x, 4), - [[0, 1], - [2, 3]]) - assert_array_equal(label(x, 8), - [[0, 1], - [1, 0]]) + with catch_warnings(): + assert_array_equal(label(x, 4), + [[0, 1], + [2, 3]]) + assert_array_equal(label(x, 8), + [[0, 1], + [1, 0]]) def test_background(self): x = np.array([[1, 0, 0], [1, 1, 5], [0, 0, 0]]) - assert_array_equal(label(x), [[0, 1, 1], - [0, 0, 2], - [3, 3, 3]]) + with catch_warnings(): + assert_array_equal(label(x), [[0, 1, 1], + [0, 0, 2], + [3, 3, 3]]) assert_array_equal(label(x, background=0), [[0, -1, -1], @@ -87,7 +92,9 @@ class TestConnectedComponents: [0, 0, 6], [5, 5, 5]]) - assert_array_equal(label(x, return_num=True)[1], 4) + with catch_warnings(): + assert_array_equal(label(x, return_num=True)[1], 4) + assert_array_equal(label(x, background=0, return_num=True)[1], 3) diff --git a/skimage/morphology/tests/test_convex_hull.py b/skimage/morphology/tests/test_convex_hull.py index ee3b6bfa..67850cf2 100644 --- a/skimage/morphology/tests/test_convex_hull.py +++ b/skimage/morphology/tests/test_convex_hull.py @@ -32,6 +32,25 @@ def test_basic(): assert_array_equal(convex_hull_image(image), expected) +@skipif(not scipy_spatial) +def test_qhull_offset_example(): + nonzeros = (([1367, 1368, 1368, 1368, 1369, 1369, 1369, 1369, 1369, 1370, 1370, + 1370, 1370, 1370, 1370, 1370, 1371, 1371, 1371, 1371, 1371, 1371, + 1371, 1371, 1371, 1372, 1372, 1372, 1372, 1372, 1372, 1372, 1372, + 1372, 1373, 1373, 1373, 1373, 1373, 1373, 1373, 1373, 1373, 1374, + 1374, 1374, 1374, 1374, 1374, 1374, 1375, 1375, 1375, 1375, 1375, + 1376, 1376, 1376, 1377]), + ([151, 150, 151, 152, 149, 150, 151, 152, 153, 148, 149, 150, 151, + 152, 153, 154, 147, 148, 149, 150, 151, 152, 153, 154, 155, 146, + 147, 148, 149, 150, 151, 152, 153, 154, 146, 147, 148, 149, 150, + 151, 152, 153, 154, 147, 148, 149, 150, 151, 152, 153, 148, 149, + 150, 151, 152, 149, 150, 151, 150])) + image = np.zeros((1392, 1040), dtype=bool) + image[nonzeros] = True + expected = image.copy() + assert_array_equal(convex_hull_image(image), expected) + + @skipif(not scipy_spatial) def test_pathological_qhull_example(): image = np.array( diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index e2a3928d..f0099ee5 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -155,5 +155,15 @@ class TestDTypes(): self._test_image(image) +def test_inplace(): + selem = np.ones((3, 3)) + image = np.zeros((5, 5)) + out = image + + for f in (grey.erosion, grey.dilation, + grey.white_tophat, grey.black_tophat): + testing.assert_raises(NotImplementedError, f, image, selem, out=out) + + if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/morphology/tests/test_reconstruction.py b/skimage/morphology/tests/test_reconstruction.py index 8e40ac67..f5678c08 100644 --- a/skimage/morphology/tests/test_reconstruction.py +++ b/skimage/morphology/tests/test_reconstruction.py @@ -8,7 +8,8 @@ All rights reserved. Original author: Lee Kamentsky """ import numpy as np -from numpy.testing import assert_array_almost_equal as assert_close +from numpy.testing import (assert_array_almost_equal as assert_close, + assert_raises) from skimage.morphology.greyreconstruct import reconstruction @@ -77,6 +78,25 @@ def test_fill_hole(): assert_close(result, np.array([0, 3, 6, 4, 4, 4, 4, 4, 2, 0])) +def test_invalid_seed(): + seed = np.ones((5, 5)) + mask = np.ones((5, 5)) + assert_raises(ValueError, reconstruction, seed * 2, mask, + method='dilation') + assert_raises(ValueError, reconstruction, seed * 0.5, mask, + method='erosion') + + +def test_invalid_selem(): + seed = np.ones((5, 5)) + mask = np.ones((5, 5)) + assert_raises(ValueError, reconstruction, seed, mask, + selem=np.ones((4, 4))) + assert_raises(ValueError, reconstruction, seed, mask, + selem=np.ones((3, 4))) + reconstruction(seed, mask, selem=np.ones((3, 3))) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/morphology/tests/test_watershed.py b/skimage/morphology/tests/test_watershed.py index 5dc0f07c..788d999e 100644 --- a/skimage/morphology/tests/test_watershed.py +++ b/skimage/morphology/tests/test_watershed.py @@ -154,7 +154,7 @@ class TestWatershed(unittest.TestCase): [-1, -1, 1, 1, 1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]], out) - self.failUnless(error < eps) + self.assertTrue(error < eps) def test_watershed03(self): "watershed 3" @@ -189,7 +189,7 @@ class TestWatershed(unittest.TestCase): [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]], out) - self.failUnless(error < eps) + self.assertTrue(error < eps) def test_watershed04(self): "watershed 4" @@ -224,7 +224,7 @@ class TestWatershed(unittest.TestCase): [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]], out) - self.failUnless(error < eps) + self.assertTrue(error < eps) def test_watershed05(self): "watershed 5" @@ -259,7 +259,7 @@ class TestWatershed(unittest.TestCase): [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]], out) - self.failUnless(error < eps) + self.assertTrue(error < eps) def test_watershed06(self): "watershed 6" @@ -291,7 +291,7 @@ class TestWatershed(unittest.TestCase): [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]], out) - self.failUnless(error < eps) + self.assertTrue(error < eps) def test_watershed07(self): "A regression test of a competitive case that failed" diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py index 2bdb57b2..7318ceea 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -118,7 +118,8 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): >>> distance = ndimage.distance_transform_edt(image) >>> from skimage.feature import peak_local_max >>> local_maxi = peak_local_max(distance, labels=image, - ... footprint=np.ones((3, 3))) + ... footprint=np.ones((3, 3)), + ... indices=False) >>> markers = ndimage.label(local_maxi)[0] >>> labels = watershed(-distance, markers, mask=image) diff --git a/skimage/novice/__init__.py b/skimage/novice/__init__.py new file mode 100644 index 00000000..029d9a64 --- /dev/null +++ b/skimage/novice/__init__.py @@ -0,0 +1,75 @@ +""" +skimage.novice +============== +A special Python image submodule for beginners. + +Description +----------- +``skimage.novice`` provides a simple image manipulation interface for +beginners. It allows for easy loading, manipulating, and saving of image +files. + +This module is primarily intended for teaching and differs significantly from +the normal, array-oriented image functions used by scikit-image. + +.. note:: + + This module uses the Cartesian coordinate system, where the origin is at + the lower-left corner instead of the upper-right and the order is x, y + instead of row, column. + + +Example +------- + +We can create a Picture object open opening an image file +>>> from skimage import novice +>>> from skimage import data +>>> picture = novice.open(data.data_dir + '/chelsea.png') + +Pictures know their format +>>> picture.format +'png' + +... and where they came from +>>> picture.path.endswith('chelsea.png') +True + +... and their size +>>> picture.size +(451, 300) +>>> picture.width +451 + +Changing `size` resizes the picture. +>>> picture.size = (45, 30) + +You can iterate over pixels, which have RGB values between 0 and 255, +and know their location in the picture. +>>> for pixel in picture: +... if (pixel.red > 128) and (pixel.x < picture.width): +... pixel.red /= 2 + +Pictures know if they've been modified from the original file +>>> picture.modified +True +>>> print(picture.path) +None + +Pictures can be indexed like arrays +>>> picture[0:20, 0:20] = (0, 0, 0) + +Saving the picture updates the path attribute, format, and modified state. +>>> picture.save('save-demo.jpg') +>>> picture.path.endswith('save-demo.jpg') +True +>>> picture.format +'jpeg' +>>> picture.modified +False + +""" +from ._novice import Picture, open, colors, color_dict + + +__all__ = ['Picture', 'open', 'colors', 'color_dict'] diff --git a/skimage/novice/_novice.py b/skimage/novice/_novice.py new file mode 100644 index 00000000..b953b0c8 --- /dev/null +++ b/skimage/novice/_novice.py @@ -0,0 +1,494 @@ +import os +import imghdr +from collections import namedtuple +from io import BytesIO + +import numpy as np +from skimage import io +from skimage import img_as_ubyte +from skimage.transform import resize +from skimage.color import color_dict +from skimage.io.util import file_or_url_context, is_url + +import six +from six.moves.urllib_parse import urlparse +from six.moves.urllib import request +urlopen = request.urlopen + +# Convert colors from `skimage.color` to uint8 and allow access through +# dict or a named tuple. +color_dict = dict((name, tuple(int(255 * c + 0.5) for c in rgb)) + for name, rgb in six.iteritems(color_dict)) +colors = namedtuple('colors', color_dict.keys())(**color_dict) + + +def open(path): + """Return Picture object from the given image path.""" + return Picture(path) + + +def _verify_picture_index(index): + """Raise error if picture index is not a 2D index/slice.""" + if not (isinstance(index, tuple) and len(index) == 2): + raise IndexError("Expected 2D index but got {0!r}".format(index)) + + if all(isinstance(i, int) for i in index): + return index + + # In case we need to fix the array index, convert tuple to list. + index = list(index) + + for i, dim_slice in enumerate(index): + # If either index is a slice, ensure index object returns 2D array. + if isinstance(dim_slice, int): + index[i] = dim_slice = slice(dim_slice, dim_slice + 1) + + return tuple(index) + + +def rgb_transpose(array): + """Return RGB array with first 2 axes transposed.""" + return np.transpose(array, (1, 0, 2)) + + +def array_to_xy_origin(image): + """Return view of image transformed from array to Cartesian origin.""" + return rgb_transpose(image[::-1]) + + +def xy_to_array_origin(image): + """Return view of image transformed from Cartesian to array origin.""" + return rgb_transpose(image[:, ::-1]) + + +class Pixel(object): + """A single pixel in a Picture. + + Attributes + ---------- + pic : Picture + The Picture object that this pixel references. + array : array_like + Byte array with raw image data (RGB). + x : int + Horizontal coordinate of this pixel (left = 0). + y : int + Vertical coordinate of this pixel (bottom = 0). + rgb : tuple + RGB tuple with red, green, and blue components (0-255) + alpha : int + Transparency component (0-255), 255 (opaque) by default + + """ + def __init__(self, pic, array, x, y, rgb, alpha=255): + self._picture = pic + self._x = x + self._y = y + self._red = self._validate(rgb[0]) + self._green = self._validate(rgb[1]) + self._blue = self._validate(rgb[2]) + self._alpha = self._validate(alpha) + + @property + def x(self): + """Horizontal location of this pixel in the parent image(left = 0).""" + return self._x + + @property + def y(self): + """Vertical location of this pixel in the parent image (bottom = 0).""" + return self._y + + @property + def red(self): + """The red component of the pixel (0-255).""" + return self._red + + @red.setter + def red(self, value): + self._red = self._validate(value) + self._setpixel() + + @property + def green(self): + """The green component of the pixel (0-255).""" + return self._green + + @green.setter + def green(self, value): + self._green = self._validate(value) + self._setpixel() + + @property + def blue(self): + """The blue component of the pixel (0-255).""" + return self._blue + + @blue.setter + def blue(self, value): + self._blue = self._validate(value) + self._setpixel() + + @property + def alpha(self): + """The transparency component of the pixel (0-255).""" + return self._alpha + + @alpha.setter + def alpha(self, value): + self._alpha = self._validate(value) + self._setpixel() + + @property + def rgb(self): + """The RGB color components of the pixel (3 values 0-255).""" + return (self.red, self.green, self.blue) + + @rgb.setter + def rgb(self, value): + if len(value) == 4: + self.rgba = value + else: + self._red, self._green, self._blue \ + = (self._validate(v) for v in value) + self._alpha = 255 + self._setpixel() + + @property + def rgba(self): + """The RGB color and transparency components of the pixel + (4 values 0-255). + """ + return (self.red, self.green, self.blue, self.alpha) + + @rgba.setter + def rgba(self, value): + self._red, self._green, self._blue, self._alpha \ + = (self._validate(v) for v in value) + self._setpixel() + + def _validate(self, value): + """Verifies that the pixel value is in [0, 255].""" + try: + value = int(value) + if (value < 0) or (value > 255): + raise ValueError() + except ValueError: + msg = "Expected an integer between 0 and 255, but got {0} instead!" + raise ValueError(msg.format(value)) + + return value + + def _setpixel(self): + # RGB + alpha + self._picture.xy_array[self._x, self._y] = self.rgba + self._picture._array_modified() + + def __eq__(self, other): + if isinstance(other, Pixel): + return self.rgba == other.rgba + + def __repr__(self): + args = self.red, self.green, self.blue, self.alpha + return "Pixel(red={0}, green={1}, blue={2}, alpha={3})".format(*args) + + +class Picture(object): + """A 2-D picture made up of pixels. + + Attributes + ---------- + path : str + Path to an image file to load / URL of an image + array : array + Raw RGB or RGBA image data [0-255], with origin at top-left. + xy_array : array + Raw RGB or RGBA image data [0-255], with origin at bottom-left. + + Examples + -------- + Load an image from a file + >>> from skimage import novice + >>> from skimage import data + >>> picture = novice.open(data.data_dir + '/chelsea.png') + + Load an image from a URL. URL must start with http(s):// or ftp(s):// + >>> picture = novice.open('http://scikit-image.org/_static/img/logo.png') + + Create a blank 100 pixel wide, 200 pixel tall white image + >>> pic = Picture.from_size((100, 200), color=(255, 255, 255)) + + Use numpy to make an RGB byte array (shape is height x width x 3) + >>> import numpy as np + >>> data = np.zeros(shape=(200, 100, 3), dtype=np.uint8) + >>> data[:, :, 0] = 255 # Set red component to maximum + >>> pic = Picture(array=data) + + Get the bottom-left pixel + >>> pic[0, 0] + Pixel(red=255, green=0, blue=0, alpha=255) + + Get the top row of the picture + >>> pic[:, pic.height-1] + Picture(100 x 1) + + Set the bottom-left pixel to black + >>> pic[0, 0] = (0, 0, 0) + + Set the top row to red + >>> pic[:, pic.height-1] = (255, 0, 0) + + """ + def __init__(self, path=None, array=None, xy_array=None): + self._modified = False + self.scale = 1 + self._path = None + self._format = None + + n_args = len([a for a in [path, array, xy_array] if a is not None]) + if n_args != 1: + msg = "Must provide a single keyword arg (path, array, xy_array)." + ValueError(msg) + elif path is not None: + if not is_url(path): + path = os.path.abspath(path) + self._path = path + with file_or_url_context(path) as context: + self.array = img_as_ubyte(io.imread(context)) + self._format = imghdr.what(context) + elif array is not None: + self.array = array + elif xy_array is not None: + self.xy_array = xy_array + + # Force RGBA internally (use max alpha) + if self.array.shape[-1] == 3: + self.array = np.insert(self.array, 3, values=255, axis=2) + + @staticmethod + def from_size(size, color='black'): + """Return a Picture of the specified size and a uniform color. + + Parameters + ---------- + size : tuple + Width and height of the picture in pixels. + color : tuple or str + RGB or RGBA tuple with the fill color for the picture [0-255] or + a valid key in `color_dict`. + """ + if isinstance(color, six.string_types): + color = color_dict[color] + rgb_size = tuple(size) + (len(color),) + array = np.ones(rgb_size, dtype=np.uint8) * color + + # Force RGBA internally (use max alpha) + if array.shape[-1] == 3: + array = np.insert(array, 3, values=255, axis=2) + + return Picture(array=array) + + @property + def array(self): + """Image data stored as numpy array.""" + return self._array + + @array.setter + def array(self, array): + self._array = array + self._xy_array = array_to_xy_origin(array) + + @property + def xy_array(self): + """Image data stored as numpy array with origin at the bottom-left.""" + return self._xy_array + + @xy_array.setter + def xy_array(self, array): + self._xy_array = array + self._array = xy_to_array_origin(array) + + def save(self, path): + """Saves the picture to the given path. + + Parameters + ---------- + path : str + Path (with file extension) where the picture is saved. + """ + io.imsave(path, self._rescale(self.array)) + self._modified = False + self._path = os.path.abspath(path) + self._format = imghdr.what(path) + + @property + def path(self): + """The path to the picture.""" + return self._path + + @property + def modified(self): + """True if the picture has changed.""" + return self._modified + + def _array_modified(self): + self._modified = True + self._path = None + + @property + def format(self): + """The image format of the picture.""" + return self._format + + @property + def size(self): + """The size (width, height) of the picture.""" + return self.xy_array.shape[:2] + + @size.setter + def size(self, value): + # Don't resize if no change in size + if (value[0] != self.width) or (value[1] != self.height): + # skimage dimensions are flipped: y, x + new_size = (int(value[1]), int(value[0])) + new_array = resize(self.array, new_size, order=0) + self.array = img_as_ubyte(new_array) + + self._array_modified() + + @property + def width(self): + """The width of the picture.""" + return self.size[0] + + @width.setter + def width(self, value): + self.size = (value, self.height) + + @property + def height(self): + """The height of the picture.""" + return self.size[1] + + @height.setter + def height(self, value): + self.size = (self.width, value) + + def _repr_png_(self): + return io.Image(self._rescale(self.array))._repr_png_() + + def show(self): + """Display the image.""" + io.imshow(self._rescale(self.array)) + io.show() + + def _makepixel(self, x, y): + """Create a Pixel object for a given x, y location.""" + rgb = self.xy_array[x, y] + return Pixel(self, self.array, x, y, rgb) + + def _rescale(self, array): + """Rescale image according to scale factor.""" + if self.scale == 1: + return array + new_size = (self.height * self.scale, self.width * self.scale) + return img_as_ubyte(resize(array, new_size, order=0)) + + def _get_channel(self, channel): + """Return a specific dimension out of the raw image data slice.""" + return self._array[:, :, channel] + + def _set_channel(self, channel, value): + """Set a specific dimension in the raw image data slice.""" + self._array[:, :, channel] = value + + @property + def red(self): + """The red component of the pixel (0-255).""" + return self._get_channel(0).ravel() + + @red.setter + def red(self, value): + self._set_channel(0, value) + + @property + def green(self): + """The green component of the pixel (0-255).""" + return self._get_channel(1).ravel() + + @green.setter + def green(self, value): + self._set_channel(1, value) + + @property + def blue(self): + """The blue component of the pixel (0-255).""" + return self._get_channel(2).ravel() + + @blue.setter + def blue(self, value): + self._set_channel(2, value) + + @property + def alpha(self): + """The transparency component of the pixel (0-255).""" + return self._get_channel(3).ravel() + + @alpha.setter + def alpha(self, value): + self._set_channel(3, value) + + @property + def rgb(self): + """The RGB color components of the pixel (3 values 0-255).""" + return self.xy_array[:, :, :3] + + @rgb.setter + def rgb(self, value): + self.xy_array[:, :, :3] = value + + @property + def rgba(self): + """The RGBA color components of the pixel (4 values 0-255).""" + return self.xy_array + + @rgba.setter + def rgba(self, value): + self.xy_array[:] = value + + def __iter__(self): + """Iterates over all pixels in the image.""" + for x in range(self.width): + for y in range(self.height): + yield self._makepixel(x, y) + + def __getitem__(self, xy_index): + """Return `Picture`s for slices and `Pixel`s for indexes.""" + xy_index = _verify_picture_index(xy_index) + if all(isinstance(index, int) for index in xy_index): + return self._makepixel(*xy_index) + else: + return Picture(xy_array=self.xy_array[xy_index]) + + def __setitem__(self, xy_index, value): + xy_index = _verify_picture_index(xy_index) + if isinstance(value, tuple): + self[xy_index].rgb = value + elif isinstance(value, Picture): + self.xy_array[xy_index] = value.xy_array + else: + raise TypeError("Invalid value type") + self._array_modified() + + def __eq__(self, other): + if not isinstance(other, Picture): + raise NotImplementedError() + return np.all(self.array == other.array) + + def __repr__(self): + return "Picture({0} x {1})".format(*self.size) + + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/skimage/novice/tests/test_novice.py b/skimage/novice/tests/test_novice.py new file mode 100644 index 00000000..a1f750ab --- /dev/null +++ b/skimage/novice/tests/test_novice.py @@ -0,0 +1,313 @@ +import os +import tempfile + +import numpy as np +from numpy.testing import assert_equal, raises, assert_allclose +from skimage import novice +from skimage.novice._novice import (array_to_xy_origin, xy_to_array_origin, + rgb_transpose) +from skimage import data_dir + + +IMAGE_PATH = os.path.join(data_dir, "chelsea.png") +SMALL_IMAGE_PATH = os.path.join(data_dir, "block.png") + + +def _array_2d_to_RGBA(array): + return np.tile(array[:, :, np.newaxis], (1, 1, 4)) + + +def _array_2d_to_RGBA(array): + return np.tile(array[:, :, np.newaxis], (1, 1, 4)) + + +def test_xy_to_array_origin(): + h, w = 3, 5 + array = np.arange(h * w).reshape(h, w, 1) + out = xy_to_array_origin(array_to_xy_origin(array.copy())) + assert np.allclose(out, array) + + +def test_pic_info(): + pic = novice.open(IMAGE_PATH) + assert_equal(pic.format, "png") + assert_equal(pic.path, os.path.abspath(IMAGE_PATH)) + assert_equal(pic.size, (451, 300)) + assert_equal(pic.width, 451) + assert_equal(pic.height, 300) + assert not pic.modified + assert_equal(pic.scale, 1) + + +def test_pixel_iteration(): + pic = novice.open(SMALL_IMAGE_PATH) + num_pixels = sum(1 for p in pic) + assert_equal(num_pixels, pic.width * pic.height) + + +def test_modify(): + pic = novice.open(SMALL_IMAGE_PATH) + assert_equal(pic.modified, False) + + for p in pic: + if p.x < (pic.width / 2): + p.red /= 2 + p.green /= 2 + p.blue /= 2 + + for p in pic: + if p.x < (pic.width / 2): + assert p.red <= 128 + assert p.green <= 128 + assert p.blue <= 128 + + s = pic.size + pic.size = (pic.width / 2, pic.height / 2) + assert_equal(pic.size, (int(s[0] / 2), int(s[1] / 2))) + + assert pic.modified + assert pic.path is None + + +def test_pixel_rgb(): + pic = novice.Picture.from_size((3, 3), color=(10, 10, 10)) + pixel = pic[0, 0] + pixel.rgb = np.arange(3) + + assert_equal(pixel.rgb, np.arange(3)) + for i, channel in enumerate((pixel.red, pixel.green, pixel.blue)): + assert_equal(channel, i) + + pixel.red = 3 + pixel.green = 4 + pixel.blue = 5 + assert_equal(pixel.rgb, np.arange(3) + 3) + + for i, channel in enumerate((pixel.red, pixel.green, pixel.blue)): + assert_equal(channel, i + 3) + + pixel.rgb = np.arange(4) + assert_equal(pixel.rgb, np.arange(3)) + + +def test_pixel_rgba(): + pic = novice.Picture.from_size((3, 3), color=(10, 10, 10)) + pixel = pic[0, 0] + pixel.rgba = np.arange(4) + + assert_equal(pixel.rgba, np.arange(4)) + for i, channel in enumerate((pixel.red, pixel.green, pixel.blue, pixel.alpha)): + assert_equal(channel, i) + + pixel.red = 3 + pixel.green = 4 + pixel.blue = 5 + pixel.alpha = 6 + assert_equal(pixel.rgba, np.arange(4) + 3) + + for i, channel in enumerate((pixel.red, pixel.green, pixel.blue, pixel.alpha)): + assert_equal(channel, i + 3) + + +def test_pixel_rgb_float(): + pixel = novice.Picture.from_size((1, 1))[0, 0] + pixel.rgb = (1.1, 1.1, 1.1) + assert_equal(pixel.rgb, (1, 1, 1)) + + +def test_pixel_rgba_float(): + pixel = novice.Picture.from_size((1, 1))[0, 0] + pixel.rgba = (1.1, 1.1, 1.1, 1.1) + assert_equal(pixel.rgba, (1, 1, 1, 1)) + + +def test_modified_on_set(): + pic = novice.Picture(SMALL_IMAGE_PATH) + pic[0, 0] = (1, 1, 1) + assert pic.modified + assert pic.path is None + + +def test_modified_on_set_pixel(): + data = np.zeros(shape=(10, 5, 3), dtype=np.uint8) + pic = novice.Picture(array=data) + + pixel = pic[0, 0] + pixel.green = 1 + assert pic.modified + + +def test_update_on_save(): + pic = novice.Picture(array=np.zeros((3, 3, 3))) + pic.size = (6, 6) + assert pic.modified + assert pic.path is None + + fd, filename = tempfile.mkstemp(suffix=".jpg") + os.close(fd) + try: + pic.save(filename) + + assert not pic.modified + assert_equal(pic.path, os.path.abspath(filename)) + assert_equal(pic.format, "jpeg") + finally: + os.unlink(filename) + + +def test_indexing(): + array = 128 * np.ones((10, 10, 3), dtype=np.uint8) + pic = novice.Picture(array=array) + + pic[0:5, 0:5] = (0, 0, 0) + for p in pic: + if (p.x < 5) and (p.y < 5): + assert_equal(p.rgb, (0, 0, 0)) + assert_equal(p.red, 0) + assert_equal(p.green, 0) + assert_equal(p.blue, 0) + + pic[:5, :5] = (255, 255, 255) + for p in pic: + if (p.x < 5) and (p.y < 5): + assert_equal(p.rgb, (255, 255, 255)) + assert_equal(p.red, 255) + assert_equal(p.green, 255) + assert_equal(p.blue, 255) + + pic[5:pic.width, 5:pic.height] = (255, 0, 255) + for p in pic: + if (p.x >= 5) and (p.y >= 5): + assert_equal(p.rgb, (255, 0, 255)) + assert_equal(p.red, 255) + assert_equal(p.green, 0) + assert_equal(p.blue, 255) + + pic[5:, 5:] = (0, 0, 255) + for p in pic: + if (p.x >= 5) and (p.y >= 5): + assert_equal(p.rgb, (0, 0, 255)) + assert_equal(p.red, 0) + assert_equal(p.green, 0) + assert_equal(p.blue, 255) + + +def test_picture_slice(): + array = _array_2d_to_RGBA(np.arange(0, 10)[np.newaxis, :]) + pic = novice.Picture(array=array) + + x_slice = slice(3, 8) + subpic = pic[:, x_slice] + assert_allclose(subpic.array, array[x_slice, :]) + + +def test_move_slice(): + h, w = 3, 12 + array = _array_2d_to_RGBA(np.linspace(0, 255, h * w).reshape(h, w)) + array = array.astype(np.uint8) + + pic = novice.Picture(array=array) + pic_orig = novice.Picture(array=array.copy()) + + # Move left cut of image to the right side. + cut = 5 + rest = pic.width - cut + temp = pic[:cut, :] + temp.array = temp.array.copy() + pic[:rest, :] = pic[cut:, :] + pic[rest:, :] = temp + + assert pic[rest:, :] == pic_orig[:cut, :] + assert pic[:rest, :] == pic_orig[cut:, :] + + +def test_negative_index(): + n = 10 + array = _array_2d_to_RGBA(np.arange(0, n)[np.newaxis, :]) + # Test both x and y indices. + pic = novice.Picture(array=array) + assert pic[-1, 0] == pic[n - 1, 0] + pic = novice.Picture(array=rgb_transpose(array)) + assert pic[0, -1] == pic[0, n - 1] + + +def test_negative_slice(): + n = 10 + array = _array_2d_to_RGBA(np.arange(0, n)[np.newaxis, :]) + # Test both x and y slices. + pic = novice.Picture(array=array) + assert pic[-3:, 0] == pic[n - 3:, 0] + pic = novice.Picture(array=rgb_transpose(array)) + assert pic[0, -3:] == pic[0, n - 3:] + + +def test_getitem_with_step(): + h, w = 5, 5 + array = _array_2d_to_RGBA(np.linspace(0, 255, h * w).reshape(h, w)) + pic = novice.Picture(array=array) + sliced_pic = pic[::2, ::2] + assert sliced_pic == novice.Picture(array=array[::2, ::2]) + + +@raises(IndexError) +def test_1d_getitem_raises(): + pic = novice.Picture.from_size((1, 1)) + pic[1] + + +@raises(IndexError) +def test_3d_getitem_raises(): + pic = novice.Picture.from_size((1, 1)) + pic[1, 2, 3] + + +@raises(IndexError) +def test_1d_setitem_raises(): + pic = novice.Picture.from_size((1, 1)) + pic[1] = 0 + + +@raises(IndexError) +def test_3d_setitem_raises(): + pic = novice.Picture.from_size((1, 1)) + pic[1, 2, 3] = 0 + + +@raises(IndexError) +def test_out_of_bounds_indexing(): + pic = novice.open(SMALL_IMAGE_PATH) + pic[pic.width, pic.height] + + +@raises(ValueError) +def test_pixel_rgb_raises(): + pixel = novice.Picture.from_size((1, 1))[0, 0] + pixel.rgb = (-1, -1, -1) + + +@raises(ValueError) +def test_pixel_red_raises(): + pixel = novice.Picture.from_size((1, 1))[0, 0] + pixel.red = 256 + + +@raises(ValueError) +def test_pixel_green_raises(): + pixel = novice.Picture.from_size((1, 1))[0, 0] + pixel.green = 256 + + +@raises(ValueError) +def test_pixel_blue_raises(): + pixel = novice.Picture.from_size((1, 1))[0, 0] + pixel.blue = 256 + + +@raises(ValueError) +def test_pixel_alpha_raises(): + pixel = novice.Picture.from_size((1, 1))[0, 0] + pixel.alpha = 256 + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/restoration/__init__.py b/skimage/restoration/__init__.py new file mode 100644 index 00000000..2b593ccf --- /dev/null +++ b/skimage/restoration/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""Image restoration module. + +References +---------- +.. [1] François Orieux, Jean-François Giovannelli, and Thomas + Rodet, "Bayesian estimation of regularization and point + spread function parameters for Wiener-Hunt deconvolution", + J. Opt. Soc. Am. A 27, 1593-1607 (2010) + + http://www.opticsinfobase.org/josaa/abstract.cfm?URI=josaa-27-7-1593 + +.. [2] Richardson, William Hadley, "Bayesian-Based Iterative Method of + Image Restoration". JOSA 62 (1): 55–59. doi:10.1364/JOSA.62.000055, 1972 + +.. [3] B. R. Hunt "A matrix theory proof of the discrete + convolution theorem", IEEE Trans. on Audio and + Electroacoustics, vol. au-19, no. 4, pp. 285-288, dec. 1971 +""" + +from .deconvolution import wiener, unsupervised_wiener, richardson_lucy +from .unwrap import unwrap_phase +from ._denoise import denoise_tv_chambolle, denoise_tv_bregman, \ + denoise_bilateral + + +__all__ = ['wiener', + 'unsupervised_wiener', + 'richardson_lucy', + 'unwrap_phase', + 'denoise_tv_bregman', + 'denoise_tv_chambolle', + 'denoise_bilateral'] diff --git a/skimage/filter/_denoise.py b/skimage/restoration/_denoise.py similarity index 63% rename from skimage/filter/_denoise.py rename to skimage/restoration/_denoise.py index 043399e0..260b1078 100644 --- a/skimage/filter/_denoise.py +++ b/skimage/restoration/_denoise.py @@ -1,5 +1,109 @@ +# coding: utf-8 import numpy as np from skimage import img_as_float +from skimage.restoration._denoise_cy import _denoise_bilateral, \ + _denoise_tv_bregman + + +def denoise_bilateral(image, win_size=5, sigma_range=None, sigma_spatial=1, + bins=10000, mode='constant', cval=0): + """Denoise image using bilateral filter. + + This is an edge-preserving and noise reducing denoising filter. It averages + pixels based on their spatial closeness and radiometric similarity. + + Spatial closeness is measured by the gaussian function of the euclidian + distance between two pixels and a certain standard deviation + (`sigma_spatial`). + + Radiometric similarity is measured by the gaussian function of the euclidian + distance between two color values and a certain standard deviation + (`sigma_range`). + + Parameters + ---------- + image : ndarray + Input image. + win_size : int + Window size for filtering. + sigma_range : float + Standard deviation for grayvalue/color distance (radiometric + similarity). A larger value results in averaging of pixels with larger + radiometric differences. Note, that the image will be converted using + the `img_as_float` function and thus the standard deviation is in + respect to the range `[0, 1]`. + sigma_spatial : float + Standard deviation for range distance. A larger value results in + averaging of pixels with larger spatial differences. + bins : int + Number of discrete values for gaussian weights of color filtering. + A larger value results in improved accuracy. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + denoised : ndarray + Denoised image. + + References + ---------- + .. [1] http://users.soe.ucsc.edu/~manduchi/Papers/ICCV98.pdf + + """ + return _denoise_bilateral(image, win_size, sigma_range, sigma_spatial, + bins, mode, cval) + + +def denoise_tv_bregman(image, weight, max_iter=100, eps=1e-3, isotropic=True): + """Perform total-variation denoising using split-Bregman optimization. + + Total-variation denoising (also know as total-variation regularization) + tries to find an image with less total-variation under the constraint + of being similar to the input image, which is controlled by the + regularization parameter. + + Parameters + ---------- + image : ndarray + Input data to be denoised (converted using img_as_float`). + weight : float, optional + Denoising weight. The smaller the `weight`, the more denoising (at + the expense of less similarity to the `input`). The regularization + parameter `lambda` is chosen as `2 * weight`. + eps : float, optional + Relative difference of the value of the cost function that determines + the stop criterion. The algorithm stops when:: + + SUM((u(n) - u(n-1))**2) < eps + + max_iter : int, optional + Maximal number of iterations used for the optimization. + isotropic : boolean, optional + Switch between isotropic and anisotropic TV denoising. + + Returns + ------- + u : ndarray + Denoised image. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Total_variation_denoising + .. [2] Tom Goldstein and Stanley Osher, "The Split Bregman Method For L1 + Regularized Problems", + ftp://ftp.math.ucla.edu/pub/camreport/cam08-29.pdf + .. [3] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising + using Split Bregman" in Image Processing On Line on 2012–05–19, + http://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf + .. [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf + + """ + return _denoise_tv_bregman(image, weight, max_iter, eps, isotropic) def _denoise_tv_chambolle_3d(im, weight=100, eps=2.e-4, n_iter_max=200): @@ -30,14 +134,6 @@ def _denoise_tv_chambolle_3d(im, weight=100, eps=2.e-4, n_iter_max=200): ----- Rudin, Osher and Fatemi algorithm. - Examples - -------- - >>> x, y, z = np.ogrid[0:40, 0:40, 0:40] - >>> mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 - >>> mask = mask.astype(np.float) - >>> mask += 0.2 * np.random.randn(*mask.shape) - >>> res = denoise_tv_chambolle(mask, weight=100) - """ px = np.zeros_like(im) @@ -121,13 +217,6 @@ def _denoise_tv_chambolle_2d(im, weight=50, eps=2.e-4, n_iter_max=200): applications, Journal of Mathematical Imaging and Vision, Springer, 2004, 20, 89-97. - Examples - -------- - >>> from skimage import color, data - >>> lena = color.rgb2gray(data.lena()) - >>> lena += 0.5 * lena.std() * np.random.randn(*lena.shape) - >>> denoised_lena = denoise_tv_chambolle(lena, weight=60) - """ px = np.zeros_like(im) @@ -224,13 +313,13 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, 2D example on Lena image: >>> from skimage import color, data - >>> lena = color.rgb2gray(data.lena()) + >>> lena = color.rgb2gray(data.lena())[:50, :50] >>> lena += 0.5 * lena.std() * np.random.randn(*lena.shape) >>> denoised_lena = denoise_tv_chambolle(lena, weight=60) 3D example on synthetic data: - >>> x, y, z = np.ogrid[0:40, 0:40, 0:40] + >>> x, y, z = np.ogrid[0:20, 0:20, 0:20] >>> 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) diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/restoration/_denoise_cy.pyx similarity index 68% rename from skimage/filter/_denoise_cy.pyx rename to skimage/restoration/_denoise_cy.pyx index 0c4f2539..34404dce 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/restoration/_denoise_cy.pyx @@ -10,7 +10,6 @@ from libc.stdlib cimport malloc, free from libc.float cimport DBL_MAX from skimage._shared.interpolation cimport get_pixel3d from skimage.util import img_as_float -from skimage._shared.utils import deprecated cdef inline double _gaussian_weight(double sigma, double value): @@ -45,58 +44,9 @@ cdef double* _compute_range_lut(Py_ssize_t win_size, double sigma): return range_lut -def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, - double sigma_spatial=1, Py_ssize_t bins=10000, - mode='constant', double cval=0): - """Denoise image using bilateral filter. - - This is an edge-preserving and noise reducing denoising filter. It averages - pixels based on their spatial closeness and radiometric similarity. - - Spatial closeness is measured by the gaussian function of the euclidian - distance between two pixels and a certain standard deviation - (`sigma_spatial`). - - Radiometric similarity is measured by the gaussian function of the euclidian - distance between two color values and a certain standard deviation - (`sigma_range`). - - Parameters - ---------- - image : ndarray - Input image. - win_size : int - Window size for filtering. - sigma_range : float - Standard deviation for grayvalue/color distance (radiometric - similarity). A larger value results in averaging of pixels with larger - radiometric differences. Note, that the image will be converted using - the `img_as_float` function and thus the standard deviation is in - respect to the range `[0, 1]`. - sigma_spatial : float - Standard deviation for range distance. A larger value results in - averaging of pixels with larger spatial differences. - bins : int - Number of discrete values for gaussian weights of color filtering. - A larger value results in improved accuracy. - mode : string - How to handle values outside the image borders. See - `scipy.ndimage.map_coordinates` for detail. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Returns - ------- - denoised : ndarray - Denoised image. - - References - ---------- - .. [1] http://users.soe.ucsc.edu/~manduchi/Papers/ICCV98.pdf - - """ - +def _denoise_bilateral(image, Py_ssize_t win_size, sigma_range, + double sigma_spatial, Py_ssize_t bins, + mode, double cval): image = np.atleast_3d(img_as_float(image)) # if image.max() is 0, then dist_scale can have an unverified value @@ -194,52 +144,8 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, return np.squeeze(np.asarray(out)) -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) - tries to find an image with less total-variation under the constraint - of being similar to the input image, which is controlled by the - regularization parameter. - - Parameters - ---------- - image : ndarray - Input data to be denoised (converted using img_as_float`). - weight : float, optional - Denoising weight. The smaller the `weight`, the more denoising (at - the expense of less similarity to the `input`). The regularization - parameter `lambda` is chosen as `2 * weight`. - eps : float, optional - Relative difference of the value of the cost function that determines - the stop criterion. The algorithm stops when:: - - SUM((u(n) - u(n-1))**2) < eps - - max_iter : int, optional - Maximal number of iterations used for the optimization. - isotropic : boolean, optional - Switch between isotropic and anisotropic TV denoising. - - Returns - ------- - u : ndarray - Denoised image. - - References - ---------- - .. [1] http://en.wikipedia.org/wiki/Total_variation_denoising - .. [2] Tom Goldstein and Stanley Osher, "The Split Bregman Method For L1 - Regularized Problems", - ftp://ftp.math.ucla.edu/pub/camreport/cam08-29.pdf - .. [3] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising - using Split Bregman" in Image Processing On Line on 2012–05–19, - http://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf - .. [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf - - """ - +def _denoise_tv_bregman(image, double weight, int max_iter, double eps, + char isotropic): image = np.atleast_3d(img_as_float(image)) cdef: diff --git a/skimage/restoration/_unwrap_1d.pyx b/skimage/restoration/_unwrap_1d.pyx new file mode 100644 index 00000000..f23e5f40 --- /dev/null +++ b/skimage/restoration/_unwrap_1d.pyx @@ -0,0 +1,22 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +from libc.math cimport M_PI + + +def unwrap_1d(double[::1] image, double[::1] unwrapped_image): + '''Phase unwrapping using the naive approach.''' + cdef: + Py_ssize_t i + double difference + long periods = 0 + unwrapped_image[0] = image[0] + for i in range(1, image.shape[0]): + difference = image[i] - image[i - 1] + if difference > M_PI: + periods -= 1 + elif difference < -M_PI: + periods += 1 + unwrapped_image[i] = image[i] + 2 * M_PI * periods diff --git a/skimage/restoration/_unwrap_2d.pyx b/skimage/restoration/_unwrap_2d.pyx new file mode 100644 index 00000000..6e889729 --- /dev/null +++ b/skimage/restoration/_unwrap_2d.pyx @@ -0,0 +1,16 @@ +cdef extern void unwrap2D(double* wrapped_image, + double* unwrapped_image, + unsigned char* input_mask, + int image_width, int image_height, + int wrap_around_x, int wrap_around_y) + +def unwrap_2d(double[:, ::1] image, + unsigned char[:, ::1] mask, + double[:, ::1] unwrapped_image, + wrap_around): + unwrap2D(&image[0, 0], + &unwrapped_image[0, 0], + &mask[0, 0], + image.shape[1], image.shape[0], + wrap_around[1], wrap_around[0], + ) diff --git a/skimage/restoration/_unwrap_3d.pyx b/skimage/restoration/_unwrap_3d.pyx new file mode 100644 index 00000000..370d58be --- /dev/null +++ b/skimage/restoration/_unwrap_3d.pyx @@ -0,0 +1,16 @@ +cdef extern void unwrap3D(double* wrapped_volume, + double* unwrapped_volume, + unsigned char* input_mask, + int image_width, int image_height, int volume_depth, + int wrap_around_x, int wrap_around_y, int wrap_around_z) + +def unwrap_3d(double[:, :, ::1] image, + unsigned char[:, :, ::1] mask, + double[:, :, ::1] unwrapped_image, + wrap_around): + unwrap3D(&image[0, 0, 0], + &unwrapped_image[0, 0, 0], + &mask[0, 0, 0], + image.shape[2], image.shape[1], image.shape[0], #TODO: check!!! + wrap_around[2], wrap_around[1], wrap_around[0], + ) diff --git a/skimage/restoration/deconvolution.py b/skimage/restoration/deconvolution.py new file mode 100644 index 00000000..51c2c705 --- /dev/null +++ b/skimage/restoration/deconvolution.py @@ -0,0 +1,382 @@ +# -*- coding: utf-8 -*- +# deconvolution.py --- Image deconvolution + +"""Implementations restoration functions""" + +from __future__ import division + +import numpy as np +import numpy.random as npr +from scipy.signal import convolve2d + +from . import uft + +__keywords__ = "restoration, image, deconvolution" + + +def wiener(image, psf, balance, reg=None, is_real=True, clip=True): + """Wiener-Hunt deconvolution + + Return the deconvolution with a Wiener-Hunt approach (i.e. with + Fourier diagonalisation). + + Parameters + ---------- + image : (M, N) ndarray + Input degraded image + psf : ndarray + Point Spread Function. This is assumed to be the impulse + response (input image space) if the data-type is real, or the + transfer function (Fourier space) if the data-type is + complex. There is no constraints on the shape of the impulse + response. The transfer function must be of shape `(M, N)` if + `is_real is True`, `(M, N // 2 + 1)` otherwise (see + `np.fft.rfftn`). + balance : float + The regularisation parameter value that tunes the balance + between the data adequacy that improve frequency restoration + and the prior adequacy that reduce frequency restoration (to + avoid noise artifact). + reg : ndarray, optional + The regularisation operator. The Laplacian by default. It can + be an impulse response or a transfer function, as for the + psf. Shape constraint is the same than for the `psf` parameter. + is_real : boolean, optional + True by default. Specify if ``psf`` and ``reg`` are provided + with hermitian hypothesis, that is only half of the frequency + plane is provided (due to the redundancy of Fourier transform + of real signal). It's apply only if ``psf`` and/or ``reg`` are + provided as transfer function. For the hermitian property see + ``uft`` module or ``np.fft.rfftn``. + clip : boolean, optional + True by default. If true, pixel value of the result above 1 or + under -1 are thresholded for skimage pipeline + compatibility. + + Returns + ------- + im_deconv : (M, N) ndarray + The deconvolved image + + Examples + -------- + >>> from skimage import color, data, restoration + >>> lena = color.rgb2gray(data.lena()) + >>> from scipy.signal import convolve2d + >>> psf = np.ones((5, 5)) / 25 + >>> lena = convolve2d(lena, psf, 'same') + >>> lena += 0.1 * lena.std() * np.random.standard_normal(lena.shape) + >>> deconvolved_lena = restoration.wiener(lena, psf, 1100) + + Notes + ----- + This function applies the Wiener filter to a noisy and degraded + image by an impulse response (or PSF). If the data model is + + .. math:: y = Hx + n + + where :math:`n` is noise, :math:`H` the PSF and :math:`x` the + unknown original image, the Wiener filter is + + .. math:: + \hat x = F^\dag (|\Lambda_H|^2 + \lambda |\Lambda_D|^2) + \Lambda_H^\dag F y + + where :math:`F` and :math:`F^\dag` are the Fourier and inverse + Fourier transfroms respectively, :math:`\Lambda_H` the transfer + function (or the Fourier transfrom of the PSF, see [Hunt] below) + and :math:`\Lambda_D` the filter to penalize the restored image + frequencies (Laplacian by default, that is penalization of high + frequency). The parameter :math:`\lambda` tunes the balance + between the data (that tends to increase high frequency, even + those coming from noise), and the regularization. + + These methods are then specific to a prior model. Consequently, + the application or the true image nature must corresponds to the + prior model. By default, the prior model (Laplacian) introduce + image smoothness or pixel correlation. It can also be interpreted + as high-frequency penalization to compensate the instability of + the solution wrt. data (sometimes called noise amplification or + "explosive" solution). + + Finally, the use of Fourier space implies a circulant property of + :math:`H`, see [Hunt]. + + References + ---------- + .. [1] François Orieux, Jean-François Giovannelli, and Thomas + Rodet, "Bayesian estimation of regularization and point + spread function parameters for Wiener-Hunt deconvolution", + J. Opt. Soc. Am. A 27, 1593-1607 (2010) + + http://www.opticsinfobase.org/josaa/abstract.cfm?URI=josaa-27-7-1593 + + http://research.orieux.fr/files/papers/OGR-JOSA10.pdf + + .. [2] B. R. Hunt "A matrix theory proof of the discrete + convolution theorem", IEEE Trans. on Audio and + Electroacoustics, vol. au-19, no. 4, pp. 285-288, dec. 1971 + """ + if reg is None: + reg, _ = uft.laplacian(image.ndim, image.shape, is_real=is_real) + if not np.iscomplexobj(reg): + reg = uft.ir2tf(reg, image.shape, is_real=is_real) + + if psf.shape != reg.shape: + trans_func = uft.ir2tf(psf, image.shape, is_real=is_real) + else: + trans_func = psf + + wiener_filter = np.conj(trans_func) / (np.abs(trans_func)**2 + + balance * np.abs(reg)**2) + if is_real: + deconv = uft.uirfft2(wiener_filter * uft.urfft2(image)) + else: + deconv = uft.uifft2(wiener_filter * uft.ufft2(image)) + + if clip: + deconv[deconv > 1] = 1 + deconv[deconv < -1] = -1 + + return deconv + + +def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, + clip=True): + """Unsupervised Wiener-Hunt deconvolution + + Return the deconvolution with a Wiener-Hunt approach, where the + hyperparameters are automatically estimated. The algorithm is a + stochastic iterative process (Gibbs sampler) described in the + reference below. See also ``wiener`` function. + + Parameters + ---------- + image : (M, N) ndarray + The input degraded image + psf : ndarray + The impulse response (input image's space) or the transfer + function (Fourier space). Both are accepted. The transfer + function is recognize as being complex + (``np.iscomplexobj(psf)``). + reg : ndarray, optional + The regularisation operator. The Laplacian by default. It can + be an impulse response or a transfer function, as for the psf. + user_params : dict + dictionary of gibbs parameters. See below. + clip : boolean, optional + True by default. If true, pixel value of the result above 1 or + under -1 are thresholded for skimage pipeline + compatibility. + + Returns + ------- + x_postmean : (M, N) ndarray + The deconvolved image (the posterior mean). + chains : dict + The keys ``noise`` and ``prior`` contain the chain list of + noise and prior precision respectively. + + Other parameters + ---------------- + The keys of ``user_params`` are: + + threshold : float + The stopping criterion: the norm of the difference between to + successive approximated solution (empirical mean of object + samples, see Notes section). 1e-4 by default. + burnin : int + The number of sample to ignore to start computation of the + mean. 100 by default. + min_iter : int + The minimum number of iterations. 30 by default. + max_iter : int + The maximum number of iterations if ``threshold`` is not + satisfied. 150 by default. + callback : callable (None by default) + A user provided callable to which is passed, if the function + exists, the current image sample for whatever purpose. The user + can store the sample, or compute other moments than the + mean. It has no influence on the algorithm execution and is + only for inspection. + + Examples + -------- + >>> from skimage import color, data, restoration + >>> lena = color.rgb2gray(data.lena()) + >>> from scipy.signal import convolve2d + >>> psf = np.ones((5, 5)) / 25 + >>> lena = convolve2d(lena, psf, 'same') + >>> lena += 0.1 * lena.std() * np.random.standard_normal(lena.shape) + >>> deconvolved_lena = restoration.unsupervised_wiener(lena, psf) + + Notes + ----- + The estimated image is design as the posterior mean of a + probability law (from a Bayesian analysis). The mean is defined as + a sum over all the possible images weighted by their respective + probability. Given the size of the problem, the exact sum is not + tractable. This algorithm use of MCMC to draw image under the + posterior law. The practical idea is to only draw high probable + image since they have the biggest contribution to the mean. At the + opposite, the lowest probable image are draw less often since + their contribution are low. Finally the empirical mean of these + samples give us an estimation of the mean, and an exact + computation with an infinite sample set. + + References + ---------- + .. [1] François Orieux, Jean-François Giovannelli, and Thomas + Rodet, "Bayesian estimation of regularization and point + spread function parameters for Wiener-Hunt deconvolution", + J. Opt. Soc. Am. A 27, 1593-1607 (2010) + + http://www.opticsinfobase.org/josaa/abstract.cfm?URI=josaa-27-7-1593 + + http://research.orieux.fr/files/papers/OGR-JOSA10.pdf + """ + params = {'threshold': 1e-4, 'max_iter': 200, + 'min_iter': 30, 'burnin': 15, 'callback': None} + params.update(user_params or {}) + + if reg is None: + reg, _ = uft.laplacian(image.ndim, image.shape, is_real=is_real) + if not np.iscomplexobj(reg): + reg = uft.ir2tf(reg, image.shape, is_real=is_real) + + if psf.shape != reg.shape: + trans_fct = uft.ir2tf(psf, image.shape, is_real=is_real) + else: + trans_fct = psf + + # The mean of the object + x_postmean = np.zeros(trans_fct.shape) + # The previous computed mean in the iterative loop + prev_x_postmean = np.zeros(trans_fct.shape) + + # Difference between two successive mean + delta = np.NAN + + # Initial state of the chain + gn_chain, gx_chain = [1], [1] + + # The correlation of the object in Fourier space (if size is big, + # this can reduce computation time in the loop) + areg2 = np.abs(reg)**2 + atf2 = np.abs(trans_fct)**2 + + # The Fourier transfrom may change the image.size attribut, so we + # store it. + if is_real: + data_spectrum = uft.urfft2(image.astype(np.float)) + else: + data_spectrum = uft.ufft2(image.astype(np.float)) + + # Gibbs sampling + for iteration in range(params['max_iter']): + # Sample of Eq. 27 p(circX^k | gn^k-1, gx^k-1, y). + + # weighting (correlation in direct space) + precision = gn_chain[-1] * atf2 + gx_chain[-1] * areg2 # Eq. 29 + excursion = np.sqrt(0.5) / np.sqrt(precision) * ( + np.random.standard_normal(data_spectrum.shape) + + 1j * np.random.standard_normal(data_spectrum.shape)) + + # mean Eq. 30 (RLS for fixed gn, gamma0 and gamma1 ...) + wiener_filter = gn_chain[-1] * np.conj(trans_fct) / precision + + # sample of X in Fourier space + x_sample = wiener_filter * data_spectrum + excursion + if params['callback']: + params['callback'](x_sample) + + # sample of Eq. 31 p(gn | x^k, gx^k, y) + gn_chain.append(npr.gamma(image.size / 2, + 2 / uft.image_quad_norm(data_spectrum - + x_sample * + trans_fct))) + + # sample of Eq. 31 p(gx | x^k, gn^k-1, y) + gx_chain.append(npr.gamma((image.size - 1) / 2, + 2 / uft.image_quad_norm(x_sample * reg))) + + # current empirical average + if iteration > params['burnin']: + x_postmean = prev_x_postmean + x_sample + + if iteration > (params['burnin'] + 1): + current = x_postmean / (iteration - params['burnin']) + previous = prev_x_postmean / (iteration - params['burnin'] - 1) + + delta = np.sum(np.abs(current - previous)) / \ + np.sum(np.abs(x_postmean)) / (iteration - params['burnin']) + + prev_x_postmean = x_postmean + + # stop of the algorithm + if (iteration > params['min_iter']) and (delta < params['threshold']): + break + + # Empirical average \approx POSTMEAN Eq. 44 + x_postmean = x_postmean / (iteration - params['burnin']) + if is_real: + x_postmean = uft.uirfft2(x_postmean) + else: + x_postmean = uft.uifft2(x_postmean) + + if clip: + x_postmean[x_postmean > 1] = 1 + x_postmean[x_postmean < -1] = -1 + + return (x_postmean, {'noise': gn_chain, 'prior': gx_chain}) + + +def richardson_lucy(image, psf, iterations=50, clip=True): + """Richardson-Lucy deconvolution. + + Parameters + ---------- + image : ndarray + Input degraded image + psf : ndarray + The point spread function + iterations : int + Number of iterations. This parameter play to role of + regularisation. + clip : boolean, optional + True by default. If true, pixel value of the result above 1 or + under -1 are thresholded for skimage pipeline + compatibility. + + Returns + ------- + im_deconv : ndarray + The deconvolved image + + Examples + -------- + >>> from skimage import color, data, restoration + >>> camera = color.rgb2gray(data.camera()) + >>> from scipy.signal import convolve2d + >>> psf = np.ones((5, 5)) / 25 + >>> camera = convolve2d(camera, psf, 'same') + >>> camera += 0.1 * camera.std() * np.random.standard_normal(camera.shape) + >>> deconvolved = restoration.richardson_lucy(camera, psf, 5) + + References + ---------- + .. [2] http://en.wikipedia.org/wiki/Richardson%E2%80%93Lucy_deconvolution + """ + image = image.astype(np.float) + psf = psf.astype(np.float) + im_deconv = 0.5 * np.ones(image.shape) + psf_mirror = psf[::-1, ::-1] + for _ in range(iterations): + relative_blur = image / convolve2d(im_deconv, psf, 'same') + im_deconv *= convolve2d(relative_blur, psf_mirror, 'same') + + if clip: + im_deconv[im_deconv > 1] = 1 + im_deconv[im_deconv < -1] = -1 + + return im_deconv diff --git a/skimage/restoration/setup.py b/skimage/restoration/setup.py new file mode 100644 index 00000000..e20073e0 --- /dev/null +++ b/skimage/restoration/setup.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +import os + +from skimage._build import cython + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs + + config = Configuration('restoration', parent_package, top_path) + config.add_data_dir('tests') + + cython(['_unwrap_1d.pyx'], working_path=base_path) + cython(['_unwrap_2d.pyx'], working_path=base_path) + cython(['_unwrap_3d.pyx'], working_path=base_path) + cython(['_denoise_cy.pyx'], working_path=base_path) + + config.add_extension('_unwrap_1d', sources=['_unwrap_1d.c'], + include_dirs=[get_numpy_include_dirs()]) + unwrap_sources_2d = ['_unwrap_2d.c', 'unwrap_2d_ljmu.c'] + config.add_extension('_unwrap_2d', sources=unwrap_sources_2d, + include_dirs=[get_numpy_include_dirs()]) + unwrap_sources_3d = ['_unwrap_3d.c', 'unwrap_3d_ljmu.c'] + config.add_extension('_unwrap_3d', sources=unwrap_sources_3d, + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_denoise_cy', sources=['_denoise_cy.c'], + include_dirs=[get_numpy_include_dirs(), '../_shared']) + + return config + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='scikit-image Developers', + author='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Restoration', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/restoration/tests/camera_rl.npy b/skimage/restoration/tests/camera_rl.npy new file mode 100644 index 00000000..10995f85 Binary files /dev/null and b/skimage/restoration/tests/camera_rl.npy differ diff --git a/skimage/restoration/tests/camera_unsup.npy b/skimage/restoration/tests/camera_unsup.npy new file mode 100644 index 00000000..12d2e8b8 Binary files /dev/null and b/skimage/restoration/tests/camera_unsup.npy differ diff --git a/skimage/restoration/tests/camera_unsup2.npy b/skimage/restoration/tests/camera_unsup2.npy new file mode 100644 index 00000000..5bb974db Binary files /dev/null and b/skimage/restoration/tests/camera_unsup2.npy differ diff --git a/skimage/restoration/tests/camera_wiener.npy b/skimage/restoration/tests/camera_wiener.npy new file mode 100644 index 00000000..831b2be8 Binary files /dev/null and b/skimage/restoration/tests/camera_wiener.npy differ diff --git a/skimage/filter/tests/test_denoise.py b/skimage/restoration/tests/test_denoise.py similarity index 71% rename from skimage/filter/tests/test_denoise.py rename to skimage/restoration/tests/test_denoise.py index f81ba07b..d451aa74 100644 --- a/skimage/filter/tests/test_denoise.py +++ b/skimage/restoration/tests/test_denoise.py @@ -1,7 +1,7 @@ import numpy as np from numpy.testing import run_module_suite, assert_raises, assert_equal -from skimage import filter, data, color, img_as_float +from skimage import restoration, data, color, img_as_float np.random.seed(1234) @@ -19,7 +19,7 @@ def test_denoise_tv_chambolle_2d(): # clip noise so that it does not exceed allowed range for float images. img = np.clip(img, 0, 1) # denoise - denoised_lena = filter.denoise_tv_chambolle(img, weight=60.0) + denoised_lena = restoration.denoise_tv_chambolle(img, weight=60.0) # which dtype? assert denoised_lena.dtype in [np.float, np.float32, np.float64] from scipy import ndimage @@ -33,8 +33,9 @@ def test_denoise_tv_chambolle_2d(): def test_denoise_tv_chambolle_multichannel(): - denoised0 = filter.denoise_tv_chambolle(lena[..., 0], weight=60.0) - denoised = filter.denoise_tv_chambolle(lena, weight=60.0, multichannel=True) + denoised0 = restoration.denoise_tv_chambolle(lena[..., 0], weight=60.0) + denoised = restoration.denoise_tv_chambolle(lena, weight=60.0, + multichannel=True) assert_equal(denoised[..., 0], denoised0) @@ -43,7 +44,7 @@ def test_denoise_tv_chambolle_float_result_range(): img = lena_gray int_lena = np.multiply(img, 255).astype(np.uint8) assert np.max(int_lena) > 1 - denoised_int_lena = filter.denoise_tv_chambolle(int_lena, weight=60.0) + denoised_int_lena = restoration.denoise_tv_chambolle(int_lena, weight=60.0) # test if the value range of output float data is within [0.0:1.0] assert denoised_int_lena.dtype == np.float assert np.max(denoised_int_lena) <= 1.0 @@ -59,12 +60,12 @@ def test_denoise_tv_chambolle_3d(): mask += 20 * np.random.random(mask.shape) mask[mask < 0] = 0 mask[mask > 255] = 255 - res = filter.denoise_tv_chambolle(mask.astype(np.uint8), weight=100) + res = restoration.denoise_tv_chambolle(mask.astype(np.uint8), weight=100) assert res.dtype == np.float assert res.std() * 255 < mask.std() # test wrong number of dimensions - assert_raises(ValueError, filter.denoise_tv_chambolle, + assert_raises(ValueError, restoration.denoise_tv_chambolle, np.random.random((8, 8, 8, 8))) @@ -74,8 +75,8 @@ def test_denoise_tv_bregman_2d(): img += 0.5 * img.std() * np.random.random(img.shape) img = np.clip(img, 0, 1) - out1 = filter.denoise_tv_bregman(img, weight=10) - out2 = filter.denoise_tv_bregman(img, weight=5) + out1 = restoration.denoise_tv_bregman(img, weight=10) + out2 = restoration.denoise_tv_bregman(img, weight=5) # make sure noise is reduced assert img.std() > out1.std() @@ -87,7 +88,7 @@ def test_denoise_tv_bregman_float_result_range(): img = lena_gray int_lena = np.multiply(img, 255).astype(np.uint8) assert np.max(int_lena) > 1 - denoised_int_lena = filter.denoise_tv_bregman(int_lena, weight=60.0) + denoised_int_lena = restoration.denoise_tv_bregman(int_lena, weight=60.0) # test if the value range of output float data is within [0.0:1.0] assert denoised_int_lena.dtype == np.float assert np.max(denoised_int_lena) <= 1.0 @@ -100,8 +101,8 @@ def test_denoise_tv_bregman_3d(): img += 0.5 * img.std() * np.random.random(img.shape) img = np.clip(img, 0, 1) - out1 = filter.denoise_tv_bregman(img, weight=10) - out2 = filter.denoise_tv_bregman(img, weight=5) + out1 = restoration.denoise_tv_bregman(img, weight=10) + out2 = restoration.denoise_tv_bregman(img, weight=5) # make sure noise is reduced assert img.std() > out1.std() @@ -114,8 +115,10 @@ def test_denoise_bilateral_2d(): img += 0.5 * img.std() * np.random.random(img.shape) img = np.clip(img, 0, 1) - out1 = filter.denoise_bilateral(img, sigma_range=0.1, sigma_spatial=20) - out2 = filter.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) + out1 = restoration.denoise_bilateral(img, sigma_range=0.1, + sigma_spatial=20) + out2 = restoration.denoise_bilateral(img, sigma_range=0.2, + sigma_spatial=30) # make sure noise is reduced assert img.std() > out1.std() @@ -128,8 +131,10 @@ def test_denoise_bilateral_3d(): img += 0.5 * img.std() * np.random.random(img.shape) img = np.clip(img, 0, 1) - out1 = filter.denoise_bilateral(img, sigma_range=0.1, sigma_spatial=20) - out2 = filter.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) + out1 = restoration.denoise_bilateral(img, sigma_range=0.1, + sigma_spatial=20) + out2 = restoration.denoise_bilateral(img, sigma_range=0.2, + sigma_spatial=30) # make sure noise is reduced assert img.std() > out1.std() diff --git a/skimage/restoration/tests/test_restoration.py b/skimage/restoration/tests/test_restoration.py new file mode 100644 index 00000000..915640af --- /dev/null +++ b/skimage/restoration/tests/test_restoration.py @@ -0,0 +1,64 @@ +from os.path import abspath, dirname, join as pjoin + +import numpy as np +from scipy.signal import convolve2d + +import skimage +from skimage.data import camera +from skimage import restoration +from skimage.restoration import uft + +test_img = skimage.img_as_float(camera()) + + +def test_wiener(): + psf = np.ones((5, 5)) / 25 + data = convolve2d(test_img, psf, 'same') + np.random.seed(0) + data += 0.1 * data.std() * np.random.standard_normal(data.shape) + deconvolved = restoration.wiener(data, psf, 0.05) + + path = pjoin(dirname(abspath(__file__)), 'camera_wiener.npy') + np.testing.assert_allclose(deconvolved, np.load(path), rtol=1e-3) + + _, laplacian = uft.laplacian(2, data.shape) + otf = uft.ir2tf(psf, data.shape, is_real=False) + deconvolved = restoration.wiener(data, otf, 0.05, + reg=laplacian, + is_real=False) + np.testing.assert_allclose(np.real(deconvolved), + np.load(path), + rtol=1e-3) + + +def test_unsupervised_wiener(): + psf = np.ones((5, 5)) / 25 + data = convolve2d(test_img, psf, 'same') + np.random.seed(0) + data += 0.1 * data.std() * np.random.standard_normal(data.shape) + deconvolved, _ = restoration.unsupervised_wiener(data, psf) + + path = pjoin(dirname(abspath(__file__)), 'camera_unsup.npy') + np.testing.assert_allclose(deconvolved, np.load(path), rtol=1e-3) + + _, laplacian = uft.laplacian(2, data.shape) + otf = uft.ir2tf(psf, data.shape, is_real=False) + np.random.seed(0) + deconvolved = restoration.unsupervised_wiener( + data, otf, reg=laplacian, is_real=False, + user_params={"callback": lambda x: None})[0] + path = pjoin(dirname(abspath(__file__)), 'camera_unsup2.npy') + np.testing.assert_allclose(np.real(deconvolved), + np.load(path), + rtol=1e-3) + + +def test_richardson_lucy(): + psf = np.ones((5, 5)) / 25 + data = convolve2d(test_img, psf, 'same') + np.random.seed(0) + data += 0.1 * data.std() * np.random.standard_normal(data.shape) + deconvolved = restoration.richardson_lucy(data, psf, 5) + + path = pjoin(dirname(abspath(__file__)), 'camera_rl.npy') + np.testing.assert_allclose(deconvolved, np.load(path), rtol=1e-3) diff --git a/skimage/restoration/tests/test_unwrap.py b/skimage/restoration/tests/test_unwrap.py new file mode 100644 index 00000000..fca63724 --- /dev/null +++ b/skimage/restoration/tests/test_unwrap.py @@ -0,0 +1,148 @@ +from __future__ import print_function, division + +import numpy as np +from numpy.testing import (run_module_suite, assert_array_almost_equal, + assert_almost_equal, assert_array_equal, + assert_raises) +import warnings + +from skimage.restoration import unwrap_phase + + +def assert_phase_almost_equal(a, b, *args, **kwargs): + '''An assert_almost_equal insensitive to phase shifts of n*2*pi.''' + shift = 2 * np.pi * np.round((b.mean() - a.mean()) / (2 * np.pi)) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + print('assert_phase_allclose, abs', np.max(np.abs(a - (b - shift)))) + print('assert_phase_allclose, rel', + np.max(np.abs((a - (b - shift)) / a))) + if np.ma.isMaskedArray(a): + assert np.ma.isMaskedArray(b) + assert_array_equal(a.mask, b.mask) + au = np.asarray(a) + bu = np.asarray(b) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + print('assert_phase_allclose, no mask, abs', + np.max(np.abs(au - (bu - shift)))) + print('assert_phase_allclose, no mask, rel', + np.max(np.abs((au - (bu - shift)) / au))) + assert_array_almost_equal(a + shift, b, *args, **kwargs) + + +def check_unwrap(image, mask=None): + image_wrapped = np.angle(np.exp(1j * image)) + if not mask is None: + print('Testing a masked image') + image = np.ma.array(image, mask=mask) + image_wrapped = np.ma.array(image_wrapped, mask=mask) + image_unwrapped = unwrap_phase(image_wrapped) + assert_phase_almost_equal(image_unwrapped, image) + + +def test_unwrap_1d(): + image = np.linspace(0, 10 * np.pi, 100) + check_unwrap(image) + # Masked arrays are not allowed in 1D + assert_raises(ValueError, check_unwrap, image, True) + # wrap_around is not allowed in 1D + assert_raises(ValueError, unwrap_phase, image, True) + + +def test_unwrap_2d(): + x, y = np.ogrid[:8, :16] + image = 2 * np.pi * (x * 0.2 + y * 0.1) + yield check_unwrap, image + mask = np.zeros(image.shape, dtype=np.bool) + mask[4:6, 4:8] = True + yield check_unwrap, image, mask + + +def test_unwrap_3d(): + x, y, z = np.ogrid[:8, :12, :16] + image = 2 * np.pi * (x * 0.2 + y * 0.1 + z * 0.05) + yield check_unwrap, image + mask = np.zeros(image.shape, dtype=np.bool) + mask[4:6, 4:6, 1:3] = True + yield check_unwrap, image, mask + + +def check_wrap_around(ndim, axis): + # create a ramp, but with the last pixel along axis equalling the first + elements = 100 + ramp = np.linspace(0, 12 * np.pi, elements) + ramp[-1] = ramp[0] + image = ramp.reshape(tuple([elements if n == axis else 1 + for n in range(ndim)])) + image_wrapped = np.angle(np.exp(1j * image)) + + index_first = tuple([0] * ndim) + index_last = tuple([-1 if n == axis else 0 for n in range(ndim)]) + # unwrap the image without wrap around + with warnings.catch_warnings(): + # We do not want warnings about length 1 dimensions + warnings.simplefilter("ignore") + image_unwrap_no_wrap_around = unwrap_phase(image_wrapped) + print('endpoints without wrap_around:', + image_unwrap_no_wrap_around[index_first], + image_unwrap_no_wrap_around[index_last]) + # without wrap around, the endpoints of the image should differ + assert abs(image_unwrap_no_wrap_around[index_first] + - image_unwrap_no_wrap_around[index_last]) > np.pi + # unwrap the image with wrap around + wrap_around = [n == axis for n in range(ndim)] + with warnings.catch_warnings(): + # We do not want warnings about length 1 dimensions + warnings.simplefilter("ignore") + image_unwrap_wrap_around = unwrap_phase(image_wrapped, wrap_around) + print('endpoints with wrap_around:', + image_unwrap_wrap_around[index_first], + image_unwrap_wrap_around[index_last]) + # with wrap around, the endpoints of the image should be equal + assert_almost_equal(image_unwrap_wrap_around[index_first], + image_unwrap_wrap_around[index_last]) + + +def test_wrap_around(): + for ndim in (2, 3): + for axis in range(ndim): + yield check_wrap_around, ndim, axis + + +def test_mask(): + length = 100 + ramps = [np.linspace(0, 4 * np.pi, length), + np.linspace(0, 8 * np.pi, length), + np.linspace(0, 6 * np.pi, length)] + image = np.vstack(ramps) + mask_1d = np.ones((length,), dtype=np.bool) + mask_1d[0] = mask_1d[-1] = False + for i in range(len(ramps)): + # mask all ramps but the i'th one + mask = np.zeros(image.shape, dtype=np.bool) + mask |= mask_1d.reshape(1, -1) + mask[i, :] = False # unmask i'th ramp + image_wrapped = np.ma.array(np.angle(np.exp(1j * image)), mask=mask) + image_unwrapped = unwrap_phase(image_wrapped) + image_unwrapped -= image_unwrapped[0, 0] # remove phase shift + # The end of the unwrapped array should have value equal to the + # endpoint of the unmasked ramp + assert_array_almost_equal(image_unwrapped[:, -1], image[i, -1]) + + # Same tests, but forcing use of the 3D unwrapper by reshaping + image_wrapped_3d = image_wrapped.reshape((1,) + image_wrapped.shape) + image_unwrapped_3d = unwrap_phase(image_wrapped_3d) + image_unwrapped_3d -= image_unwrapped_3d[0, 0, 0] # remove phase shift + assert_array_almost_equal(image_unwrapped_3d[:, :, -1], image[i, -1]) + + +def test_invalid_input(): + assert_raises(ValueError, unwrap_phase, np.zeros([])) + assert_raises(ValueError, unwrap_phase, np.zeros((1, 1, 1, 1))) + assert_raises(ValueError, unwrap_phase, np.zeros((1, 1)), 3 * [False]) + assert_raises(ValueError, unwrap_phase, np.zeros((1, 1)), 'False') + + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/restoration/uft.py b/skimage/restoration/uft.py new file mode 100644 index 00000000..fedcc6f6 --- /dev/null +++ b/skimage/restoration/uft.py @@ -0,0 +1,445 @@ +# -*- coding: utf-8 -*- +# uft.py --- Unitary fourier transform + +"""Function of unitary fourier transform and utilities + +This module implement unitary fourier transform, that is ortho-normal +transform. They are especially and useful for convolution [1]: they +respect the Parseval equality, the value of the null frequency is +equal to + +.. math:: \frac{1}{\sqrt{n}} \sum_i x_i + +or the Fourier tranform have the same energy than the original image +(see ``image_quad_norm`` function). The transform is applied from the +last axes for performance reason (c order array). You may use directly +the numpy.fft module for more sophisticated purpose. + +References +---------- +.. [1] B. R. Hunt "A matrix theory proof of the discrete convolution + theorem", IEEE Trans. on Audio and Electroacoustics, + vol. au-19, no. 4, pp. 285-288, dec. 1971 + +""" + +from __future__ import division, print_function + +import numpy as np + +__keywords__ = "fft, Fourier Transform, orthonormal, unitary" + + +def ufftn(inarray, dim=None): + """N-dim unitary Fourier transform + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The ``dim`` last axis along wich to compute the transform. All + axes by default. + + Returns + ------- + outarray : ndarray (same shape than inarray) + The unitary N-D Fourier transform of ``inarray``. + + Examples + -------- + >>> input = np.ones((3, 3, 3)) + >>> output = ufftn(input) + >>> np.allclose(np.sum(input) / np.sqrt(input.size), output[0, 0, 0]) + True + >>> output.shape + (3, 3, 3) + """ + if dim is None: + dim = inarray.ndim + outarray = np.fft.fftn(inarray, axes=range(-dim, 0)) + return outarray / np.sqrt(np.prod(inarray.shape[-dim:])) + + +def uifftn(inarray, dim=None): + """N-dim unitary inverse Fourier transform + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The ``dim`` last axis along wich to compute the transform. All + axes by default. + + Returns + ------- + outarray : ndarray (same shape than inarray) + The unitary inverse N-D Fourier transform of ``inarray``. + + Examples + -------- + >>> input = np.ones((3, 3, 3)) + >>> output = uifftn(input) + >>> np.allclose(np.sum(input) / np.sqrt(input.size), output[0, 0, 0]) + True + >>> output.shape + (3, 3, 3) + """ + if dim is None: + dim = inarray.ndim + outarray = np.fft.ifftn(inarray, axes=range(-dim, 0)) + return outarray * np.sqrt(np.prod(inarray.shape[-dim:])) + + +def urfftn(inarray, dim=None): + """N-dim real unitary Fourier transform + + This transform consider the Hermitian property of the transform on + real input + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The ``dim`` last axis along wich to compute the transform. All + axes by default. + + Returns + ------- + outarray : ndarray (the last dim as N / 2 + 1 lenght) + The unitary N-D real Fourier transform of ``inarray``. + + Notes + ----- + The ``urfft`` functions assume an input array of real + values. Consequently, the output have an Hermitian property and + redondant values are not computed and returned. + + Examples + -------- + >>> input = np.ones((5, 5, 5)) + >>> output = urfftn(input) + >>> np.allclose(np.sum(input) / np.sqrt(input.size), output[0, 0, 0]) + True + >>> output.shape + (5, 5, 3) + """ + if dim is None: + dim = inarray.ndim + outarray = np.fft.rfftn(inarray, axes=range(-dim, 0)) + return outarray / np.sqrt(np.prod(inarray.shape[-dim:])) + + +def uirfftn(inarray, dim=None, shape=None): + """N-dim real unitary Fourier transform + + This transform consider the Hermitian property of the transform + from complex to real real input. + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The ``dim`` last axis along wich to compute the transform. All + axes by default. + shape : tuple of int + The shape of the output. The shape of ``rfft`` is ambiguous in + case of odd shape. In this case, the parameter must be + used. see ``np.fft.irfftn``. + + Returns + ------- + outarray : ndarray + The unitary N-D inverse real Fourier transform of ``inarray``. + + Notes + ----- + The ``uirfft`` function assume that output array is of real + values. Consequently, the input is assumed of having an Hermitian + property and redondant values are implicit. + + Examples + -------- + >>> input = np.ones((5, 5, 5)) + >>> output = uirfftn(urfftn(input), shape=input.shape) + >>> np.allclose(input, output) + True + >>> output.shape + (5, 5, 5) + """ + if dim is None: + dim = inarray.ndim + outarray = np.fft.irfftn(inarray, shape, axes=range(-dim, 0)) + return outarray * np.sqrt(np.prod(outarray.shape[-dim:])) + + +def ufft2(inarray): + """2-dim unitary Fourier transform + + Compute the Fourier transform on the last 2 axes. + + Parameters + ---------- + inarray : ndarray + The array to transform. + + Returns + ------- + outarray : ndarray (same shape than inarray) + The unitary 2-D Fourier transform of ``inarray``. + + See Also + -------- + uifft2, ufftn, urfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = ufft2(input) + >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), output[1, 0, 0]) + True + >>> output.shape + (10, 128, 128) + """ + return ufftn(inarray, 2) + + +def uifft2(inarray): + """2-dim inverse unitary Fourier transform + + Compute the inverse Fourier transform on the last 2 axes. + + Parameters + ---------- + inarray : ndarray + The array to transform. + + Returns + ------- + outarray : ndarray (same shape than inarray) + The unitary 2-D inverse Fourier transform of ``inarray``. + + See Also + -------- + uifft2, uifftn, uirfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = uifft2(input) + >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), output[0, 0, 0]) + True + >>> output.shape + (10, 128, 128) + """ + return uifftn(inarray, 2) + + +def urfft2(inarray): + """2-dim real unitary Fourier transform + + Compute the real Fourier transform on the last 2 axes. This + transform consider the Hermitian property of the transform from + complex to real real input. + + Parameters + ---------- + inarray : ndarray + The array to transform. + + Returns + ------- + outarray : ndarray (the last dim as (N - 1) *2 lenght) + The unitary 2-D real Fourier transform of ``inarray``. + + See Also + -------- + ufft2, ufftn, urfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = urfft2(input) + >>> np.allclose(np.sum(input[1,...]) / np.sqrt(input[1,...].size), output[1, 0, 0]) + True + >>> output.shape + (10, 128, 65) + """ + return urfftn(inarray, 2) + + +def uirfft2(inarray, shape=None): + """2-dim real unitary Fourier transform + + Compute the real inverse Fourier transform on the last 2 axes. + This transform consider the Hermitian property of the transform + from complex to real real input. + + Parameters + ---------- + inarray : ndarray + The array to transform. + + Returns + ------- + outarray : ndarray (the last dim as (N - 1) *2 lenght) + The unitary 2-D inverse real Fourier transform of ``inarray``. + + See Also + -------- + urfft2, uifftn, uirfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = uirfftn(urfftn(input), shape=input.shape) + >>> np.allclose(input, output) + True + >>> output.shape + (10, 128, 128) + """ + return uirfftn(inarray, 2, shape=shape) + + +def image_quad_norm(inarray): + """Return quadratic norm of images in Fourier space + + This function detect if the image suppose the hermitian property. + + Parameters + ---------- + inarray : ndarray + The images are supposed to be in the last two axes + + Returns + ------- + norm : float + The quadratic norm of ``inarray``. + + Examples + -------- + >>> input = np.ones((5, 5)) + >>> image_quad_norm(ufft2(input)) == np.sum(np.abs(input)**2) + True + >>> image_quad_norm(ufft2(input)) == image_quad_norm(urfft2(input)) + True + """ + # If there is an hermitian symmetry + if inarray.shape[-1] != inarray.shape[-2]: + return 2 * np.sum(np.sum(np.abs(inarray)**2, axis=-1), axis=-1) - \ + np.sum(np.abs(inarray[..., 0])**2, axis=-1) + else: + return np.sum(np.sum(np.abs(inarray)**2, axis=-1), axis=-1) + + +def ir2tf(imp_resp, shape, dim=None, is_real=True): + """Compute the transfer function of IR + + This function make the necessary correct zero-padding, zero + convention, correct fft2 etc... to compute the transfer function + of IR. To use with unitary Fourier transform for the signal (ufftn + or equivalent). + + Parameters + ---------- + imp_resp : ndarray + The impulsionnal responses. + shape : tuple of int + A tuple of integer corresponding to the target shape of the + tranfert function. + dim : int, optional + The ``dim`` last axis along wich to compute the transform. All + axes by default. + is_real : boolean (optionnal, default True) + If True, imp_resp is supposed real and the hermissian property + is used with rfftn Fourier transform. + + Returns + ------- + y : complex ndarray + The tranfert function of shape ``shape``. + + See Also + -------- + ufftn, uifftn, urfftn, uirfftn + + Examples + -------- + >>> np.all(np.array([[4, 0], [0, 0]]) == ir2tf(np.ones((2, 2)), (2, 2))) + True + >>> ir2tf(np.ones((2, 2)), (512, 512)).shape == (512, 257) + True + >>> ir2tf(np.ones((2, 2)), (512, 512), is_real=False).shape == (512, 512) + True + + Notes + ----- + The input array can be composed of multiple dimentionnal IR with + an arbitraru number of IR. The individual IR must be accesed + through first axes. The last ``dim`` axes of space definition. The + ``dim`` parameter must be specified to compute the transform only + along these last axes. + """ + if not dim: + dim = imp_resp.ndim + # Zero padding and fill + irpadded = np.zeros(shape) + irpadded[tuple([slice(0, s) for s in imp_resp.shape])] = imp_resp + # Roll for zero convention of the fft to avoid the phase + # problem. Work with odd and even size. + for axis, axis_size in enumerate(imp_resp.shape): + if axis >= imp_resp.ndim - dim: + irpadded = np.roll(irpadded, + shift=-int(np.floor(axis_size / 2)), + axis=axis) + if is_real: + return np.fft.rfftn(irpadded, axes=range(-dim, 0)) + else: + return np.fft.fftn(irpadded, axes=range(-dim, 0)) + + +def laplacian(ndim, shape, is_real=True): + """Return the transfer function of the Laplacian + + Laplacian is the second order difference, on line and column. + + Parameters + ---------- + ndim : int + The dimension of the Laplacian + shape : tuple, shape + The support on which to compute the transfer function + is_real : boolean (optionnal, default True) + If True, imp_resp is supposed real and the hermissian property + is used with rfftn Fourier transform to return the transfer + function. + + Returns + ------- + tf : array_like, complex + The transfer function + impr : array_like, real + The Laplacian + + Examples + -------- + >>> tf, ir = laplacian(2, (32, 32)) + >>> np.all(ir == np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]])) + True + >>> np.all(tf == ir2tf(ir, (32, 32))) + True + """ + impr = np.zeros([3] * ndim) + for dim in range(ndim): + idx = tuple([slice(1, 2)] * dim + + [slice(None)] + + [slice(1, 2)] * (ndim - dim - 1)) + impr[idx] = np.array([-1.0, + 0.0, + -1.0]).reshape([-1 if i == dim else 1 + for i in range(ndim)]) + impr[([slice(1, 2)] * ndim)] = 2.0 * ndim + return ir2tf(impr, shape, is_real=is_real), impr diff --git a/skimage/restoration/unwrap.py b/skimage/restoration/unwrap.py new file mode 100644 index 00000000..085bef0b --- /dev/null +++ b/skimage/restoration/unwrap.py @@ -0,0 +1,104 @@ +import numpy as np +import warnings +from six import string_types + +from ._unwrap_1d import unwrap_1d +from ._unwrap_2d import unwrap_2d +from ._unwrap_3d import unwrap_3d + + +def unwrap_phase(image, wrap_around=False): + '''From ``image``, wrapped to lie in the interval [-pi, pi), recover the + original, unwrapped image. + + Parameters + ---------- + image : 1D, 2D or 3D ndarray of floats, optionally a masked array + The values should be in the range ``[-pi, pi)``. If a masked array is + provided, the masked entries will not be changed, and their values + will not be used to guide the unwrapping of neighboring, unmasked + values. Masked 1D arrays are not allowed, and will raise a + ``ValueError``. + wrap_around : bool or sequence of bool + When an element of the sequence is ``True``, the unwrapping process + will regard the edges along the corresponding axis of the image to be + connected and use this connectivity to guide the phase unwrapping + process. If only a single boolean is given, it will apply to all axes. + Wrap around is not supported for 1D arrays. + + Returns + ------- + image_unwrapped : array_like, float32 + Unwrapped image of the same shape as the input. If the input ``image`` + was a masked array, the mask will be preserved. + + Raises + ------ + ValueError + If called with a masked 1D array or called with a 1D array and + ``wrap_around=True``. + + Examples + -------- + >>> c0, c1 = np.ogrid[-1:1:128j, -1:1:128j] + >>> image = 12 * np.pi * np.exp(-(c0**2 + c1**2)) + >>> image_wrapped = np.angle(np.exp(1j * image)) + >>> image_unwrapped = unwrap_phase(image_wrapped) + >>> np.std(image_unwrapped - image) < 1e-6 # A constant offset is normal + True + + References + ---------- + .. [1] Miguel Arevallilo Herraez, David R. Burton, Michael J. Lalor, + and Munther A. Gdeisat, "Fast two-dimensional phase-unwrapping + algorithm based on sorting by reliability following a noncontinuous + path", Journal Applied Optics, Vol. 41, No. 35 (2002) 7437, + .. [2] Abdul-Rahman, H., Gdeisat, M., Burton, D., & Lalor, M., "Fast + three-dimensional phase-unwrapping algorithm based on sorting by + reliability following a non-continuous path. In W. Osten, + C. Gorecki, & E. L. Novak (Eds.), Optical Metrology (2005) 32--40, + International Society for Optics and Photonics. + ''' + if image.ndim not in (1, 2, 3): + raise ValueError('image must be 1, 2 or 3 dimensional') + if isinstance(wrap_around, bool): + wrap_around = [wrap_around] * image.ndim + elif (hasattr(wrap_around, '__getitem__') + and not isinstance(wrap_around, string_types)): + if len(wrap_around) != image.ndim: + raise ValueError('Length of wrap_around must equal the ' + 'dimensionality of image') + wrap_around = [bool(wa) for wa in wrap_around] + else: + raise ValueError('wrap_around must be a bool or a sequence with ' + 'length equal to the dimensionality of image') + if image.ndim == 1: + if np.ma.isMaskedArray(image): + raise ValueError('1D masked images cannot be unwrapped') + if wrap_around[0]: + raise ValueError('wrap_around is not supported for 1D images') + if image.ndim in (2, 3) and 1 in image.shape: + warnings.warn('image has a length 1 dimension; consider using an ' + 'array of lower dimensionality to use a more efficient ' + 'algorithm') + + if np.ma.isMaskedArray(image): + mask = np.require(image.mask, np.uint8, ['C']) + else: + mask = np.zeros_like(image, dtype=np.uint8, order='C') + image_not_masked = np.asarray(image, dtype=np.float64, order='C') + image_unwrapped = np.empty_like(image, dtype=np.float64, order='C') + + if image.ndim == 1: + unwrap_1d(image_not_masked, image_unwrapped) + elif image.ndim == 2: + unwrap_2d(image_not_masked, mask, image_unwrapped, + wrap_around) + elif image.ndim == 3: + unwrap_3d(image_not_masked, mask, image_unwrapped, + wrap_around) + + if np.ma.isMaskedArray(image): + return np.ma.array(image_unwrapped, mask=mask) + else: + return image_unwrapped diff --git a/skimage/restoration/unwrap_2d_ljmu.c b/skimage/restoration/unwrap_2d_ljmu.c new file mode 100644 index 00000000..9b5c53b8 --- /dev/null +++ b/skimage/restoration/unwrap_2d_ljmu.c @@ -0,0 +1,729 @@ +// 2D phase unwrapping, modified for inclusion in scipy by Gregor Thalhammer +// Original file name: Miguel_2D_unwrapper_with_mask_and_wrap_around_option.c + +//This program was written by Munther Gdeisat and Miguel Arevallilo Herraez to program the two-dimensional unwrapper +//entitled "Fast two-dimensional phase-unwrapping algorithm based on sorting by +//reliability following a noncontinuous path" +//by Miguel Arevallilo Herraez, David R. Burton, Michael J. Lalor, and Munther A. Gdeisat +//published in the Journal Applied Optics, Vol. 41, No. 35, pp. 7437, 2002. +//This program was written by Munther Gdeisat, Liverpool John Moores University, United Kingdom. +//Date 26th August 2007 +//The wrapped phase map is assumed to be of floating point data type. The resultant unwrapped phase map is also of floating point type. +//The mask is of byte data type. +//When the mask is 255 this means that the pixel is valid +//When the mask is 0 this means that the pixel is invalid (noisy or corrupted pixel) +//This program takes into consideration the image wrap around problem encountered in MRI imaging. + +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif + +#define PI M_PI +#define TWOPI (2 * M_PI) + +//TODO: remove global variables +//TODO: make thresholds independent + +#define NOMASK 0 +#define MASK 1 + +typedef struct +{ + double mod; + int x_connectivity; + int y_connectivity; + int no_of_edges; +} params_t; + +//PIXELM information +struct PIXELM +{ + int increment; //No. of 2*pi to add to the pixel to unwrap it + int number_of_pixels_in_group;//No. of pixel in the pixel group + double value; //value of the pixel + double reliability; + unsigned char input_mask; //0 pixel is masked. NOMASK pixel is not masked + unsigned char extended_mask; //0 pixel is masked. NOMASK pixel is not masked + int group; //group No. + int new_group; + struct PIXELM *head; //pointer to the first pixel in the group in the linked list + struct PIXELM *last; //pointer to the last pixel in the group + struct PIXELM *next; //pointer to the next pixel in the group +}; + +typedef struct PIXELM PIXELM; + +//the EDGE is the line that connects two pixels. +//if we have S pixels, then we have S horizontal edges and S vertical edges +struct EDGE +{ + double reliab; //reliabilty of the edge and it depends on the two pixels + PIXELM *pointer_1; //pointer to the first pixel + PIXELM *pointer_2; //pointer to the second pixel + int increment; //No. of 2*pi to add to one of the pixels to + //unwrap it with respect to the second +}; + +typedef struct EDGE EDGE; + +//---------------start quicker_sort algorithm -------------------------------- +#define swap(x,y) {EDGE t; t=x; x=y; y=t;} +#define order(x,y) if (x.reliab > y.reliab) swap(x,y) +#define o2(x,y) order(x,y) +#define o3(x,y,z) o2(x,y); o2(x,z); o2(y,z) + +typedef enum {yes, no} yes_no; + +yes_no find_pivot(EDGE *left, EDGE *right, double *pivot_ptr) +{ + EDGE a, b, c, *p; + + a = *left; + b = *(left + (right - left) / 2); + c = *right; + o3(a, b, c); + + if (a.reliab < b.reliab) + { + *pivot_ptr = b.reliab; + return yes; + } + + if (b.reliab < c.reliab) + { + *pivot_ptr = c.reliab; + return yes; + } + + for (p = left + 1; p <= right; ++p) + { + if (p->reliab != left->reliab) + { + *pivot_ptr = (p->reliab < left->reliab) ? left->reliab : p->reliab; + return yes; + } + } + return no; +} + +EDGE *partition(EDGE *left, EDGE *right, double pivot) +{ + while (left <= right) + { + while (left->reliab < pivot) + ++left; + while (right->reliab >= pivot) + --right; + if (left < right) + { + swap (*left, *right); + ++left; + --right; + } + } + return left; +} + +void quicker_sort(EDGE *left, EDGE *right) +{ + EDGE *p; + double pivot; + + if (find_pivot(left, right, &pivot) == yes) + { + p = partition(left, right, pivot); + quicker_sort(left, p - 1); + quicker_sort(p, right); + } +} +//--------------end quicker_sort algorithm ----------------------------------- + +//--------------------start initialize pixels ---------------------------------- +//initialize pixels. See the explination of the pixel class above. +//initially every pixel is assumed to belong to a group consisting of only itself +void initialisePIXELs(double *wrapped_image, unsigned char *input_mask, unsigned char *extended_mask, PIXELM *pixel, int image_width, int image_height) +{ + PIXELM *pixel_pointer = pixel; + double *wrapped_image_pointer = wrapped_image; + unsigned char *input_mask_pointer = input_mask; + unsigned char *extended_mask_pointer = extended_mask; + int i, j; + + for (i=0; i < image_height; i++) + { + for (j=0; j < image_width; j++) + { + pixel_pointer->increment = 0; + pixel_pointer->number_of_pixels_in_group = 1; + pixel_pointer->value = *wrapped_image_pointer; + pixel_pointer->reliability = 9999999. + rand(); + pixel_pointer->input_mask = *input_mask_pointer; + pixel_pointer->extended_mask = *extended_mask_pointer; + pixel_pointer->head = pixel_pointer; + pixel_pointer->last = pixel_pointer; + pixel_pointer->next = NULL; + pixel_pointer->new_group = 0; + pixel_pointer->group = -1; + pixel_pointer++; + wrapped_image_pointer++; + input_mask_pointer++; + extended_mask_pointer++; + } + } +} +//-------------------end initialize pixels ----------- + +//gamma function in the paper +double wrap(double pixel_value) +{ + double wrapped_pixel_value; + if (pixel_value > PI) wrapped_pixel_value = pixel_value - TWOPI; + else if (pixel_value < -PI) wrapped_pixel_value = pixel_value + TWOPI; + else wrapped_pixel_value = pixel_value; + return wrapped_pixel_value; +} + +// pixelL_value is the left pixel, pixelR_value is the right pixel +int find_wrap(double pixelL_value, double pixelR_value) +{ + double difference; + int wrap_value; + difference = pixelL_value - pixelR_value; + + if (difference > PI) wrap_value = -1; + else if (difference < -PI) wrap_value = 1; + else wrap_value = 0; + + return wrap_value; +} + +void extend_mask(unsigned char *input_mask, unsigned char *extended_mask, + int image_width, int image_height, + params_t *params) +{ + int i,j; + int image_width_plus_one = image_width + 1; + int image_width_minus_one = image_width - 1; + unsigned char *IMP = input_mask + image_width + 1; //input mask pointer + unsigned char *EMP = extended_mask + image_width + 1; //extended mask pointer + + //extend the mask for the image except borders + for (i=1; i < image_height - 1; ++i) + { + for (j=1; j < image_width - 1; ++j) + { + if ( (*IMP) == NOMASK && (*(IMP + 1) == NOMASK) && (*(IMP - 1) == NOMASK) && + (*(IMP + image_width) == NOMASK) && (*(IMP - image_width) == NOMASK) && + (*(IMP - image_width_minus_one) == NOMASK) && (*(IMP - image_width_plus_one) == NOMASK) && + (*(IMP + image_width_minus_one) == NOMASK) && (*(IMP + image_width_plus_one) == NOMASK) ) + { + *EMP = NOMASK; + } + ++EMP; + ++IMP; + } + EMP += 2; + IMP += 2; + } + + if (params->x_connectivity == 1) + { + //extend the mask for the right border of the image + IMP = input_mask + 2 * image_width - 1; + EMP = extended_mask + 2 * image_width -1; + for (i=1; i < image_height - 1; ++ i) + { + if ( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + image_width) == NOMASK) && (*(IMP - image_width) == NOMASK) && + (*(IMP - image_width - 1) == NOMASK) && (*(IMP - image_width + 1) == NOMASK) && + (*(IMP + image_width - 1) == NOMASK) && (*(IMP - 2 * image_width + 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP += image_width; + IMP += image_width; + } + + //extend the mask for the left border of the image + IMP = input_mask + image_width; + EMP = extended_mask + image_width; + for (i=1; i < image_height - 1; ++i) + { + if ( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + image_width) == NOMASK) && (*(IMP - image_width) == NOMASK) && + (*(IMP - image_width + 1) == NOMASK) && (*(IMP + image_width + 1) == NOMASK) && + (*(IMP + image_width - 1) == NOMASK) && (*(IMP + 2 * image_width - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP += image_width; + IMP += image_width; + } + } + + if (params->y_connectivity == 1) + { + //extend the mask for the top border of the image + IMP = input_mask + 1; + EMP = extended_mask + 1; + for (i=1; i < image_width - 1; ++i) + { + if ( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + image_width) == NOMASK) && (*(IMP + image_width * (image_height - 1)) == NOMASK) && + (*(IMP + image_width + 1) == NOMASK) && (*(IMP + image_width - 1) == NOMASK) && + (*(IMP + image_width * (image_height - 1) - 1) == NOMASK) && (*(IMP + image_width * (image_height - 1) + 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + + //extend the mask for the bottom border of the image + IMP = input_mask + image_width * (image_height - 1) + 1; + EMP = extended_mask + image_width * (image_height - 1) + 1; + for (i=1; i < image_width - 1; ++i) + { + if ( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP - image_width) == NOMASK) && (*(IMP - image_width - 1) == NOMASK) && (*(IMP - image_width + 1) == NOMASK) && + (*(IMP - image_width * (image_height - 1) ) == NOMASK) && + (*(IMP - image_width * (image_height - 1) - 1) == NOMASK) && + (*(IMP - image_width * (image_height - 1) + 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + } +} + +void calculate_reliability(double *wrappedImage, PIXELM *pixel, + int image_width, int image_height, + params_t *params) +{ + int image_width_plus_one = image_width + 1; + int image_width_minus_one = image_width - 1; + PIXELM *pixel_pointer = pixel + image_width_plus_one; + double *WIP = wrappedImage + image_width_plus_one; //WIP is the wrapped image pointer + double H, V, D1, D2; + int i, j; + + for (i = 1; i < image_height -1; ++i) + { + for (j = 1; j < image_width - 1; ++j) + { + if (pixel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WIP - 1) - *WIP) - wrap(*WIP - *(WIP + 1)); + V = wrap(*(WIP - image_width) - *WIP) - wrap(*WIP - *(WIP + image_width)); + D1 = wrap(*(WIP - image_width_plus_one) - *WIP) - wrap(*WIP - *(WIP + image_width_plus_one)); + D2 = wrap(*(WIP - image_width_minus_one) - *WIP) - wrap(*WIP - *(WIP + image_width_minus_one)); + pixel_pointer->reliability = H*H + V*V + D1*D1 + D2*D2; + } + pixel_pointer++; + WIP++; + } + pixel_pointer += 2; + WIP += 2; + } + + if (params->x_connectivity == 1) + { + //calculating the reliability for the left border of the image + PIXELM *pixel_pointer = pixel + image_width; + double *WIP = wrappedImage + image_width; + + for (i = 1; i < image_height - 1; ++i) + { + if (pixel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WIP + image_width - 1) - *WIP) - wrap(*WIP - *(WIP + 1)); + V = wrap(*(WIP - image_width) - *WIP) - wrap(*WIP - *(WIP + image_width)); + D1 = wrap(*(WIP - 1) - *WIP) - wrap(*WIP - *(WIP + image_width_plus_one)); + D2 = wrap(*(WIP - image_width_minus_one) - *WIP) - wrap(*WIP - *(WIP + 2* image_width - 1)); + pixel_pointer->reliability = H*H + V*V + D1*D1 + D2*D2; + } + pixel_pointer += image_width; + WIP += image_width; + } + + //calculating the reliability for the right border of the image + pixel_pointer = pixel + 2 * image_width - 1; + WIP = wrappedImage + 2 * image_width - 1; + + for (i = 1; i < image_height - 1; ++i) + { + if (pixel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WIP - 1) - *WIP) - wrap(*WIP - *(WIP - image_width_minus_one)); + V = wrap(*(WIP - image_width) - *WIP) - wrap(*WIP - *(WIP + image_width)); + D1 = wrap(*(WIP - image_width_plus_one) - *WIP) - wrap(*WIP - *(WIP + 1)); + D2 = wrap(*(WIP - 2 * image_width - 1) - *WIP) - wrap(*WIP - *(WIP + image_width_minus_one)); + pixel_pointer->reliability = H*H + V*V + D1*D1 + D2*D2; + } + pixel_pointer += image_width; + WIP += image_width; + } + } + + if (params->y_connectivity == 1) + { + //calculating the reliability for the top border of the image + PIXELM *pixel_pointer = pixel + 1; + double *WIP = wrappedImage + 1; + + for (i = 1; i < image_width - 1; ++i) + { + if (pixel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WIP - 1) - *WIP) - wrap(*WIP - *(WIP + 1)); + V = wrap(*(WIP + image_width*(image_height - 1)) - *WIP) - wrap(*WIP - *(WIP + image_width)); + D1 = wrap(*(WIP + image_width*(image_height - 1) - 1) - *WIP) - wrap(*WIP - *(WIP + image_width_plus_one)); + D2 = wrap(*(WIP + image_width*(image_height - 1) + 1) - *WIP) - wrap(*WIP - *(WIP + image_width_minus_one)); + pixel_pointer->reliability = H*H + V*V + D1*D1 + D2*D2; + } + pixel_pointer++; + WIP++; + } + + //calculating the reliability for the bottom border of the image + pixel_pointer = pixel + (image_height - 1) * image_width + 1; + WIP = wrappedImage + (image_height - 1) * image_width + 1; + + for (i = 1; i < image_width - 1; ++i) + { + if (pixel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WIP - 1) - *WIP) - wrap(*WIP - *(WIP + 1)); + V = wrap(*(WIP - image_width) - *WIP) - wrap(*WIP - *(WIP -(image_height - 1) * (image_width))); + D1 = wrap(*(WIP - image_width_plus_one) - *WIP) - wrap(*WIP - *(WIP - (image_height - 1) * (image_width) + 1)); + D2 = wrap(*(WIP - image_width_minus_one) - *WIP) - wrap(*WIP - *(WIP - (image_height - 1) * (image_width) - 1)); + pixel_pointer->reliability = H*H + V*V + D1*D1 + D2*D2; + } + pixel_pointer++; + WIP++; + } + } +} + +//calculate the reliability of the horizontal edges of the image +//it is calculated by adding the reliability of pixel and the relibility of +//its right-hand neighbour +//edge is calculated between a pixel and its next neighbour +void horizontalEDGEs(PIXELM *pixel, EDGE *edge, + int image_width, int image_height, + params_t *params) +{ + int i, j; + EDGE *edge_pointer = edge; + PIXELM *pixel_pointer = pixel; + int no_of_edges = params->no_of_edges; + + for (i = 0; i < image_height; i++) + { + for (j = 0; j < image_width - 1; j++) + { + if (pixel_pointer->input_mask == NOMASK && (pixel_pointer + 1)->input_mask == NOMASK) + { + edge_pointer->pointer_1 = pixel_pointer; + edge_pointer->pointer_2 = (pixel_pointer+1); + edge_pointer->reliab = pixel_pointer->reliability + (pixel_pointer + 1)->reliability; + edge_pointer->increment = find_wrap(pixel_pointer->value, (pixel_pointer + 1)->value); + edge_pointer++; + no_of_edges++; + } + pixel_pointer++; + } + pixel_pointer++; + } + //construct edges at the right border of the image + if (params->x_connectivity == 1) + { + pixel_pointer = pixel + image_width - 1; + for (i = 0; i < image_height; i++) + { + if (pixel_pointer->input_mask == NOMASK && (pixel_pointer - image_width + 1)->input_mask == NOMASK) + { + edge_pointer->pointer_1 = pixel_pointer; + edge_pointer->pointer_2 = (pixel_pointer - image_width + 1); + edge_pointer->reliab = pixel_pointer->reliability + (pixel_pointer - image_width + 1)->reliability; + edge_pointer->increment = find_wrap(pixel_pointer->value, (pixel_pointer - image_width + 1)->value); + edge_pointer++; + no_of_edges++; + } + pixel_pointer+=image_width; + } + } + params->no_of_edges = no_of_edges; +} + +//calculate the reliability of the vertical edges of the image +//it is calculated by adding the reliability of pixel and the relibility of +//its lower neighbour in the image. +void verticalEDGEs(PIXELM *pixel, EDGE *edge, + int image_width, int image_height, + params_t *params) +{ + int i, j; + int no_of_edges = params->no_of_edges; + PIXELM *pixel_pointer = pixel; + EDGE *edge_pointer = edge + no_of_edges; + + for (i=0; i < image_height - 1; i++) + { + for (j=0; j < image_width; j++) + { + if (pixel_pointer->input_mask == NOMASK && (pixel_pointer + image_width)->input_mask == NOMASK) + { + edge_pointer->pointer_1 = pixel_pointer; + edge_pointer->pointer_2 = (pixel_pointer + image_width); + edge_pointer->reliab = pixel_pointer->reliability + (pixel_pointer + image_width)->reliability; + edge_pointer->increment = find_wrap(pixel_pointer->value, (pixel_pointer + image_width)->value); + edge_pointer++; + no_of_edges++; + } + pixel_pointer++; + } //j loop + } // i loop + + //construct edges that connect at the bottom border of the image + if (params->y_connectivity == 1) + { + pixel_pointer = pixel + image_width *(image_height - 1); + for (i = 0; i < image_width; i++) + { + if (pixel_pointer->input_mask == NOMASK && (pixel_pointer - image_width *(image_height - 1))->input_mask == NOMASK) + { + edge_pointer->pointer_1 = pixel_pointer; + edge_pointer->pointer_2 = (pixel_pointer - image_width *(image_height - 1)); + edge_pointer->reliab = pixel_pointer->reliability + (pixel_pointer - image_width *(image_height - 1))->reliability; + edge_pointer->increment = find_wrap(pixel_pointer->value, (pixel_pointer - image_width *(image_height - 1))->value); + edge_pointer++; + no_of_edges++; + } + pixel_pointer++; + } + } + params->no_of_edges = no_of_edges; +} + +//gather the pixels of the image into groups +void gatherPIXELs(EDGE *edge, params_t *params) +{ + int k; + PIXELM *PIXEL1; + PIXELM *PIXEL2; + PIXELM *group1; + PIXELM *group2; + EDGE *pointer_edge = edge; + int incremento; + + for (k = 0; k < params->no_of_edges; k++) + { + PIXEL1 = pointer_edge->pointer_1; + PIXEL2 = pointer_edge->pointer_2; + + //PIXELM 1 and PIXELM 2 belong to different groups + //initially each pixel is a group by it self and one pixel can construct a group + //no else or else if to this if + if (PIXEL2->head != PIXEL1->head) + { + //PIXELM 2 is alone in its group + //merge this pixel with PIXELM 1 group and find the number of 2 pi to add + //to or subtract to unwrap it + if ((PIXEL2->next == NULL) && (PIXEL2->head == PIXEL2)) + { + PIXEL1->head->last->next = PIXEL2; + PIXEL1->head->last = PIXEL2; + (PIXEL1->head->number_of_pixels_in_group)++; + PIXEL2->head=PIXEL1->head; + PIXEL2->increment = PIXEL1->increment-pointer_edge->increment; + } + + //PIXELM 1 is alone in its group + //merge this pixel with PIXELM 2 group and find the number of 2 pi to add + //to or subtract to unwrap it + else if ((PIXEL1->next == NULL) && (PIXEL1->head == PIXEL1)) + { + PIXEL2->head->last->next = PIXEL1; + PIXEL2->head->last = PIXEL1; + (PIXEL2->head->number_of_pixels_in_group)++; + PIXEL1->head = PIXEL2->head; + PIXEL1->increment = PIXEL2->increment+pointer_edge->increment; + } + + //PIXELM 1 and PIXELM 2 both have groups + else + { + group1 = PIXEL1->head; + group2 = PIXEL2->head; + //if the no. of pixels in PIXELM 1 group is larger than the + //no. of pixels in PIXELM 2 group. Merge PIXELM 2 group to + //PIXELM 1 group and find the number of wraps between PIXELM 2 + //group and PIXELM 1 group to unwrap PIXELM 2 group with respect + //to PIXELM 1 group. the no. of wraps will be added to PIXELM 2 + //group in the future + if (group1->number_of_pixels_in_group > group2->number_of_pixels_in_group) + { + //merge PIXELM 2 with PIXELM 1 group + group1->last->next = group2; + group1->last = group2->last; + group1->number_of_pixels_in_group = group1->number_of_pixels_in_group + group2->number_of_pixels_in_group; + incremento = PIXEL1->increment-pointer_edge->increment - PIXEL2->increment; + //merge the other pixels in PIXELM 2 group to PIXELM 1 group + while (group2 != NULL) + { + group2->head = group1; + group2->increment += incremento; + group2 = group2->next; + } + } + + //if the no. of pixels in PIXELM 2 group is larger than the + //no. of pixels in PIXELM 1 group. Merge PIXELM 1 group to + //PIXELM 2 group and find the number of wraps between PIXELM 2 + //group and PIXELM 1 group to unwrap PIXELM 1 group with respect + //to PIXELM 2 group. the no. of wraps will be added to PIXELM 1 + //group in the future + else + { + //merge PIXELM 1 with PIXELM 2 group + group2->last->next = group1; + group2->last = group1->last; + group2->number_of_pixels_in_group = group2->number_of_pixels_in_group + group1->number_of_pixels_in_group; + incremento = PIXEL2->increment + pointer_edge->increment - PIXEL1->increment; + //merge the other pixels in PIXELM 2 group to PIXELM 1 group + while (group1 != NULL) + { + group1->head = group2; + group1->increment += incremento; + group1 = group1->next; + } // while + + } // else + } //else + } //if + pointer_edge++; + } +} + +//unwrap the image +void unwrapImage(PIXELM *pixel, int image_width, int image_height) +{ + int i; + int image_size = image_width * image_height; + PIXELM *pixel_pointer=pixel; + + for (i = 0; i < image_size; i++) + { + pixel_pointer->value += TWOPI * (double)(pixel_pointer->increment); + pixel_pointer++; + } +} + +//set the masked pixels (mask = 0) to the minimum of the unwrapper phase +void maskImage(PIXELM *pixel, unsigned char *input_mask, int image_width, int image_height) +{ + int image_width_plus_one = image_width + 1; + int image_height_plus_one = image_height + 1; + int image_width_minus_one = image_width - 1; + int image_height_minus_one = image_height - 1; + + PIXELM *pointer_pixel = pixel; + unsigned char *IMP = input_mask; //input mask pointer + double min=99999999; + int i; + int image_size = image_width * image_height; + + //find the minimum of the unwrapped phase + for (i = 0; i < image_size; i++) + { + if ((pointer_pixel->value < min) && (*IMP == NOMASK)) + min = pointer_pixel->value; + + pointer_pixel++; + IMP++; + } + + pointer_pixel = pixel; + IMP = input_mask; + + //set the masked pixels to minimum + for (i = 0; i < image_size; i++) + { + if ((*IMP) == MASK) + { + pointer_pixel->value = min; + } + pointer_pixel++; + IMP++; + } +} + +//the input to this unwrapper is an array that contains the wrapped +//phase map. copy the image on the buffer passed to this unwrapper to +//over-write the unwrapped phase map on the buffer of the wrapped +//phase map. +void returnImage(PIXELM *pixel, double *unwrapped_image, int image_width, int image_height) +{ + int i; + int image_size = image_width * image_height; + double *unwrapped_image_pointer = unwrapped_image; + PIXELM *pixel_pointer = pixel; + + for (i=0; i < image_size; i++) + { + *unwrapped_image_pointer = pixel_pointer->value; + pixel_pointer++; + unwrapped_image_pointer++; + } +} + +//the main function of the unwrapper +void +unwrap2D(double* wrapped_image, double* UnwrappedImage, unsigned char* input_mask, + int image_width, int image_height, + int wrap_around_x, int wrap_around_y) +{ + params_t params = {TWOPI, wrap_around_x, wrap_around_y, 0}; + unsigned char *extended_mask; + PIXELM *pixel; + EDGE *edge; + int image_size = image_height * image_width; + int No_of_Edges_initially = 2 * image_width * image_height; + + extended_mask = (unsigned char *) calloc(image_size, sizeof(unsigned char)); + pixel = (PIXELM *) calloc(image_size, sizeof(PIXELM)); + edge = (EDGE *) calloc(No_of_Edges_initially, sizeof(EDGE)); + + extend_mask(input_mask, extended_mask, image_width, image_height, ¶ms); + initialisePIXELs(wrapped_image, input_mask, extended_mask, pixel, image_width, image_height); + calculate_reliability(wrapped_image, pixel, image_width, image_height, ¶ms); + horizontalEDGEs(pixel, edge, image_width, image_height, ¶ms); + verticalEDGEs(pixel, edge, image_width, image_height, ¶ms); + + //sort the EDGEs depending on their reiability. The PIXELs with higher + //relibility (small value) first + quicker_sort(edge, edge + params.no_of_edges - 1); + + //gather PIXELs into groups + gatherPIXELs(edge, ¶ms); + + unwrapImage(pixel, image_width, image_height); + maskImage(pixel, input_mask, image_width, image_height); + + //copy the image from PIXELM structure to the unwrapped phase array + //passed to this function + //TODO: replace by (cython?) function to directly write into numpy array ? + returnImage(pixel, UnwrappedImage, image_width, image_height); + + free(edge); + free(pixel); + free(extended_mask); +} diff --git a/skimage/restoration/unwrap_3d_ljmu.c b/skimage/restoration/unwrap_3d_ljmu.c new file mode 100644 index 00000000..88012f18 --- /dev/null +++ b/skimage/restoration/unwrap_3d_ljmu.c @@ -0,0 +1,1058 @@ +// 3D phase unwrapping, modified for inclusion in scipy by Gregor Thalhammer +// Original file name: Hussein_3D_unwrapper_with_mask_and_wrap_around_option.c + +//This program was written by Hussein Abdul-Rahman and Munther Gdeisat to program the three-dimensional phase unwrapper +//entitled "Fast three-dimensional phase-unwrapping algorithm based on sorting by +//reliability following a noncontinuous path" +//by Hussein Abdul-Rahman, Munther A. Gdeisat, David R. Burton, and Michael J. Lalor, +//published in the Proceedings of SPIE - +//The International Society for Optical Engineering, Vol. 5856, No. 1, 2005, pp. 32-40 +//This program was written by Munther Gdeisat, Liverpool John Moores University, United Kingdom. +//Date 31st August 2007 +//The wrapped phase volume is assumed to be of floating point data type. The resultant unwrapped phase volume is also of floating point type. +//Read the data from the file frame by frame +//The mask is of byte data type. +//When the mask is 255 this means that the voxel is valid +//When the mask is 0 this means that the voxel is invalid (noisy or corrupted voxel) +//This program takes into consideration the image wrap around problem encountered in MRI imaging. + +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif + +#define PI M_PI +#define TWOPI (2 * M_PI) + +#define NOMASK 0 +#define MASK 1 + +typedef struct +{ + double mod; + int x_connectivity; + int y_connectivity; + int z_connectivity; + int no_of_edges; +} params_t; + +//VOXELM information +struct VOXELM +{ + int increment; //No. of 2*pi to add to the voxel to unwrap it + int number_of_voxels_in_group;//No. of voxel in the voxel group + double value; //value of the voxel + double reliability; + unsigned char input_mask; //MASK voxel is masked. NOMASK voxel is not masked + unsigned char extended_mask; //MASK voxel is masked. NOMASK voxel is not masked + int group; //group No. + int new_group; + struct VOXELM *head; //pointer to the first voxel in the group in the linked list + struct VOXELM *last; //pointer to the last voxel in the group + struct VOXELM *next; //pointer to the next voxel in the group +}; + +typedef struct VOXELM VOXELM; + +//the EDGE is the line that connects two voxels. +//if we have S voxels, then we have S horizontal edges and S vertical edges +struct EDGE +{ + double reliab; //reliabilty of the edge and it depends on the two voxels + VOXELM *pointer_1; //pointer to the first voxel + VOXELM *pointer_2; //pointer to the second voxel + int increment; //No. of 2*pi to add to one of the + //voxels to unwrap it with respect to + //the second +}; + +typedef struct EDGE EDGE; + +//---------------start quicker_sort algorithm -------------------------------- +#define swap(x,y) {EDGE t; t=x; x=y; y=t;} +#define order(x,y) if (x.reliab > y.reliab) swap(x,y) +#define o2(x,y) order(x,y) +#define o3(x,y,z) o2(x,y); o2(x,z); o2(y,z) + +typedef enum {yes, no} yes_no; + +yes_no find_pivot(EDGE *left, EDGE *right, double *pivot_ptr) +{ + EDGE a, b, c, *p; + + a = *left; + b = *(left + (right - left) /2 ); + c = *right; + o3(a,b,c); + + if (a.reliab < b.reliab) + { + *pivot_ptr = b.reliab; + return yes; + } + + if (b.reliab < c.reliab) + { + *pivot_ptr = c.reliab; + return yes; + } + + for (p = left + 1; p <= right; ++p) + { + if (p->reliab != left->reliab) + { + *pivot_ptr = (p->reliab < left->reliab) ? left->reliab : p->reliab; + return yes; + } + } + return no; +} + +EDGE *partition(EDGE *left, EDGE *right, double pivot) +{ + while (left <= right) + { + while (left->reliab < pivot) + ++left; + while (right->reliab >= pivot) + --right; + if (left < right) + { + swap (*left, *right); + ++left; + --right; + } + } + return left; +} + +void quicker_sort(EDGE *left, EDGE *right) +{ + EDGE *p; + double pivot; + + if (find_pivot(left, right, &pivot) == yes) + { + p = partition(left, right, pivot); + quicker_sort(left, p - 1); + quicker_sort(p, right); + } +} + +//--------------end quicker_sort algorithm ----------------------------------- + +//--------------------start initialize voxels ---------------------------------- +//initiale voxels. See the explanation of the voxel class above. +//initially every voxel is assumed to belong to a group consisting of only itself +void initialiseVOXELs(double *WrappedVolume, unsigned char *input_mask, unsigned char *extended_mask, VOXELM *voxel, int volume_width, int volume_height, int volume_depth) +{ + VOXELM *voxel_pointer = voxel; + double *wrapped_volume_pointer = WrappedVolume; + unsigned char *input_mask_pointer = input_mask; + unsigned char *extended_mask_pointer = extended_mask; + int n, i, j; + + for (n=0; n < volume_depth; n++) + { + for (i=0; i < volume_height; i++) + { + for (j=0; j < volume_width; j++) + { + voxel_pointer->increment = 0; + voxel_pointer->number_of_voxels_in_group = 1; + voxel_pointer->value = *wrapped_volume_pointer; + voxel_pointer->reliability = 9999999 + rand(); + voxel_pointer->input_mask = *input_mask_pointer; + voxel_pointer->extended_mask = *extended_mask_pointer; + voxel_pointer->head = voxel_pointer; + voxel_pointer->last = voxel_pointer; + voxel_pointer->next = NULL; + voxel_pointer->new_group = 0; + voxel_pointer->group = -1; + voxel_pointer++; + wrapped_volume_pointer++; + input_mask_pointer++; + extended_mask_pointer++; + } + } + } +} +//-------------------end initialize voxels ----------- + +//gamma function in the paper +double wrap(double voxel_value) +{ + double wrapped_voxel_value; + if (voxel_value > PI) wrapped_voxel_value = voxel_value - TWOPI; + else if (voxel_value < -PI) wrapped_voxel_value = voxel_value + TWOPI; + else wrapped_voxel_value = voxel_value; + return wrapped_voxel_value; +} + +// voxelL_value is the left voxel, voxelR_value is the right voxel +int find_wrap(double voxelL_value, double voxelR_value) +{ + double difference; + int wrap_value; + difference = voxelL_value - voxelR_value; + + if (difference > PI) wrap_value = -1; + else if (difference < -PI) wrap_value = 1; + else wrap_value = 0; + + return wrap_value; +} + +void extend_mask(unsigned char *input_mask, unsigned char *extended_mask, int volume_width, int volume_height, int volume_depth, params_t *params) +{ + int n, i, j; + int vw = volume_width, vh = volume_height, vd = volume_depth; + int fs = volume_width * volume_height; //frame size + int frame_size = volume_width * volume_height; + int volume_size = volume_width * volume_height * volume_depth; //volume size + int vs = volume_size; + unsigned char *IMP = input_mask + frame_size + volume_width + 1; //input mask pointer + unsigned char *EMP = extended_mask + frame_size + volume_width + 1; //extended mask pointer + + //extend the mask for the volume except borders + for (n=1; n < volume_depth - 1; n++) + { + for (i=1; i < volume_height - 1; i++) + { + for (j=1; j < volume_width - 1; j++) + { + if( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + vw) == NOMASK) && (*(IMP + vw - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP - vw - 1) == NOMASK) && (*(IMP - vw + 1) == NOMASK) && + (*(IMP + fs) == NOMASK) && (*(IMP + fs - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP + fs - vw) == NOMASK) && (*(IMP + fs - vw - 1) == NOMASK) && (*(IMP + fs - vw + 1) == NOMASK) && + (*(IMP + fs + vw) == NOMASK) && (*(IMP + fs + vw - 1) == NOMASK) && (*(IMP + fs + vw + 1) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP - fs - 1) == NOMASK) && (*(IMP - fs + 1) == NOMASK) && + (*(IMP - fs - vw) == NOMASK) && (*(IMP - fs - vw - 1) == NOMASK) && (*(IMP - fs - vw + 1) == NOMASK) && + (*(IMP - fs + vw) == NOMASK) && (*(IMP - fs + vw - 1) == NOMASK) && (*(IMP - fs + vw + 1) == NOMASK)) + { + *EMP = NOMASK; + } + ++EMP; + ++IMP; + } + EMP += 2; + IMP += 2; + } + EMP += 2 * volume_width; + IMP += 2 * volume_width; + } + + if (params->x_connectivity == 1) + { + //extend the mask to the front side of the phase volume + IMP = input_mask + frame_size + volume_width; //input mask pointer + EMP = extended_mask + frame_size + volume_width; //extended mask pointer + for (n=1; n < volume_depth - 1; n++) + { + for (i=1; i < volume_height - 1; i++) + { + if( (*IMP) == NOMASK && (*(IMP + vw - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP + vw) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP + fs) == NOMASK) && + (*(IMP - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP - vw + 1) == NOMASK) && (*(IMP + 2 * vw - 1) == NOMASK) && + (*(IMP - fs - 1) == NOMASK) && (*(IMP + fs + vw + 1) == NOMASK) && + (*(IMP - fs - vw) == NOMASK) && (*(IMP + fs + vw) == NOMASK) && + (*(IMP - fs - vw + 1) == NOMASK) && (*(IMP + fs + 2 * vw - 1) == NOMASK) && + (*(IMP - fs + vw - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP - fs + 1) == NOMASK) && (*(IMP + fs + vw - 1) == NOMASK) && + (*(IMP - fs + 2 * vw - 1) == NOMASK) && (*(IMP + fs - vw + 1) == NOMASK) && + (*(IMP - fs + vw) == NOMASK) && (*(IMP + fs - vw) == NOMASK) && + (*(IMP - fs + vw + 1) == NOMASK) && (*(IMP + fs - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP += vw; + IMP += vw; + } + EMP += 2 * vw; + IMP += 2 *vw; + } + + //extend the mask to the rear side of the phase volume + IMP = input_mask + frame_size + 2 * volume_width - 1; //input mask pointer + EMP = extended_mask + frame_size + 2 * volume_width - 1; //extended mask pointer + for (n=1; n < volume_depth - 1; n++) + { + for (i=1; i < volume_height - 1; i++) + { + if( (*IMP) == NOMASK && (*(IMP - vw + 1) == NOMASK) && (*(IMP - 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP + vw) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP + fs) == NOMASK) && + (*(IMP - vw - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + vw - 1) == NOMASK) && (*(IMP - 2 * vw + 1) == NOMASK) && + (*(IMP - fs - vw - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP - fs - 2 * vw + 1) == NOMASK) && (*(IMP + fs + vw - 1) == NOMASK) && + (*(IMP - fs - 1) == NOMASK) && (*(IMP + fs - vw + 1) == NOMASK) && + (*(IMP - fs - vw + 1) == NOMASK) && (*(IMP + fs - 1) == NOMASK) && + (*(IMP - fs - vw) == NOMASK) && (*(IMP + fs + vw) == NOMASK) && + (*(IMP - fs + vw - 1) == NOMASK) && (*(IMP + fs - 2 * vw + 1) == NOMASK) && + (*(IMP - fs + vw) == NOMASK) && (*(IMP + fs - vw) == NOMASK) && + (*(IMP - fs + 1) == NOMASK) && (*(IMP + fs - vw - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP += vw; + IMP += vw; + } + EMP += 2 * vw; + IMP += 2 *vw; + } + } + + if (params->y_connectivity == 1) + { + //extend the mask to the left side of the phase volume + IMP = input_mask + frame_size + 1; + EMP = extended_mask + frame_size + 1; + for (n=1; n < volume_depth - 1; n++) + { + for (j=1; j < volume_width - 1; j++) + { + if( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + fs - vw) == NOMASK) && (*(IMP + vw) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP + fs) == NOMASK) && + (*(IMP + fs - vw - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP + fs - vw + 1) == NOMASK) && (*(IMP + vw - 1) == NOMASK) && + (*(IMP - vw - 1) == NOMASK) && (*(IMP + fs + vw + 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP + fs + vw) == NOMASK) && + (*(IMP - vw + 1) == NOMASK) && (*(IMP + fs + vw - 1) == NOMASK) && + (*(IMP - fs - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP - fs + 1) == NOMASK) && (*(IMP + fs - 1) == NOMASK) && + (*(IMP - fs + vw - 1) == NOMASK) && (*(IMP + 2 * fs - vw + 1) == NOMASK) && + (*(IMP - fs + vw) == NOMASK) && (*(IMP + 2 * fs - vw) == NOMASK) && + (*(IMP - fs + vw + 1) == NOMASK) && (*(IMP + 2 * fs - vw - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + EMP += fs - vw + 2; + IMP += fs - vw + 2; + } + + //extend the mask to the right side of the phase volume + IMP = input_mask + 2 * frame_size - volume_width + 1; + EMP = extended_mask + 2 * frame_size - volume_width + 1; + for (n=1; n < volume_depth - 1; n++) + { + for (j=1; j < volume_width - 1; j++) + { + if( (*IMP) == NOMASK && (*(IMP + 1) == NOMASK) && (*(IMP - 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP - fs + vw) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP + fs) == NOMASK) && + (*(IMP - vw - 1) == NOMASK) && (*(IMP - fs + vw + 1) == NOMASK) && + (*(IMP - vw + 1) == NOMASK) && (*(IMP - fs + vw - 1) == NOMASK) && + (*(IMP - fs - vw - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP - fs - vw + 1) == NOMASK) && (*(IMP + vw - 1) == NOMASK) && + (*(IMP - fs - vw) == NOMASK) && (*(IMP + vw) == NOMASK) && + (*(IMP - fs - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP - fs + 1) == NOMASK) && (*(IMP + fs - 1) == NOMASK) && + (*(IMP - 2 * fs + vw - 1) == NOMASK) && (*(IMP + fs - vw + 1) == NOMASK) && + (*(IMP - 2 * fs + vw) == NOMASK) && (*(IMP + fs - vw) == NOMASK) && + (*(IMP - 2 * fs + vw + 1) == NOMASK) && (*(IMP + fs - vw - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + EMP += fs - vw + 2; + IMP += fs - vw + 2; + } + } + + if (params->z_connectivity == 1) + { + //extend the mask to the bottom side of the phase volume + IMP = input_mask + volume_width + 1; + EMP = extended_mask + volume_width + 1; + for (i=1; i < volume_height - 1; ++i) + { + for (j=1; j < volume_width - 1; ++j) + { + if( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP + vw) == NOMASK) && + (*(IMP + fs) == NOMASK) && (*(IMP + vs - fs) == NOMASK) && + (*(IMP - vw - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP - vw + 1) == NOMASK) && (*(IMP + vw - 1) == NOMASK) && + (*(IMP + vs - fs - vw - 1) == NOMASK) && (*(IMP + fs + vw + 1) == NOMASK) && + (*(IMP + vs - fs - vw) == NOMASK) && (*(IMP + fs + vw) == NOMASK) && + (*(IMP + vs - fs - vw + 1) == NOMASK) && (*(IMP + fs + vw - 1) == NOMASK) && + (*(IMP + vs - fs - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP + vs - fs + 1) == NOMASK) && (*(IMP + fs - 1) == NOMASK) && + (*(IMP + vs - fs + vw - 1) == NOMASK) && (*(IMP + fs - vw + 1) == NOMASK) && + (*(IMP + vs - fs + vw) == NOMASK) && (*(IMP + fs - vw) == NOMASK) && + (*(IMP + vs - fs + vw + 1) == NOMASK) && (*(IMP + fs - vw - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + EMP += 2; + IMP += 2; + } + + //extend the mask to the top side of the phase volume + IMP = input_mask + volume_size - frame_size + volume_width + 1; + EMP = extended_mask + volume_size - frame_size + volume_width + 1; + for (i=1; i < volume_height - 1; ++i) + { + for (j=1; j < volume_width - 1; ++j) + { + if( (*IMP) == NOMASK && (*(IMP + 1) == NOMASK) && (*(IMP - 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP - fs + vw) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP - vs + fs) == NOMASK) && + (*(IMP - vw - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP - vw + 1) == NOMASK) && (*(IMP + vw - 1) == NOMASK) && + (*(IMP - fs - vw - 1) == NOMASK) && (*(IMP - vs + fs + vw + 1) == NOMASK) && + (*(IMP - fs - vw + 1) == NOMASK) && (*(IMP - vs + fs + vw - 1) == NOMASK) && + (*(IMP - fs - vw) == NOMASK) && (*(IMP - vs + fs + vw) == NOMASK) && + (*(IMP - fs - 1) == NOMASK) && (*(IMP - vs + fs + 1) == NOMASK) && + (*(IMP - fs + 1) == NOMASK) && (*(IMP - vs + fs - 1) == NOMASK) && + (*(IMP - fs + vw - 1) == NOMASK) && (*(IMP - vs + fs - vw + 1) == NOMASK) && + (*(IMP - fs + vw) == NOMASK) && (*(IMP - vs + fs - vw) == NOMASK) && + (*(IMP - fs + vw + 1) == NOMASK) && (*(IMP - vs + fs - vw - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + EMP += 2; + IMP += 2; + } + } +} + +void calculate_reliability(double *wrappedVolume, VOXELM *voxel, int volume_width, int volume_height, int volume_depth, params_t *params) +{ + int frame_size = volume_width * volume_height; + int volume_size = volume_width * volume_height * volume_depth; + VOXELM *voxel_pointer; + double H, V, N, D1, D2, D3, D4, D5, D6, D7, D8, D9, D10; + double *WVP; + int n, i, j; + + WVP = wrappedVolume + frame_size + volume_width + 1; + voxel_pointer = voxel + frame_size + volume_width + 1; + for (n=1; n < volume_depth - 1; n++) + { + for (i=1; i < volume_height - 1; i++) + { + for (j=1; j < volume_width - 1; j++) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP - 1) - *WVP) - wrap(*WVP - *(WVP + 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP + frame_size)); + D1 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1)); + D2 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + volume_width - 1)); + D3 = wrap(*(WVP - frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width + 1)); + D4 = wrap(*(WVP - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width)); + D5 = wrap(*(WVP - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width - 1)); + D6 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D7 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + D8 = wrap(*(WVP - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width + 1)); + D9 = wrap(*(WVP - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width)); + D10 = wrap(*(WVP - frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer++; + WVP++; + } + voxel_pointer += 2; + WVP += 2; + } + voxel_pointer += 2 * volume_width; + WVP += 2 * volume_width; + } + + if (params->x_connectivity == 1) + { + //calculating reliability for the front side of the phase volume...add volume_width + WVP = wrappedVolume + frame_size + volume_width; + voxel_pointer = voxel + frame_size + volume_width; + for (n=1; n < volume_depth - 1; ++n) + { + for (i=1; i < volume_height - 1; ++i) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP + frame_size)); + D1 = wrap(*(WVP - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1)); + D2 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + 2 * volume_width - 1)); + D3 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width + 1)); + D4 = wrap(*(WVP - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width)); + D5 = wrap(*(WVP - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 2 * volume_width - 1)); + D6 = wrap(*(WVP - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D7 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width - 1)); + D8 = wrap(*(WVP - frame_size + 2 * volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width + 1)); + D9 = wrap(*(WVP - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width)); + D10 = wrap(*(WVP - frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer += volume_width; + WVP += volume_width; + } + voxel_pointer += 2 * volume_width; + WVP += 2 * volume_width; + } + + //calculating reliability for the rear side of the phase volume..... subtract volume_width + WVP = wrappedVolume + frame_size + 2 * volume_width - 1; + voxel_pointer = voxel + frame_size + 2 * volume_width - 1; + for (n=1; n < volume_depth - 1; ++n) + { + for (i=1; i < volume_height - 1; ++i) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP - 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP + frame_size)); + D1 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + 1)); + D2 = wrap(*(WVP + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP - 2 * volume_width + 1)); + D3 = wrap(*(WVP - frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D4 = wrap(*(WVP - frame_size - 2 * volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width - 1)); + D5 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width + 1)); + D6 = wrap(*(WVP - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + D7 = wrap(*(WVP - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width)); + D8 = wrap(*(WVP - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 2 * volume_width + 1)); + D9 = wrap(*(WVP - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width)); + D10 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer += volume_width; + WVP += volume_width; + } + voxel_pointer += 2 * volume_width; + WVP += 2 * volume_width; + } + } + + if (params->y_connectivity == 1) + { + //calculating reliability for the left side of the phase volume...add frame_size + WVP = wrappedVolume + frame_size + 1; + voxel_pointer = voxel + frame_size + 1; + for (n=1; n < volume_depth - 1; ++n) + { + for (j=1; j < volume_width - 1; ++j) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP - 1) - *WVP) - wrap(*WVP - *(WVP + 1)); + V = wrap(*(WVP + frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP + frame_size)); + D1 = wrap(*(WVP + frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1)); + D2 = wrap(*(WVP + frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + volume_width - 1)); + D3 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width + 1)); + D4 = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width)); + D5 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width - 1)); + D6 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D7 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + D8 = wrap(*(WVP - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + 2 * frame_size - volume_width + 1)); + D9 = wrap(*(WVP - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + 2 * frame_size - volume_width)); + D10 = wrap(*(WVP - frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + 2 * frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer++; + WVP++; + } + voxel_pointer += frame_size - volume_width + 2; + WVP += frame_size - volume_width + 2; + } + + //calculating reliability for the right side of the phase volume...subtract frame_size + WVP = wrappedVolume + 2 * frame_size - volume_width + 1; + voxel_pointer = voxel + 2 * frame_size - volume_width + 1; + for (n=1; n < volume_depth - 1; ++n) + { + for (j=1; j < volume_width - 1; ++j) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP + 1) - *WVP) - wrap(*WVP - *(WVP - 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP - frame_size + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP + frame_size)); + D1 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP - frame_size + volume_width + 1)); + D2 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP - frame_size + volume_width - 1)); + D3 = wrap(*(WVP - frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1) ); + D4 = wrap(*(WVP - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + volume_width - 1)); + D5 = wrap(*(WVP - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + D6 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D7 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + D8 = wrap(*(WVP - 2 * frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width + 1)); + D9 = wrap(*(WVP - 2 * frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width)); + D10 = wrap(*(WVP - 2 * frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer++; + WVP++; + } + voxel_pointer += frame_size - volume_width + 2; + WVP += frame_size - volume_width + 2; + } + } + + if (params->z_connectivity == 1) + { + //calculating reliability for the bottom side of the phase volume...add volume_size + WVP = wrappedVolume + volume_width + 1; + voxel_pointer = voxel + volume_width + 1; + for (i=1; i < volume_height - 1; ++i) + { + for (j=1; j < volume_width - 1; ++j) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP - 1) - *WVP) - wrap(*WVP - *(WVP + 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP + frame_size) - *WVP) - wrap(*WVP - *(WVP + volume_size - frame_size)); + D1 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1)); + D2 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + volume_width - 1)); + D3 = wrap(*(WVP + volume_size - frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width + 1)); + D4 = wrap(*(WVP + volume_size - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width)); + D5 = wrap(*(WVP + volume_size - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width - 1)); + D6 = wrap(*(WVP + volume_size - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D7 = wrap(*(WVP + volume_size - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + D8 = wrap(*(WVP + volume_size - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width + 1)); + D9 = wrap(*(WVP + volume_size - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width)); + D10 = wrap(*(WVP + volume_size - frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer++; + WVP++; + } + voxel_pointer += 2; + WVP += 2; + } + + //calculating reliability for the top side of the phase volume...subtract volume_size + WVP = wrappedVolume + volume_size - frame_size + volume_width + 1; + voxel_pointer = voxel + volume_size - frame_size + volume_width + 1; + for (i=1; i < volume_height - 1; ++i) + { + for (j=1; j < volume_width - 1; ++j) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP + 1) - *WVP) - wrap(*WVP - *(WVP - 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size)); + D1 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1)); + D2 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + volume_width - 1)); + D3 = wrap(*(WVP - frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size + volume_width + 1)); + D4 = wrap(*(WVP - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size + volume_width - 1)); + D5 = wrap(*(WVP - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size + volume_width)); + D6 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size + 1)); + D7 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size - 1)); + D8 = wrap(*(WVP - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size - volume_width + 1)); + D9 = wrap(*(WVP - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size - volume_width)); + D10 = wrap(*(WVP - frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer++; + WVP++; + } + voxel_pointer += 2; + WVP += 2; + } + } +} + +//calculate the reliability of the horizontal edges of the volume. it +//is calculated by adding the reliability of voxel and the relibility +//of its right neighbour. edge is calculated between a voxel and its +//next neighbour +void horizontalEDGEs(VOXELM *voxel, EDGE *edge, int volume_width, int volume_height, int volume_depth, params_t *params) +{ + int n, i, j; + EDGE *edge_pointer = edge; + VOXELM *voxel_pointer = voxel; + int no_of_edges = params->no_of_edges; + + for (n=0; n < volume_depth; n++) + { + for (i = 0; i < volume_height; i++) + { + for (j = 0; j < volume_width - 1; j++) + { + if (voxel_pointer->input_mask == NOMASK && (voxel_pointer + 1)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer+1); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer + 1)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer + 1)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer++; + } + voxel_pointer++; + } + } + if (params->x_connectivity == 1) + { + voxel_pointer = voxel + volume_width - 1; + for (n=0; n < volume_depth; n++) + { + for (i = 0; i < volume_height; i++) + { + if (voxel_pointer->input_mask == NOMASK && (voxel_pointer - volume_width + 1)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer - volume_width + 1); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer - volume_width + 1)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer - volume_width + 1)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer += volume_width; + } + } + } + params->no_of_edges = no_of_edges; +} + +void verticalEDGEs(VOXELM *voxel, EDGE *edge, int volume_width, int volume_height, int volume_depth, params_t *params) +{ + int n, i, j; + int no_of_edges = params->no_of_edges; + VOXELM *voxel_pointer = voxel; + EDGE *edge_pointer = edge + no_of_edges; + int frame_size = volume_width * volume_height; + int next_voxel = frame_size - volume_width; + + for (n=0; n < volume_depth; n++) + { + for (i=0; iinput_mask == NOMASK && (voxel_pointer + volume_width)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer + volume_width); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer + volume_width)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer + volume_width)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer++; + } + } + voxel_pointer += volume_width; + } + + if (params->y_connectivity == 1) + { + voxel_pointer = voxel + frame_size - volume_width; + for (n=0; n < volume_depth; n++) + { + for (i = 0; i < volume_width; i++) + { + if (voxel_pointer->input_mask == NOMASK && (voxel_pointer - next_voxel)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer - next_voxel); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer - next_voxel)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer - next_voxel)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer++; + } + voxel_pointer += next_voxel + 1; + } + } + params->no_of_edges = no_of_edges; +} + +void normalEDGEs(VOXELM *voxel, EDGE *edge, int volume_width, int volume_height, int volume_depth, params_t *params) +{ + int n, i, j; + int no_of_edges = params->no_of_edges; + int frame_size = volume_width * volume_height; + int volume_size = volume_width * volume_height * volume_depth; + VOXELM *voxel_pointer = voxel; + EDGE *edge_pointer = edge + no_of_edges; + int next_voxel = volume_size - frame_size; + + for (n=0; n < volume_depth - 1; n++) + { + for (i=0; iinput_mask == NOMASK && (voxel_pointer + frame_size)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer + frame_size); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer + frame_size)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer + frame_size)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer++; + } + } + } + + + if (params->z_connectivity == 1) + { + voxel_pointer = voxel + next_voxel; + for (i=0; i < volume_height; i++) + { + for (j = 0; j < volume_width; j++) + { + if (voxel_pointer->input_mask == NOMASK && (voxel_pointer - next_voxel)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer - next_voxel); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer - next_voxel)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer - next_voxel)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer++; + } + } + } + params->no_of_edges = no_of_edges; +} + +//gather the voxels of the volume into groups +void gatherVOXELs(EDGE *edge, params_t *params) +{ + int k; + VOXELM *VOXEL1; + VOXELM *VOXEL2; + VOXELM *group1; + VOXELM *group2; + EDGE *pointer_edge = edge; + int incremento; + + for (k = 0; k < params->no_of_edges; k++) + { + VOXEL1 = pointer_edge->pointer_1; + VOXEL2 = pointer_edge->pointer_2; + + //VOXELM 1 and VOXELM 2 belong to different groups + //initially each voxel is in a group by itself and one voxel can construct a group + //no else or else if to this if + if (VOXEL2->head != VOXEL1->head) + { + //VOXELM 2 is alone in its group + //merge this voxel with VOXELM 1 group and find the number of 2 pi to add + //to or subtract to unwrap it + if ((VOXEL2->next == NULL) && (VOXEL2->head == VOXEL2)) + { + VOXEL1->head->last->next = VOXEL2; + VOXEL1->head->last = VOXEL2; + (VOXEL1->head->number_of_voxels_in_group)++; + VOXEL2->head=VOXEL1->head; + VOXEL2->increment = VOXEL1->increment-pointer_edge->increment; + } + + //VOXELM 1 is alone in its group + //merge this voxel with VOXELM 2 group and find the number of 2 pi to add + //to or subtract to unwrap it + else if ((VOXEL1->next == NULL) && (VOXEL1->head == VOXEL1)) + { + VOXEL2->head->last->next = VOXEL1; + VOXEL2->head->last = VOXEL1; + (VOXEL2->head->number_of_voxels_in_group)++; + VOXEL1->head = VOXEL2->head; + VOXEL1->increment = VOXEL2->increment+pointer_edge->increment; + } + + //VOXELM 1 and VOXELM 2 both have groups + else + { + group1 = VOXEL1->head; + group2 = VOXEL2->head; + //if the no. of voxels in VOXELM 1 group is larger than the no. of voxels + //in VOXELM 2 group. Merge VOXELM 2 group to VOXELM 1 group + //and find the number of wraps between VOXELM 2 group and VOXELM 1 group + //to unwrap VOXELM 2 group with respect to VOXELM 1 group. + //the no. of wraps will be added to VOXELM 2 grop in the future + if (group1->number_of_voxels_in_group > group2->number_of_voxels_in_group) + { + //merge VOXELM 2 with VOXELM 1 group + group1->last->next = group2; + group1->last = group2->last; + group1->number_of_voxels_in_group = group1->number_of_voxels_in_group + group2->number_of_voxels_in_group; + incremento = VOXEL1->increment-pointer_edge->increment - VOXEL2->increment; + //merge the other voxels in VOXELM 2 group to VOXELM 1 group + while (group2 != NULL) + { + group2->head = group1; + group2->increment += incremento; + group2 = group2->next; + } + } + + //if the no. of voxels in VOXELM 2 group is larger than the no. of voxels + //in VOXELM 1 group. Merge VOXELM 1 group to VOXELM 2 group + //and find the number of wraps between VOXELM 2 group and VOXELM 1 group + //to unwrap VOXELM 1 group with respect to VOXELM 2 group. + //the no. of wraps will be added to VOXELM 1 grop in the future + else + { + //merge VOXELM 1 with VOXELM 2 group + group2->last->next = group1; + group2->last = group1->last; + group2->number_of_voxels_in_group = group2->number_of_voxels_in_group + group1->number_of_voxels_in_group; + incremento = VOXEL2->increment + pointer_edge->increment - VOXEL1->increment; + //merge the other voxels in VOXELM 2 group to VOXELM 1 group + while (group1 != NULL) + { + group1->head = group2; + group1->increment += incremento; + group1 = group1->next; + } // while + + } // else + } //else + } //if + pointer_edge++; + } +} + +//unwrap the volume +void unwrapVolume(VOXELM *voxel, int volume_width, int volume_height, int volume_depth) +{ + int i; + int volume_size = volume_width * volume_height * volume_depth; + VOXELM *voxel_pointer=voxel; + + for (i = 0; i < volume_size; i++) + { + voxel_pointer->value += TWOPI * (double)(voxel_pointer->increment); + voxel_pointer++; + } +} + +//set the masked voxels (mask = 0) to the minimum of the unwrapper phase +void maskVolume(VOXELM *voxel, unsigned char *input_mask, int volume_width, int volume_height, int volume_depth) +{ + int volume_width_plus_one = volume_width + 1; + int volume_height_plus_one = volume_height + 1; + int volume_width_minus_one = volume_width - 1; + int volume_height_minus_one = volume_height - 1; + + VOXELM *pointer_voxel = voxel; + unsigned char *IMP = input_mask; //input mask pointer + double min=99999999.; + int i, j; + int volume_size = volume_width * volume_height * volume_depth; + + //find the minimum of the unwrapped phase + for (i = 0; i < volume_size; i++) + { + if ((pointer_voxel->value < min) && (*IMP == NOMASK)) + min = pointer_voxel->value; + + pointer_voxel++; + IMP++; + } + + pointer_voxel = voxel; + IMP = input_mask; + + //set the masked voxels to minimum + for (i = 0; i < volume_size; i++) + { + if ((*IMP) == MASK) + { + pointer_voxel->value = min; + } + pointer_voxel++; + IMP++; + } +} + +//the input to this unwrapper is an array that contains the wrapped +//phase map. copy the volume on the buffer passed to this unwrapper +//to over-write the unwrapped phase map on the buffer of the wrapped +//phase map. +void returnVolume(VOXELM *voxel, double *unwrappedVolume, int volume_width, int volume_height, int volume_depth) +{ + int i; + int volume_size = volume_width * volume_height * volume_depth; + double *unwrappedVolume_pointer = unwrappedVolume; + VOXELM *voxel_pointer = voxel; + + for (i=0; i < volume_size; i++) + { + *unwrappedVolume_pointer = voxel_pointer->value; + voxel_pointer++; + unwrappedVolume_pointer++; + } +} + +//the main function of the unwrapper +void +unwrap3D(double* wrapped_volume, double* unwrapped_volume, unsigned char* input_mask, + int volume_width, int volume_height, int volume_depth, + int wrap_around_x, int wrap_around_y, int wrap_around_z) +{ + params_t params = {TWOPI, wrap_around_x, wrap_around_y, wrap_around_z, 0}; + unsigned char *extended_mask; + VOXELM *voxel; + EDGE *edge; + int volume_size = volume_height * volume_width * volume_depth; + int No_of_Edges_initially = 3 * volume_width * volume_height * volume_depth; + + extended_mask = (unsigned char *) calloc(volume_size, sizeof(unsigned char)); + voxel = (VOXELM *) calloc(volume_size, sizeof(VOXELM)); + edge = (EDGE *) calloc(No_of_Edges_initially, sizeof(EDGE));; + + extend_mask(input_mask, extended_mask, volume_width, volume_height, volume_depth, ¶ms); + initialiseVOXELs(wrapped_volume, input_mask, extended_mask, voxel, volume_width, volume_height, volume_depth); + calculate_reliability(wrapped_volume, voxel, volume_width, volume_height, volume_depth, ¶ms); + horizontalEDGEs(voxel, edge, volume_width, volume_height, volume_depth, ¶ms); + verticalEDGEs(voxel, edge, volume_width, volume_height, volume_depth, ¶ms); + normalEDGEs(voxel, edge, volume_width, volume_height, volume_depth, ¶ms); + + //sort the EDGEs depending on their reiability. The VOXELs with higher relibility (small value) first + quicker_sort(edge, edge + params.no_of_edges - 1); + + //gather VOXELs into groups + gatherVOXELs(edge, ¶ms); + + unwrapVolume(voxel, volume_width, volume_height, volume_depth); + maskVolume(voxel, input_mask, volume_width, volume_height, volume_depth); + + //copy the volume from VOXELM structure to the unwrapped phase array passed to this function + returnVolume(voxel, unwrapped_volume, volume_width, volume_height, volume_depth); + + free(edge); + free(voxel); + free(extended_mask); +} diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index aea6c70f..f79fb482 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -2,7 +2,7 @@ from .random_walker_segmentation import random_walker from ._felzenszwalb import felzenszwalb from .slic_superpixels import slic from ._quickshift import quickshift -from .boundaries import find_boundaries, visualize_boundaries, mark_boundaries +from .boundaries import find_boundaries, mark_boundaries from ._clear_border import clear_border from ._join import join_segmentations, relabel_from_one, relabel_sequential @@ -12,7 +12,6 @@ __all__ = ['random_walker', 'slic', 'quickshift', 'find_boundaries', - 'visualize_boundaries', 'mark_boundaries', 'clear_border', 'join_segmentations', diff --git a/skimage/segmentation/_felzenszwalb.py b/skimage/segmentation/_felzenszwalb.py index 67971a96..56642f8d 100644 --- a/skimage/segmentation/_felzenszwalb.py +++ b/skimage/segmentation/_felzenszwalb.py @@ -43,10 +43,9 @@ def felzenszwalb(image, scale=1, sigma=0.8, min_size=20): Huttenlocher, D.P. International Journal of Computer Vision, 2004 """ - #image = img_as_float(image) if image.ndim == 2: # assume single channel image - return _felzenszwalb_grey(image, scale=scale, sigma=sigma) + return _felzenszwalb_grey(image, scale=scale, sigma=sigma, min_size=min_size) elif image.ndim != 3: raise ValueError("Felzenswalb segmentation can only operate on RGB and" diff --git a/skimage/segmentation/_felzenszwalb_cy.pyx b/skimage/segmentation/_felzenszwalb_cy.pyx index 8590e17d..80afaedb 100644 --- a/skimage/segmentation/_felzenszwalb_cy.pyx +++ b/skimage/segmentation/_felzenszwalb_cy.pyx @@ -7,7 +7,7 @@ import scipy cimport cython cimport numpy as cnp -from skimage.morphology.ccomp cimport find_root, join_trees +from skimage.measure._ccomp cimport find_root, join_trees from ..util import img_as_float @@ -104,6 +104,8 @@ def _felzenszwalb_grey(image, double scale=1, sigma=0.8, seg0 = find_root(segments_p, edges_p[0]) seg1 = find_root(segments_p, edges_p[1]) edges_p += 2 + if seg0 == seg1: + continue if segment_size[seg0] < min_size or segment_size[seg1] < min_size: join_trees(segments_p, seg0, seg1) diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index 6095382d..7b6d2a3e 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -71,7 +71,7 @@ def relabel_sequential(label_field, offset=1): ------- relabeled : numpy array of int, same shape as `label_field` The input label field with labels mapped to - {1, ..., number_of_labels}. + {offset, ..., number_of_labels + offset - 1}. 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 @@ -114,12 +114,16 @@ def relabel_sequential(label_field, offset=1): >>> relab array([5, 5, 6, 6, 7, 9, 8]) """ + m = label_field.max() + if not np.issubdtype(label_field.dtype, np.int): + new_type = np.min_scalar_type(int(m)) + label_field = label_field.astype(new_type) + m = m.astype(new_type) # Ensures m is an integer labels = np.unique(label_field) labels0 = labels[labels != 0] - m = labels.max() if m == len(labels0): # nothing to do, already 1...n labels return label_field, labels, labels - forward_map = np.zeros(m+1, int) + forward_map = np.zeros(m + 1, int) forward_map[labels0] = np.arange(offset, offset + len(labels0) + 1) if not (labels == 0).any(): labels = np.concatenate(([0], labels)) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index c3d95ee0..766cd317 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -3,6 +3,7 @@ #cython: nonecheck=False #cython: wraparound=False from libc.float cimport DBL_MAX +from cpython cimport bool import numpy as np cimport numpy as cnp @@ -12,8 +13,10 @@ from skimage.util import regular_grid def _slic_cython(double[:, :, :, ::1] image_zyx, double[:, ::1] segments, + float step, Py_ssize_t max_iter, - double[::1] spacing): + double[::1] spacing, + bint slic_zero): """Helper function for SLIC segmentation. Parameters @@ -22,12 +25,16 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, The input image. segments : 2D array of double, shape (N, 3 + C) The initial centroids obtained by SLIC as [Z, Y, X, C...]. + step : double + The size of the step between two seeds in voxels. max_iter : int 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. + k-means clustering. + slic_zero : bool + True to run SLIC-zero, False to run original SLIC. Returns ------- @@ -86,6 +93,14 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, sy = spacing[1] sx = spacing[2] + # The colors are scaled before being passed to _slic_cython so + # max_color_sq can be initialised as all ones + cdef double[::1] max_dist_color = np.ones(n_segments, dtype=np.double) + cdef double dist_color + + # The reference implementation (Achanta et al.) calls this invxywt + cdef double spatial_weight = float(1) / (step ** 2) + for i in range(max_iter): change = 0 distance[:, :, :] = DBL_MAX @@ -111,10 +126,16 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, 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 + dist_center = (dz + dy + (sx * (cx - x)) ** 2) * spatial_weight + dist_color = 0 for c in range(3, n_features): - dist_center += (image_zyx[z, y, x, c - 3] + dist_color += (image_zyx[z, y, x, c - 3] - segments[k, c]) ** 2 + if slic_zero: + dist_center += dist_color / max_dist_color[k] + else: + dist_center += dist_color + if distance[z, y, x] > dist_center: nearest_segments[z, y, x] = k distance[z, y, x] = dist_center @@ -145,4 +166,123 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, for c in range(n_features): segments[k, c] /= n_segment_elems[k] + # If in SLICO mode, update the color distance maxima + if slic_zero: + for z in range(depth): + for y in range(height): + for x in range(width): + + k = nearest_segments[z, y, x] + dist_color = 0 + + for c in range(3, n_features): + dist_color += (image_zyx[z, y, x, c - 3] - + segments[k, c]) ** 2 + + # The reference implementation seems to only change + # the color if it increases from previous iteration + if max_dist_color[k] < dist_color: + max_dist_color[k] = dist_color + return np.asarray(nearest_segments) + + +def _enforce_label_connectivity_cython(Py_ssize_t[:, :, ::1] segments, + Py_ssize_t n_segments, + Py_ssize_t min_size, + Py_ssize_t max_size): + """ Helper function to remove small disconnected regions from the labels + + Parameters + ---------- + segments : 3D array of int, shape (Z, Y, X) + The label field/superpixels found by SLIC. + n_segments: int + Number of specified segments + min_size: int + Minimum size of the segment + max_size: int + Maximum size of the segment. This is done for performance reasons, + to pre-allocate a sufficiently large array for the breadth first search + Returns + ------- + connected_segments : 3D array of int, shape (Z, Y, X) + A label field with connected labels starting at label=1 + """ + + # get image dimensions + cdef Py_ssize_t depth, height, width + depth = segments.shape[0] + height = segments.shape[1] + width = segments.shape[2] + + # neighborhood arrays + cdef Py_ssize_t[::1] ddx = np.array((1, -1, 0, 0, 0, 0), dtype=np.intp) + cdef Py_ssize_t[::1] ddy = np.array((0, 0, 1, -1, 0, 0), dtype=np.intp) + cdef Py_ssize_t[::1] ddz = np.array((0, 0, 0, 0, 1, -1), dtype=np.intp) + + # new object with connected segments initialized to -1 + cdef Py_ssize_t[:, :, ::1] connected_segments \ + = -1 * np.ones_like(segments, dtype=np.intp) + + cdef Py_ssize_t current_new_label = 0 + cdef Py_ssize_t label = 0 + + # variables for the breadth first search + cdef Py_ssize_t current_segment_size = 1 + cdef Py_ssize_t bfs_visited = 0 + cdef Py_ssize_t adjacent + + cdef Py_ssize_t zz, yy, xx + + cdef Py_ssize_t[:, ::1] coord_list = np.zeros((max_size, 3), dtype=np.intp) + + # loop through all image + for z in range(depth): + for y in range(height): + for x in range(width): + if connected_segments[z, y, x] >= 0: + continue + # find the component size + adjacent = 0 + label = segments[z, y, x] + connected_segments[z, y, x] = current_new_label + current_segment_size = 1 + bfs_visited = 0 + coord_list[bfs_visited, 0] = z + coord_list[bfs_visited, 1] = y + coord_list[bfs_visited, 2] = x + + #perform a breadth first search to find + # the size of the connected component + while bfs_visited != current_segment_size: + for i in range(6): + zz = coord_list[bfs_visited, 0] + ddz[i] + yy = coord_list[bfs_visited, 1] + ddy[i] + xx = coord_list[bfs_visited, 2] + ddx[i] + if (0 <= xx < width and + 0 <= yy < height and + 0 <= zz < depth): + if (segments[zz, yy, xx] == label and + connected_segments[zz, yy, xx] == -1): + connected_segments[zz, yy, xx] = \ + current_new_label + coord_list[current_segment_size, 0] = zz + coord_list[current_segment_size, 1] = yy + coord_list[current_segment_size, 2] = xx + current_segment_size += 1 + elif (connected_segments[zz, yy, xx] >= 0 and + connected_segments[zz, yy, xx] != current_new_label): + adjacent = connected_segments[zz, yy, xx] + bfs_visited += 1 + + # change to an adjacent one, like in the original paper + if current_segment_size < min_size: + for i in range(current_segment_size): + connected_segments[coord_list[i, 0], + coord_list[i, 1], + coord_list[i, 2]] = adjacent + else: + current_new_label += 1 + + return np.asarray(connected_segments) diff --git a/skimage/segmentation/boundaries.py b/skimage/segmentation/boundaries.py index c5bf985b..d2633f97 100644 --- a/skimage/segmentation/boundaries.py +++ b/skimage/segmentation/boundaries.py @@ -38,8 +38,3 @@ def mark_boundaries(image, label_img, color=(1, 1, 0), outline_color=(0, 0, 0)): image[outer_boundaries != 0, :] = np.array(outline_color) image[boundaries, :] = np.array(color) return image - - -@deprecated('mark_boundaries') -def visualize_boundaries(*args, **kwargs): - return mark_boundaries(*args, **kwargs) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 17c4c98b..731518e3 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -9,7 +9,6 @@ significantly the performance. """ import warnings - import numpy as np from scipy import sparse, ndimage @@ -22,6 +21,7 @@ from scipy import sparse, ndimage try: from scipy.sparse.linalg.dsolve import umfpack old_del = umfpack.UmfpackContext.__del__ + def new_del(self): try: old_del(self) @@ -190,8 +190,7 @@ def _build_laplacian(data, spacing, mask=None, beta=50, def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, - multichannel=False, return_full_prob=False, depth=1., - spacing=None): + multichannel=False, return_full_prob=False, spacing=None): """Random walker algorithm for segmentation from markers. Random walker algorithm is implemented for gray-level or multichannel @@ -203,8 +202,8 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, Image to be segmented in phases. Gray-level `data` can be two- or three-dimensional; multichannel data can be three- or four- dimensional (multichannel=True) with the highest dimension denoting - channels. Data spacing is assumed isotropic unless depth keyword - argument is used. + channels. Data spacing is assumed isotropic unless the `spacing` + keyword argument is used. labels : array of ints, of same shape as `data` without channels dimension Array of seed markers labeled with different positive integers for different phases. Zero-labeled pixels are unlabeled pixels. @@ -217,14 +216,14 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, beta : float Penalization coefficient for the random walker motion (the greater `beta`, the more difficult the diffusion). - mode : {'bf', 'cg_mg', 'cg'} (default: 'bf') - Mode for solving the linear system in the random walker - algorithm. + mode : string, available options {'cg_mg', 'cg', 'bf'} + Mode for solving the linear system in the random walker algorithm. + If no preference given, automatically attempt to use the fastest + option available ('cg_mg' from pyamg >> 'cg' with UMFPACK > 'bf'). - - 'bf' (brute force, default): an LU factorization of the Laplacian is + - 'bf' (brute force): an LU factorization of the Laplacian is computed. This is fast for small images (<1024x1024), but very slow - (due to the memory cost) and memory-consuming for big images (in 3-D - for example). + and memory-intensive for large images (e.g., 3-D volumes). - 'cg' (conjugate gradient): the linear system is solved iteratively using the Conjugate Gradient method from scipy.sparse.linalg. This is less memory-consuming than the brute force method for large images, @@ -249,13 +248,6 @@ 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. [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. @@ -324,11 +316,11 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, Examples -------- - >>> a = np.zeros((10, 10)) + 0.2*np.random.random((10, 10)) + >>> a = np.zeros((10, 10)) + 0.2 * np.random.random((10, 10)) >>> a[5:8, 5:8] += 1 >>> b = np.zeros_like(a) - >>> b[3,3] = 1 #Marker for first phase - >>> b[6,6] = 2 #Marker for second phase + >>> b[3, 3] = 1 # Marker for first phase + >>> b[6, 6] = 2 # Marker for second phase >>> random_walker(a, b) array([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], @@ -342,57 +334,85 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], dtype=int32) """ - + # Parse input data 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 amg_loaded: + mode = 'cg_mg' + elif UmfpackContext is not None: + mode = 'cg' + else: + mode = 'bf' 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.') + warnings.warn('"cg" mode will be used, but it may be slower than ' + '"bf" because SciPy was built without UMFPACK. Consider' + ' rebuilding SciPy with UMFPACK; this will greatly ' + 'accelerate the conjugate gradient ("cg") solver. ' + 'You may also install pyamg and run the random_walker ' + 'function in "cg_mg" mode (see docstring).') + + if (labels != 0).all(): + warnings.warn('Random walker only segments unlabeled areas, where ' + 'labels == 0. No zero valued areas in labels were ' + 'found. Returning provided labels.') + + if return_full_prob: + # Find and iterate over valid labels + unique_labels = np.unique(labels) + unique_labels = unique_labels[unique_labels > 0] + + out_labels = np.empty(labels.shape + (len(unique_labels),), + dtype=np.bool) + for n, i in enumerate(unique_labels): + out_labels[..., n] = (labels == i) + + else: + out_labels = labels + return out_labels + + # This algorithm expects 4-D arrays of floats, where the first three + # dimensions are spatial and the final denotes channels. 2-D images have + # a singleton placeholder dimension added for the third spatial dimension, + # and single channel images likewise have a singleton added for channels. + # The following block ensures valid input and coerces it to the correct + # form. + if not multichannel: + if data.ndim < 2 or data.ndim > 3: + raise ValueError('For non-multichannel input, data must be of ' + 'dimension 2 or 3.') + dims = data.shape # To reshape final labeled result + data = np.atleast_3d(img_as_float(data))[..., np.newaxis] + else: + if data.ndim < 3: + raise ValueError('For multichannel input, data must have 3 or 4 ' + 'dimensions.') + dims = data[..., 0].shape # To reshape final labeled result + data = img_as_float(data) + if data.ndim == 3: # 2D multispectral, needs singleton in 3rd axis + data = data[:, :, np.newaxis, :] # Spacing kwarg checks if spacing is None: - spacing = (1., 1.) + (depth, ) - elif len(spacing) == 2: - spacing = tuple(spacing) + (depth, ) - elif len(spacing) == 3: - pass + spacing = np.asarray((1.,) * 3) + elif len(spacing) == len(dims): + if len(spacing) == 2: # Need a dummy spacing for singleton 3rd dim + spacing = np.r_[spacing, 1.] + else: # Convert to array + spacing = np.asarray(spacing) 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))[..., 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 = data[..., np.newaxis].transpose((0, 1, 3, 2)) + raise ValueError('Input argument `spacing` incorrect, should be an ' + 'iterable with one number per spatial dimension.') if copy: labels = np.copy(labels) label_values = np.unique(labels) + # Reorder label values to have consecutive integers (no gaps) if np.any(np.diff(label_values) != 1): mask = labels >= 0 labels[mask] = rank_order(labels[mask])[0].astype(labels.dtype) labels = labels.astype(np.int32) + # If the array has pruned zones, be sure that no isolated pixels # exist between pruned zones (they could not be determined) if np.any(labels < 0): @@ -407,6 +427,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, lap_sparse = _build_laplacian(data, spacing, beta=beta, multichannel=multichannel) lap_sparse, B = _buildAB(lap_sparse, labels) + # We solve the linear system # lap_sparse X = B # where X[i, j] is the probability that a marker of label i arrives @@ -428,6 +449,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, if mode == 'bf': X = _solve_bf(lap_sparse, B, return_full_prob=return_full_prob) + # Clean up results if return_full_prob: labels = labels.astype(np.float) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 8276b1a8..7135047e 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -6,12 +6,14 @@ from scipy import ndimage import warnings from skimage.util import img_as_float, regular_grid -from skimage.segmentation._slic import _slic_cython +from skimage.segmentation._slic import _slic_cython, _enforce_label_connectivity_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): +def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0, + spacing=None, multichannel=True, convert2lab=True, + enforce_connectivity=False, min_size_factor=0.5, max_size_factor=3, + slic_zero=False): """Segments image using k-means clustering in Color-(x,y,z) space. Parameters @@ -24,7 +26,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, 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. + infinity, superpixel shapes become square/cubic. In SLICO mode, this + is the initial compactness. max_iter : int, optional Maximum number of iterations of k-means. sigma : float or (3,) array-like of floats, optional @@ -45,8 +48,16 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, 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. + enforce_connectivity: bool, optional (default False) + Whether the generated segments are connected or not + min_size_factor: float, optional + Proportion of the minimum segment size to be removed with respect + to the supposed segment size ```depth*width*height/n_segments``` + max_size_factor: float, optional + Proportion of the maximum connected segment size. A value of 3 works + in most of the cases. + slic_zero: bool, optional + Run SLIC-zero, the zero-parameter mode of SLIC Returns ------- @@ -88,18 +99,16 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, >>> 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 + Increasing the compactness parameter yields more square regions: + + >>> segments = slic(img, n_segments=100, compactness=20) + + """ + if enforce_connectivity is None: + warnings.warn('Deprecation: enforce_connectivity will default to' + ' True in future versions.') + enforce_connectivity = False image = img_as_float(image) is_2d = False @@ -149,16 +158,27 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, 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_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 + step = float(max((step_z, step_y, step_x))) + ratio = 1.0 / compactness + image = np.ascontiguousarray(image * ratio) - labels = _slic_cython(image, segments, max_iter, spacing) + labels = _slic_cython(image, segments, step, max_iter, spacing, slic_zero) + + if enforce_connectivity: + segment_size = depth * height * width / n_segments + min_size = int(min_size_factor * segment_size) + max_size = int(max_size_factor * segment_size) + labels = _enforce_label_connectivity_cython(labels, + n_segments, + min_size, + max_size) if is_2d: labels = labels[0] diff --git a/skimage/segmentation/tests/test_boundaries.py b/skimage/segmentation/tests/test_boundaries.py new file mode 100644 index 00000000..2fff52f8 --- /dev/null +++ b/skimage/segmentation/tests/test_boundaries.py @@ -0,0 +1,59 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.segmentation import find_boundaries, mark_boundaries + + +def test_find_boundaries(): + image = np.zeros((10, 10)) + image[2:7, 2:7] = 1 + + ref = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 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, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + + result = find_boundaries(image) + assert_array_equal(result, ref) + + +def test_mark_boundaries(): + image = np.zeros((10, 10)) + label_image = np.zeros((10, 10)) + label_image[2:7, 2:7] = 1 + + ref = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 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, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + result = mark_boundaries(image, label_image, color=(1, 1, 1)).mean(axis=2) + assert_array_equal(result, ref) + + ref = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 2, 0], + [0, 0, 1, 2, 2, 2, 2, 1, 2, 0], + [0, 0, 1, 2, 0, 0, 0, 1, 2, 0], + [0, 0, 1, 2, 0, 0, 0, 1, 2, 0], + [0, 0, 1, 2, 0, 0, 0, 1, 2, 0], + [0, 0, 1, 1, 1, 1, 1, 2, 2, 0], + [0, 0, 2, 2, 2, 2, 2, 2, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + result = mark_boundaries(image, label_image, color=(1, 1, 1), + outline_color=(2, 2, 2)).mean(axis=2) + assert_array_equal(result, ref) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/segmentation/tests/test_felzenszwalb.py b/skimage/segmentation/tests/test_felzenszwalb.py index 9fd0b018..5324995d 100644 --- a/skimage/segmentation/tests/test_felzenszwalb.py +++ b/skimage/segmentation/tests/test_felzenszwalb.py @@ -1,7 +1,9 @@ import numpy as np from numpy.testing import assert_equal, assert_array_equal + from skimage._shared.testing import assert_greater from skimage.segmentation import felzenszwalb +from skimage import data def test_grey(): @@ -18,6 +20,24 @@ def test_grey(): hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0] assert_greater(hist[i], 40) +def test_minsize(): + # single-channel: + img = data.coins()[20:168,0:128] + for min_size in np.arange(10, 100, 10): + segments = felzenszwalb(img, min_size=min_size, sigma=3) + counts = np.bincount(segments.ravel()) + # actually want to test greater or equal. + assert_greater(counts.min() + 1, min_size) + # multi-channel: + coffee = data.coffee()[::4, ::4] + for min_size in np.arange(10, 100, 10): + segments = felzenszwalb(coffee, min_size=min_size, sigma=3) + counts = np.bincount(segments.ravel()) + # actually want to test greater or equal. + # the construction doesn't guarantee min_size is respected + # after intersecting the sementations for the colors + assert_greater(np.mean(counts) + 1, min_size) + def test_color(): # very weak tests. This algorithm is pretty unstable. diff --git a/skimage/segmentation/tests/test_join.py b/skimage/segmentation/tests/test_join.py index 548fcc8d..1b59f889 100644 --- a/skimage/segmentation/tests/test_join.py +++ b/skimage/segmentation/tests/test_join.py @@ -61,5 +61,17 @@ def test_relabel_sequential_offset5_with0(): assert_array_equal(inv, inv_ref) +def test_relabel_sequential_dtype(): + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=float) + 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 46a82d8e..9d30473b 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -184,7 +184,7 @@ def test_multispectral_3d(): return data, multi_labels, single_labels, labels -def test_depth(): +def test_spacing_0(): n = 30 lx, ly, lz = n, n, n data, _ = make_3d_syntheticdata(lx, ly, lz) @@ -202,14 +202,14 @@ def test_depth(): ly // 2 - small_l // 4, lz // 4 - small_l // 8] = 2 - # Test with `depth` kwarg + # Test with `spacing` kwarg labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', - depth=0.5) + spacing=(1., 1., 0.5)) assert (labels_aniso[13:17, 13:17, 7:9] == 2).all() -def test_spacing(): +def test_spacing_1(): n = 30 lx, ly, lz = n, n, n data, _ = make_3d_syntheticdata(lx, ly, lz) @@ -255,6 +255,56 @@ def test_spacing(): assert (labels_aniso2[26:34, 13:17, 13:17] == 2).all() +def test_trivial_cases(): + # When all voxels are labeled + img = np.ones((10, 10)) + labels = np.ones((10, 10)) + pass_through = random_walker(img, labels) + np.testing.assert_array_equal(pass_through, labels) + + # When all voxels are labeled AND return_full_prob is True + labels[:, :5] = 3 + expected = np.concatenate(((labels == 1)[..., np.newaxis], + (labels == 3)[..., np.newaxis]), axis=2) + test = random_walker(img, labels, return_full_prob=True) + np.testing.assert_array_equal(test, expected) + + +def test_length2_spacing(): + # If this passes without raising an exception (warnings OK), the new + # spacing code is working properly. + np.random.seed(42) + img = np.ones((10, 10)) + 0.2 * np.random.normal(size=(10, 10)) + labels = np.zeros((10, 10), dtype=np.uint8) + labels[2, 4] = 1 + labels[6, 8] = 4 + random_walker(img, labels, spacing=(1., 2.)) + + +def test_bad_inputs(): + # Too few dimensions + img = np.ones(10) + labels = np.arange(10) + np.testing.assert_raises(ValueError, random_walker, img, labels) + np.testing.assert_raises(ValueError, + random_walker, img, labels, multichannel=True) + + # Too many dimensions + np.random.seed(42) + img = np.random.normal(size=(3, 3, 3, 3, 3)) + labels = np.arange(3 ** 5).reshape(img.shape) + np.testing.assert_raises(ValueError, random_walker, img, labels) + np.testing.assert_raises(ValueError, + random_walker, img, labels, multichannel=True) + + # Spacing incorrect length + img = np.random.normal(size=(10, 10)) + labels = np.zeros((10, 10)) + labels[2, 4] = 2 + labels[6, 8] = 5 + np.testing.assert_raises(ValueError, + random_walker, img, labels, spacing=(1,)) + + if __name__ == '__main__': - from numpy import testing - testing.run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index a4657785..7dda66d2 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -1,7 +1,7 @@ import itertools as it import warnings import numpy as np -from numpy.testing import assert_equal, assert_array_equal +from numpy.testing import assert_equal, assert_raises from skimage.segmentation import slic @@ -21,10 +21,10 @@ def test_color_2d(): # 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) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) def test_gray_2d(): @@ -41,10 +41,10 @@ def test_gray_2d(): 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) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) def test_color_3d(): @@ -65,7 +65,7 @@ def test_color_3d(): assert_equal(len(np.unique(seg)), 8) for s, c in zip(slices, range(8)): - assert_array_equal(seg[s], c) + assert_equal(seg[s], c) def test_gray_3d(): @@ -76,7 +76,7 @@ def test_gray_3d(): 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) + 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) @@ -87,7 +87,7 @@ def test_gray_3d(): assert_equal(len(np.unique(seg)), 8) for s, c in zip(slices, range(8)): - assert_array_equal(seg[s], c) + assert_equal(seg[s], c) def test_list_sigma(): @@ -118,7 +118,60 @@ def test_spacing(): assert_equal(seg_spaced, result_spaced) +def test_invalid_lab_conversion(): + img = np.array([[1, 1, 1, 0, 0], + [1, 1, 0, 0, 0]], np.float) + 1 + assert_raises(ValueError, slic, img, multichannel=True, convert2lab=True) + + +def test_enforce_connectivity(): + img = np.array([[0, 0, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 0], + [0, 0, 0, 1, 1, 0]], np.float) + + segments_connected = slic(img, 2, compactness=0.0001, + enforce_connectivity=True, + convert2lab=False) + segments_disconnected = slic(img, 2, compactness=0.0001, + enforce_connectivity=False, + convert2lab=False) + + result_connected = np.array([[0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1]], np.float) + + result_disconnected = np.array([[0, 0, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 0], + [0, 0, 0, 1, 1, 0]], np.float) + + assert_equal(segments_connected, result_connected) + assert_equal(segments_disconnected, result_disconnected) + + +def test_slic_zero(): + # Same as test_color_2d but with slic_zero=True + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.01 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + seg = slic(img, n_segments=4, sigma=0, slic_zero=True) + + # we expect 4 segments + assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape[:-1]) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) + if __name__ == '__main__': from numpy import testing + testing.run_module_suite() diff --git a/skimage/setup.py b/skimage/setup.py index 1082ba07..962adb97 100644 --- a/skimage/setup.py +++ b/skimage/setup.py @@ -10,7 +10,9 @@ def configuration(parent_package='', top_path=None): config.add_subpackage('color') config.add_subpackage('data') config.add_subpackage('draw') + config.add_subpackage('exposure') config.add_subpackage('feature') + config.add_subpackage('restoration') config.add_subpackage('filter') config.add_subpackage('graph') config.add_subpackage('io') diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 4f00a076..877f1614 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -1,10 +1,11 @@ -from ._hough_transform import (hough_circle, hough_ellipse, hough_line, +from ._hough_transform import (hough_ellipse, hough_line, probabilistic_hough_line) -from .hough_transform import hough_line_peaks +from .hough_transform import hough_circle, 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, + matrix_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) @@ -30,6 +31,7 @@ __all__ = ['hough_circle', 'warp', 'warp_coords', 'estimate_transform', + 'matrix_transform', 'SimilarityTransform', 'AffineTransform', 'ProjectiveTransform', diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index c1f49fa5..b3019135 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1,12 +1,13 @@ +import six import math +import warnings import numpy as np from scipy import ndimage, spatial + +from skimage._shared.utils import get_bound_method_class, safe_as_int 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. @@ -103,6 +104,11 @@ class ProjectiveTransform(GeometricTransform): matrix : (3, 3) array, optional Homogeneous transformation matrix. + Attributes + ---------- + params : (3, 3) array + Homogeneous transformation matrix. + """ _coeffs = range(8) @@ -113,11 +119,17 @@ class ProjectiveTransform(GeometricTransform): matrix = np.eye(3) if matrix.shape != (3, 3): raise ValueError("invalid shape of transformation matrix") - self._matrix = matrix + self.params = matrix + + @property + def _matrix(self): + warnings.warn('`_matrix` attribute is deprecated, ' + 'use `params` instead.') + return self.params @property def _inv_matrix(self): - return np.linalg.inv(self._matrix) + return np.linalg.inv(self.params) def _apply_mat(self, coords, matrix): coords = np.array(coords, copy=False, ndmin=2) @@ -133,7 +145,7 @@ class ProjectiveTransform(GeometricTransform): return dst[:, :2] def __call__(self, coords): - return self._apply_mat(coords, self._matrix) + return self._apply_mat(coords, self.params) def inverse(self, coords): """Apply inverse transformation. @@ -235,7 +247,7 @@ class ProjectiveTransform(GeometricTransform): H.flat[list(self._coeffs) + [8]] = - V[-1, :-1] / V[-1, -1] H[2, 2] = 1 - self._matrix = H + self.params = H def __add__(self, other): """Combine this transformation with another. @@ -248,7 +260,7 @@ class ProjectiveTransform(GeometricTransform): tform = self.__class__ else: tform = ProjectiveTransform - return tform(other._matrix.dot(self._matrix)) + return tform(other.params.dot(self.params)) else: raise TypeError("Cannot combine transformations of differing " "types.") @@ -284,6 +296,11 @@ class AffineTransform(ProjectiveTransform): translation : (tx, ty) as array, list or tuple, optional Translation parameters. + Attributes + ---------- + params : (3, 3) array + Homogeneous transformation matrix. + """ _coeffs = range(6) @@ -299,7 +316,7 @@ class AffineTransform(ProjectiveTransform): elif matrix is not None: if matrix.shape != (3, 3): raise ValueError("Invalid shape of transformation matrix.") - self._matrix = matrix + self.params = matrix elif params: if scale is None: scale = (1, 1) @@ -311,34 +328,34 @@ class AffineTransform(ProjectiveTransform): translation = (0, 0) sx, sy = scale - self._matrix = np.array([ + self.params = np.array([ [sx * math.cos(rotation), -sy * math.sin(rotation + shear), 0], [sx * math.sin(rotation), sy * math.cos(rotation + shear), 0], [ 0, 0, 1] ]) - self._matrix[0:2, 2] = translation + self.params[0:2, 2] = translation else: # default to an identity transform - self._matrix = np.eye(3) + self.params = np.eye(3) @property def scale(self): - sx = math.sqrt(self._matrix[0, 0] ** 2 + self._matrix[1, 0] ** 2) - sy = math.sqrt(self._matrix[0, 1] ** 2 + self._matrix[1, 1] ** 2) + sx = math.sqrt(self.params[0, 0] ** 2 + self.params[1, 0] ** 2) + sy = math.sqrt(self.params[0, 1] ** 2 + self.params[1, 1] ** 2) return sx, sy @property def rotation(self): - return math.atan2(self._matrix[1, 0], self._matrix[0, 0]) + return math.atan2(self.params[1, 0], self.params[0, 0]) @property def shear(self): - beta = math.atan2(- self._matrix[0, 1], self._matrix[1, 1]) + beta = math.atan2(- self.params[0, 1], self.params[1, 1]) return beta - self.rotation @property def translation(self): - return self._matrix[0:2, 2] + return self.params[0:2, 2] class PiecewiseAffineTransform(GeometricTransform): @@ -349,13 +366,20 @@ class PiecewiseAffineTransform(GeometricTransform): a Delaunay triangulation of the points to form a mesh. Each triangle is used to find a local affine transform. + Attributes + ---------- + affines : list of AffineTransform objects + Affine transformations for each triangle in the mesh. + inverse_affines : list of AffineTransform objects + Inverse affine transformations for each triangle in the mesh. + """ def __init__(self): self._tesselation = None self._inverse_tesselation = None - self.affines = [] - self.inverse_affines = [] + self.affines = None + self.inverse_affines = None def estimate(self, src, dst): """Set the control points with which to perform the piecewise mapping. @@ -465,11 +489,11 @@ class PiecewiseAffineTransform(GeometricTransform): class SimilarityTransform(ProjectiveTransform): """2D similarity transformation of the form:: - X = a0*x - b0*y + a1 = - = m*x*cos(rotation) + m*y*sin(rotation) + a1 + X = a0 * x - b0 * y + a1 = + = m * x * cos(rotation) - m * y * sin(rotation) + a1 - Y = b0*x + a0*y + b1 = - = m*x*sin(rotation) + m*y*cos(rotation) + b1 + Y = b0 * x + a0 * y + b1 = + = m * x * sin(rotation) + m * y * cos(rotation) + b1 where ``m`` is a zoom factor and the homogeneous transformation matrix is:: @@ -488,6 +512,11 @@ class SimilarityTransform(ProjectiveTransform): translation : (tx, ty) as array, list or tuple, optional x, y translation parameters. + Attributes + ---------- + params : (3, 3) array + Homogeneous transformation matrix. + """ def __init__(self, matrix=None, scale=None, rotation=None, @@ -501,7 +530,7 @@ class SimilarityTransform(ProjectiveTransform): elif matrix is not None: if matrix.shape != (3, 3): raise ValueError("Invalid shape of transformation matrix.") - self._matrix = matrix + self.params = matrix elif params: if scale is None: scale = 1 @@ -510,16 +539,16 @@ class SimilarityTransform(ProjectiveTransform): if translation is None: translation = (0, 0) - self._matrix = np.array([ + self.params = np.array([ [math.cos(rotation), - math.sin(rotation), 0], [math.sin(rotation), math.cos(rotation), 0], [ 0, 0, 1] ]) - self._matrix[0:2, 0:2] *= scale - self._matrix[0:2, 2] = translation + self.params[0:2, 0:2] *= scale + self.params[0:2, 2] = translation else: # default to an identity transform - self._matrix = np.eye(3) + self.params = np.eye(3) def estimate(self, src, dst): """Set the transformation matrix with the explicit parameters. @@ -531,13 +560,13 @@ class SimilarityTransform(ProjectiveTransform): The transformation is defined as:: - X = a0*x - b0*y + a1 - Y = b0*x + a0*y + b1 + X = a0 * x - b0 * y + a1 + Y = b0 * x + a0 * y + b1 These equations can be transformed to the following form:: - 0 = a0*x - b0*y + a1 - X - 0 = b0*x + a0*y + b1 - Y + 0 = a0 * x - b0 * y + a1 - X + 0 = b0 * x + a0 * y + b1 - Y which exist for each set of corresponding points, so we have a set of N * 2 equations. The coefficients appear linearly so we can write @@ -585,26 +614,26 @@ class SimilarityTransform(ProjectiveTransform): # singular value a0, a1, b0, b1 = - V[-1, :-1] / V[-1, -1] - self._matrix = np.array([[a0, -b0, a1], - [b0, a0, b1], - [ 0, 0, 1]]) + self.params = np.array([[a0, -b0, a1], + [b0, a0, b1], + [ 0, 0, 1]]) @property def scale(self): - if math.cos(self.rotation) == 0: + if abs(math.cos(self.rotation)) < np.spacing(1): # sin(self.rotation) == 1 - scale = self._matrix[0, 1] + scale = self.params[1, 0] else: - scale = self._matrix[0, 0] / math.cos(self.rotation) + scale = self.params[0, 0] / math.cos(self.rotation) return scale @property def rotation(self): - return math.atan2(self._matrix[1, 0], self._matrix[1, 1]) + return math.atan2(self.params[1, 0], self.params[1, 1]) @property def translation(self): - return self._matrix[0:2, 2] + return self.params[0:2, 2] class PolynomialTransform(GeometricTransform): @@ -619,6 +648,12 @@ class PolynomialTransform(GeometricTransform): Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So, a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`. + Attributes + ---------- + params : (2, N) array + Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So, + a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`. + """ def __init__(self, params=None): @@ -627,7 +662,13 @@ class PolynomialTransform(GeometricTransform): params = np.array([[0, 1, 0], [0, 0, 1]]) if params.shape[0] != 2: raise ValueError("invalid shape of transformation parameters") - self._params = params + self.params = params + + @property + def _params(self): + warnings.warn('`_params` attribute is deprecated, ' + 'use `params` instead.') + return self.params def estimate(self, src, dst, order=2): """Set the transformation matrix with the explicit transformation @@ -681,6 +722,7 @@ class PolynomialTransform(GeometricTransform): rows = src.shape[0] # number of unknown polynomial coefficients + order = safe_as_int(order) u = (order + 1) * (order + 2) A = np.zeros((rows * 2, u + 1)) @@ -688,7 +730,7 @@ class PolynomialTransform(GeometricTransform): for j in range(order + 1): for i in range(j + 1): A[:rows, pidx] = xs ** (j - i) * ys ** i - A[rows:, pidx + u / 2] = xs ** (j - i) * ys ** i + A[rows:, pidx + u // 2] = xs ** (j - i) * ys ** i pidx += 1 A[:rows, -1] = xd @@ -700,7 +742,7 @@ class PolynomialTransform(GeometricTransform): # singular value params = - V[-1, :-1] / V[-1, -1] - self._params = params.reshape((2, u / 2)) + self.params = params.reshape((2, u // 2)) def __call__(self, coords): """Apply forward transformation. @@ -718,7 +760,7 @@ class PolynomialTransform(GeometricTransform): """ x = coords[:, 0] y = coords[:, 1] - u = len(self._params.ravel()) + u = len(self.params.ravel()) # number of coefficients -> u = (order + 1) * (order + 2) order = int((- 3 + math.sqrt(9 - 4 * (2 - u))) / 2) dst = np.zeros(coords.shape) @@ -726,8 +768,8 @@ class PolynomialTransform(GeometricTransform): pidx = 0 for j in range(order + 1): for i in range(j + 1): - dst[:, 0] += self._params[0, pidx] * x ** (j - i) * y ** i - dst[:, 1] += self._params[1, pidx] * x ** (j - i) * y ** i + dst[:, 0] += self.params[0, pidx] * x ** (j - i) * y ** i + dst[:, 1] += self.params[1, pidx] * x ** (j - i) * y ** i pidx += 1 return dst @@ -797,13 +839,14 @@ def estimate_transform(ttype, src, dst, **kwargs): >>> tform = tf.estimate_transform('similarity', src, dst) - >>> tform.inverse(tform(src)) # == src + >>> np.allclose(tform.inverse(tform(src)), src) + True >>> # warp image using the estimated transformation >>> from skimage import data >>> image = data.camera() - >>> warp(image, inverse_map=tform.inverse) + >>> warp(image, inverse_map=tform.inverse) # doctest: +SKIP >>> # create transformation with explicit parameters >>> tform2 = tf.SimilarityTransform(scale=1.1, rotation=1, @@ -811,7 +854,8 @@ def estimate_transform(ttype, src, dst, **kwargs): >>> # unite transformations, applied in order from left to right >>> tform3 = tform + tform2 - >>> tform3(src) # == tform2(tform(src)) + >>> np.allclose(tform3(src), tform2(tform(src))) + True """ ttype = ttype.lower() @@ -915,6 +959,7 @@ def warp_coords(coord_map, shape, dtype=np.float64): >>> warped_image = map_coordinates(image, coords) """ + shape = safe_as_int(shape) rows, cols = shape[0], shape[1] coords_shape = [len(shape), rows, cols] if len(shape) == 3: @@ -960,7 +1005,8 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Keyword arguments passed to `inverse_map`. output_shape : tuple (rows, cols), optional Shape of the output image generated. By default the shape of the input - image is preserved. + image is preserved. Note that, even for multi-band images, only rows + and columns need to be specified. order : int, optional The order of interpolation. The order has to be in the range 0-5: * 0: Nearest-neighbor @@ -990,45 +1036,53 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, >>> image = data.camera() The following image warps are all equal but differ substantially in - execution time. + execution time. The image is shifted to the bottom. Use a geometric transform to warp an image (fast): >>> from skimage.transform import SimilarityTransform >>> tform = SimilarityTransform(translation=(0, -10)) - >>> warp(image, tform) + >>> warped = warp(image, tform) - Shift an image to the right with a callable (slow): + Use a callable (slow): - >>> def shift(xy): + >>> def shift_down(xy): ... xy[:, 1] -= 10 ... return xy - >>> warp(image, shift_right) + >>> warped = warp(image, shift_down) Use a transformation matrix to warp an image (fast): >>> matrix = np.array([[1, 0, 0], [0, 1, -10], [0, 0, 1]]) - >>> warp(image, matrix) + >>> warped = warp(image, matrix) >>> from skimage.transform import ProjectiveTransform - >>> warp(image, ProjectiveTransform(matrix=matrix)) + >>> warped = warp(image, ProjectiveTransform(matrix=matrix)) You can also use the inverse of a geometric transformation (fast): - >>> warp(image, tform.inverse) + >>> warped = warp(image, tform.inverse) """ # Backward API compatibility if reverse_map is not None: + warnings.warn('`reverse_map` parameter is deprecated and replaced by ' + 'the `inverse_map` parameter.') inverse_map = reverse_map - if image.ndim < 2: - raise ValueError("Input must have more than 1 dimension.") + if image.ndim < 2 or image.ndim > 3: + raise ValueError("Input must have 2 or 3 dimensions.") orig_ndim = image.ndim image = np.atleast_3d(img_as_float(image)) ishape = np.array(image.shape) bands = ishape[2] + if output_shape is None: + output_shape = ishape + else: + output_shape = safe_as_int(output_shape) + + out = None # use fast Cython version for specific interpolation orders and input @@ -1042,14 +1096,14 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # inverse_map is a homography elif isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): - matrix = inverse_map._matrix + matrix = inverse_map.params # inverse_map is the inverse of a homography elif (hasattr(inverse_map, '__name__') and inverse_map.__name__ == 'inverse' - and isinstance(get_bound_method_class(inverse_map), - HOMOGRAPHY_TRANSFORMS)): - matrix = np.linalg.inv(six.get_method_self(inverse_map)._matrix) + and get_bound_method_class(inverse_map) \ + in HOMOGRAPHY_TRANSFORMS): + matrix = np.linalg.inv(six.get_method_self(inverse_map).params) if matrix is not None: matrix = matrix.astype(np.double) @@ -1057,17 +1111,13 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, dims = [] for dim in range(image.shape[2]): dims.append(_warp_fast(image[..., dim], matrix, - output_shape=output_shape, - order=order, mode=mode, cval=cval)) + output_shape=output_shape, + order=order, mode=mode, cval=cval)) out = np.dstack(dims) if orig_ndim == 2: out = out[..., 0] if out is None: # use ndimage.map_coordinates - - if output_shape is None: - output_shape = ishape - rows, cols = output_shape[:2] # inverse_map is a transformation matrix as numpy array diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 29344fa8..104fed62 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -15,14 +15,12 @@ from skimage.draw import circle_perimeter cdef double PI_2 = 1.5707963267948966 cdef double NEG_PI_2 = -PI_2 - -cdef inline Py_ssize_t round(double r): - return ((r + 0.5) if (r > 0.0) else (r - 0.5)) +from skimage._shared.interpolation cimport round -def hough_circle(cnp.ndarray img, - cnp.ndarray[ndim=1, dtype=cnp.intp_t] radius, - char normalize=True, char full_output=False): +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 diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 64b129dd..23b56164 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -252,16 +252,16 @@ def downscale_local_mean(image, factors, cval=0): image : ndarray Down-sampled image with same number of dimensions as input image. - Example - ------- + Examples + -------- >>> 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]]) + array([[ 3.5, 4. ], + [ 5.5, 4.5]]) """ return block_reduce(image, factors, np.mean, cval) diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 0a7d35a2..cbb4caa6 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -1,6 +1,7 @@ import numpy as np from scipy import ndimage from skimage import measure, morphology +from ._hough_transform import _hough_circle def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, @@ -40,8 +41,7 @@ def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, Examples -------- - >>> import numpy as np - >>> from skimage.transform import hough_line, hough_peaks + >>> from skimage.transform import hough_line, hough_line_peaks >>> from skimage.draw import line >>> img = np.zeros((15, 15), dtype=np.bool_) >>> rr, cc = line(0, 0, 14, 14) @@ -49,11 +49,9 @@ def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, >>> rr, cc = line(0, 14, 14, 0) >>> img[cc, rr] = 1 >>> hspace, angles, dists = hough_line(img) - >>> hspace, angles, dists = hough_peaks(hspace, angles, dists) - >>> angles - array([ 0.74590887, -0.79856126]) - >>> dists - array([ 10.74418605, 0.51162791]) + >>> hspace, angles, dists = hough_line_peaks(hspace, angles, dists) + >>> len(angles) + 2 """ @@ -73,9 +71,9 @@ def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, hspace *= mask hspace_t = hspace > threshold - label_hspace = morphology.label(hspace_t) - props = measure.regionprops(label_hspace, ['Centroid']) - coords = np.array([np.round(p['Centroid']) for p in props], dtype=int) + label_hspace = measure.label(hspace_t) + props = measure.regionprops(label_hspace) + coords = np.array([np.round(p.centroid) for p in props], dtype=int) hspace_peaks = [] dist_peaks = [] @@ -125,3 +123,31 @@ def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, angle_peaks = angle_peaks[idx_maxsort] return hspace_peaks, angle_peaks, dist_peaks + + +def hough_circle(image, radius, normalize=True, full_output=False): + """Perform a circular Hough transform. + + Parameters + ---------- + image : (M, N) ndarray + Input image with nonzero values representing edges. + radius : ndarray + Radii at which to compute the Hough transform. + normalize : boolean, optional (default True) + Normalize the accumulator with the number + 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 + 2R, N + 2R) ndarray) + Hough transform accumulator for each radius. + R designates the larger radius if full_output is True. + Otherwise, R = 0. + """ + return _hough_circle(image, radius.astype(np.intp), + normalize=normalize, full_output=full_output) diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index c7f9f832..14a9305c 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -1,7 +1,9 @@ import numpy as np -from numpy.testing import assert_equal, assert_array_almost_equal +from numpy.testing import (assert_equal, assert_array_almost_equal, + assert_raises) from skimage.transform._geometric import _stackcopy -from skimage.transform import (estimate_transform, +from skimage.transform._geometric import GeometricTransform +from skimage.transform import (estimate_transform, matrix_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) @@ -38,23 +40,35 @@ def test_stackcopy(): assert_array_almost_equal(x[..., i], y) +def test_estimate_transform(): + for tform in ('similarity', 'affine', 'projective', 'polynomial'): + estimate_transform(tform, SRC[:2, :], DST[:2, :]) + assert_raises(ValueError, estimate_transform, 'foobar', + SRC[:2, :], DST[:2, :]) + + +def test_matrix_transform(): + tform = AffineTransform(scale=(0.1, 0.5), rotation=2) + assert_equal(tform(SRC), matrix_transform(SRC, tform._matrix)) + + def test_similarity_estimation(): # exact solution tform = estimate_transform('similarity', SRC[:2, :], DST[:2, :]) assert_array_almost_equal(tform(SRC[:2, :]), DST[:2, :]) - assert_equal(tform._matrix[0, 0], tform._matrix[1, 1]) - assert_equal(tform._matrix[0, 1], - tform._matrix[1, 0]) + assert_equal(tform.params[0, 0], tform.params[1, 1]) + assert_equal(tform.params[0, 1], - tform.params[1, 0]) # over-determined tform2 = estimate_transform('similarity', SRC, DST) assert_array_almost_equal(tform2.inverse(tform2(SRC)), SRC) - assert_equal(tform2._matrix[0, 0], tform2._matrix[1, 1]) - assert_equal(tform2._matrix[0, 1], - tform2._matrix[1, 0]) + assert_equal(tform2.params[0, 0], tform2.params[1, 1]) + assert_equal(tform2.params[0, 1], - tform2.params[1, 0]) # via estimate method tform3 = SimilarityTransform() tform3.estimate(SRC, DST) - assert_array_almost_equal(tform3._matrix, tform2._matrix) + assert_array_almost_equal(tform3.params, tform2.params) def test_similarity_init(): @@ -69,11 +83,32 @@ def test_similarity_init(): assert_array_almost_equal(tform.translation, translation) # init with transformation matrix - tform2 = SimilarityTransform(tform._matrix) + tform2 = SimilarityTransform(tform.params) assert_array_almost_equal(tform2.scale, scale) assert_array_almost_equal(tform2.rotation, rotation) assert_array_almost_equal(tform2.translation, translation) + # test special case for scale if rotation=0 + scale = 0.1 + rotation = 0 + translation = (1, 1) + tform = SimilarityTransform(scale=scale, rotation=rotation, + translation=translation) + assert_array_almost_equal(tform.scale, scale) + assert_array_almost_equal(tform.rotation, rotation) + assert_array_almost_equal(tform.translation, translation) + + + # test special case for scale if rotation=90deg + scale = 0.1 + rotation = np.pi / 2 + translation = (1, 1) + tform = SimilarityTransform(scale=scale, rotation=rotation, + translation=translation) + assert_array_almost_equal(tform.scale, scale) + assert_array_almost_equal(tform.rotation, rotation) + assert_array_almost_equal(tform.translation, translation) + def test_affine_estimation(): # exact solution @@ -87,7 +122,7 @@ def test_affine_estimation(): # via estimate method tform3 = AffineTransform() tform3.estimate(SRC, DST) - assert_array_almost_equal(tform3._matrix, tform2._matrix) + assert_array_almost_equal(tform3.params, tform2.params) def test_affine_init(): @@ -104,7 +139,7 @@ def test_affine_init(): assert_array_almost_equal(tform.translation, translation) # init with transformation matrix - tform2 = AffineTransform(tform._matrix) + tform2 = AffineTransform(tform.params) assert_array_almost_equal(tform2.scale, scale) assert_array_almost_equal(tform2.rotation, rotation) assert_array_almost_equal(tform2.shear, shear) @@ -131,14 +166,14 @@ def test_projective_estimation(): # via estimate method tform3 = ProjectiveTransform() tform3.estimate(SRC, DST) - assert_array_almost_equal(tform3._matrix, tform2._matrix) + assert_array_almost_equal(tform3.params, tform2.params) def test_projective_init(): tform = estimate_transform('projective', SRC, DST) # init with transformation matrix - tform2 = ProjectiveTransform(tform._matrix) - assert_array_almost_equal(tform2._matrix, tform._matrix) + tform2 = ProjectiveTransform(tform.params) + assert_array_almost_equal(tform2.params, tform.params) def test_polynomial_estimation(): @@ -149,31 +184,75 @@ def test_polynomial_estimation(): # via estimate method tform2 = PolynomialTransform() tform2.estimate(SRC, DST, order=10) - assert_array_almost_equal(tform2._params, tform._params) + assert_array_almost_equal(tform2.params, tform.params) def test_polynomial_init(): tform = estimate_transform('polynomial', SRC, DST, order=10) # init with transformation parameters - tform2 = PolynomialTransform(tform._params) - assert_array_almost_equal(tform2._params, tform._params) + tform2 = PolynomialTransform(tform.params) + assert_array_almost_equal(tform2.params, tform.params) def test_polynomial_default_order(): tform = estimate_transform('polynomial', SRC, DST) tform2 = estimate_transform('polynomial', SRC, DST, order=2) - assert_array_almost_equal(tform2._params, tform._params) + assert_array_almost_equal(tform2.params, tform.params) + + +def test_polynomial_inverse(): + assert_raises(Exception, PolynomialTransform().inverse, 0) def test_union(): tform1 = SimilarityTransform(scale=0.1, rotation=0.3) tform2 = SimilarityTransform(scale=0.1, rotation=0.9) tform3 = SimilarityTransform(scale=0.1 ** 2, rotation=0.3 + 0.9) - tform = tform1 + tform2 - assert_array_almost_equal(tform._matrix, tform3._matrix) + tform1 = AffineTransform(scale=(0.1, 0.1), rotation=0.3) + tform2 = SimilarityTransform(scale=0.1, rotation=0.9) + tform3 = SimilarityTransform(scale=0.1 ** 2, rotation=0.3 + 0.9) + tform = tform1 + tform2 + assert_array_almost_equal(tform._matrix, tform3._matrix) + assert tform.__class__ == ProjectiveTransform + + +def test_union_differing_types(): + tform1 = SimilarityTransform() + tform2 = PolynomialTransform() + assert_raises(TypeError, tform1.__add__, tform2) + + +def test_geometric_tform(): + tform = GeometricTransform() + assert_raises(NotImplementedError, tform, 0) + assert_raises(NotImplementedError, tform.inverse, 0) + assert_raises(NotImplementedError, tform.__add__, 0) + + +def test_invalid_input(): + assert_raises(ValueError, ProjectiveTransform, np.zeros((2, 3))) + assert_raises(ValueError, AffineTransform, np.zeros((2, 3))) + assert_raises(ValueError, SimilarityTransform, np.zeros((2, 3))) + + assert_raises(ValueError, AffineTransform, + matrix=np.zeros((2, 3)), scale=1) + assert_raises(ValueError, SimilarityTransform, + matrix=np.zeros((2, 3)), scale=1) + + assert_raises(ValueError, PolynomialTransform, np.zeros((3, 3))) + + +def test_deprecated_params_attributes(): + for t in ('projective', 'affine', 'similarity'): + tform = estimate_transform(t, SRC, DST) + assert_equal(tform._matrix, tform.params) + + tform = estimate_transform('polynomial', SRC, DST, order=3) + assert_equal(tform._params, tform.params) + if __name__ == "__main__": from numpy.testing import run_module_suite diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 6fbc3f62..9e51ad0a 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -5,17 +5,14 @@ from numpy.testing import assert_raises import itertools import os.path -from skimage.transform import radon, iradon +from skimage.transform import radon, iradon, iradon_sart, rescale from skimage.io import imread from skimage import data_dir -__PHANTOM = imread(os.path.join(data_dir, "phantom.png"), +PHANTOM = imread(os.path.join(data_dir, "phantom.png"), as_grey=True)[::2, ::2] - - -def _get_phantom(): - return __PHANTOM +PHANTOM = rescale(PHANTOM, 0.5, order=1) def _debug_plot(original, result, sinogram=None): @@ -39,7 +36,7 @@ def _debug_plot(original, result, sinogram=None): plt.show() -def rescale(x): +def _rescale_intensity(x): x = x.astype(float) x -= x.min() x /= x.max() @@ -117,7 +114,7 @@ def test_iradon_center(): def check_radon_iradon(interpolation_type, filter_type): debug = False - image = _get_phantom() + image = PHANTOM reconstructed = iradon(radon(image), filter=filter_type, interpolation=interpolation_type) delta = np.mean(np.abs(image - reconstructed)) @@ -128,7 +125,7 @@ def check_radon_iradon(interpolation_type, filter_type): if interpolation_type == 'nearest': allowed_delta = 0.03 else: - allowed_delta = 0.02 + allowed_delta = 0.025 else: allowed_delta = 0.05 assert delta < allowed_delta @@ -156,7 +153,7 @@ def test_iradon_angles(): radon_image_200 = radon(image, theta=np.linspace(0, 180, nb_angles, endpoint=False)) reconstructed = iradon(radon_image_200) - delta_200 = np.mean(abs(rescale(image) - rescale(reconstructed))) + delta_200 = np.mean(abs(_rescale_intensity(image) - _rescale_intensity(reconstructed))) assert delta_200 < 0.03 # Lower number of projections nb_angles = 80 @@ -225,7 +222,7 @@ def test_radon_circle(): 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) + image = _rescale_intensity(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) @@ -314,14 +311,9 @@ def test_order_angles_golden_ratio(): 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) + image = rescale(PHANTOM, 0.8) 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.), @@ -344,15 +336,15 @@ def test_iradon_sart(): delta = np.mean(np.abs(reconstructed - image)) print('delta (1 iteration) =', delta) - assert delta < 0.016 * error_factor + assert delta < 0.02 * 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 + assert delta < 0.014 * 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 + assert delta < 0.018 * error_factor np.random.seed(1239867) shifts = np.random.uniform(-3, 3, sinogram.shape[1]) @@ -377,7 +369,8 @@ def test_iradon_sart(): delta = np.mean(np.abs(reconstructed - image)) print('delta (1 iteration, shifted sinogram) =', delta) - assert delta < 0.018 * error_factor + assert delta < 0.022 * error_factor + if __name__ == "__main__": from numpy.testing import run_module_suite diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 7f7ef47d..07054ab7 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -1,4 +1,5 @@ -from numpy.testing import assert_array_almost_equal, run_module_suite, assert_array_equal +from numpy.testing import (assert_array_almost_equal, run_module_suite, + assert_array_equal, assert_raises) import numpy as np from scipy.ndimage import map_coordinates @@ -234,5 +235,27 @@ def test_downscale_local_mean(): assert_array_equal(expected2, out2) +def test_invalid(): + assert_raises(ValueError, warp, np.ones((4, )), SimilarityTransform()) + assert_raises(ValueError, warp, np.ones((4, 3, 3, 3)), + SimilarityTransform()) + + +def test_inverse(): + tform = SimilarityTransform(scale=0.5, rotation=0.1) + inverse_tform = SimilarityTransform(matrix=np.linalg.inv(tform.params)) + image = np.arange(10 * 10).reshape(10, 10).astype(np.double) + assert_array_equal(warp(image, inverse_tform), warp(image, tform.inverse)) + + +def test_slow_warp_nonint_oshape(): + image = np.random.random((5, 5)) + + assert_raises(ValueError, warp, image, lambda xy: xy, + output_shape=(13.1, 19.5)) + + warp(image, lambda xy: xy, output_shape=(13.0001, 19.9999)) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index 9cd2bc50..5577e46b 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -3,14 +3,7 @@ from .dtype import (img_as_float, img_as_int, img_as_uint, img_as_ubyte, 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 .arraypad import pad from ._regular_grid import regular_grid from .unique import unique_rows diff --git a/skimage/util/_regular_grid.py b/skimage/util/_regular_grid.py index 898a4aed..92b32082 100644 --- a/skimage/util/_regular_grid.py +++ b/skimage/util/_regular_grid.py @@ -29,21 +29,21 @@ def regular_grid(ar_shape, n_points): >>> ar = np.zeros((20, 40)) >>> g = regular_grid(ar.shape, 8) >>> g - [slice(5.0, None, 10.0), slice(5.0, None, 10.0)] + [slice(5, None, 10), slice(5, None, 10)] >>> 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)] + [slice(2, None, 5), slice(2, None, 5)] >>> 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)] + [slice(1, None, 3), slice(5, None, 10), slice(5, None, 10)] >>> ar[g] = 1 >>> ar.sum() 8.0 @@ -64,8 +64,8 @@ def regular_grid(ar_shape, n_points): (1.0 / (ndim - dim - 1))) if (sorted_dims >= stepsizes).all(): break - starts = stepsizes // 2 - stepsizes = np.round(stepsizes) + starts = (stepsizes // 2).astype(int) + stepsizes = np.round(stepsizes).astype(int) slices = [slice(start, None, step) for start, step in zip(starts, stepsizes)] slices = [slices[i] for i in unsort_dim_idxs] diff --git a/skimage/util/arraypad.py b/skimage/util/arraypad.py index 93f66bb8..0bc92d7b 100644 --- a/skimage/util/arraypad.py +++ b/skimage/util/arraypad.py @@ -4,8 +4,8 @@ of an n-dimensional array. """ from __future__ import division, absolute_import, print_function -from skimage._shared.six import integer_types +from six import integer_types import numpy as np try: @@ -1027,7 +1027,11 @@ def _normalize_shape(narray, shape): """ normshp = None shapelen = len(np.shape(narray)) - if (isinstance(shape, int)) or shape is None: + + if isinstance(shape, np.ndarray): + shape = shape.tolist() + + if isinstance(shape, (int, float)) or shape is None: normshp = ((shape, shape), ) * shapelen elif (isinstance(shape, (tuple, list)) and isinstance(shape[0], (tuple, list)) @@ -1220,26 +1224,26 @@ def pad(array, pad_width, mode=None, **kwargs): Examples -------- >>> a = [1, 2, 3, 4, 5] - >>> np.lib.pad(a, (2,3), 'constant', constant_values=(4,6)) + >>> 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') + >>> 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)) + >>> 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') + >>> pad(a, (2,), 'maximum') array([5, 5, 1, 2, 3, 4, 5, 5, 5]) - >>> np.lib.pad(a, (2,), 'mean') + >>> pad(a, (2,), 'mean') array([3, 3, 1, 2, 3, 4, 5, 3, 3]) - >>> np.lib.pad(a, (2,), 'median') + >>> 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') + >>> 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], @@ -1249,19 +1253,19 @@ def pad(array, pad_width, mode=None, **kwargs): [1, 1, 1, 2, 1, 1, 1]]) >>> a = [1, 2, 3, 4, 5] - >>> np.lib.pad(a, (2,3), 'reflect') + >>> 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') + >>> 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') + >>> 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') + >>> 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') + >>> pad(a, (2,3), 'wrap') array([4, 5, 1, 2, 3, 4, 5, 1, 2, 3]) >>> def padwithtens(vector, pad_width, iaxis, kwargs): @@ -1272,7 +1276,7 @@ def pad(array, pad_width, mode=None, **kwargs): >>> a = np.arange(6) >>> a = a.reshape((2,3)) - >>> np.lib.pad(a, 2, padwithtens) + >>> 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], diff --git a/skimage/util/montage.py b/skimage/util/montage.py index 805b78a8..4bc6aca8 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -51,31 +51,32 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=None): >>> 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 - [[[ 0 1] - [ 2 3]] - [[ 4 5] - [ 6 7]] - [[ 8 9] - [10 11]]] + >>> arr_in # doctest: +NORMALIZE_WHITESPACE + array([[[ 0, 1], + [ 2, 3]], + [[ 4, 5], + [ 6, 7]], + [[ 8, 9], + [10, 11]]]) >>> arr_out = montage2d(arr_in) - >>> print(arr_out.shape) + >>> arr_out.shape (4, 4) - >>> 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()) + >>> arr_out + array([[ 0. , 1. , 4. , 5. ], + [ 2. , 3. , 6. , 7. ], + [ 8. , 9. , 5.5, 5.5], + [ 10. , 11. , 5.5, 5.5]]) + >>> 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) + >>> arr_out_nonsquare = montage2d(arr_in, grid_shape=(1, 3)) + >>> arr_out_nonsquare + array([[ 0., 1., 4., 5., 8., 9.], + [ 2., 3., 6., 7., 10., 11.]]) + >>> arr_out_nonsquare.shape + (2, 6) + """ + assert arr_in.ndim == 3 n_images, height, width = arr_in.shape diff --git a/skimage/util/shape.py b/skimage/util/shape.py index f91286c3..4633ada6 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -2,6 +2,7 @@ __all__ = ['view_as_blocks', 'view_as_windows'] import numpy as np from numpy.lib.stride_tricks import as_strided +from warnings import warn def view_as_blocks(arr_in, block_shape): @@ -11,17 +12,17 @@ def view_as_blocks(arr_in, block_shape): Parameters ---------- - arr_in: ndarray - The n-dimensional input array. - - block_shape: tuple + arr_in : ndarray + N-d input array. + block_shape : tuple The shape of the block. Each dimension must divide evenly into the corresponding dimensions of `arr_in`. Returns ------- - arr_out: ndarray - Block view of the input array. + arr_out : ndarray + Block view of the input array. If `arr_in` is non-contiguous, a copy + is made. Examples -------- @@ -70,8 +71,6 @@ def view_as_blocks(arr_in, block_shape): [[[76, 77], [82, 83]]]]) """ - - # -- basic checks on arguments if not isinstance(block_shape, tuple): raise TypeError('block needs to be a tuple') @@ -88,9 +87,14 @@ def view_as_blocks(arr_in, block_shape): raise ValueError("'block_shape' is not compatible with 'arr_in'") # -- restride the array to build the block view + + if not arr_in.flags.contiguous: + warn(RuntimeWarning("Cannot provide views on a non-contiguous input " + "array without copying.")) + arr_in = np.ascontiguousarray(arr_in) - new_shape = tuple(arr_shape / block_shape) + tuple(block_shape) + new_shape = tuple(arr_shape // block_shape) + tuple(block_shape) new_strides = tuple(arr_in.strides * block_shape) + arr_in.strides arr_out = as_strided(arr_in, shape=new_shape, strides=new_strides) @@ -106,19 +110,21 @@ def view_as_windows(arr_in, window_shape, step=1): Parameters ---------- - arr_in: ndarray - The n-dimensional input array. - window_shape: tuple + arr_in : ndarray + N-d 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 + step : int, optional Number of elements to skip when moving the window forward (by - default, move forward by one). + default, move forward by one). The value must be equal or larger + than one. Returns ------- - arr_out: ndarray - (rolling) window view of the input array. + arr_out : ndarray + (rolling) window view of the input array. If `arr_in` is + non-contiguous, a copy is made. Notes ----- @@ -227,6 +233,10 @@ def view_as_windows(arr_in, window_shape, step=1): raise ValueError("`window_shape` is too small") # -- build rolling window view + if not arr_in.flags.contiguous: + warn(RuntimeWarning("Cannot provide views on a non-contiguous input " + "array without copying.")) + arr_in = np.ascontiguousarray(arr_in) new_shape = tuple((arr_shape - window_shape) // step + 1) + \ diff --git a/skimage/util/tests/test_arraypad.py b/skimage/util/tests/test_arraypad.py index 008c3516..3fbe50a1 100644 --- a/skimage/util/tests/test_arraypad.py +++ b/skimage/util/tests/test_arraypad.py @@ -13,209 +13,219 @@ 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, + 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., + 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.]) + 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, + 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, + 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]) + 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, + 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, + 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]) + 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, + 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, 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]) + 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, + 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, 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]) + 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, + 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., + 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]) + 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], + b = np.array( + [[4, 4, 5, 4, 4], - [3, 3, 1, 4, 3], - [5, 4, 5, 9, 5], - [8, 9, 8, 2, 8], + [3, 3, 1, 4, 3], + [5, 4, 5, 9, 5], + [8, 9, 8, 2, 8], - [4, 4, 5, 4, 4]]) + [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], + b = np.array( + [[5, 4, 5, 4, 5], - [3, 3, 1, 4, 3], - [5, 4, 5, 9, 5], - [8, 9, 8, 2, 8], + [3, 3, 1, 4, 3], + [5, 4, 5, 9, 5], + [8, 9, 8, 2, 8], - [5, 4, 5, 4, 5]]) + [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], + 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], - [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, + 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., + 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]) + 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) @@ -223,23 +233,73 @@ 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, + 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, + 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]) + 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) + + def test_check_constant_float(self): + a = np.arange(100) + a = pad(a, (25, 20), 'constant', constant_values=-1.1) + b = np.array( + [-1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, + -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, + -1.1, -1.1, -1.1, -1.1, -1.1, + + 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, + + -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] + ) + assert_array_equal(a, b) + + def test_check_constant_float(self): + a = np.arange(100, dtype=float) + a = pad(a, (25, 20), 'constant', constant_values=(-1.1, -1.2)) + b = np.array( + [-1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, + -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, + -1.1, -1.1, -1.1, -1.1, -1.1, + + 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, + + -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, + -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2] + ) assert_array_equal(a, b) @@ -247,24 +307,25 @@ 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, + 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, + 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.]) + 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) @@ -272,67 +333,70 @@ 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, + 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, + 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]) + 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], + 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], - [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]]) + [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], + 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], - [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): @@ -355,83 +419,85 @@ 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, + 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, + 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]) + 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], + 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], - [ 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): @@ -450,19 +516,21 @@ class TestStatLen(TestCase): 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], + 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], + [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]]) + [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) @@ -471,58 +539,90 @@ class TestEdge(TestCase): 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], + 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], + [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]]) + [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) +class TestZeroPadWidth(TestCase): + def test_zero_pad_width(self): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + for pad_width in (0, (0, 0), ((0, 0), (0, 0))): + assert_array_equal(arr, pad(arr, pad_width, mode='constant')) -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) +class TestNdarrayPadWidth(TestCase): + def test_check_simple(self): + a = np.arange(12) + a = np.reshape(a, (4, 3)) + a = pad(a, np.array(((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_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) +class ValueError1(TestCase): + def test_check_simple(self): + 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(self): + 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(self): + 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) +class ValueError2(TestCase): + def test_check_simple(self): + 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')) +class ValueError3(TestCase): + def test_check_simple(self): + 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) if __name__ == "__main__": diff --git a/skimage/util/tests/test_shape.py b/skimage/util/tests/test_shape.py index b6975d0f..ee897e7c 100644 --- a/skimage/util/tests/test_shape.py +++ b/skimage/util/tests/test_shape.py @@ -1,46 +1,42 @@ import numpy as np from nose.tools import raises -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_warns + from skimage.util.shape import view_as_blocks, view_as_windows +from skimage._shared.utils import all_warnings @raises(TypeError) def test_view_as_blocks_block_not_a_tuple(): - A = np.arange(10) view_as_blocks(A, [5]) @raises(ValueError) def test_view_as_blocks_negative_shape(): - A = np.arange(10) view_as_blocks(A, (-2,)) @raises(ValueError) def test_view_as_blocks_block_too_large(): - A = np.arange(10) view_as_blocks(A, (11,)) @raises(ValueError) def test_view_as_blocks_wrong_block_dimension(): - A = np.arange(10) view_as_blocks(A, (2, 2)) @raises(ValueError) def test_view_as_blocks_1D_array_wrong_block_shape(): - A = np.arange(10) view_as_blocks(A, (3,)) def test_view_as_blocks_1D_array(): - A = np.arange(10) B = view_as_blocks(A, (5,)) assert_equal(B, np.array([[0, 1, 2, 3, 4], @@ -48,7 +44,6 @@ def test_view_as_blocks_1D_array(): def test_view_as_blocks_2D_array(): - A = np.arange(4 * 4).reshape(4, 4) B = view_as_blocks(A, (2, 2)) assert_equal(B[0, 1], np.array([[2, 3], @@ -57,7 +52,6 @@ def test_view_as_blocks_2D_array(): def test_view_as_blocks_3D_array(): - A = np.arange(4 * 4 * 6).reshape(4, 4, 6) B = view_as_blocks(A, (1, 2, 2)) assert_equal(B.shape, (4, 2, 3, 1, 2, 2)) @@ -69,41 +63,40 @@ def test_view_as_blocks_3D_array(): @raises(TypeError) def test_view_as_windows_input_not_array(): - A = [1, 2, 3, 4, 5] view_as_windows(A, (2,)) @raises(TypeError) def test_view_as_windows_window_not_tuple(): - A = np.arange(10) view_as_windows(A, [2]) @raises(ValueError) def test_view_as_windows_wrong_window_dimension(): - A = np.arange(10) view_as_windows(A, (2, 2)) @raises(ValueError) def test_view_as_windows_negative_window_length(): - A = np.arange(10) view_as_windows(A, (-1,)) @raises(ValueError) def test_view_as_windows_window_too_large(): - A = np.arange(10) view_as_windows(A, (11,)) -def test_view_as_windows_1D(): +@raises(ValueError) +def test_view_as_windows_step_below_one(): + A = np.arange(10) + view_as_windows(A, (11,), step=0.9) +def test_view_as_windows_1D(): A = np.arange(10) window_shape = (3,) B = view_as_windows(A, window_shape) @@ -118,7 +111,6 @@ def test_view_as_windows_1D(): def test_view_as_windows_2D(): - A = np.arange(5 * 4).reshape(5, 4) window_shape = (4, 3) B = view_as_windows(A, window_shape) @@ -141,7 +133,7 @@ def test_view_as_windows_2D(): [17, 18, 19]]]])) -def test_view_as_windows_With_skip(): +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], @@ -157,5 +149,14 @@ def test_view_as_windows_With_skip(): assert_equal(C.shape, (1, 1, 2, 2)) +def test_views_non_contiguous(): + A = np.arange(16).reshape((4, 4)) + A = A[::2, :] + + with all_warnings(): + assert_warns(RuntimeWarning, view_as_blocks, A, (2, 2)) + assert_warns(RuntimeWarning, view_as_windows, A, (2, 2)) + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index 3b4132f0..cc5f9705 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -50,8 +50,7 @@ class PaintTool(CanvasToolBase): self.alpha = alpha self.cmap = LABELS_CMAP self._overlay_plot = None - self._shape = overlay_shape - self.overlay = np.zeros(overlay_shape, dtype='uint8') + self.shape = overlay_shape self._cursor = plt.Rectangle((0, 0), 0, 0, **props) self._cursor.set_visible(False) @@ -109,6 +108,19 @@ class PaintTool(CanvasToolBase): self._overlay_plot.set_data(image) self.redraw() + @property + def shape(self): + return self._shape + + @shape.setter + def shape(self, shape): + self._shape = shape + if not self._overlay_plot is None: + self._overlay_plot.set_extent((-0.5, shape[1] + 0.5, + shape[0] + 0.5, -0.5)) + self.radius = self._radius + self.overlay = np.zeros(shape, dtype='uint8') + def _on_key_press(self, event): if event.key == 'enter': self.callback_on_enter(self.geometry) @@ -158,8 +170,8 @@ class PaintTool(CanvasToolBase): class CenteredWindow(object): """Window that create slices numpy arrays over 2D windows. - Example - ------- + Examples + -------- >>> a = np.arange(16).reshape(4, 4) >>> w = CenteredWindow(1, a.shape) >>> a[w.at(1, 1)] diff --git a/skimage/viewer/plugins/__init__.py b/skimage/viewer/plugins/__init__.py index e69de29b..7bb07333 100644 --- a/skimage/viewer/plugins/__init__.py +++ b/skimage/viewer/plugins/__init__.py @@ -0,0 +1,9 @@ +from .base import Plugin +from .canny import CannyPlugin +from .color_histogram import ColorHistogram +from .crop import Crop +from .labelplugin import LabelPainter +from .lineprofile import LineProfile +from .measure import Measure +from .overlayplugin import OverlayPlugin +from .plotplugin import PlotPlugin diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 50b7601c..baded1cb 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -5,9 +5,15 @@ from warnings import warn import numpy as np -from ..qt import QtGui +from ..qt import QtGui, qt_api from ..qt.QtCore import Qt, Signal from ..utils import RequiredAttr, init_qtapp +from skimage._shared.testing import doctest_skip_parser + +if qt_api is not None: + has_qt = True +else: + has_qt = False class Plugin(QtGui.QDialog): @@ -52,13 +58,14 @@ class Plugin(QtGui.QDialog): >>> from skimage.viewer.widgets import Slider >>> from skimage import data >>> - >>> plugin = Plugin(image_filter=lambda img, threshold: img > threshold) - >>> plugin += Slider('threshold', 0, 255) + >>> plugin = Plugin(image_filter=lambda img, + ... threshold: img > threshold) # doctest: +SKIP + >>> plugin += Slider('threshold', 0, 255) # doctest: +SKIP >>> >>> image = data.coins() - >>> viewer = ImageViewer(image) - >>> viewer += plugin - >>> # viewer.show() + >>> viewer = ImageViewer(image) # doctest: +SKIP + >>> viewer += plugin # doctest: +SKIP + >>> thresholded = viewer.show()[0][0] # doctest: +SKIP The plugin will automatically delegate parameters to `image_filter` based on its parameter type, i.e., `ptype` (widgets for required arguments must @@ -99,7 +106,7 @@ class Plugin(QtGui.QDialog): self.row = 0 self.arguments = [] - self.keyword_arguments= {} + self.keyword_arguments = {} self.useblit = useblit self.cids = [] @@ -182,8 +189,13 @@ class Plugin(QtGui.QDialog): This method is called by the viewer when the original image is updated. """ self.arguments[0] = image + self._on_new_image(image) self.filter_image() + def _on_new_image(self, image): + """Override this method to update your plugin for new images.""" + pass + @property def filtered_image(self): """Return filtered image.""" @@ -235,3 +247,22 @@ class Plugin(QtGui.QDialog): """Remove artists that are connected to the image viewer.""" for a in self.artists: a.remove() + + def output(self): + """Return the plugin's representation and data. + + Returns + ------- + image : array, same shape as ``self.image_viewer.image``, or None + The filtered image. + data : None + Any data associated with the plugin. + + Notes + ----- + Derived classes should override this method to return a tuple + containing an *overlay* of the same shape of the image, and a + *data* object. Either of these is optional: return ``None`` if + you don't want to return a value. + """ + return (self.image_viewer.image, None) diff --git a/skimage/viewer/plugins/color_histogram.py b/skimage/viewer/plugins/color_histogram.py index 39a75004..dd54a93d 100644 --- a/skimage/viewer/plugins/color_histogram.py +++ b/skimage/viewer/plugins/color_histogram.py @@ -20,16 +20,21 @@ class ColorHistogram(PlotPlugin): 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) + self._on_new_image(image_viewer.image) + + def _on_new_image(self, image): + self.lab_image = color.rgb2lab(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] + self.mask = np.ones(L.shape, bool) bins = np.arange(left, right) - hist, x_edges, y_edges = np.histogram2d(a.flatten(), b.flatten(), bins, - normed=True) - + hist, x_edges, y_edges = np.histogram2d(a.flatten(), b.flatten(), + bins, normed=True) + self.data = {'bins': bins, 'hist': hist, 'edges': (x_edges, y_edges), + 'extents': (left, right, left, right)} # 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)) @@ -46,15 +51,36 @@ class ColorHistogram(PlotPlugin): def ab_selected(self, extents): x0, x1, y0, y1 = extents + self.data['extents'] = 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.mask = ((a > y0) & (a < y1)) & ((b > x0) & (b < x1)) + lab_masked[..., 1:][~self.mask.T] = 0 self.image_viewer.image = color.lab2rgb(lab_masked) + def output(self): + """Return the image mask and the histogram data. + + Returns + ------- + mask : array of bool, same shape as image + The selected pixels. + data : dict + The data describing the histogram and the selected region. + Keys: + - 'bins' : array of float, the bin boundaries for both + `a` and `b` channels. + - 'hist' : 2D array of float, the normalized histogram. + - 'edges' : tuple of array of float, the bin edges + along each dimension + - 'extents' : tuple of float, the left and right and + top and bottom of the selected region. + """ + return (self.mask, self.data) + def pct_total_area(image, percentile=0.80): """Return threshold value based on percentage of total area. @@ -65,5 +91,3 @@ def pct_total_area(image, percentile=0.80): sorted_pixels = np.sort(image.flat) return sorted_pixels[idx] - - diff --git a/skimage/viewer/plugins/labelplugin.py b/skimage/viewer/plugins/labelplugin.py index b3c289f1..06ba2dd9 100644 --- a/skimage/viewer/plugins/labelplugin.py +++ b/skimage/viewer/plugins/labelplugin.py @@ -43,6 +43,10 @@ class LabelPainter(Plugin): self.paint_tool.label = self._label_widget.index = 1 self.artists.append(self.paint_tool) + def _on_new_image(self, image): + """Update plugin for new images.""" + self.paint_tool.shape = image.shape + def on_enter(self, overlay): pass diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 0d555eb5..d0011c75 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -1,8 +1,9 @@ -import warnings +from __future__ import division import numpy as np -import scipy.ndimage as ndi from skimage.util.dtype import dtype_range +from skimage import draw +from skimage import measure from .plotplugin import PlotPlugin from ..canvastools import ThickLineTool @@ -20,8 +21,6 @@ class LineProfile(PlotPlugin): ---------- 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: @@ -35,10 +34,6 @@ class LineProfile(PlotPlugin): def __init__(self, maxdist=10, epsilon='deprecated', limits='image', **kwargs): super(LineProfile, self).__init__(**kwargs) - - if not epsilon == 'deprecated': - warnings.warn("Parameter `epsilon` deprecated; use `maxdist`.") - maxdist = epsilon self.maxdist = maxdist self._limit_type = limits print(self.help()) @@ -70,7 +65,11 @@ class LineProfile(PlotPlugin): on_change=self.line_changed) self.line_tool.end_points = np.transpose([x, y]) - scan_data = profile_line(image, self.line_tool.end_points) + scan_data = measure.profile_line(image, + *self.line_tool.end_points[:, ::-1]) + self.scan_data = scan_data + if scan_data.ndim == 1: + scan_data = scan_data[:, np.newaxis] self.reset_axes(scan_data) @@ -104,8 +103,12 @@ class LineProfile(PlotPlugin): 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) + scan = measure.profile_line(self.image_viewer.original_image, + *end_points[:, ::-1], + linewidth=self.line_tool.linewidth) + self.scan_data = scan + if scan.ndim == 1: + scan = scan[:, np.newaxis] if scan.shape[1] != len(self.profile): self.reset_axes(scan) @@ -131,79 +134,32 @@ class LineProfile(PlotPlugin): scan_data[:, 1], 'g-', scan_data[:, 2], 'b-') + def output(self): + """Return the drawn line and the resulting scan. -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] + Returns + ------- + line_image : (M, N) uint8 array, same shape as image + An array of 0s with the scanned line set to 255. + If the linewidth of the line tool is greater than 1, + sets the values within the profiled polygon to 128. + scan : (P,) or (P, 3) array of int or float + The line scan values across the image. + """ + end_points = self.line_tool.end_points + line_image = np.zeros(self.image_viewer.original_image.shape[:2], + np.uint8) + width = self.line_tool.linewidth + if width > 1: + rp, cp = measure.profile._line_profile_coordinates( + *end_points[:, ::-1], linewidth=width) + # the points are aliased, so create a polygon using the corners + yp = np.rint(rp[[0, 0, -1, -1],[0, -1, -1, 0]]).astype(int) + xp = np.rint(cp[[0, 0, -1, -1],[0, -1, -1, 0]]).astype(int) + rp, cp = draw.polygon(yp, xp, line_image.shape) + line_image[rp, cp] = 128 + (x1, y1), (x2, y2) = end_points.astype(int) + rr, cc = draw.line(y1, x1, y2, x2) + line_image[rr, cc] = 255 + return line_image, self.scan_data - # 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 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 - - Returns - ------- - return_value : array - The intensity profile along the scan line. The length of the profile - is the ceil of the computed length of the scan line. - """ - point1, point2 = end_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 vertical; shortcuts div0 error - if x1 == x2: - 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) - a = dy / dx - b = y1 - a * x1 - length = np.hypot(dx, dy) - - 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, - yi + y_width, linewidth) for yi in line_y]) - perp_xs = - a * perp_ys + (line_x + a * line_y)[:, np.newaxis] - - perp_lines = np.array([perp_ys, perp_xs]) - 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) - - 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 index 71412a3b..eeaa13b0 100644 --- a/skimage/viewer/plugins/measure.py +++ b/skimage/viewer/plugins/measure.py @@ -47,4 +47,4 @@ class Measure(Plugin): 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) + self._angle.text = '%.1f°' % (180 - np.arctan2(dy, dx) * rad2deg) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index dc5ca060..373d6bf4 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -3,7 +3,8 @@ from warnings import warn from skimage.util.dtype import dtype_range from .base import Plugin from ..utils import ClearColormap, update_axes_image -from skimage._shared import six + +import six __all__ = ['OverlayPlugin'] @@ -44,7 +45,7 @@ class OverlayPlugin(Plugin): self._overlay_plot = None self._overlay = None self.cmap = None - self.color_names = self.colors.keys() + self.color_names = list(self.colors.keys()) def attach(self, image_viewer): super(OverlayPlugin, self).attach(image_viewer) @@ -107,3 +108,15 @@ class OverlayPlugin(Plugin): # clear overlay from ImageViewer on close self.overlay = None super(OverlayPlugin, self).closeEvent(event) + + def output(self): + """Return the overlaid image. + + Returns + ------- + overlay : array, same shape as image + The overlay currently displayed. + data : None + """ + return (self.overlay, None) + diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index 0ce5df73..5de6be7b 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -47,3 +47,7 @@ class PlotPlugin(Plugin): bgcolor = str(bgcolor / 255.) self.fig.patch.set_facecolor(bgcolor) self.layout.addWidget(self.canvas, self.row, 0) + + def _update_original_image(self, image): + super(PlotPlugin, self)._update_original_image(image) + self.redraw() diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py new file mode 100644 index 00000000..7fa4e374 --- /dev/null +++ b/skimage/viewer/tests/test_viewer.py @@ -0,0 +1,48 @@ +import skimage +import skimage.data as data +from skimage.viewer import ImageViewer +from skimage.viewer.qt import qt_api +from numpy.testing import assert_equal, assert_allclose +from numpy.testing.decorators import skipif + + +def setup_line_profile(image): + from skimage.viewer.plugins.lineprofile import LineProfile + viewer = ImageViewer(skimage.img_as_float(image)) + plugin = LineProfile() + viewer += plugin + return plugin + + +@skipif(qt_api is None) +def test_line_profile(): + """ Test a line profile using an ndim=2 image""" + plugin = setup_line_profile(data.camera()) + line_image, scan_data = plugin.output() + for inp in [line_image.nonzero()[0].size, + line_image.sum() / line_image.max(), + scan_data.size]: + assert_equal(inp, 172) + assert_equal(line_image.shape, (512, 512)) + assert_allclose(scan_data.max(), 0.9139, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.2828, rtol=1e-3) + + +@skipif(qt_api is None) +def test_line_profile_rgb(): + """ Test a line profile using an ndim=3 image""" + plugin = setup_line_profile(data.chelsea()) + for i in range(6): + plugin.line_tool._thicken_scan_line() + line_image, scan_data = plugin.output() + assert_equal(line_image[line_image == 128].size, 755) + assert_equal(line_image[line_image == 255].size, 151) + assert_equal(line_image.shape, (300, 451)) + assert_equal(scan_data.shape, (152, 3)) + assert_allclose(scan_data.max(), 0.772, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.4355, rtol=1e-3) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 3b9b33fb..95176fcd 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -14,6 +14,9 @@ try: else: from matplotlib.backends.backend_qt4 import FigureManagerQT from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg + if 'agg' not in mpl.get_backend().lower(): + print("Recommended matplotlib backend is `Agg` for full " + "skimage.viewer functionality.") except ImportError: FigureCanvasQTAgg = object # hack to prevent nosetest and autodoc errors LinearSegmentedColormap = object diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index c3ffeb1e..32c10cd4 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -1,12 +1,18 @@ """ ImageViewer class for viewing and interacting with images. """ -from ..qt import QtGui +from ..qt import QtGui, qt_api from ..qt.QtCore import Qt, Signal +if qt_api is not None: + has_qt = True +else: + has_qt = False + from skimage import io, img_as_float from skimage.util.dtype import dtype_range from skimage.exposure import rescale_intensity +from skimage._shared.testing import doctest_skip_parser import numpy as np from .. import utils from ..widgets import Slider @@ -20,12 +26,28 @@ __all__ = ['ImageViewer', 'CollectionViewer'] 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 + Each image in a matplotlib figure has its own colormap and normalization function. Return RGBA (RGB + alpha channel) image with float dtype. + + Parameters + ---------- + mpl_image : matplotlib.image.AxesImage object + The image being converted. + + Returns + ------- + img : array of float, shape (M, N, 4) + An image of float values in [0, 1]. """ - 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 + image = mpl_image.get_array() + if image.ndim == 2: + input_range = (mpl_image.norm.vmin, mpl_image.norm.vmax) + image = rescale_intensity(image, in_range=input_range) + # cmap complains on bool arrays + image = mpl_image.cmap(img_as_float(image)) + elif image.ndim == 3 and image.shape[2] == 3: + # add alpha channel if it's missing + image = np.dstack((image, np.ones_like(image))) return img_as_float(image) @@ -56,8 +78,8 @@ class ImageViewer(QtGui.QMainWindow): -------- >>> from skimage import data >>> image = data.coins() - >>> viewer = ImageViewer(image) - >>> # viewer.show() + >>> viewer = ImageViewer(image) # doctest: +SKIP + >>> viewer.show() # doctest: +SKIP """ @@ -212,6 +234,7 @@ class ImageViewer(QtGui.QMainWindow): self._show() if main_window: utils.start_qtapp() + return [p.output() for p in self.plugins] def redraw(self): self.canvas.draw_idle() diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index b9714d38..2bbf53d2 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -232,7 +232,7 @@ class ComboBox(BaseWidget): self.name_label.setAlignment(QtCore.Qt.AlignLeft) self._combo_box = QtGui.QComboBox() - self._combo_box.addItems(items) + self._combo_box.addItems(list(items)) self.layout = QtGui.QHBoxLayout(self) self.layout.addWidget(self.name_label) diff --git a/tools/build_versions.py b/tools/build_versions.py new file mode 100755 index 00000000..2c0e32ba --- /dev/null +++ b/tools/build_versions.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import numpy as np +import scipy as sp +import matplotlib as mpl +import six + +for m in (np, sp, mpl, six): + print(m.__name__.rjust(10), ' ', m.__version__) + diff --git a/tools/header.py b/tools/header.py new file mode 100755 index 00000000..d7a4c50b --- /dev/null +++ b/tools/header.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +from __future__ import print_function +import sys + +screen_width = 50 + +print('*' * screen_width) + +if len(sys.argv) > 1: + header = ' '.join(sys.argv[1:]) + print('*', header.center(screen_width - 4), '*') + print('*' * screen_width) + diff --git a/viewer_examples/plugins/canny.py b/viewer_examples/plugins/canny.py index eaa33330..686034b2 100644 --- a/viewer_examples/plugins/canny.py +++ b/viewer_examples/plugins/canny.py @@ -6,4 +6,4 @@ from skimage.viewer.plugins.canny import CannyPlugin image = data.camera() viewer = ImageViewer(image) viewer += CannyPlugin() -viewer.show() +canny_edges = viewer.show()[0][0] diff --git a/viewer_examples/plugins/canny_simple.py b/viewer_examples/plugins/canny_simple.py index c26ca08d..641ca138 100644 --- a/viewer_examples/plugins/canny_simple.py +++ b/viewer_examples/plugins/canny_simple.py @@ -21,4 +21,4 @@ plugin += SaveButtons(name='Save overlay to:') # Finally, attach the plugin to an image viewer. viewer = ImageViewer(image) viewer += plugin -viewer.show() +canny_edges = viewer.show()[0][0] diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py index 2f1b2cdc..dd80e277 100644 --- a/viewer_examples/plugins/lineprofile.py +++ b/viewer_examples/plugins/lineprofile.py @@ -6,4 +6,4 @@ from skimage.viewer.plugins.lineprofile import LineProfile image = data.camera() viewer = ImageViewer(image) viewer += LineProfile() -viewer.show() +line, profile = viewer.show()[0] diff --git a/viewer_examples/plugins/lineprofile_rgb.py b/viewer_examples/plugins/lineprofile_rgb.py index 86b71d5d..577933a1 100644 --- a/viewer_examples/plugins/lineprofile_rgb.py +++ b/viewer_examples/plugins/lineprofile_rgb.py @@ -6,4 +6,4 @@ from skimage.viewer.plugins.lineprofile import LineProfile image = data.chelsea() viewer = ImageViewer(image) viewer += LineProfile() -viewer.show() +line, rgb_profiles = viewer.show()[0]