diff --git a/.coveragerc b/.coveragerc index 5a372d61..ffab8979 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,6 +6,7 @@ source = skimage include = */skimage/* omit = */setup.py + */skimage/external/* [report] exclude_lines = diff --git a/.gitignore b/.gitignore index 2623ea74..68bfaecb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *# *egg-info *.so +*.pyd *.bak *.c *.new @@ -18,6 +19,7 @@ skimage/version.py .coverage doc/source/auto_examples/*.py doc/source/auto_examples/*.txt +doc/source/auto_examples/notebook doc/source/auto_examples/images/plot_*.png doc/source/auto_examples/images/thumb doc/source/auto_examples/applications/ diff --git a/.travis.yml b/.travis.yml index 169b512b..a46b7171 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,94 +1,40 @@ # vim ft=yaml # After changing this file, check it on: -# http://lint.travis-ci.org/ +# http://yaml-online-parser.appspot.com/ +# See doc/travis_notes.txt for some guidelines language: python +notifications: + webhooks: + urls: + - https://webhooks.gitter.im/e/1fea29525e8b929dd7c7 + on_success: change # options: [always|never|change] default: always + on_failure: always # options: [always|never|change] default: always + on_start: false # default: false + 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 +- 2.6 +- 2.7 +- 3.2 +- 3.3 +- 3.4 before_install: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start - - sudo apt-get update + - source tools/travis_before_install.sh - - 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 libfreeimage3 - - - 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 - + - which python; python --version - python check_bento_build.py - -install: - - tools/header.py "Dependency versions" - tools/build_versions.py +install: - python setup.py build_ext --inplace + - python setup.py install -script: - # Matplotlib settings - - mkdir -p $HOME/.matplotlib - - touch $HOME/.matplotlib/matplotlibrc - - "echo 'backend : Agg' > $HOME/.matplotlib/matplotlibrc" - - "echo 'backend.qt4 : PyQt4' >> $HOME/.matplotlib/matplotlibrc" - - # 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 +script: tools/travis_script.sh after_success: - - if [[ $PYVER == '3.x' ]]; then - - coveralls - - fi + - coveralls diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 202210a1..58b83296 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -77,6 +77,12 @@ For a more detailed discussion, read these :doc:`detailed documents Travis fails, you can find out why by clicking on the "failed" icon (red cross) and inspecting the build and test log. +5. Document changes + + Before merging your commits, you must add a description of your changes + to the release notes of the upcoming version in + ``doc/release/release_dev.txt``. + .. note:: To reviewers: if it is not obvious, add a short explanation of what a branch @@ -85,7 +91,7 @@ For a more detailed discussion, read these :doc:`detailed documents Divergence between ``upstream master`` and your feature branch -.............................................................. +-------------------------------------------------------------- Do *not* ever merge the main branch into yours. If GitHub indicates that the branch of your Pull Request can no longer be merged automatically, rebase @@ -117,8 +123,7 @@ Guidelines * All code should have tests (see `test coverage`_ below for more details). * All code should be documented, to the same - `standard `_ - as NumPy and SciPy. + `standard <://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt#docstring-standard>`_ as NumPy and SciPy. * For new functionality, always add an example to the gallery. * No changes are ever committed without review. Ask on the @@ -188,8 +193,8 @@ successfully passes all tests. To do so, * Go to `Travis-CI `__ and follow the Sign In link at the top - * Go to your `profile page `__ and switch on your - scikit-image fork + * Go to your `profile page `__ and switch + on your scikit-image fork It corresponds to steps one and two in `Travis-CI documentation `__ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 140842ab..bc58df84 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -176,9 +176,42 @@ - François Orieux Image deconvolution http://research.orieux.fr - + - Vighnesh Birodkar Blob Detection - + - Axel Donath Blob Detection + +- Adam Feuer + PIL Image import and export improvements + +- Rebecca Murphy + astronaut in examples + +- Geoffrey French + skimage.filters.rank.windowed_histogram and plot_windowed_histogram example. + +- Alexey Umnov + skimage.draw.ellipse bug fix and tests. + +- Ivana Kajic + Updated description and examples in documentation for gabor filters + +- Matěj Týč + Extended the image labelling implementation so it also works on 3D images. + +- Salvatore Scaramuzzino + RectTool example + +- Kevin Keraudren + Fix and test for feature.peak_local_max + +- Jeremy Metz + Adaptation of ImageJ Autothresholder.Li, fixed Qhull error QH6228 + +- Mike Sarahan + Sub-pixel shift registration + +- Jim Fienup, Alexander Iacchetta + In-depth review of sub-pixel shift registration diff --git a/DEPENDS.txt b/DEPENDS.txt index 152e935b..5cf37564 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -1,11 +1,9 @@ Build Requirements ------------------ -* `Python >= 2.5 `__ +* `Python >= 2.6 `__ * `Numpy >= 1.6 `__ -* `Cython >= 0.17 `__ - -`Matplotlib >= 1.0 `__ is needed to generate the -examples in the documentation. +* `Cython >= 0.21 `__ +* `Six >=1.3 `__ You can use pip to automatically install the base dependencies as follows:: @@ -13,7 +11,11 @@ You can use pip to automatically install the base dependencies as follows:: Runtime requirements -------------------- -* `SciPy >= 0.10 `__ +* `SciPy `__ +* `Matplotlib `__ +* `NetworkX `__ +* `Pillow `__ + (or `PIL `__) Known build errors ------------------ @@ -25,11 +27,6 @@ example at ``C:\Python26\Lib\distutils\distutils.cfg``) to contain:: [build] compiler=mingw32 - -Usage Requirements ------------------- -* `Scipy `__ - Optional Requirements --------------------- You can use this scikit with the basic requirements listed above, but some @@ -46,11 +43,15 @@ 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 `__) - The ``Pillow`` library (or equivalently ``PIL``) is used for Input/Output. +* `Astropy `__ provides FITS io capability. + +* `SimpleITK ` + Optional io plugin providing a wide variety of `formats `__. + including specialized formats using in medical imaging. + +* `imread ` + Optional io plugin providing most standard `formats `__. -* `Astropy `__ is required to use the FITS io plug-in. Testing requirements -------------------- diff --git a/Makefile b/Makefile index 2ace534e..b80eba0d 100644 --- a/Makefile +++ b/Makefile @@ -14,3 +14,7 @@ doctest: coverage: nosetests skimage --with-coverage --cover-package=skimage + +html: + pip install -q sphinx + export SPHINXOPTS=-W; make -C doc html diff --git a/README.md b/README.md index 64a7dfe9..b7ae96d1 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,71 @@ -Image Processing SciKit -======================= +# scikit-image: Image processing in Python -Source ------- -https://github.com/scikit-image/scikit-image +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/scikit-image/scikit-image?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![Coverage Status](https://img.shields.io/coveralls/scikit-image/scikit-image.svg)](https://coveralls.io/r/scikit-image/scikit-image?branch=master) -Mailing List ------------- -http://groups.google.com/group/scikit-image +- **Website (including documentation):** [http://scikit-image.org/](http://scikit-image.org) +- **Mailing list:** [http://groups.google.com/group/scikit-image](http://groups.google.com/group/scikit-image) +- **Source:** [https://github.com/scikit-image/scikit-image](https://github.com/scikit-image/scikit-image) -Installation from source ------------------------- -Refer to DEPENDS.txt for a list of dependencies. +## Installation from binaries -The SciKit may be installed globally using +- **Debian/Ubuntu:** ``sudo apt-get install python-skimage`` +- **OSX:** ``pip install scikit-image`` +- **Anaconda:** ``conda install scikit-image +- **Windows:** Download [Windows binaries](http://www.lfd.uci.edu/~gohlke/pythonlibs/#scikits.image) - $ python setup.py install +Also see +[http://scikit-image.org/docs/dev/install.html](http://scikit-image.org/docs/dev/install.html) -or locally using +## Installation from source - $ python setup.py install --prefix=${HOME} +Install [dependencies](DEPENDS.txt) using: -If you prefer, you can use it without installing, by simply adding -this path to your PYTHONPATH variable and compiling the extensions: +``` +pip install -r requirements.txt +``` - $ python setup.py build_ext -i +Then, install scikit-image using: + +``` +$ pip install . +``` + +If you plan to develop the package, you may run it directly from source: + +``` +$ python setup.py develop # Do this once to add pkg to Python path +$ python setup.py build_ext -i # Build binary extensions +``` + +## License (Modified BSD) + +Copyright (C) 2011, the scikit-image team +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. Neither the name of skimage nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. -License -------- -Please read LICENSE.txt in this directory. diff --git a/RELEASE.txt b/RELEASE.txt index 36c3acf5..66860662 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -5,8 +5,15 @@ How to make a new release of ``skimage`` - Update release notes. - - To show a list of contributors and changes, run - ``doc/release/contribs.py ``. + 1. Review and cleanup ``doc/release/release_dev.txt`` + + - To show a list of merges and contributors, run + ``doc/release/contribs.py ``. + + 2. Rename to ``doc/release/release_X.txt`` + + 3. Copy ``doc/release/release_template.txt`` to + ``doc/release/release_dev.txt`` for the next release. - Update the version number in ``setup.py`` and ``bento.info`` and commit @@ -19,6 +26,7 @@ How to make a new release of ``skimage`` place. Double check ``random.js``, otherwise the skimage.org front page gets broken! - Build using ``make gh-pages``. + - Update the symlink to ``stable``. - Push upstream: ``git push origin gh-pages`` in ``doc/gh-pages``. - Add the version number as a tag in git:: @@ -34,6 +42,15 @@ How to make a new release of ``skimage`` python setup.py register python setup.py sdist upload + Go to https://travis-ci.org/scikit-image/scikit-image-wheels, select the + "Current" tab, and click (on the right) on the "Restart Build" icon. After + the wheels become available at http://wheels.scikit-image.org/ (approx 15 + mins), execute ``tools/osx_wheel_upload.sh``. Note that, if you rebuild the + same wheels, it can take up to 15 minutes for the the files in the http + directory to update to the versions that Travis-CI uploaded. You may want to + check the timestamps in the http directory listing to check that you will get + the latest version. + - Increase the version number - In ``setup.py``, set to ``0.Xdev``. @@ -45,8 +62,6 @@ How to make a new release of ``skimage`` - Sync your branch with the remote repo: ``git pull``. If you try to ``make gh-pages`` when your branch is out of sync, it creates headaches. - - Update stable and development version numbers in - ``_templates/sidebar_versions.html``. - Add release date to ``index.rst`` under "Announcements". - Add previous stable version documentation path to disallowed paths in `robots.txt` diff --git a/TODO.txt b/TODO.txt index adc70f48..98e8653b 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,5 +1,17 @@ Remember to list any API changes below in `doc/source/api_changes.txt`. +Version 0.13 +------------ +* Remove deprecated `None` defaults for `skimage.exposure.rescale_intensity` +* Remove deprecated `skimage.filters.canny` import in `filters/__init__.py` + file (canny is now in `skimage.feature.canny`). + * Don't forget to complete api_changes.txt. + (`GitHub discuss `__ ) +* Remove deprecated ``skimage.filter`` module. +* Remove deprecated edge filters `hsobel`, `vsobel`, `hscharr`, `vscharr`, + `hprewitt`, `vprewitt`, `roberts_positive_diagonal`, + `roberts_negative_diagonal` in `skimage/filters/edges.py` + Version 0.12 ------------ * Change `label` to mark background as 0, not -1, which is consistent with @@ -7,11 +19,13 @@ Version 0.12 * 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 +* Change deprecated `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_*` +* Remove deprecated functions `skimage.filters.denoise_*` +* Add 3D phantom in `skimage.data` +* Add 3D test case of `skimage.feature.phase_correlate` diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..df1ffd3e --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,68 @@ +# AppVeyor.com is a Continuous Integration service to build and run tests under +# Windows + +environment: + global: + # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the + # /E:ON and /V:ON options are not enabled in the batch script intepreter + # See: http://stackoverflow.com/a/13751649/163740 + CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\tools\\appveyor\\run_with_env.cmd" + + matrix: + - PYTHON: "C:\\Python27_32" + PYTHON_VERSION: "2.7" + PYTHON_ARCH: "32" + + - PYTHON: "C:\\Python27_64" + PYTHON_VERSION: "2.7" + PYTHON_ARCH: "64" + + - PYTHON: "C:\\Python34_32" + PYTHON_VERSION: "3.4.2" + PYTHON_ARCH: "32" + + - PYTHON: "C:\\Python24_64" + PYTHON_VERSION: "3.4.2" + PYTHON_ARCH: "64" + +install: + # Install Python (from the official .msi of http://python.org) and pip when + # not already installed. + - "powershell ./tools/appveyor/install.ps1" + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + + # Check that we have the expected version and architecture for Python + - "python --version" + - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" + + # Install the build and runtime dependencies of the project. + - "%CMD_IN_ENV% pip install -v %WHEELHOUSE% -r tools/appveyor/requirements.txt" + - "%CMD_IN_ENV% pip install -v -r requirements.txt" + - "%CMD_IN_ENV% python setup.py bdist_wheel bdist_wininst" + - ps: "ls dist" + + # Install the generated wheel package to test it + - "pip install --pre --no-index --find-links dist/ scikit-image" + + + +# Not a .NET project, we build scikit-image in the install step instead +build: false + +test_script: + # Change to a non-source folder to make sure we run the tests on the + # installed library. + - "cd C:\\" + + # Use the Agg backend in Matplotlib + - echo backend:Agg > matplotlibrc + + # Run unit tests with nose + - "python -c \"import nose; nose.main()\" -v -s skimage" + +artifacts: + # Archive the generated wheel package in the ci.appveyor.com build report. + - path: dist\* + +#on_success: +# - TODO: upload the content of dist/*.whl to a public wheelhouse diff --git a/bento.info b/bento.info index 99790855..0af161d4 100644 --- a/bento.info +++ b/bento.info @@ -1,5 +1,5 @@ Name: scikit-image -Version: 0.10.1 +Version: 0.11.0 Summary: Image processing routines for SciPy Url: http://scikit-image.org DownloadUrl: http://github.com/scikit-image/scikit-image @@ -33,22 +33,23 @@ UseBackends: Waf Library: Packages: skimage, skimage.color, skimage.data, skimage.draw, skimage.exposure, - skimage.feature, skimage.filter, skimage.graph, skimage.io, + skimage.feature, skimage.filters, skimage.future, skimage.future.graph, + skimage.graph, skimage.io, skimage.io._plugins, skimage.measure, skimage.morphology, skimage.scripts, skimage.restoration, skimage.segmentation, skimage.transform, skimage.util - Extension: skimage.morphology._pnpoly - Sources: - skimage/morphology/_pnpoly.pyx Extension: skimage.io._plugins._colormixer Sources: skimage/io/_plugins/_colormixer.pyx + Extension: skimage.measure._pnpoly + Sources: + skimage/measure/_pnpoly.pyx Extension: skimage.measure._find_contours_cy Sources: skimage/measure/_find_contours_cy.pyx - Extension: skimage.measure._moments + Extension: skimage.measure._moments_cy Sources: - skimage/measure/_moments.pyx + skimage/measure/_moments_cy.pyx Extension: skimage.measure._marching_cubes_cy Sources: skimage/measure/_marching_cubes_cy.pyx @@ -61,9 +62,9 @@ Library: Extension: skimage.transform._hough_transform Sources: skimage/transform/_hough_transform.pyx - Extension: skimage.filter._ctmf + Extension: skimage.filters._ctmf Sources: - skimage/filter/_ctmf.pyx + skimage/filters/_ctmf.pyx Extension: skimage.measure._ccomp Sources: skimage/measure/_ccomp.pyx @@ -79,9 +80,6 @@ Library: Extension: skimage.graph._spath Sources: skimage/graph/_spath.pyx - Extension: skimage.morphology.cmorph - Sources: - skimage/morphology/cmorph.pyx Extension: skimage.graph.heap Sources: skimage/graph/heap.pyx @@ -127,18 +125,18 @@ Library: Extension: skimage._shared.geometry Sources: skimage/_shared/geometry.pyx - Extension: skimage.filter.rank.generic_cy + Extension: skimage.filters.rank.generic_cy Sources: - skimage/filter/rank/generic_cy.pyx - Extension: skimage.filter.rank.percentile_cy + skimage/filters/rank/generic_cy.pyx + Extension: skimage.filters.rank.percentile_cy Sources: skimage/filter/rank/percentile_cy.pyx - Extension: skimage.filter.rank.core_cy + Extension: skimage.filters.rank.core_cy Sources: skimage/filter/rank/core_cy.pyx - Extension: skimage.filter.rank.bilateral_cy + Extension: skimage.filters.rank.bilateral_cy Sources: - skimage/filter/rank/bilateral_cy.pyx + skimage/filters/rank/bilateral_cy.pyx Extension: skimage.restoration._unwrap_1d Sources: skimage/restoration/_unwrap_1d.pyx @@ -151,9 +149,18 @@ Library: Extension: skimage.restoration._denoise_cy Sources: skimage/restoration/_denoise_cy.pyx + Extension: skimage.restoration._nl_means_denoising + Sources: + skimage/restoration/_nl_means_denoising.pyx Extension: skimage.feature._hessian_det_appx Sources: skimage/exposure/_hessian_det_appx.pyx + Extension: skimage.future.graph._ncut_cy + Sources: + skimage/future/graph/_ncut_cy.pyx + Extension: skimage.external.tifffile._tifffile + Sources: + skimage/external/tifffile/_tifffile.c Executable: skivi Module: skimage.scripts.skivi diff --git a/doc/Makefile b/doc/Makefile index a593aa56..093f65ea 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line. PYTHON ?= python SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build +SPHINXBUILD ?= python $(shell which sphinx-build) PAPER ?= # Internal variables. @@ -30,7 +30,7 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" - + @echo " gitwash to update the gitwash documentation" clean: -rm -rf $(DEST)/* -rm -rf source/api diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 00000000..50deb4e2 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,19 @@ +# Building docs # +To build docs, run `make` in this directory. `make help` lists all targets. + +## Requirements ## +Sphinx is needed to build doc. Install with `pip install sphinx`. + +## Fixing Warnings ## + +- "citation not found: R###" + $ cd doc/build; grep -rin R### . + There is probably an underscore after a reference + in the first line of a docstring (e.g. [1]_) + +- "Duplicate citation R###, other instance in..."" + There is probably a [2] without a [1] in one of + the docstrings + +- Make sure to use pre-sphinxification paths to images + (not the _images directory) diff --git a/doc/examples/applications/plot_coins_segmentation.py b/doc/examples/applications/plot_coins_segmentation.py index 49ff399f..98f5212f 100644 --- a/doc/examples/applications/plot_coins_segmentation.py +++ b/doc/examples/applications/plot_coins_segmentation.py @@ -57,7 +57,7 @@ segmentation. To do this, we first get the edges of features using the Canny edge-detector. """ -from skimage.filter import canny +from skimage.feature import canny edges = canny(coins/255.) fig, ax = plt.subplots(figsize=(4, 3)) @@ -109,7 +109,7 @@ find an elevation map using the Sobel gradient of the image. """ -from skimage.filter import sobel +from skimage.filters import sobel elevation_map = sobel(coins) diff --git a/doc/examples/applications/plot_rank_filters.py b/doc/examples/applications/plot_rank_filters.py index 6530c6d6..4defb8e6 100644 --- a/doc/examples/applications/plot_rank_filters.py +++ b/doc/examples/applications/plot_rank_filters.py @@ -62,7 +62,7 @@ randomly set to 0. The **median** filter is applied to remove the noise. """ -from skimage.filter.rank import median +from skimage.filters.rank import median from skimage.morphology import disk noise = np.random.random(noisy_image.shape) @@ -107,7 +107,7 @@ image. """ -from skimage.filter.rank import mean +from skimage.filters.rank import mean fig, (ax1, ax2) = plt.subplots(1, 2, figsize=[10, 7]) @@ -133,11 +133,11 @@ the central one. .. note:: A different implementation is available for color images in - `skimage.filter.denoise_bilateral`. + `skimage.filters.denoise_bilateral`. """ -from skimage.filter.rank import mean_bilateral +from skimage.filters.rank import mean_bilateral noisy_image = img_as_ubyte(data.camera()) @@ -183,7 +183,7 @@ equalization emphasizes every local gray-level variations. """ from skimage import exposure -from skimage.filter import rank +from skimage.filters import rank noisy_image = img_as_ubyte(data.camera()) @@ -230,7 +230,7 @@ picture. """ -from skimage.filter.rank import autolevel +from skimage.filters.rank import autolevel noisy_image = img_as_ubyte(data.camera()) @@ -260,7 +260,7 @@ result. """ -from skimage.filter.rank import autolevel_percentile +from skimage.filters.rank import autolevel_percentile image = data.camera() @@ -298,7 +298,7 @@ otherwise by the minimum local. """ -from skimage.filter.rank import enhance_contrast +from skimage.filters.rank import enhance_contrast noisy_image = img_as_ubyte(data.camera()) @@ -330,7 +330,7 @@ percentile *p0* and *p1* instead of the local minimum and maximum. """ -from skimage.filter.rank import enhance_contrast_percentile +from skimage.filters.rank import enhance_contrast_percentile noisy_image = img_as_ubyte(data.camera()) @@ -366,19 +366,19 @@ threshold is determined by maximizing the variance between two classes of pixels of the local neighborhood defined by a structuring element. The example compares the local threshold with the global threshold -`skimage.filter.threshold_otsu`. +`skimage.filters.threshold_otsu`. .. note:: Local is much slower than global thresholding. A function for global Otsu - thresholding can be found in : `skimage.filter.threshold_otsu`. + thresholding can be found in : `skimage.filters.threshold_otsu`. .. [4] http://en.wikipedia.org/wiki/Otsu's_method """ -from skimage.filter.rank import otsu -from skimage.filter import threshold_otsu +from skimage.filters.rank import otsu +from skimage.filters import threshold_otsu p8 = data.page() @@ -459,7 +459,7 @@ closing and morphological gradient. """ -from skimage.filter.rank import maximum, minimum, gradient +from skimage.filters.rank import maximum, minimum, gradient noisy_image = img_as_ubyte(data.camera()) @@ -511,7 +511,7 @@ images. """ from skimage import data -from skimage.filter.rank import entropy +from skimage.filters.rank import entropy from skimage.morphology import disk import numpy as np import matplotlib.pyplot as plt @@ -549,7 +549,7 @@ from time import time from scipy.ndimage.filters import percentile_filter from skimage.morphology import dilation -from skimage.filter.rank import median, maximum +from skimage.filters.rank import median, maximum def exec_and_timeit(func): @@ -586,7 +586,7 @@ def ndi_med(image, n): Comparison between -* `filter.rank.maximum` +* `filters.rank.maximum` * `morphology.dilate` on increasing structuring element size: @@ -610,7 +610,7 @@ 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']) +ax.legend(['filters.rank.maximum', 'morphology.dilate']) """ @@ -638,7 +638,7 @@ 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']) +ax.legend(['filters.rank.maximum', 'morphology.dilate']) """ @@ -647,7 +647,7 @@ ax.legend(['filter.rank.maximum', 'morphology.dilate']) Comparison between: -* `filter.rank.median` +* `filters.rank.median` * `scipy.ndimage.percentile` on increasing structuring element size: @@ -669,7 +669,7 @@ rec = np.asarray(rec) 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.legend(['filters.rank.median', 'scipy.ndimage.percentile']) ax.set_ylabel('Time (ms)') ax.set_xlabel('Element radius') @@ -682,7 +682,7 @@ Comparison of outcome of the three methods: fig, ax = plt.subplots() ax.imshow(np.hstack((rc, rndi))) -ax.set_title('filter.rank.median vs. scipy.ndimage.percentile') +ax.set_title('filters.rank.median vs. scipy.ndimage.percentile') ax.axis('off') """ @@ -708,7 +708,7 @@ rec = np.asarray(rec) 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.legend(['filters.rank.median', 'scipy.ndimage.percentile']) ax.set_ylabel('Time (ms)') ax.set_xlabel('Image size') diff --git a/doc/examples/plot_adapt_rgb.py b/doc/examples/plot_adapt_rgb.py new file mode 100644 index 00000000..fce93245 --- /dev/null +++ b/doc/examples/plot_adapt_rgb.py @@ -0,0 +1,111 @@ +""" +========================================= +Adapting gray-scale filters to RGB images +========================================= + +There are many filters that are designed work with gray-scale images but not +color images. To simplify the process of creating functions that can adapt to +RGB images, scikit-image provides the ``adapt_rgb`` decorator. + +To actually use the ``adapt_rgb`` decorator, you have to decide how you want to +adapt the RGB image for use with the gray-scale filter. There are two +pre-defined handlers: + +``each_channel`` + Pass each of the RGB channels to the filter one-by-one, and stitch the + results back into an RGB image. +``hsv_value`` + Convert the RGB image to HSV and pass the value channel to the filter. + The filtered result is inserted back into the HSV image and converted + back to RGB. + +Below, we demonstrate the use of ``adapt_rgb`` on a couple of gray-scale +filters: +""" +from skimage.color.adapt_rgb import adapt_rgb, each_channel, hsv_value +from skimage import filters + + +@adapt_rgb(each_channel) +def sobel_each(image): + return filters.sobel(image) + + +@adapt_rgb(hsv_value) +def sobel_hsv(image): + return filters.sobel(image) + +""" +We can use these functions as we would normally use them, but now they work +with both gray-scale and color images. Let's plot the results with a color +image: +""" + +from skimage import data +import matplotlib.pyplot as plt + +image = data.lena() + +fig, (ax_each, ax_hsv) = plt.subplots(ncols=2) +ax_each.imshow(sobel_each(image)) +ax_hsv.imshow(sobel_hsv(image)) + +""" +.. image:: PLOT2RST.current_figure + +Notice that the result for the value-filtered image preserves the color of the +original image, but channel filtered image combines in a more surprising way. +In other common cases, smoothing for example, the channel filtered image will +produce a better result than the value-filtered image. + +You can also create your own handler functions for ``adapt_rgb``. To do so, +just create a function with the following signature:: + + def handler(image_filter, image, *args, **kwargs): + # Manipulate RGB image here... + image = image_filter(image, *args, **kwargs) + # Manipulate filtered image here... + return image + +Note that ``adapt_rgb`` handlers are written for filters where the image is the +first argument. + +As a very simple example, we can just convert any RGB image to grayscale and +then return the filtered result: + +""" +from skimage.color import rgb2gray + + +def as_gray(image_filter, image, *args, **kwargs): + gray_image = rgb2gray(image) + return image_filter(gray_image, *args, **kwargs) + +""" +It's important to create a signature that uses ``*args`` and ``**kwargs`` to +pass arguments along to the filter so that the decorated function is allowed to +have any number of positional and keyword arguments. + +Finally, we can use this handler with ``adapt_rgb`` just as before: +""" + + +@adapt_rgb(as_gray) +def sobel_gray(image): + return filters.sobel(image) + +fig, ax = plt.subplots() +ax.imshow(sobel_gray(image), cmap=plt.cm.gray) + +plt.show() + +""" +.. image:: PLOT2RST.current_figure + +.. note:: + + A very simple check of the array shape is used for detecting RGB images, so + ``adapt_rgb`` is not recommended for functions that support 3D volumes or + color images in non-RGB spaces. + +""" diff --git a/doc/examples/plot_brief.py b/doc/examples/plot_brief.py index 47c9ad5f..247cf1dd 100644 --- a/doc/examples/plot_brief.py +++ b/doc/examples/plot_brief.py @@ -22,7 +22,7 @@ from skimage.color import rgb2gray import matplotlib.pyplot as plt -img1 = rgb2gray(data.lena()) +img1 = rgb2gray(data.astronaut()) tform = tf.AffineTransform(scale=(1.2, 1.2), translation=(0, -100)) img2 = tf.warp(img1, tform) img3 = tf.rotate(img1, 25) diff --git a/doc/examples/plot_camera_numpy.py b/doc/examples/plot_camera_numpy.py new file mode 100644 index 00000000..36d4be34 --- /dev/null +++ b/doc/examples/plot_camera_numpy.py @@ -0,0 +1,29 @@ +""" +Using simple NumPy operations for manipulating images +===================================================== + +This script illustrates how to use basic NumPy operations, such as slicing, +masking and fancy indexing, in order to modify the pixel values of an image. +""" + +import numpy as np +from skimage import data +import matplotlib.pyplot as plt + +camera = data.camera() +camera[:10] = 0 +mask = camera < 87 +camera[mask] = 255 +inds_x = np.arange(len(camera)) +inds_y = (4 * inds_x) % len(camera) +camera[inds_x, inds_y] = 0 + +l_x, l_y = camera.shape[0], camera.shape[1] +X, Y = np.ogrid[:l_x, :l_y] +outer_disk_mask = (X - l_x / 2)**2 + (Y - l_y / 2)**2 > (l_x / 2)**2 +camera[outer_disk_mask] = 0 + +plt.figure(figsize=(4, 4)) +plt.imshow(camera, cmap='gray', interpolation='nearest') +plt.axis('off') +plt.show() diff --git a/doc/examples/plot_canny.py b/doc/examples/plot_canny.py index f1caf264..82a930e5 100644 --- a/doc/examples/plot_canny.py +++ b/doc/examples/plot_canny.py @@ -19,7 +19,7 @@ import numpy as np import matplotlib.pyplot as plt from scipy import ndimage -from skimage import filter +from skimage import feature # Generate noisy image of a square @@ -31,8 +31,8 @@ im = ndimage.gaussian_filter(im, 4) im += 0.2 * np.random.random(im.shape) # Compute the Canny filter for two values of sigma -edges1 = filter.canny(im) -edges2 = filter.canny(im, sigma=3) +edges1 = feature.canny(im) +edges2 = feature.canny(im, sigma=3) # display results fig, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3, figsize=(8, 3)) diff --git a/doc/examples/plot_censure.py b/doc/examples/plot_censure.py index c7d70ea5..b2a52440 100644 --- a/doc/examples/plot_censure.py +++ b/doc/examples/plot_censure.py @@ -15,7 +15,7 @@ from skimage.color import rgb2gray import matplotlib.pyplot as plt -img1 = rgb2gray(data.lena()) +img1 = rgb2gray(data.astronaut()) tform = tf.AffineTransform(scale=(1.5, 1.5), rotation=0.5, translation=(150, -200)) img2 = tf.warp(img1, tform) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index fbdd4f2c..3d24e57c 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -37,16 +37,16 @@ Its size is extended by two times the larger radius. import numpy as np import matplotlib.pyplot as plt -from skimage import data, filter, color +from skimage import data, color from skimage.transform import hough_circle -from skimage.feature import peak_local_max +from skimage.feature import peak_local_max, canny from skimage.draw import circle_perimeter from skimage.util import img_as_ubyte # Load picture and detect edges image = img_as_ubyte(data.coins()[0:95, 70:370]) -edges = filter.canny(image, sigma=3, low_threshold=10, high_threshold=50) +edges = canny(image, sigma=3, low_threshold=10, high_threshold=50) fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(5, 2)) @@ -60,10 +60,11 @@ radii = [] for radius, h in zip(hough_radii, hough_res): # For each radius, extract two circles - peaks = peak_local_max(h, num_peaks=2) + num_peaks = 2 + peaks = peak_local_max(h, num_peaks=num_peaks) centers.extend(peaks) accums.extend(h[peaks[:, 0], peaks[:, 1]]) - radii.extend([radius, radius]) + radii.extend([radius] * num_peaks) # Draw the most prominent 5 circles image = color.gray2rgb(image) @@ -106,14 +107,15 @@ References import matplotlib.pyplot as plt -from skimage import data, filter, color +from skimage import data, color +from skimage.feature import canny from skimage.transform import hough_ellipse from skimage.draw import ellipse_perimeter # Load picture, convert to grayscale and detect edges image_rgb = data.coffee()[0:220, 160:420] image_gray = color.rgb2gray(image_rgb) -edges = filter.canny(image_gray, sigma=2.0, +edges = canny(image_gray, sigma=2.0, low_threshold=0.55, high_threshold=0.8) # Perform a Hough Transform diff --git a/doc/examples/plot_denoise.py b/doc/examples/plot_denoise.py index 3a05c99f..1313bc04 100644 --- a/doc/examples/plot_denoise.py +++ b/doc/examples/plot_denoise.py @@ -1,10 +1,10 @@ """ -============================= -Denoising the picture of Lena -============================= +==================== +Denoising a picture +==================== -In this example, we denoise a noisy version of the picture of Lena using the -total variation and bilateral denoising filter. +In this example, we denoise a noisy version of the picture of the astronaut +Eileen Collins using the total variation and bilateral denoising filter. These algorithms typically produce "posterized" images with flat domains separated by sharp edges. It is possible to change the degree of posterization @@ -32,10 +32,10 @@ from skimage import data, img_as_float from skimage.restoration import denoise_tv_chambolle, denoise_bilateral -lena = img_as_float(data.lena()) -lena = lena[220:300, 220:320] +astro = img_as_float(data.astronaut()) +astro = astro[220:300, 220:320] -noisy = lena + 0.6 * lena.std() * np.random.random(lena.shape) +noisy = astro + 0.6 * astro.std() * np.random.random(astro.shape) noisy = np.clip(noisy, 0, 1) fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(8, 5)) @@ -58,7 +58,7 @@ ax[1, 0].set_title('(more) TV') ax[1, 1].imshow(denoise_bilateral(noisy, sigma_range=0.1, sigma_spatial=15)) ax[1, 1].axis('off') ax[1, 1].set_title('(more) Bilateral') -ax[1, 2].imshow(lena) +ax[1, 2].imshow(astro) ax[1, 2].axis('off') ax[1, 2].set_title('original') diff --git a/doc/examples/plot_edge_filter.py b/doc/examples/plot_edge_filter.py index b43aae38..0bb6c567 100644 --- a/doc/examples/plot_edge_filter.py +++ b/doc/examples/plot_edge_filter.py @@ -8,10 +8,11 @@ They are discrete differentiation operators, computing an approximation of the gradient of the image intensity function. """ +import numpy as np import matplotlib.pyplot as plt from skimage.data import camera -from skimage.filter import roberts, sobel +from skimage.filters import roberts, sobel, scharr image = camera() @@ -28,4 +29,55 @@ ax1.imshow(edge_sobel, cmap=plt.cm.gray) ax1.set_title('Sobel Edge Detection') ax1.axis('off') +plt.tight_layout() + +""" +.. image:: PLOT2RST.current_figure + +Different operators compute different finite-difference approximations of the +gradient. For example, the Scharr filter results in a better rotational +variance than other filters such as the Sobel filter [1]_ [2]_. The difference +between the two filters is illustrated below on an image that is the +discretization of a rotation-invariant continuous function. The discrepancy +between the two filters is stronger for regions of the image where the +direction of the gradient is close to diagonal, and for regions with high +spatial frequencies. + +.. [1] http://en.wikipedia.org/wiki/Sobel_operator#Alternative_operators + +.. [2] B. Jaehne, H. Scharr, and S. Koerkel. Principles of filter design. In + Handbook of Computer Vision and Applications. Academic Press, 1999. +""" + +x, y = np.ogrid[:100, :100] +# Rotation-invariant image with different spatial frequencies +img = np.exp(1j * np.hypot(x, y)**1.3 / 20.).real + +edge_sobel = sobel(img) +edge_scharr = scharr(img) + +fig, ((ax0, ax1), (ax2, ax3)) = plt.subplots(nrows=2, ncols=2) + +ax0.imshow(edge_sobel, cmap=plt.cm.gray) +ax0.set_title('Sobel Edge Detection') +ax0.axis('off') + +ax1.imshow(edge_scharr, cmap=plt.cm.gray) +ax1.set_title('Scharr Edge Detection') +ax1.axis('off') + +ax2.imshow(img, cmap=plt.cm.gray) +ax2.set_title('Original image') +ax2.axis('off') + +ax3.imshow(edge_scharr - edge_sobel, cmap=plt.cm.jet) +ax3.set_title('difference (Scharr - Sobel)') +ax3.axis('off') + +plt.tight_layout() + plt.show() + +""" +.. image:: PLOT2RST.current_figure +""" diff --git a/doc/examples/plot_entropy.py b/doc/examples/plot_entropy.py index b92a5bf3..f33f26ff 100644 --- a/doc/examples/plot_entropy.py +++ b/doc/examples/plot_entropy.py @@ -10,7 +10,7 @@ coded in an image. import matplotlib.pyplot as plt from skimage import data -from skimage.filter.rank import entropy +from skimage.filters.rank import entropy from skimage.morphology import disk from skimage.util import img_as_ubyte diff --git a/doc/examples/plot_gabor.py b/doc/examples/plot_gabor.py index 87d99589..e5cc7760 100644 --- a/doc/examples/plot_gabor.py +++ b/doc/examples/plot_gabor.py @@ -21,7 +21,7 @@ from scipy import ndimage as nd from skimage import data from skimage.util import img_as_float -from skimage.filter import gabor_kernel +from skimage.filters import gabor_kernel def compute_feats(image, kernels): diff --git a/doc/examples/plot_gabors_from_lena.py b/doc/examples/plot_gabors_from_astronaut.py similarity index 64% rename from doc/examples/plot_gabors_from_lena.py rename to doc/examples/plot_gabors_from_astronaut.py index 207230aa..d7edfa51 100644 --- a/doc/examples/plot_gabors_from_lena.py +++ b/doc/examples/plot_gabors_from_astronaut.py @@ -1,7 +1,7 @@ """ -======================================================= -Gabors / Primary Visual Cortex "Simple Cells" from Lena -======================================================= +============================================================ +Gabors / Primary Visual Cortex "Simple Cells" from an Image +============================================================ How to build a (bio-plausible) "sparse" dictionary (or 'codebook', or 'filterbank') for e.g. image classification without any fancy math and @@ -10,15 +10,16 @@ with just standard python scientific libraries? Please find below a short answer ;-) This simple example shows how to get Gabor-like filters [1]_ using just -the famous Lena image. Gabor filters are good approximations of the -"Simple Cells" [2]_ receptive fields [3]_ found in the mammalian primary -visual cortex (V1) (for details, see e.g. the Nobel-prize winning work -of Hubel & Wiesel done in the 60s [4]_ [5]_). +a simple image. In our example, we use a photograph of the astronaut Eileen +Collins. Gabor filters are good approximations of the "Simple Cells" [2]_ +receptive fields [3]_ found in the mammalian primary visual cortex (V1) +(for details, see e.g. the Nobel-prize winning work of Hubel & Wiesel done +in the 60s [4]_ [5]_). Here we use McQueen's 'kmeans' algorithm [6]_, as a simple biologically plausible hebbian-like learning rule and we apply it (a) to patches of -the original Lena image (retinal projection), and (b) to patches of an -LGN-like [7]_ Lena image using a simple difference of gaussians (DoG) +the original image (retinal projection), and (b) to patches of an +LGN-like [7]_ image using a simple difference of gaussians (DoG) approximation. Enjoy ;-) And keep in mind that getting Gabors on natural image patches @@ -50,18 +51,18 @@ np.random.seed(42) patch_shape = 8, 8 n_filters = 49 -lena = color.rgb2gray(data.lena()) +astro = color.rgb2gray(data.astronaut()) -# -- filterbank1 on original Lena -patches1 = view_as_windows(lena, patch_shape) +# -- filterbank1 on original image +patches1 = view_as_windows(astro, patch_shape) patches1 = patches1.reshape(-1, patch_shape[0] * patch_shape[1])[::8] fb1, _ = kmeans2(patches1, n_filters, minit='points') fb1 = fb1.reshape((-1,) + patch_shape) fb1_montage = montage2d(fb1, rescale_intensity=True) -# -- filterbank2 LGN-like Lena -lena_dog = ndi.gaussian_filter(lena, .5) - ndi.gaussian_filter(lena, 1) -patches2 = view_as_windows(lena_dog, patch_shape) +# -- filterbank2 LGN-like image +astro_dog = ndi.gaussian_filter(astro, .5) - ndi.gaussian_filter(astro, 1) +patches2 = view_as_windows(astro_dog, patch_shape) patches2 = patches2.reshape(-1, patch_shape[0] * patch_shape[1])[::8] fb2, _ = kmeans2(patches2, n_filters, minit='points') fb2 = fb2.reshape((-1,) + patch_shape) @@ -71,17 +72,17 @@ fb2_montage = montage2d(fb2, rescale_intensity=True) fig, axes = plt.subplots(2, 2, figsize=(7, 6)) ax0, ax1, ax2, ax3 = axes.ravel() -ax0.imshow(lena, cmap=plt.cm.gray) -ax0.set_title("Lena (original)") +ax0.imshow(astro, cmap=plt.cm.gray) +ax0.set_title("Image (original)") ax1.imshow(fb1_montage, cmap=plt.cm.gray, interpolation='nearest') -ax1.set_title("K-means filterbank (codebook)\non Lena (original)") +ax1.set_title("K-means filterbank (codebook)\non original image") -ax2.imshow(lena_dog, cmap=plt.cm.gray) -ax2.set_title("Lena (LGN-like DoG)") +ax2.imshow(astro_dog, cmap=plt.cm.gray) +ax2.set_title("Image (LGN-like DoG)") ax3.imshow(fb2_montage, cmap=plt.cm.gray, interpolation='nearest') -ax3.set_title("K-means filterbank (codebook)\non Lena (LGN-like DoG)") +ax3.set_title("K-means filterbank (codebook)\non LGN-like DoG image") for ax in axes.ravel(): ax.axis('off') diff --git a/doc/examples/plot_hog.py b/doc/examples/plot_hog.py index 4899c590..15b279bd 100644 --- a/doc/examples/plot_hog.py +++ b/doc/examples/plot_hog.py @@ -85,7 +85,7 @@ from skimage.feature import hog from skimage import data, color, exposure -image = color.rgb2gray(data.lena()) +image = color.rgb2gray(data.astronaut()) fd, hog_image = hog(image, orientations=8, pixels_per_cell=(16, 16), cells_per_block=(1, 1), visualise=True) diff --git a/doc/examples/plot_join_segmentations.py b/doc/examples/plot_join_segmentations.py index 849f087c..c10105c5 100644 --- a/doc/examples/plot_join_segmentations.py +++ b/doc/examples/plot_join_segmentations.py @@ -14,7 +14,7 @@ import numpy as np from scipy import ndimage as nd import matplotlib.pyplot as plt -from skimage.filter import sobel +from skimage.filters import sobel from skimage.segmentation import slic, join_segmentations from skimage.morphology import watershed from skimage.color import label2rgb diff --git a/doc/examples/plot_label.py b/doc/examples/plot_label.py index 9912eddf..f80271ce 100644 --- a/doc/examples/plot_label.py +++ b/doc/examples/plot_label.py @@ -17,9 +17,10 @@ import matplotlib.pyplot as plt import matplotlib.patches as mpatches from skimage import data -from skimage.filter import threshold_otsu +from skimage.filters import threshold_otsu from skimage.segmentation import clear_border -from skimage.morphology import label, closing, square +from skimage.measure import label +from skimage.morphology import closing, square from skimage.measure import regionprops from skimage.color import label2rgb diff --git a/doc/examples/plot_line_hough_transform.py b/doc/examples/plot_line_hough_transform.py index bdb05661..4293c409 100644 --- a/doc/examples/plot_line_hough_transform.py +++ b/doc/examples/plot_line_hough_transform.py @@ -58,7 +58,7 @@ References from skimage.transform import (hough_line, hough_line_peaks, probabilistic_hough_line) -from skimage.filter import canny +from skimage.feature import canny from skimage import data import numpy as np diff --git a/doc/examples/plot_local_equalize.py b/doc/examples/plot_local_equalize.py index b3c11d55..507921b7 100644 --- a/doc/examples/plot_local_equalize.py +++ b/doc/examples/plot_local_equalize.py @@ -28,7 +28,7 @@ from skimage.util.dtype import dtype_range from skimage.util import img_as_ubyte from skimage import exposure from skimage.morphology import disk -from skimage.filter import rank +from skimage.filters import rank matplotlib.rcParams['font.size'] = 9 diff --git a/doc/examples/plot_local_otsu.py b/doc/examples/plot_local_otsu.py index d40af6b6..655e9328 100644 --- a/doc/examples/plot_local_otsu.py +++ b/doc/examples/plot_local_otsu.py @@ -20,7 +20,7 @@ import matplotlib.pyplot as plt from skimage import data from skimage.morphology import disk -from skimage.filter import threshold_otsu, rank +from skimage.filters import threshold_otsu, rank from skimage.util import img_as_ubyte diff --git a/doc/examples/plot_marked_watershed.py b/doc/examples/plot_marked_watershed.py index 0e1f1c58..766a1d25 100644 --- a/doc/examples/plot_marked_watershed.py +++ b/doc/examples/plot_marked_watershed.py @@ -19,7 +19,7 @@ import matplotlib.pyplot as plt from skimage.morphology import watershed, disk from skimage import data -from skimage.filter import rank +from skimage.filters import rank from skimage.util import img_as_ubyte diff --git a/doc/examples/plot_ncut.py b/doc/examples/plot_ncut.py new file mode 100644 index 00000000..5596a297 --- /dev/null +++ b/doc/examples/plot_ncut.py @@ -0,0 +1,33 @@ +""" +============== +Normalized Cut +============== + +This example constructs a Region Adjacency Graph (RAG) and recursively performs +a Normalized Cut on it. + +References +---------- +.. [1] Shi, J.; Malik, J., "Normalized cuts and image segmentation", + Pattern Analysis and Machine Intelligence, + IEEE Transactions on, vol. 22, no. 8, pp. 888-905, August 2000. +""" +from skimage import data, io, segmentation, color +from skimage.future import graph +from matplotlib import pyplot as plt + + +img = data.coffee() + +labels1 = segmentation.slic(img, compactness=30, n_segments=400) +out1 = color.label2rgb(labels1, img, kind='avg') + +g = graph.rag_mean_color(img, labels1, mode='similarity') +labels2 = graph.cut_normalized(labels1, g) +out2 = color.label2rgb(labels2, img, kind='avg') + +plt.figure() +io.imshow(out1) +plt.figure() +io.imshow(out2) +io.show() diff --git a/doc/examples/plot_nonlocal_means.py b/doc/examples/plot_nonlocal_means.py new file mode 100644 index 00000000..8349caa9 --- /dev/null +++ b/doc/examples/plot_nonlocal_means.py @@ -0,0 +1,41 @@ +""" +================================================= +Non-local means denoising for preserving textures +================================================= + +In this example, we denoise a detail of the astronaut image using the non-local +means filter. The non-local means algorithm replaces the value of a pixel by an +average of a selection of other pixels values: small patches centered on the +other pixels are compared to the patch centered on the pixel of interest, and +the average is performed only for pixels that have patches close to the current +patch. As a result, this algorithm can restore well textures, that would be +blurred by other denoising algoritm. +""" +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data, img_as_float +from skimage.restoration import nl_means_denoising + + +astro = img_as_float(data.astronaut()) +astro = astro[30:180, 150:300] + +noisy = astro + 0.3 * np.random.random(astro.shape) +noisy = np.clip(noisy, 0, 1) + +denoise = nl_means_denoising(noisy, 7, 9, 0.08) + +fig, ax = plt.subplots(ncols=2, figsize=(8, 4)) + +ax[0].imshow(noisy) +ax[0].axis('off') +ax[0].set_title('noisy') +ax[1].imshow(denoise) +ax[1].axis('off') +ax[1].set_title('non-local means') + +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_orb.py b/doc/examples/plot_orb.py index 1a73fc7f..8d4e1485 100644 --- a/doc/examples/plot_orb.py +++ b/doc/examples/plot_orb.py @@ -20,7 +20,7 @@ from skimage.color import rgb2gray import matplotlib.pyplot as plt -img1 = rgb2gray(data.lena()) +img1 = rgb2gray(data.astronaut()) img2 = tf.rotate(img1, 180) tform = tf.AffineTransform(scale=(1.3, 1.1), rotation=0.5, translation=(0, -200)) diff --git a/doc/examples/plot_otsu.py b/doc/examples/plot_otsu.py index 059093ab..14a7e7cb 100644 --- a/doc/examples/plot_otsu.py +++ b/doc/examples/plot_otsu.py @@ -18,7 +18,7 @@ import matplotlib import matplotlib.pyplot as plt from skimage.data import camera -from skimage.filter import threshold_otsu +from skimage.filters import threshold_otsu matplotlib.rcParams['font.size'] = 9 diff --git a/doc/examples/plot_phase_unwrap.py b/doc/examples/plot_phase_unwrap.py index ded87ada..f2bbbc4c 100644 --- a/doc/examples/plot_phase_unwrap.py +++ b/doc/examples/plot_phase_unwrap.py @@ -92,7 +92,7 @@ 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 +vertical direction (lower right), 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. diff --git a/doc/examples/plot_piecewise_affine.py b/doc/examples/plot_piecewise_affine.py index b1fb551f..d05986ca 100644 --- a/doc/examples/plot_piecewise_affine.py +++ b/doc/examples/plot_piecewise_affine.py @@ -12,7 +12,7 @@ from skimage.transform import PiecewiseAffineTransform, warp from skimage import data -image = data.lena() +image = data.astronaut() rows, cols = image.shape[0], image.shape[1] src_cols = np.linspace(0, cols, 20) diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py index 8e309edf..b1e22022 100644 --- a/doc/examples/plot_pyramid.py +++ b/doc/examples/plot_pyramid.py @@ -16,7 +16,7 @@ from skimage import data from skimage.transform import pyramid_gaussian -image = data.lena() +image = data.astronaut() rows, cols, dim = image.shape pyramid = tuple(pyramid_gaussian(image, downscale=2)) diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index 04cf7bab..91775ba0 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -4,11 +4,11 @@ Radon transform =============== In computed tomography, the tomography reconstruction problem is to obtain -a tomographic slice image from a set of projections [1]_. A projection is formed -by drawing a set of parallel rays through the 2D object of interest, assigning -the integral of the object's contrast along each ray to a single pixel in the -projection. A single projection of a 2D object is one dimensional. To -enable computed tomography reconstruction of the object, several projections +a tomographic slice image from a set of projections [1]_. A projection is +formed by drawing a set of parallel rays through the 2D object of interest, +assigning the integral of the object's contrast along each ray to a single +pixel in the projection. A single projection of a 2D object is one dimensional. +To enable computed tomography reconstruction of the object, several projections must be acquired, each of them corresponding to a different angle between the rays with respect to the object. A collection of projections at several angles is called a sinogram, which is a linear transform of the original image. @@ -29,7 +29,7 @@ and reconstructing the original image are compared: The Filtered Back Projection (FBP) and the Simultaneous Algebraic Reconstruction Technique (SART). -.. seealso:: +For further information on tomographic reconstruction, see - AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", http://www.slaney.org/pct/pct-toc.html @@ -65,7 +65,7 @@ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4.5)) ax1.set_title("Original") ax1.imshow(image, cmap=plt.cm.Greys_r) -theta = np.linspace(0., 180., max(image.shape), endpoint=True) +theta = np.linspace(0., 180., max(image.shape), endpoint=False) sinogram = radon(image, theta=theta, circle=True) ax2.set_title("Radon transform\n(Sinogram)") ax2.set_xlabel("Projection angle (deg)") diff --git a/doc/examples/plot_rag.py b/doc/examples/plot_rag.py new file mode 100644 index 00000000..d4f2711c --- /dev/null +++ b/doc/examples/plot_rag.py @@ -0,0 +1,81 @@ +""" +======================= +Region Adjacency Graphs +======================= + +This example demonstrates the use of the `merge_nodes` function of a Region +Adjacency Graph (RAG). The `RAG` class represents a undirected weighted graph +which inherits from `networkx.graph` class. When a new node is formed by +merging two nodes, the edge weight of all the edges incident on the resulting +node can be updated by a user defined function `weight_func`. + +The default behaviour is to use the smaller edge weight in case of a conflict. +The example below also shows how to use a custom function to select the larger +weight instead. + +""" +from skimage.future.graph import rag +import networkx as nx +from matplotlib import pyplot as plt +import numpy as np + + +def max_edge(g, src, dst, n): + """Callback to handle merging nodes by choosing maximum weight. + + Returns either the weight between (`src`, `n`) or (`dst`, `n`) + in `g` or the maximum of the two when both exist. + + Parameters + ---------- + g : RAG + The graph under consideration. + src, dst : int + The vertices in `g` to be merged. + n : int + A neighbor of `src` or `dst` or both. + + Returns + ------- + weight : float + The weight between (`src`, `n`) or (`dst`, `n`) in `g` or the + maximum of the two when both exist. + + """ + + w1 = g[n].get(src, {'weight': -np.inf})['weight'] + w2 = g[n].get(dst, {'weight': -np.inf})['weight'] + return max(w1, w2) + + +def display(g, title): + """Displays a graph with the given title.""" + pos = nx.circular_layout(g) + plt.figure() + plt.title(title) + nx.draw(g, pos) + nx.draw_networkx_edge_labels(g, pos, font_size=20) + + +g = rag.RAG() +g.add_edge(1, 2, weight=10) +g.add_edge(2, 3, weight=20) +g.add_edge(3, 4, weight=30) +g.add_edge(4, 1, weight=40) +g.add_edge(1, 3, weight=50) + +# Assigning dummy labels. +for n in g.nodes(): + g.node[n]['labels'] = [n] + +gc = g.copy() + +display(g, "Original Graph") + +g.merge_nodes(1, 3) +display(g, "Merged with default (min)") + +gc.merge_nodes(1, 3, weight_func=max_edge, in_place=False) +display(gc, "Merged with max without in_place") + +plt.show() diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py new file mode 100644 index 00000000..bdc39678 --- /dev/null +++ b/doc/examples/plot_rag_draw.py @@ -0,0 +1,40 @@ +""" +====================================== +Drawing Region Adjacency Graphs (RAGs) +====================================== + +This example constructs a Region Adjacency Graph (RAG) and draws it with +the `rag_draw` method. +""" +from skimage import data, segmentation +from skimage.future import graph +from matplotlib import pyplot as plt, colors + + +img = data.coffee() +labels = segmentation.slic(img, compactness=30, n_segments=400) +g = graph.rag_mean_color(img, labels) + +out = graph.draw_rag(labels, g, img) +plt.figure() +plt.title("RAG with all edges shown in green.") +plt.imshow(out) + +# The color palette used was taken from +# http://www.colorcombos.com/color-schemes/2/ColorCombo2.html +cmap = colors.ListedColormap(['#6599FF', '#ff9900']) +out = graph.draw_rag(labels, g, img, node_color="#ffde00", colormap=cmap, + thresh=30, desaturate=True) +plt.figure() +plt.title("RAG with edge weights less than 30, color " + "mapped between blue and orange.") +plt.imshow(out) + +plt.figure() +plt.title("All edges drawn with cubehelix colormap") +cmap = plt.get_cmap('cubehelix') +out = graph.draw_rag(labels, g, img, colormap=cmap, + desaturate=True) + +plt.imshow(out) +plt.show() diff --git a/doc/examples/plot_rag_mean_color.py b/doc/examples/plot_rag_mean_color.py new file mode 100644 index 00000000..4b2bfcae --- /dev/null +++ b/doc/examples/plot_rag_mean_color.py @@ -0,0 +1,30 @@ +""" +================ +RAG Thresholding +================ + +This example constructs a Region Adjacency Graph (RAG) and merges regions +which are similar in color. We construct a RAG and define edges as the +difference in mean color. We then join regions with similar mean color. + +""" + +from skimage import data, io, segmentation, color +from skimage.future import graph +from matplotlib import pyplot as plt + + +img = data.coffee() + +labels1 = segmentation.slic(img, compactness=30, n_segments=400) +out1 = color.label2rgb(labels1, img, kind='avg') + +g = graph.rag_mean_color(img, labels1) +labels2 = graph.cut_threshold(labels1, g, 29) +out2 = color.label2rgb(labels2, img, kind='avg') + +plt.figure() +io.imshow(out1) +plt.figure() +io.imshow(out2) +io.show() diff --git a/doc/examples/plot_rag_merge.py b/doc/examples/plot_rag_merge.py new file mode 100644 index 00000000..37712d5b --- /dev/null +++ b/doc/examples/plot_rag_merge.py @@ -0,0 +1,75 @@ +""" +=========== +RAG Merging +=========== + +This example constructs a Region Adjacency Graph (RAG) and progressively merges +regions that are similar in color. Merging two adjacent regions produces +a new region with all the pixels from the merged regions. Regions are merged +until no highly similar region pairs remain. + +""" + +from skimage import data, io, segmentation, color +from skimage.future import graph +import numpy as np + + +def _weight_mean_color(graph, src, dst, n): + """Callback to handle merging nodes by recomputing mean color. + + The method expects that the mean color of `dst` is already computed. + + Parameters + ---------- + graph : RAG + The graph under consideration. + src, dst : int + The vertices in `graph` to be merged. + n : int + A neighbor of `src` or `dst` or both. + + Returns + ------- + weight : float + The absolute difference of the mean color between node `dst` and `n`. + """ + + diff = graph.node[dst]['mean color'] - graph.node[n]['mean color'] + diff = np.linalg.norm(diff) + return diff + + +def merge_mean_color(graph, src, dst): + """Callback called before merging two nodes of a mean color distance graph. + + This method computes the mean color of `dst`. + + Parameters + ---------- + graph : RAG + The graph under consideration. + src, dst : int + The vertices in `graph` to be merged. + """ + graph.node[dst]['total color'] += graph.node[src]['total color'] + graph.node[dst]['pixel count'] += graph.node[src]['pixel count'] + graph.node[dst]['mean color'] = (graph.node[dst]['total color'] / + graph.node[dst]['pixel count']) + + +img = data.coffee() +labels = segmentation.slic(img, compactness=30, n_segments=400) +g = graph.rag_mean_color(img, labels) + +labels2 = graph.merge_hierarchical(labels, g, thresh=40, rag_copy=False, + in_place_merge=True, + merge_func=merge_mean_color, + weight_func=_weight_mean_color) + +g2 = graph.rag_mean_color(img, labels2) + +out = color.label2rgb(labels2, img, kind='avg') +out = segmentation.mark_boundaries(out, labels2, (0, 0, 0)) +io.imshow(out) +io.show() diff --git a/doc/examples/plot_rank_mean.py b/doc/examples/plot_rank_mean.py index 6f16c440..e8f2394c 100644 --- a/doc/examples/plot_rank_mean.py +++ b/doc/examples/plot_rank_mean.py @@ -23,7 +23,7 @@ import matplotlib.pyplot as plt from skimage import data from skimage.morphology import disk -from skimage.filter import rank +from skimage.filters import rank image = (data.coins()).astype(np.uint16) * 16 diff --git a/doc/examples/plot_regionprops.py b/doc/examples/plot_regionprops.py index 065bae55..c4a8fc29 100644 --- a/doc/examples/plot_regionprops.py +++ b/doc/examples/plot_regionprops.py @@ -11,8 +11,7 @@ import matplotlib.pyplot as plt import numpy as np from skimage.draw import ellipse -from skimage.morphology import label -from skimage.measure import regionprops +from skimage.measure import label, regionprops from skimage.transform import rotate diff --git a/doc/examples/plot_register_translation.py b/doc/examples/plot_register_translation.py new file mode 100644 index 00000000..ec254e16 --- /dev/null +++ b/doc/examples/plot_register_translation.py @@ -0,0 +1,85 @@ +""" +===================================== +Cross-Correlation (Phase Correlation) +===================================== + +In this example, we use phase correlation to identify the relative shift +between two similar-sized images. + +The ``register_translation`` function uses cross-correlation in Fourier space, +optionally employing an upsampled matrix-multiplication DFT to achieve +arbitrary subpixel precision. [1]_ + +.. [1] Manuel Guizar-Sicairos, Samuel T. Thurman, and James R. Fienup, + "Efficient subpixel image registration algorithms," Optics Letters 33, + 156-158 (2008). + +""" +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data +from skimage.feature import register_translation +from skimage.feature.register_translation import _upsampled_dft +from scipy.ndimage.fourier import fourier_shift + +image = data.camera() +shift = (-2.4, 1.32) +# (-2.4, 1.32) pixel offset relative to reference coin +offset_image = fourier_shift(np.fft.fftn(image), shift) +offset_image = np.fft.ifftn(offset_image) +print("Known offset (y, x):") +print(shift) + +# pixel precision first +shift, error, diffphase = register_translation(image, offset_image) + +fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(8, 3)) + +ax1.imshow(image) +ax1.set_axis_off() +ax1.set_title('Reference image') + +ax2.imshow(offset_image.real) +ax2.set_axis_off() +ax2.set_title('Offset image') + +# View the output of a cross-correlation to show what the algorithm is +# doing behind the scenes +image_product = np.fft.fft2(image) * np.fft.fft2(offset_image).conj() +cc_image = np.fft.fftshift(np.fft.ifft2(image_product)) +ax3.imshow(cc_image.real) +ax3.set_axis_off() +ax3.set_title("Cross-correlation") + +plt.show() + +print("Detected pixel offset (y, x):") +print(shift) + +# subpixel precision +shift, error, diffphase = register_translation(image, offset_image, 100) + +fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(8, 3)) + +ax1.imshow(image) +ax1.set_axis_off() +ax1.set_title('Reference image') + +ax2.imshow(offset_image.real) +ax2.set_axis_off() +ax2.set_title('Offset image') + +# Calculate the upsampled DFT, again to show what the algorithm is doing +# behind the scenes. Constants correspond to calculated values in routine. +# See source code for details. +cc_image = _upsampled_dft(image_product, 150, 100, (shift*100)+75).conj() +ax3.imshow(cc_image.real) +ax3.set_axis_off() +ax3.set_title("Supersampled XC sub-area") + + +plt.show() + +print("Detected subpixel offset (y, x):") +print(shift) diff --git a/doc/examples/plot_restoration.py b/doc/examples/plot_restoration.py index fdffd953..52cbec27 100644 --- a/doc/examples/plot_restoration.py +++ b/doc/examples/plot_restoration.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """ ===================== -Deconvolution of Lena +Image Deconvolution ===================== -In this example, we deconvolve a noisy version of Lena using Wiener +In this example, we deconvolve a noisy version of an image 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. @@ -34,19 +34,19 @@ import matplotlib.pyplot as plt from skimage import color, data, restoration -lena = color.rgb2gray(data.lena()) +astro = color.rgb2gray(data.astronaut()) 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) +astro = conv2(astro, psf, 'same') +astro += 0.1 * astro.std() * np.random.standard_normal(astro.shape) -deconvolved, _ = restoration.unsupervised_wiener(lena, psf) +deconvolved, _ = restoration.unsupervised_wiener(astro, 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].imshow(astro, vmin=deconvolved.min(), vmax=deconvolved.max()) ax[0].axis('off') ax[0].set_title('Data') diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index abbf352d..d62d12a9 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -63,12 +63,12 @@ from __future__ import print_function import matplotlib.pyplot as plt import numpy as np -from skimage.data import lena +from skimage.data import astronaut from skimage.segmentation import felzenszwalb, slic, quickshift from skimage.segmentation import mark_boundaries from skimage.util import img_as_float -img = img_as_float(lena()[::2, ::2]) +img = img_as_float(astronaut()[::2, ::2]) segments_fz = felzenszwalb(img, scale=100, sigma=0.5, min_size=50) segments_slic = slic(img, n_segments=250, compactness=10, sigma=1) segments_quick = quickshift(img, kernel_size=3, max_dist=6, ratio=0.5) diff --git a/doc/examples/plot_threshold_adaptive.py b/doc/examples/plot_threshold_adaptive.py index e1e3cbee..9bf7b0c6 100644 --- a/doc/examples/plot_threshold_adaptive.py +++ b/doc/examples/plot_threshold_adaptive.py @@ -18,7 +18,7 @@ local neighborhood minus an offset value. import matplotlib.pyplot as plt from skimage import data -from skimage.filter import threshold_otsu, threshold_adaptive +from skimage.filters import threshold_otsu, threshold_adaptive image = data.page() diff --git a/doc/examples/plot_tinting_grayscale_images.py b/doc/examples/plot_tinting_grayscale_images.py index 8b07b41d..f4dde6fd 100644 --- a/doc/examples/plot_tinting_grayscale_images.py +++ b/doc/examples/plot_tinting_grayscale_images.py @@ -37,7 +37,7 @@ ax2.imshow(yellow_multiplier * image) 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), +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. @@ -46,6 +46,9 @@ 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. +.. _color spaces: + http://en.wikipedia.org/wiki/List_of_color_spaces_and_their_uses + Below, we plot a linear gradient in the hue, with the saturation and value turned all the way up: """ @@ -69,6 +72,8 @@ 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). +.. _HSV: http://en.wikipedia.org/wiki/HSL_and_HSV + Now, let's create a little utility function to take an RGB image and: 1. Transform the RGB image to HSV @@ -116,7 +121,7 @@ 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 +from skimage.filters import rank # Square regions defined as slices over the first two dimensions. top_left = (slice(100),) * 2 @@ -147,7 +152,4 @@ plt.show() 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 2237e50b..eeb510ed 100644 --- a/doc/examples/plot_view_as_blocks.py +++ b/doc/examples/plot_view_as_blocks.py @@ -7,10 +7,10 @@ This example illustrates the use of `view_as_blocks` from `skimage.util.shape`. Block views can be incredibly useful when one wants to perform local operations on non-overlapping image patches. -We use `lena` from `skimage.data` and virtually 'slice' it into square +We use `astronaut` from `skimage.data` and virtually 'slice' it into square blocks. Then, on each block, we either pool the mean, the max or the median value of that block. The results are displayed altogether, along -with a spline interpolation of order 3 rescaling of the original `lena` +with a spline interpolation of order 3 rescaling of the original `astronaut` image. """ @@ -24,20 +24,20 @@ from skimage import color from skimage.util.shape import view_as_blocks -# -- get `lena` from skimage.data in grayscale -l = color.rgb2gray(data.lena()) +# -- get `astronaut` from skimage.data in grayscale +l = color.rgb2gray(data.astronaut()) # -- size of blocks block_shape = (4, 4) -# -- see `lena` as a matrix of blocks (of shape +# -- see `astronaut` as a matrix of blocks (of shape # `block_shape`) view = view_as_blocks(l, block_shape) # -- collapse the last two dimensions in one flatten_view = view.reshape(view.shape[0], view.shape[1], -1) -# -- resampling `lena` by taking either the `mean`, +# -- resampling `astronaut` by taking either the `mean`, # the `max` or the `median` value of each blocks. mean_view = np.mean(flatten_view, axis=2) max_view = np.max(flatten_view, axis=2) diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py new file mode 100644 index 00000000..65e67706 --- /dev/null +++ b/doc/examples/plot_windowed_histogram.py @@ -0,0 +1,137 @@ +from __future__ import division +""" +======================== +Sliding window histogram +======================== + +Histogram matching can be used for object detection in images [1]_. This +example extracts a single coin from the `skimage.data.coins` image and uses +histogram matching to attempt to locate it within the original image. + +First, a box-shaped region of the image containing the target coin is +extracted and a histogram of its greyscale values is computed. + +Next, for each pixel in the test image, a histogram of the greyscale values in +a region of the image surrounding the pixel is computed. +`skimage.filters.rank.windowed_histogram` is used for this task, as it employs +an efficient sliding window based algorithm that is able to compute these +histograms quickly [2]_. The local histogram for the region surrounding each +pixel in the image is compared to that of the single coin, with a similarity +measure being computed and displayed. + +The histogram of the single coin is computed using `numpy.histogram` on a box +shaped region surrounding the coin, while the sliding window histograms are +computed using a disc shaped structural element of a slightly different size. +This is done in aid of demonstrating that the technique still finds similarity +in spite of these differences. + +To demonstrate the rotational invariance of the technique, the same test is +performed on a version of the coins image rotated by 45 degrees. + +References +---------- +.. [1] Porikli, F. "Integral Histogram: A Fast Way to Extract Histograms + in Cartesian Spaces" CVPR, 2005. Vol. 1. IEEE, 2005 +.. [2] S.Perreault and P.Hebert. Median filtering in constant time. + Trans. Image Processing, 16(9):2389-2394, 2007. +""" +import numpy as np +import matplotlib +import matplotlib.pyplot as plt + +from skimage import data, transform +from skimage.util import img_as_ubyte +from skimage.morphology import disk +from skimage.filters import rank + + +matplotlib.rcParams['font.size'] = 9 + + +def windowed_histogram_similarity(image, selem, reference_hist, n_bins): + # Compute normalized windowed histogram feature vector for each pixel + px_histograms = rank.windowed_histogram(image, selem, n_bins=n_bins) + + # Reshape coin histogram to (1,1,N) for broadcast when we want to use it in + # arithmetic operations with the windowed histograms from the image + reference_hist = reference_hist.reshape((1, 1) + reference_hist.shape) + + # Compute Chi squared distance metric: sum((X-Y)^2 / (X+Y)); + # a measure of distance between histograms + X = px_histograms + Y = reference_hist + + num = (X - Y) ** 2 + denom = X + Y + denom[denom == 0] = np.infty + frac = num / denom + + chi_sqr = 0.5 * np.sum(frac, axis=2) + + # Generate a similarity measure. It needs to be low when distance is high + # and high when distance is low; taking the reciprocal will do this. + # Chi squared will always be >= 0, add small value to prevent divide by 0. + similarity = 1 / (chi_sqr + 1.0e-4) + + return similarity + + +# Load the `skimage.data.coins` image +img = img_as_ubyte(data.coins()) + +# Quantize to 16 levels of greyscale; this way the output image will have a +# 16-dimensional feature vector per pixel +quantized_img = img // 16 + +# Select the coin from the 4th column, second row. +# Co-ordinate ordering: [x1,y1,x2,y2] +coin_coords = [184, 100, 228, 148] # 44 x 44 region +coin = quantized_img[coin_coords[1]:coin_coords[3], + coin_coords[0]:coin_coords[2]] + +# Compute coin histogram and normalize +coin_hist, _ = np.histogram(coin.flatten(), bins=16, range=(0, 16)) +coin_hist = coin_hist.astype(float) / np.sum(coin_hist) + + +# Compute a disk shaped mask that will define the shape of our sliding window +# Example coin is ~44px across, so make a disk 61px wide (2 * rad + 1) to be +# big enough for other coins too. +selem = disk(30) + + +# Compute the similarity across the complete image +similarity = windowed_histogram_similarity(quantized_img, selem, coin_hist, + coin_hist.shape[0]) + +# Now try a rotated image +rotated_img = img_as_ubyte(transform.rotate(img, 45.0, resize=True)) +# Quantize to 16 levels as before +quantized_rotated_image = rotated_img // 16 +# Similarity on rotated image +rotated_similarity = windowed_histogram_similarity(quantized_rotated_image, + selem, coin_hist, + coin_hist.shape[0]) + + +fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 10)) + +axes[0, 0].imshow(quantized_img, cmap='gray') +axes[0, 0].set_title('Quantized image') +axes[0, 0].axis('off') + +axes[0, 1].imshow(coin, cmap='gray') +axes[0, 1].set_title('Coin from 2nd row, 4th column') +axes[0, 1].axis('off') + +axes[1, 0].imshow(img, cmap='gray') +axes[1, 0].imshow(similarity, cmap='hot', alpha=0.5) +axes[1, 0].set_title('Original image with overlaid similarity') +axes[1, 0].axis('off') + +axes[1, 1].imshow(rotated_img, cmap='gray') +axes[1, 1].imshow(rotated_similarity, cmap='hot', alpha=0.5) +axes[1, 1].set_title('Rotated image with overlaid similarity') +axes[1, 1].axis('off') + +plt.show() diff --git a/doc/ext/LICENSE.txt b/doc/ext/LICENSE.txt index 3ebab893..589b78dc 100644 --- a/doc/ext/LICENSE.txt +++ b/doc/ext/LICENSE.txt @@ -15,7 +15,7 @@ The file - plot_directive.py -was derived from code in Matplotlib (http://matplotlib.sf.net/), which has the +was derived from code in Matplotlib (http://matplotlib.org), which has the following license: Copyright (c) 2002-2008 John D. Hunter; All Rights Reserved. diff --git a/doc/ext/docscrape.py b/doc/ext/docscrape.py index 615ea11f..1eb7c599 100644 --- a/doc/ext/docscrape.py +++ b/doc/ext/docscrape.py @@ -1,13 +1,16 @@ """Extract reference documentation from the NumPy source tree. """ +from __future__ import division, absolute_import, print_function import inspect import textwrap import re import pydoc -from StringIO import StringIO from warnings import warn +import collections +import sys + class Reader(object): """A line-based string reader. @@ -113,7 +116,7 @@ class NumpyDocString(object): return self._parsed_data[key] def __setitem__(self,key,val): - if not self._parsed_data.has_key(key): + if key not in self._parsed_data: warn("Unknown section %s" % key) else: self._parsed_data[key] = val @@ -265,13 +268,17 @@ class NumpyDocString(object): if self._is_at_section(): return - summary = self._doc.read_to_next_empty_line() - summary_str = " ".join([s.strip() for s in summary]).strip() - if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str): - self['Signature'] = summary_str - if not self._is_at_section(): - self['Summary'] = self._doc.read_to_next_empty_line() - else: + # If several signatures present, take the last one + while True: + summary = self._doc.read_to_next_empty_line() + summary_str = " ".join([s.strip() for s in summary]).strip() + if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str): + self['Signature'] = summary_str + if not self._is_at_section(): + continue + break + + if summary is not None: self['Summary'] = summary if not self._is_at_section(): @@ -328,7 +335,10 @@ class NumpyDocString(object): if self[name]: out += self._str_header(name) for param,param_type,desc in self[name]: - out += ['%s : %s' % (param, param_type)] + if param_type: + out += ['%s : %s' % (param, param_type)] + else: + out += [param] out += self._str_indent(desc) out += [''] return out @@ -370,7 +380,7 @@ class NumpyDocString(object): idx = self['index'] out = [] out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.iteritems(): + for section, references in idx.items(): if section == 'default': continue out += [' :%s: %s' % (section, ', '.join(references))] @@ -424,11 +434,14 @@ class FunctionDoc(NumpyDocString): func, func_name = self.get_func() try: # try to read signature - argspec = inspect.getargspec(func) + if sys.version_info[0] >= 3: + argspec = inspect.getfullargspec(func) + else: + argspec = inspect.getargspec(func) argspec = inspect.formatargspec(*argspec) argspec = argspec.replace('*','\*') signature = '%s%s' % (func_name, argspec) - except TypeError, e: + except TypeError as e: signature = '%s()' % func_name self['Signature'] = signature @@ -450,8 +463,8 @@ class FunctionDoc(NumpyDocString): 'meth': 'method'} if self._role: - if not roles.has_key(self._role): - print "Warning: invalid role %s" % self._role + if self._role not in roles: + print("Warning: invalid role %s" % self._role) out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''), func_name) @@ -460,12 +473,18 @@ class FunctionDoc(NumpyDocString): class ClassDoc(NumpyDocString): + + extra_public_methods = ['__call__'] + def __init__(self, cls, doc=None, modulename='', func_doc=FunctionDoc, config={}): if not inspect.isclass(cls) and cls is not None: raise ValueError("Expected a class or None, but got %r" % cls) self._cls = cls + self.show_inherited_members = config.get('show_inherited_class_members', + True) + if modulename and not modulename.endswith('.'): modulename += '.' self._mod = modulename @@ -478,23 +497,47 @@ class ClassDoc(NumpyDocString): NumpyDocString.__init__(self, doc) if config.get('show_class_members', True): - if not self['Methods']: - self['Methods'] = [(name, '', '') - for name in sorted(self.methods)] - if not self['Attributes']: - self['Attributes'] = [(name, '', '') - for name in sorted(self.properties)] + def splitlines_x(s): + if not s: + return [] + else: + return s.splitlines() + + for field, items in [('Methods', self.methods), + ('Attributes', self.properties)]: + if not self[field]: + doc_list = [] + for name in sorted(items): + try: + doc_item = pydoc.getdoc(getattr(self._cls, name)) + doc_list.append((name, '', splitlines_x(doc_item))) + except AttributeError: + pass # method doesn't exist + self[field] = doc_list @property def methods(self): if self._cls is None: return [] - return [name for name,func in inspect.getmembers(self._cls) - if not name.startswith('_') and callable(func)] + return [name for name, func in inspect.getmembers(self._cls) + if ((not name.startswith('_') + or name in self.extra_public_methods) + and isinstance(func, collections.Callable) + and self._is_show_member(name))] @property def properties(self): if self._cls is None: return [] - return [name for name,func in inspect.getmembers(self._cls) - if not name.startswith('_') and func is None] + return [name for name, func in inspect.getmembers(self._cls) + if (not name.startswith('_') and + (func is None or isinstance(func, property) or + inspect.isgetsetdescriptor(func)) + and self._is_show_member(name))] + + def _is_show_member(self, name): + if self.show_inherited_members: + return True # show all class members + if name not in self._cls.__dict__: + return False # class member is inherited, we do not show it + return True diff --git a/doc/ext/docscrape_sphinx.py b/doc/ext/docscrape_sphinx.py index e44e770e..9e198eaf 100644 --- a/doc/ext/docscrape_sphinx.py +++ b/doc/ext/docscrape_sphinx.py @@ -1,11 +1,24 @@ -import re, inspect, textwrap, pydoc +from __future__ import division, absolute_import, print_function + +import sys, re, inspect, textwrap, pydoc import sphinx +import collections from docscrape import NumpyDocString, FunctionDoc, ClassDoc +if sys.version_info[0] >= 3: + sixu = lambda s: s +else: + sixu = lambda s: unicode(s, 'unicode_escape') + + class SphinxDocString(NumpyDocString): def __init__(self, docstring, config={}): - self.use_plots = config.get('use_plots', False) NumpyDocString.__init__(self, docstring, config=config) + self.load_config(config) + + def load_config(self, config): + self.use_plots = config.get('use_plots', False) + self.class_members_toctree = config.get('class_members_toctree', True) # string conversion routines def _str_header(self, name, symbol='`'): @@ -33,16 +46,37 @@ class SphinxDocString(NumpyDocString): def _str_extended_summary(self): return self['Extended Summary'] + [''] + def _str_returns(self): + out = [] + if self['Returns']: + out += self._str_field_list('Returns') + out += [''] + for param, param_type, desc in self['Returns']: + if param_type: + out += self._str_indent(['**%s** : %s' % (param.strip(), + param_type)]) + else: + out += self._str_indent([param.strip()]) + if desc: + out += [''] + out += self._str_indent(desc, 8) + out += [''] + return out + def _str_param_list(self, name): out = [] if self[name]: out += self._str_field_list(name) out += [''] - for param,param_type,desc in self[name]: - out += self._str_indent(['**%s** : %s' % (param.strip(), - param_type)]) - out += [''] - out += self._str_indent(desc,8) + for param, param_type, desc in self[name]: + if param_type: + out += self._str_indent(['**%s** : %s' % (param.strip(), + param_type)]) + else: + out += self._str_indent(['**%s**' % param.strip()]) + if desc: + out += [''] + out += self._str_indent(desc, 8) out += [''] return out @@ -72,25 +106,36 @@ class SphinxDocString(NumpyDocString): others = [] for param, param_type, desc in self[name]: param = param.strip() - if not self._obj or hasattr(self._obj, param): + + # Check if the referenced member can have a docstring or not + param_obj = getattr(self._obj, param, None) + if not (callable(param_obj) + or isinstance(param_obj, property) + or inspect.isgetsetdescriptor(param_obj)): + param_obj = None + + if param_obj and (pydoc.getdoc(param_obj) or not desc): + # Referenced object has a docstring autosum += [" %s%s" % (prefix, param)] else: others.append((param, param_type, desc)) if autosum: - out += ['.. autosummary::', ' :toctree:', ''] - out += autosum + out += ['.. autosummary::'] + if self.class_members_toctree: + out += [' :toctree:'] + out += [''] + autosum if others: - maxlen_0 = max([len(x[0]) for x in others]) - maxlen_1 = max([len(x[1]) for x in others]) - hdr = "="*maxlen_0 + " " + "="*maxlen_1 + " " + "="*10 - fmt = '%%%ds %%%ds ' % (maxlen_0, maxlen_1) - n_indent = maxlen_0 + maxlen_1 + 4 - out += [hdr] + maxlen_0 = max(3, max([len(x[0]) for x in others])) + hdr = sixu("=")*maxlen_0 + sixu(" ") + sixu("=")*10 + fmt = sixu('%%%ds %%s ') % (maxlen_0,) + out += ['', hdr] for param, param_type, desc in others: - out += [fmt % (param.strip(), param_type)] - out += self._str_indent(desc, n_indent) + desc = sixu(" ").join(x.strip() for x in desc).strip() + if param_type: + desc = "(%s) %s" % (param_type, desc) + out += [fmt % (param.strip(), desc)] out += [hdr] out += [''] return out @@ -127,7 +172,7 @@ class SphinxDocString(NumpyDocString): return out out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.iteritems(): + for section, references in idx.items(): if section == 'default': continue elif section == 'refguide': @@ -178,8 +223,9 @@ class SphinxDocString(NumpyDocString): out += self._str_index() + [''] out += self._str_summary() out += self._str_extended_summary() - for param_list in ('Parameters', 'Returns', 'Other Parameters', - 'Raises', 'Warns'): + out += self._str_param_list('Parameters') + out += self._str_returns() + for param_list in ('Other Parameters', 'Raises', 'Warns'): out += self._str_param_list(param_list) out += self._str_warnings() out += self._str_see_also(func_role) @@ -193,17 +239,18 @@ class SphinxDocString(NumpyDocString): class SphinxFunctionDoc(SphinxDocString, FunctionDoc): def __init__(self, obj, doc=None, config={}): - self.use_plots = config.get('use_plots', False) + self.load_config(config) FunctionDoc.__init__(self, obj, doc=doc, config=config) class SphinxClassDoc(SphinxDocString, ClassDoc): def __init__(self, obj, doc=None, func_doc=None, config={}): - self.use_plots = config.get('use_plots', False) + self.load_config(config) ClassDoc.__init__(self, obj, doc=doc, func_doc=None, config=config) class SphinxObjDoc(SphinxDocString): def __init__(self, obj, doc=None, config={}): self._f = obj + self.load_config(config) SphinxDocString.__init__(self, doc, config=config) def get_doc_object(obj, what=None, doc=None, config={}): @@ -212,7 +259,7 @@ def get_doc_object(obj, what=None, doc=None, config={}): what = 'class' elif inspect.ismodule(obj): what = 'module' - elif callable(obj): + elif isinstance(obj, collections.Callable): what = 'function' else: what = 'object' diff --git a/doc/ext/numpydoc.py b/doc/ext/numpydoc.py index aa390056..cf298dbf 100644 --- a/doc/ext/numpydoc.py +++ b/doc/ext/numpydoc.py @@ -12,45 +12,68 @@ It will: - Renumber references. - Extract the signature from the docstring, if it can't be determined otherwise. -.. [1] http://projects.scipy.org/numpy/wiki/CodingStyleGuidelines#docstring-standard +.. [1] https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt """ +from __future__ import division, absolute_import, print_function + +import sys +import re +import pydoc +import sphinx +import inspect +import collections + +if sphinx.__version__ < '1.0.1': + raise RuntimeError("Sphinx 1.0.1 or newer is required") -import os, re, pydoc from docscrape_sphinx import get_doc_object, SphinxDocString from sphinx.util.compat import Directive -import inspect + +if sys.version_info[0] >= 3: + sixu = lambda s: s +else: + sixu = lambda s: unicode(s, 'unicode_escape') + def mangle_docstrings(app, what, name, obj, options, lines, reference_offset=[0]): - cfg = dict(use_plots=app.config.numpydoc_use_plots, - show_class_members=app.config.numpydoc_show_class_members) + cfg = dict( + use_plots=app.config.numpydoc_use_plots, + show_class_members=app.config.numpydoc_show_class_members, + show_inherited_class_members=app.config.numpydoc_show_inherited_class_members, + class_members_toctree=app.config.numpydoc_class_members_toctree, + ) if what == 'module': # Strip top title - title_re = re.compile(ur'^\s*[#*=]{4,}\n[a-z0-9 -]+\n[#*=]{4,}\s*', + title_re = re.compile(sixu('^\\s*[#*=]{4,}\\n[a-z0-9 -]+\\n[#*=]{4,}\\s*'), re.I|re.S) - lines[:] = title_re.sub(u'', u"\n".join(lines)).split(u"\n") + lines[:] = title_re.sub(sixu(''), sixu("\n").join(lines)).split(sixu("\n")) else: - doc = get_doc_object(obj, what, u"\n".join(lines), config=cfg) - lines[:] = unicode(doc).split(u"\n") + doc = get_doc_object(obj, what, sixu("\n").join(lines), config=cfg) + if sys.version_info[0] >= 3: + doc = str(doc) + else: + doc = unicode(doc) + lines[:] = doc.split(sixu("\n")) if app.config.numpydoc_edit_link and hasattr(obj, '__name__') and \ obj.__name__: if hasattr(obj, '__module__'): - v = dict(full_name=u"%s.%s" % (obj.__module__, obj.__name__)) + v = dict(full_name=sixu("%s.%s") % (obj.__module__, obj.__name__)) else: v = dict(full_name=obj.__name__) - lines += [u'', u'.. htmlonly::', ''] - lines += [u' %s' % x for x in + lines += [sixu(''), sixu('.. htmlonly::'), sixu('')] + lines += [sixu(' %s') % x for x in (app.config.numpydoc_edit_link % v).split("\n")] # replace reference numbers so that there are no duplicates references = [] for line in lines: line = line.strip() - m = re.match(ur'^.. \[([a-z0-9_.-])\]', line, re.I) + m = re.match(sixu('^.. \\[([a-z0-9_.-])\\]'), line, re.I) if m: references.append(m.group(1)) @@ -59,14 +82,14 @@ def mangle_docstrings(app, what, name, obj, options, lines, if references: for i, line in enumerate(lines): for r in references: - if re.match(ur'^\d+$', r): - new_r = u"R%d" % (reference_offset[0] + int(r)) + if re.match(sixu('^\\d+$'), r): + new_r = sixu("R%d") % (reference_offset[0] + int(r)) else: - new_r = u"%s%d" % (r, reference_offset[0]) - lines[i] = lines[i].replace(u'[%s]_' % r, - u'[%s]_' % new_r) - lines[i] = lines[i].replace(u'.. [%s]' % r, - u'.. [%s]' % new_r) + new_r = sixu("%s%d") % (r, reference_offset[0]) + lines[i] = lines[i].replace(sixu('[%s]_') % r, + sixu('[%s]_') % new_r) + lines[i] = lines[i].replace(sixu('.. [%s]') % r, + sixu('.. [%s]') % new_r) reference_offset[0] += len(references) @@ -77,15 +100,18 @@ def mangle_signature(app, what, name, obj, options, sig, retann): 'initializes x; see ' in pydoc.getdoc(obj.__init__))): return '', '' - if not (callable(obj) or hasattr(obj, '__argspec_is_invalid_')): return + if not (isinstance(obj, collections.Callable) or hasattr(obj, '__argspec_is_invalid_')): return if not hasattr(obj, '__doc__'): return doc = SphinxDocString(pydoc.getdoc(obj)) if doc['Signature']: - sig = re.sub(u"^[^(]*", u"", doc['Signature']) - return sig, u'' + sig = re.sub(sixu("^[^(]*"), sixu(""), doc['Signature']) + return sig, sixu('') def setup(app, get_doc_object_=get_doc_object): + if not hasattr(app, 'add_config_value'): + return # probably called by nose, better bail out + global get_doc_object get_doc_object = get_doc_object_ @@ -94,6 +120,8 @@ def setup(app, get_doc_object_=get_doc_object): app.add_config_value('numpydoc_edit_link', None, False) app.add_config_value('numpydoc_use_plots', None, False) app.add_config_value('numpydoc_show_class_members', True, True) + app.add_config_value('numpydoc_show_inherited_class_members', True, True) + app.add_config_value('numpydoc_class_members_toctree', True, True) # Extra mangling domains app.add_domain(NumpyPythonDomain) @@ -115,7 +143,7 @@ class ManglingDomainBase(object): self.wrap_mangling_directives() def wrap_mangling_directives(self): - for name, objtype in self.directive_mangling_map.items(): + for name, objtype in list(self.directive_mangling_map.items()): self.directives[name] = wrap_mangling_directive( self.directives[name], objtype) @@ -130,6 +158,7 @@ class NumpyPythonDomain(ManglingDomainBase, PythonDomain): 'staticmethod': 'function', 'attribute': 'attribute', } + indices = [] class NumpyCDomain(ManglingDomainBase, CDomain): name = 'np-c' @@ -161,4 +190,3 @@ def wrap_mangling_directive(base_directive, objtype): return base_directive.run(self) return directive - diff --git a/doc/ext/plot2rst.py b/doc/ext/plot2rst.py index 258a6929..99fd9a3f 100644 --- a/doc/ext/plot2rst.py +++ b/doc/ext/plot2rst.py @@ -66,10 +66,12 @@ Suggested CSS definitions """ import os +import re import shutil import token import tokenize import traceback +import itertools import numpy as np import matplotlib @@ -83,7 +85,7 @@ from skimage.util.dtype import dtype_range from notebook import Notebook from docutils.core import publish_parts - +from sphinx.domains.python import PythonDomain LITERALINCLUDE = """ .. literalinclude:: {src_name} @@ -132,8 +134,8 @@ GALLERY_IMAGE_TEMPLATE = """ class Path(str): """Path object for manipulating directory and file paths.""" - def __init__(self, path): - super(Path, self).__init__(path) + def __new__(self, path): + return str.__new__(self, path) @property def isdir(self): @@ -203,7 +205,7 @@ def generate_examples_and_gallery(example_dir, rst_dir, cfg): rst_dir.makedirs() # we create an index.rst with all examples - gallery_index = file(rst_dir.pjoin('index'+cfg.source_suffix), 'w') + gallery_index = open(rst_dir.pjoin('index'+cfg.source_suffix), 'w') # Here we don't use an os.walk, but we recurse only twice: flat is # better than nested. @@ -244,7 +246,7 @@ def write_gallery(gallery_index, src_dir, rst_dir, cfg, depth=0): print(80*'_') return - gallery_description = file(gallery_template).read() + gallery_description = open(gallery_template).read() gallery_index.write('\n\n%s\n\n' % gallery_description) rst_dir.makedirs() @@ -256,7 +258,8 @@ def write_gallery(gallery_index, src_dir, rst_dir, cfg, depth=0): else: sub_dir_list = src_dir.psplit()[-depth:] sub_dir = Path('/'.join(sub_dir_list) + '/') - gallery_index.write(TOCTREE_TEMPLATE % (sub_dir + '\n '.join(ex_names))) + joiner = '\n %s' % sub_dir + gallery_index.write(TOCTREE_TEMPLATE % (sub_dir + joiner.join(ex_names))) for src_name in examples: @@ -332,6 +335,8 @@ def write_example(src_name, src_dir, rst_dir, cfg): notebook_path.exists: return + print('plot2rst: %s' % basename) + blocks = split_code_and_text_blocks(example_file) if blocks[0][2].startswith('#!'): blocks.pop(0) # don't add shebang line to rst file. @@ -380,15 +385,57 @@ def write_example(src_name, src_dir, rst_dir, cfg): # Export example to IPython notebook nb = Notebook() - for (cell_type, _, content) in blocks: - content = content.rstrip('\n') + # Add sphinx roles to the examples, otherwise docutils + # cannot compile the ReST for the notebook + sphinx_roles = PythonDomain.roles.keys() + preamble = '\n'.join('.. role:: py:{0}(literal)\n'.format(role) + for role in sphinx_roles) + # Grab all references to inject them in cells where needed + ref_regexp = re.compile('\n(\.\. \[(\d+)\].*(?:\n[ ]{7,8}.*)+)') + math_role_regexp = re.compile(':math:`(.*?)`') + + text = '\n'.join((content for (cell_type, _, content) in blocks + if cell_type != 'code')) + + references = re.findall(ref_regexp, text) + + for (cell_type, _, content) in blocks: if cell_type == 'code': nb.add_cell(content, cell_type='code') else: - content = content.replace('"""', '') + if content.startswith('r'): + content = content.replace('r"""', '') + escaped = False + else: + content = content.replace('"""', '') + escaped = True + + if not escaped: + content = content.replace("\\", "\\\\") + + content = content.replace('.. seealso::', '**See also:**') + content = re.sub(math_role_regexp, r'$\1$', content) + + # Remove math directive when rendering notebooks + # until we implement a smarter way of capturing and replacing + # its content + content = content.replace('.. math::', '') + + if not content.strip(): + continue + + content = (preamble + content).rstrip('\n') content = '\n'.join([line for line in content.split('\n') if not line.startswith('.. image')]) + + # Remove reference links until we can figure out a better way to + # preserve them + for (reference, ref_id) in references: + ref_tag = '[{0}]_'.format(ref_id) + if ref_tag in content: + content = content.replace(ref_tag, ref_tag[:-1]) + html = publish_parts(content, writer_name='html')['html_body'] nb.add_cell(html, cell_type='markdown') diff --git a/doc/ext/plot_directive.py b/doc/ext/plot_directive.py index 4a32b6f1..2014f857 100644 --- a/doc/ext/plot_directive.py +++ b/doc/ext/plot_directive.py @@ -1,78 +1,44 @@ """ -A directive for including a matplotlib plot in a Sphinx document. +A special directive for generating a matplotlib plot. -By default, in HTML output, `plot` will include a .png file with a -link to a high-res .png and .pdf. In LaTeX output, it will include a -.pdf. +.. warning:: -The source code for the plot may be included in one of three ways: + This is a hacked version of plot_directive.py from Matplotlib. + It's very much subject to change! - 1. **A path to a source file** as the argument to the directive:: - .. plot:: path/to/plot.py +Usage +----- - When a path to a source file is given, the content of the - directive may optionally contain a caption for the plot:: +Can be used like this:: - .. plot:: path/to/plot.py + .. plot:: examples/example.py - This is the caption for the plot + .. plot:: - Additionally, one my specify the name of a function to call (with - no arguments) immediately after importing the module:: + import matplotlib.pyplot as plt + plt.plot([1,2,3], [4,5,6]) - .. plot:: path/to/plot.py plot_function1 + .. plot:: - 2. Included as **inline content** to the directive:: + A plotting example: - .. plot:: + >>> import matplotlib.pyplot as plt + >>> plt.plot([1,2,3], [4,5,6]) - import matplotlib.pyplot as plt - import matplotlib.image as mpimg - import numpy as np - img = mpimg.imread('_static/stinkbug.png') - imgplot = plt.imshow(img) +The content is interpreted as doctest formatted if it has a line starting +with ``>>>``. - 3. Using **doctest** syntax:: - - .. plot:: - A plotting example: - >>> import matplotlib.pyplot as plt - >>> plt.plot([1,2,3], [4,5,6]) - -Options -------- - -The ``plot`` directive supports the following options: +The ``plot`` directive supports the options format : {'python', 'doctest'} Specify the format of the input include-source : bool - Whether to display the source code. The default can be changed - using the `plot_include_source` variable in conf.py + Whether to display the source code. Default can be changed in conf.py - encoding : str - If this source file is in a non-UTF8 or non-ASCII encoding, - the encoding must be specified using the `:encoding:` option. - The encoding will not be inferred using the ``-*- coding -*-`` - metacomment. - - context : bool - If provided, the code will be run in the context of all - previous plot directives for which the `:context:` option was - specified. This only applies to inline code plot directives, - not those run from files. - - nofigs : bool - If specified, the code block will be run, but no figures will - be inserted. This is usually useful with the ``:context:`` - option. - -Additionally, this directive supports all of the options of the -`image` directive, except for `target` (since plot will add its own -target). These include `alt`, `height`, `width`, `scale`, `align` and -`class`. +and the ``image`` directive options ``alt``, ``height``, ``width``, +``scale``, ``align``, ``class``. Configuration options --------------------- @@ -86,9 +52,9 @@ The plot directive has the following configuration options: Code that should be executed before each plot. plot_basedir - Base directory, to which ``plot::`` file names are relative - to. (If None or empty, file names are relative to the - directoly where the file containing the directive is.) + Base directory, to which plot:: file names are relative to. + (If None or empty, file names are relative to the directoly where + the file containing the directive is.) plot_formats File formats to generate. List of tuples or strings:: @@ -101,26 +67,93 @@ The plot directive has the following configuration options: plot_html_show_formats Whether to show links to the files in HTML. - plot_rcparams - A dictionary containing any non-standard rcParams that should - be applied before each plot. +TODO +---- + +* Refactor Latex output; now it's plain images, but it would be nice + to make them appear side-by-side, or in floats. """ +from __future__ import division, absolute_import, print_function -import sys, os, glob, shutil, imp, warnings, cStringIO, re, textwrap, \ - traceback, exceptions - -from docutils.parsers.rst import directives -from docutils import nodes -from docutils.parsers.rst.directives.images import Image -align = Image.align +import sys, os, glob, shutil, imp, warnings, re, textwrap, traceback import sphinx -sphinx_version = sphinx.__version__.split(".") -# The split is necessary for sphinx beta versions where the string is -# '6b1' -sphinx_version = tuple([int(re.split('[a-z]', x)[0]) - for x in sphinx_version[:2]]) +if sys.version_info[0] >= 3: + from io import StringIO +else: + from io import StringIO + +import warnings +warnings.warn("A plot_directive module is also available under " + "matplotlib.sphinxext; expect this numpydoc.plot_directive " + "module to be deprecated after relevant features have been " + "integrated there.", + FutureWarning, stacklevel=2) + + +#------------------------------------------------------------------------------ +# Registration hook +#------------------------------------------------------------------------------ + +def setup(app): + setup.app = app + setup.config = app.config + setup.confdir = app.confdir + + app.add_config_value('plot_pre_code', '', True) + app.add_config_value('plot_include_source', False, True) + app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True) + app.add_config_value('plot_basedir', None, True) + app.add_config_value('plot_html_show_formats', True, True) + + app.add_directive('plot', plot_directive, True, (0, 1, False), + **plot_directive_options) + +#------------------------------------------------------------------------------ +# plot:: directive +#------------------------------------------------------------------------------ +from docutils.parsers.rst import directives +from docutils import nodes + +def plot_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + return run(arguments, content, options, state_machine, state, lineno) +plot_directive.__doc__ = __doc__ + +def _option_boolean(arg): + if not arg or not arg.strip(): + # no argument given, assume used as a flag + return True + elif arg.strip().lower() in ('no', '0', 'false'): + return False + elif arg.strip().lower() in ('yes', '1', 'true'): + return True + else: + raise ValueError('"%s" unknown boolean' % arg) + +def _option_format(arg): + return directives.choice(arg, ('python', 'lisp')) + +def _option_align(arg): + return directives.choice(arg, ("top", "middle", "bottom", "left", "center", + "right")) + +plot_directive_options = {'alt': directives.unchanged, + 'height': directives.length_or_unitless, + 'width': directives.length_or_percentage_or_unitless, + 'scale': directives.nonnegative_int, + 'align': _option_align, + 'class': directives.class_option, + 'include-source': _option_boolean, + 'format': _option_format, + } + +#------------------------------------------------------------------------------ +# Generating output +#------------------------------------------------------------------------------ + +from docutils import nodes, utils try: # Sphinx depends on either Jinja or Jinja2 @@ -132,20 +165,426 @@ except ImportError: def format_template(template, **kw): return jinja.from_string(template, **kw) +TEMPLATE = """ +{{ source_code }} + +{{ only_html }} + + {% if source_link or (html_show_formats and not multi_image) %} + ( + {%- if source_link -%} + `Source code <{{ source_link }}>`__ + {%- endif -%} + {%- if html_show_formats and not multi_image -%} + {%- for img in images -%} + {%- for fmt in img.formats -%} + {%- if source_link or not loop.first -%}, {% endif -%} + `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- endfor -%} + {%- endfor -%} + {%- endif -%} + ) + {% endif %} + + {% for img in images %} + .. figure:: {{ build_dir }}/{{ img.basename }}.png + {%- for option in options %} + {{ option }} + {% endfor %} + + {% if html_show_formats and multi_image -%} + ( + {%- for fmt in img.formats -%} + {%- if not loop.first -%}, {% endif -%} + `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- endfor -%} + ) + {%- endif -%} + {% endfor %} + +{{ only_latex }} + + {% for img in images %} + .. image:: {{ build_dir }}/{{ img.basename }}.pdf + {% endfor %} + +""" + +class ImageFile(object): + def __init__(self, basename, dirname): + self.basename = basename + self.dirname = dirname + self.formats = [] + + def filename(self, format): + return os.path.join(self.dirname, "%s.%s" % (self.basename, format)) + + def filenames(self): + return [self.filename(fmt) for fmt in self.formats] + +def run(arguments, content, options, state_machine, state, lineno): + if arguments and content: + raise RuntimeError("plot:: directive can't have both args and content") + + document = state_machine.document + config = document.settings.env.config + + options.setdefault('include-source', config.plot_include_source) + + # determine input + rst_file = document.attributes['source'] + rst_dir = os.path.dirname(rst_file) + + if arguments: + if not config.plot_basedir: + source_file_name = os.path.join(rst_dir, + directives.uri(arguments[0])) + else: + source_file_name = os.path.join(setup.confdir, config.plot_basedir, + directives.uri(arguments[0])) + code = open(source_file_name, 'r').read() + output_base = os.path.basename(source_file_name) + else: + source_file_name = rst_file + code = textwrap.dedent("\n".join(map(str, content))) + counter = document.attributes.get('_plot_counter', 0) + 1 + document.attributes['_plot_counter'] = counter + base, ext = os.path.splitext(os.path.basename(source_file_name)) + output_base = '%s-%d.py' % (base, counter) + + base, source_ext = os.path.splitext(output_base) + if source_ext in ('.py', '.rst', '.txt'): + output_base = base + else: + source_ext = '' + + # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames + output_base = output_base.replace('.', '-') + + # is it in doctest format? + is_doctest = contains_doctest(code) + if 'format' in options: + if options['format'] == 'python': + is_doctest = False + else: + is_doctest = True + + # determine output directory name fragment + source_rel_name = relpath(source_file_name, setup.confdir) + source_rel_dir = os.path.dirname(source_rel_name) + while source_rel_dir.startswith(os.path.sep): + source_rel_dir = source_rel_dir[1:] + + # build_dir: where to place output files (temporarily) + build_dir = os.path.join(os.path.dirname(setup.app.doctreedir), + 'plot_directive', + source_rel_dir) + if not os.path.exists(build_dir): + os.makedirs(build_dir) + + # output_dir: final location in the builder's directory + dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir, + source_rel_dir)) + + # how to link to files from the RST file + dest_dir_link = os.path.join(relpath(setup.confdir, rst_dir), + source_rel_dir).replace(os.path.sep, '/') + build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/') + source_link = dest_dir_link + '/' + output_base + source_ext + + # make figures + try: + results = makefig(code, source_file_name, build_dir, output_base, + config) + errors = [] + except PlotError as err: + reporter = state.memo.reporter + sm = reporter.system_message( + 2, "Exception occurred in plotting %s: %s" % (output_base, err), + line=lineno) + results = [(code, [])] + errors = [sm] + + # generate output restructuredtext + total_lines = [] + for j, (code_piece, images) in enumerate(results): + if options['include-source']: + if is_doctest: + lines = [''] + lines += [row.rstrip() for row in code_piece.split('\n')] + else: + lines = ['.. code-block:: python', ''] + lines += [' %s' % row.rstrip() + for row in code_piece.split('\n')] + source_code = "\n".join(lines) + else: + source_code = "" + + opts = [':%s: %s' % (key, val) for key, val in list(options.items()) + if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] + + only_html = ".. only:: html" + only_latex = ".. only:: latex" + + if j == 0: + src_link = source_link + else: + src_link = None + + result = format_template( + TEMPLATE, + dest_dir=dest_dir_link, + build_dir=build_dir_link, + source_link=src_link, + multi_image=len(images) > 1, + only_html=only_html, + only_latex=only_latex, + options=opts, + images=images, + source_code=source_code, + html_show_formats=config.plot_html_show_formats) + + total_lines.extend(result.split("\n")) + total_lines.extend("\n") + + if total_lines: + state_machine.insert_input(total_lines, source=source_file_name) + + # copy image files to builder's output directory + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + + for code_piece, images in results: + for img in images: + for fn in img.filenames(): + shutil.copyfile(fn, os.path.join(dest_dir, + os.path.basename(fn))) + + # copy script (if necessary) + if source_file_name == rst_file: + target_name = os.path.join(dest_dir, output_base + source_ext) + f = open(target_name, 'w') + f.write(unescape_doctest(code)) + f.close() + + return errors + + +#------------------------------------------------------------------------------ +# Run code and capture figures +#------------------------------------------------------------------------------ import matplotlib -import matplotlib.cbook as cbook matplotlib.use('Agg') import matplotlib.pyplot as plt +import matplotlib.image as image from matplotlib import _pylab_helpers -__version__ = 2 +import exceptions + +def contains_doctest(text): + try: + # check if it's valid Python as-is + compile(text, '', 'exec') + return False + except SyntaxError: + pass + r = re.compile(r'^\s*>>>', re.M) + m = r.search(text) + return bool(m) + +def unescape_doctest(text): + """ + Extract code from a piece of text, which contains either Python code + or doctests. + + """ + if not contains_doctest(text): + return text + + code = "" + for line in text.split("\n"): + m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line) + if m: + code += m.group(2) + "\n" + elif line.strip(): + code += "# " + line.strip() + "\n" + else: + code += "\n" + return code + +def split_code_at_show(text): + """ + Split code at plt.show() + + """ + + parts = [] + is_doctest = contains_doctest(text) + + part = [] + for line in text.split("\n"): + if (not is_doctest and line.strip() == 'plt.show()') or \ + (is_doctest and line.strip() == '>>> plt.show()'): + part.append(line) + parts.append("\n".join(part)) + part = [] + else: + part.append(line) + if "\n".join(part).strip(): + parts.append("\n".join(part)) + return parts + +class PlotError(RuntimeError): + pass + +def run_code(code, code_path, ns=None): + # Change the working directory to the directory of the example, so + # it can get at its data files, if any. + pwd = os.getcwd() + old_sys_path = list(sys.path) + if code_path is not None: + dirname = os.path.abspath(os.path.dirname(code_path)) + os.chdir(dirname) + sys.path.insert(0, dirname) + + # Redirect stdout + stdout = sys.stdout + sys.stdout = StringIO() + + # Reset sys.argv + old_sys_argv = sys.argv + sys.argv = [code_path] + + try: + try: + code = unescape_doctest(code) + if ns is None: + ns = {} + if not ns: + exec(setup.config.plot_pre_code, ns) + exec(code, ns) + except (Exception, SystemExit) as err: + raise PlotError(traceback.format_exc()) + finally: + os.chdir(pwd) + sys.argv = old_sys_argv + sys.path[:] = old_sys_path + sys.stdout = stdout + return ns + + +#------------------------------------------------------------------------------ +# Generating figures +#------------------------------------------------------------------------------ + +def out_of_date(original, derived): + """ + Returns True if derivative is out-of-date wrt original, + both of which are full file paths. + """ + return (not os.path.exists(derived) + or os.stat(derived).st_mtime < os.stat(original).st_mtime) + + +def makefig(code, code_path, output_dir, output_base, config): + """ + Run a pyplot script *code* and save the images under *output_dir* + with file names derived from *output_base* + + """ + + # -- Parse format list + default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 50} + formats = [] + for fmt in config.plot_formats: + if isinstance(fmt, str): + formats.append((fmt, default_dpi.get(fmt, 80))) + elif type(fmt) in (tuple, list) and len(fmt)==2: + formats.append((str(fmt[0]), int(fmt[1]))) + else: + raise PlotError('invalid image format "%r" in plot_formats' % fmt) + + # -- Try to determine if all images already exist + + code_pieces = split_code_at_show(code) + + # Look for single-figure output files first + all_exists = True + img = ImageFile(output_base, output_dir) + for format, dpi in formats: + if out_of_date(code_path, img.filename(format)): + all_exists = False + break + img.formats.append(format) + + if all_exists: + return [(code, [img])] + + # Then look for multi-figure output files + results = [] + all_exists = True + for i, code_piece in enumerate(code_pieces): + images = [] + for j in range(1000): + img = ImageFile('%s_%02d_%02d' % (output_base, i, j), output_dir) + for format, dpi in formats: + if out_of_date(code_path, img.filename(format)): + all_exists = False + break + img.formats.append(format) + + # assume that if we have one, we have them all + if not all_exists: + all_exists = (j > 0) + break + images.append(img) + if not all_exists: + break + results.append((code_piece, images)) + + if all_exists: + return results + + # -- We didn't find the files, so build them + + results = [] + ns = {} + + for i, code_piece in enumerate(code_pieces): + # Clear between runs + plt.close('all') + + # Run code + run_code(code_piece, code_path, ns) + + # Collect images + images = [] + fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() + for j, figman in enumerate(fig_managers): + if len(fig_managers) == 1 and len(code_pieces) == 1: + img = ImageFile(output_base, output_dir) + else: + img = ImageFile("%s_%02d_%02d" % (output_base, i, j), + output_dir) + images.append(img) + for format, dpi in formats: + try: + figman.canvas.figure.savefig(img.filename(format), dpi=dpi) + except exceptions.BaseException as err: + raise PlotError(traceback.format_exc()) + img.formats.append(format) + + # Results + results.append((code_piece, images)) + + return results + #------------------------------------------------------------------------------ # Relative pathnames #------------------------------------------------------------------------------ -# os.path.relpath is new in Python 2.6 try: from os.path import relpath except ImportError: @@ -201,567 +640,3 @@ except ImportError: return join(*rel_list) else: raise RuntimeError("Unsupported platform (no relpath available!)") - -#------------------------------------------------------------------------------ -# Registration hook -#------------------------------------------------------------------------------ - -def plot_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - return run(arguments, content, options, state_machine, state, lineno) -plot_directive.__doc__ = __doc__ - -def _option_boolean(arg): - if not arg or not arg.strip(): - # no argument given, assume used as a flag - return True - elif arg.strip().lower() in ('no', '0', 'false'): - return False - elif arg.strip().lower() in ('yes', '1', 'true'): - return True - else: - raise ValueError('"%s" unknown boolean' % arg) - -def _option_format(arg): - return directives.choice(arg, ('python', 'doctest')) - -def _option_align(arg): - return directives.choice(arg, ("top", "middle", "bottom", "left", "center", - "right")) - -def mark_plot_labels(app, document): - """ - To make plots referenceable, we need to move the reference from - the "htmlonly" (or "latexonly") node to the actual figure node - itself. - """ - for name, explicit in document.nametypes.items(): - if not explicit: - continue - labelid = document.nameids[name] - if labelid is None: - continue - node = document.ids[labelid] - if node.tagname in ('html_only', 'latex_only'): - for n in node: - if n.tagname == 'figure': - sectname = name - for c in n: - if c.tagname == 'caption': - sectname = c.astext() - break - - node['ids'].remove(labelid) - node['names'].remove(name) - n['ids'].append(labelid) - n['names'].append(name) - document.settings.env.labels[name] = \ - document.settings.env.docname, labelid, sectname - break - -def setup(app): - setup.app = app - setup.config = app.config - setup.confdir = app.confdir - - options = {'alt': directives.unchanged, - 'height': directives.length_or_unitless, - 'width': directives.length_or_percentage_or_unitless, - 'scale': directives.nonnegative_int, - 'align': _option_align, - 'class': directives.class_option, - 'include-source': _option_boolean, - 'format': _option_format, - 'context': directives.flag, - 'nofigs': directives.flag, - 'encoding': directives.encoding - } - - app.add_directive('plot', plot_directive, True, (0, 2, False), **options) - app.add_config_value('plot_pre_code', None, True) - app.add_config_value('plot_include_source', False, True) - app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True) - app.add_config_value('plot_basedir', None, True) - app.add_config_value('plot_html_show_formats', True, True) - app.add_config_value('plot_rcparams', {}, True) - - app.connect('doctree-read', mark_plot_labels) - -#------------------------------------------------------------------------------ -# Doctest handling -#------------------------------------------------------------------------------ - -def contains_doctest(text): - try: - # check if it's valid Python as-is - compile(text, '', 'exec') - return False - except SyntaxError: - pass - r = re.compile(r'^\s*>>>', re.M) - m = r.search(text) - return bool(m) - -def unescape_doctest(text): - """ - Extract code from a piece of text, which contains either Python code - or doctests. - - """ - if not contains_doctest(text): - return text - - code = "" - for line in text.split("\n"): - m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line) - if m: - code += m.group(2) + "\n" - elif line.strip(): - code += "# " + line.strip() + "\n" - else: - code += "\n" - return code - -def split_code_at_show(text): - """ - Split code at plt.show() - - """ - - parts = [] - is_doctest = contains_doctest(text) - - part = [] - for line in text.split("\n"): - if (not is_doctest and line.strip() == 'plt.show()') or \ - (is_doctest and line.strip() == '>>> plt.show()'): - part.append(line) - parts.append("\n".join(part)) - part = [] - else: - part.append(line) - if "\n".join(part).strip(): - parts.append("\n".join(part)) - return parts - -#------------------------------------------------------------------------------ -# Template -#------------------------------------------------------------------------------ - - -TEMPLATE = """ -{{ source_code }} - -{{ only_html }} - - {% if source_link or (html_show_formats and not multi_image) %} - ( - {%- if source_link -%} - `Source code <{{ source_link }}>`__ - {%- endif -%} - {%- if html_show_formats and not multi_image -%} - {%- for img in images -%} - {%- for fmt in img.formats -%} - {%- if source_link or not loop.first -%}, {% endif -%} - `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ - {%- endfor -%} - {%- endfor -%} - {%- endif -%} - ) - {% endif %} - - {% for img in images %} - .. figure:: {{ build_dir }}/{{ img.basename }}.png - {%- for option in options %} - {{ option }} - {% endfor %} - - {% if html_show_formats and multi_image -%} - ( - {%- for fmt in img.formats -%} - {%- if not loop.first -%}, {% endif -%} - `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ - {%- endfor -%} - ) - {%- endif -%} - - {{ caption }} - {% endfor %} - -{{ only_latex }} - - {% for img in images %} - .. image:: {{ build_dir }}/{{ img.basename }}.pdf - {% endfor %} - -""" - -exception_template = """ -.. htmlonly:: - - [`source code <%(linkdir)s/%(basename)s.py>`__] - -Exception occurred rendering plot. - -""" - -# the context of the plot for all directives specified with the -# :context: option -plot_context = dict() - -class ImageFile(object): - def __init__(self, basename, dirname): - self.basename = basename - self.dirname = dirname - self.formats = [] - - def filename(self, format): - return os.path.join(self.dirname, "%s.%s" % (self.basename, format)) - - def filenames(self): - return [self.filename(fmt) for fmt in self.formats] - -def out_of_date(original, derived): - """ - Returns True if derivative is out-of-date wrt original, - both of which are full file paths. - """ - return (not os.path.exists(derived) or - (os.path.exists(original) and - os.stat(derived).st_mtime < os.stat(original).st_mtime)) - -class PlotError(RuntimeError): - pass - -def run_code(code, code_path, ns=None, function_name=None): - """ - Import a Python module from a path, and run the function given by - name, if function_name is not None. - """ - - # Change the working directory to the directory of the example, so - # it can get at its data files, if any. Add its path to sys.path - # so it can import any helper modules sitting beside it. - - pwd = os.getcwd() - old_sys_path = list(sys.path) - if code_path is not None: - dirname = os.path.abspath(os.path.dirname(code_path)) - os.chdir(dirname) - sys.path.insert(0, dirname) - - # Redirect stdout - stdout = sys.stdout - sys.stdout = cStringIO.StringIO() - - # Reset sys.argv - old_sys_argv = sys.argv - sys.argv = [code_path] - - try: - try: - code = unescape_doctest(code) - if ns is None: - ns = {} - if not ns: - if setup.config.plot_pre_code is None: - exec "import numpy as np\nfrom matplotlib import pyplot as plt\n" in ns - else: - exec setup.config.plot_pre_code in ns - exec code in ns - if function_name is not None: - exec function_name + "()" in ns - except (Exception, SystemExit), err: - raise PlotError(traceback.format_exc()) - finally: - os.chdir(pwd) - sys.argv = old_sys_argv - sys.path[:] = old_sys_path - sys.stdout = stdout - return ns - -def clear_state(plot_rcparams): - plt.close('all') - matplotlib.rcdefaults() - matplotlib.rcParams.update(plot_rcparams) - -def render_figures(code, code_path, output_dir, output_base, context, - function_name, config): - """ - Run a pyplot script and save the low and high res PNGs and a PDF - in outdir. - - Save the images under *output_dir* with file names derived from - *output_base* - """ - # -- Parse format list - default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 200} - formats = [] - for fmt in config.plot_formats: - if isinstance(fmt, str): - formats.append((fmt, default_dpi.get(fmt, 80))) - elif type(fmt) in (tuple, list) and len(fmt)==2: - formats.append((str(fmt[0]), int(fmt[1]))) - else: - raise PlotError('invalid image format "%r" in plot_formats' % fmt) - - # -- Try to determine if all images already exist - - code_pieces = split_code_at_show(code) - - # Look for single-figure output files first - # Look for single-figure output files first - all_exists = True - img = ImageFile(output_base, output_dir) - for format, dpi in formats: - if out_of_date(code_path, img.filename(format)): - all_exists = False - break - img.formats.append(format) - - if all_exists: - return [(code, [img])] - - # Then look for multi-figure output files - results = [] - all_exists = True - for i, code_piece in enumerate(code_pieces): - images = [] - for j in xrange(1000): - if len(code_pieces) > 1: - img = ImageFile('%s_%02d_%02d' % (output_base, i, j), output_dir) - else: - img = ImageFile('%s_%02d' % (output_base, j), output_dir) - for format, dpi in formats: - if out_of_date(code_path, img.filename(format)): - all_exists = False - break - img.formats.append(format) - - # assume that if we have one, we have them all - if not all_exists: - all_exists = (j > 0) - break - images.append(img) - if not all_exists: - break - results.append((code_piece, images)) - - if all_exists: - return results - - # We didn't find the files, so build them - - results = [] - if context: - ns = plot_context - else: - ns = {} - - for i, code_piece in enumerate(code_pieces): - if not context: - clear_state(config.plot_rcparams) - run_code(code_piece, code_path, ns, function_name) - - images = [] - fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() - for j, figman in enumerate(fig_managers): - if len(fig_managers) == 1 and len(code_pieces) == 1: - img = ImageFile(output_base, output_dir) - elif len(code_pieces) == 1: - img = ImageFile("%s_%02d" % (output_base, j), output_dir) - else: - img = ImageFile("%s_%02d_%02d" % (output_base, i, j), - output_dir) - images.append(img) - for format, dpi in formats: - try: - figman.canvas.figure.savefig(img.filename(format), - dpi=dpi, - bbox_inches='tight') - except exceptions.BaseException as err: - raise PlotError(traceback.format_exc()) - img.formats.append(format) - - results.append((code_piece, images)) - - return results - -def run(arguments, content, options, state_machine, state, lineno): - # The user may provide a filename *or* Python code content, but not both - if arguments and content: - raise RuntimeError("plot:: directive can't have both args and content") - - document = state_machine.document - config = document.settings.env.config - nofigs = options.has_key('nofigs') - - options.setdefault('include-source', config.plot_include_source) - context = options.has_key('context') - - rst_file = document.attributes['source'] - rst_dir = os.path.dirname(rst_file) - - if len(arguments): - if not config.plot_basedir: - source_file_name = os.path.join(setup.app.builder.srcdir, - directives.uri(arguments[0])) - else: - source_file_name = os.path.join(setup.confdir, config.plot_basedir, - directives.uri(arguments[0])) - - # If there is content, it will be passed as a caption. - caption = '\n'.join(content) - - # If the optional function name is provided, use it - if len(arguments) == 2: - function_name = arguments[1] - else: - function_name = None - - fd = open(source_file_name, 'r') - code = fd.read() - fd.close() - output_base = os.path.basename(source_file_name) - else: - source_file_name = rst_file - code = textwrap.dedent("\n".join(map(str, content))) - counter = document.attributes.get('_plot_counter', 0) + 1 - document.attributes['_plot_counter'] = counter - base, ext = os.path.splitext(os.path.basename(source_file_name)) - output_base = '%s-%d.py' % (base, counter) - function_name = None - caption = '' - - base, source_ext = os.path.splitext(output_base) - if source_ext in ('.py', '.rst', '.txt'): - output_base = base - else: - source_ext = '' - - # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames - output_base = output_base.replace('.', '-') - - # is it in doctest format? - is_doctest = contains_doctest(code) - if options.has_key('format'): - if options['format'] == 'python': - is_doctest = False - else: - is_doctest = True - - # determine output directory name fragment - source_rel_name = relpath(source_file_name, setup.confdir) - source_rel_dir = os.path.dirname(source_rel_name) - while source_rel_dir.startswith(os.path.sep): - source_rel_dir = source_rel_dir[1:] - - # build_dir: where to place output files (temporarily) - build_dir = os.path.join(os.path.dirname(setup.app.doctreedir), - 'plot_directive', - source_rel_dir) - # get rid of .. in paths, also changes pathsep - # see note in Python docs for warning about symbolic links on Windows. - # need to compare source and dest paths at end - build_dir = os.path.normpath(build_dir) - - if not os.path.exists(build_dir): - os.makedirs(build_dir) - - # output_dir: final location in the builder's directory - dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir, - source_rel_dir)) - if not os.path.exists(dest_dir): - os.makedirs(dest_dir) # no problem here for me, but just use built-ins - - # how to link to files from the RST file - dest_dir_link = os.path.join(relpath(setup.confdir, rst_dir), - source_rel_dir).replace(os.path.sep, '/') - build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/') - source_link = dest_dir_link + '/' + output_base + source_ext - - # make figures - try: - results = render_figures(code, source_file_name, build_dir, output_base, - context, function_name, config) - errors = [] - except PlotError, err: - reporter = state.memo.reporter - sm = reporter.system_message( - 2, "Exception occurred in plotting %s: %s" % (output_base, err), - line=lineno) - results = [(code, [])] - errors = [sm] - - # Properly indent the caption - caption = '\n'.join(' ' + line.strip() - for line in caption.split('\n')) - - # generate output restructuredtext - total_lines = [] - for j, (code_piece, images) in enumerate(results): - if options['include-source']: - if is_doctest: - lines = [''] - lines += [row.rstrip() for row in code_piece.split('\n')] - else: - lines = ['.. code-block:: python', ''] - lines += [' %s' % row.rstrip() - for row in code_piece.split('\n')] - source_code = "\n".join(lines) - else: - source_code = "" - - if nofigs: - images = [] - - opts = [':%s: %s' % (key, val) for key, val in options.items() - if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] - - only_html = ".. only:: html" - only_latex = ".. only:: latex" - - if j == 0: - src_link = source_link - else: - src_link = None - - result = format_template( - TEMPLATE, - dest_dir=dest_dir_link, - build_dir=build_dir_link, - source_link=src_link, - multi_image=len(images) > 1, - only_html=only_html, - only_latex=only_latex, - options=opts, - images=images, - source_code=source_code, - html_show_formats=config.plot_html_show_formats, - caption=caption) - - total_lines.extend(result.split("\n")) - total_lines.extend("\n") - - if total_lines: - state_machine.insert_input(total_lines, source=source_file_name) - - # copy image files to builder's output directory, if necessary - if not os.path.exists(dest_dir): - cbook.mkdirs(dest_dir) - - for code_piece, images in results: - for img in images: - for fn in img.filenames(): - destimg = os.path.join(dest_dir, os.path.basename(fn)) - if fn != destimg: - shutil.copyfile(fn, destimg) - - # copy script (if necessary) - if source_file_name == rst_file: - target_name = os.path.join(dest_dir, output_base + source_ext) - f = open(target_name, 'w') - f.write(unescape_doctest(code)) - f.close() - - return errors diff --git a/doc/gh-pages.py b/doc/gh-pages.py index 7bcf802c..90fac504 100755 --- a/doc/gh-pages.py +++ b/doc/gh-pages.py @@ -19,7 +19,6 @@ import re import shutil import sys from os import chdir as cd -from os.path import join as pjoin from subprocess import Popen, PIPE, CalledProcessError, check_call @@ -117,8 +116,8 @@ if __name__ == '__main__': try: cd(pages_dir) status = sh2('git status | head -1') - branch = re.match('\# On branch (.*)$', status).group(1) - if branch != 'gh-pages': + branch = re.match(b'On branch (.*)$', status).group(1) + if branch != b'gh-pages': e = 'On %r, git branch is %r, MUST be "gh-pages"' % (pages_dir, branch) raise RuntimeError(e) @@ -126,7 +125,12 @@ if __name__ == '__main__': sh('git add .nojekyll') sh('git add index.html') sh('git add --all %s' % tag) - sh2('git commit -m"Updated doc release: %s"' % tag) + + status = sh2('git status | tail -1') + if not re.match(b'nothing to commit', status): + sh2('git commit -m"Updated doc release: %s"' % tag) + else: + print('\n! Note: no changes to commit\n') print('Most recent commit:') sys.stdout.flush() diff --git a/doc/logo/scikit_image_logo.py b/doc/logo/scikit_image_logo.py index 85ba5199..48e38aad 100644 --- a/doc/logo/scikit_image_logo.py +++ b/doc/logo/scikit_image_logo.py @@ -20,7 +20,7 @@ import skimage.io as sio from skimage import img_as_float from skimage.color import gray2rgb, rgb2gray from skimage.exposure import rescale_intensity -from skimage.filter import sobel +from skimage.filters import sobel import scipy_logo diff --git a/doc/logo/scipy_logo.py b/doc/logo/scipy_logo.py index c57cd95b..b23787c8 100644 --- a/doc/logo/scipy_logo.py +++ b/doc/logo/scipy_logo.py @@ -3,11 +3,11 @@ Code used to trace Scipy logo. """ import numpy as np import matplotlib.pyplot as plt -import matplotlib.nxutils as nx from skimage import io from skimage import data +from skimage.measure import points_in_poly class SymmetricAnchorPoint(object): """Anchor point in a parametric curve with symmetric handles @@ -211,7 +211,7 @@ class ScipyLogo(object): y_img, x_img = np.mgrid[:h, :w] xy_points = np.column_stack((x_img.flat, y_img.flat)) - mask = nx.points_inside_poly(xy_points, xy_poly) + mask = points_in_poly(xy_points, xy_poly) return mask.reshape((h, w)) @@ -241,18 +241,8 @@ def plot_snake_overlay(): plt.imshow(img) -def plot_lena_overlay(): - plt.figure() - logo = ScipyLogo((300, 300), 180) - logo.plot_snake_curve() - logo.plot_circle() - img = data.lena() - plt.imshow(img) - - if __name__ == '__main__': plot_scipy_trace() plot_snake_overlay() - plot_lena_overlay() plt.show() diff --git a/doc/release/contribs.py b/doc/release/contribs.py index 58cb11d2..c0a34e6e 100755 --- a/doc/release/contribs.py +++ b/doc/release/contribs.py @@ -14,7 +14,7 @@ def call(cmd): 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\n" % (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,7 +22,10 @@ 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:\n" % len(merges)) +num_commits = call("git rev-list %s..HEAD --count" % tag)[0] +print("A total of %s changes have been committed.\n" % num_commits) + +print("It contained the following %d merges:\n" % len(merges)) for (merge, message) in merges: if merge.startswith('Merge pull request #'): PR = ' (%s)' % merge.split()[3] diff --git a/doc/release/release_0.10.txt b/doc/release/release_0.10.txt index e2c3b34d..1c14e781 100644 --- a/doc/release/release_0.10.txt +++ b/doc/release/release_0.10.txt @@ -80,7 +80,7 @@ surname: - Jaime Frio - Jostein Bø Fløystad - Neeraj Gangwar -- Christopher Gohlke +- Christoph Gohlke - Michael Hansen - Almar Klein - Jeremy Metz @@ -91,13 +91,13 @@ surname: - Thomas Robitaille - Michal Romaniuk - Johannes L. Schönberger -- Steven Sylvester +- Steven Silvester - Julian Taylor - Gregor Thalhammer - Matthew Trentacoste - Siva Prasad Varma - Guillem Palou Visa - Stefan van der Walt -- Josh Warner +- Joshua Warner - Tony S Yu - radioxoma diff --git a/doc/release/release_0.11.txt b/doc/release/release_0.11.txt new file mode 100644 index 00000000..30b358d9 --- /dev/null +++ b/doc/release/release_0.11.txt @@ -0,0 +1,96 @@ +Announcement: scikit-image 0.11.0 +================================= + +We're happy to announce the release of scikit-image v0.11.0! + +scikit-image is an image processing toolbox for SciPy that includes algorithms +for segmentation, geometric transformations, color space manipulation, +analysis, filtering, morphology, feature detection, and more. + +For more information, examples, and documentation, please visit our website: + +http://scikit-image.org + +Highlights +---------- +For this release, we merged over 200 pull requests with bug fixes, +cleanups, improved documentation and new features. Highlights +include: + +- Region Adjacency Graphs + - Color distance RAGs (#1031) + - Threshold Cut on RAGs (#1031) + - Similarity RAGs (#1080) + - Normalized Cut on RAGs (#1080) + - RAG drawing (#1087) + - Hierarchical merging (#1100) +- Sub-pixel shift registration (#1066) +- Non-local means denoising (#874) +- Sliding window histogram (#1127) +- More illuminants in color conversion (#1130) +- Handling of CMYK images (#1360) +- `stop_probability` for RANSAC (#1176) +- Li thresholding (#1376) +- Signed edge operators (#1240) +- Full ndarray support for `peak_local_max` (#1355) +- Improve conditioning of geometric transformations (#1319) +- Standardize handling of multi-image files (#1200) +- Ellipse structuring element (#1298) +- Multi-line drawing tool (#1065), line handle style (#1179) +- Point in polygon testing (#1123) +- Rotation around a specified center (#1168) +- Add `shape` option to drawing functions (#1222) +- Faster regionprops (#1351) +- `skimage.future` package (#1365) +- More robust I/O module (#1189) + +API Changes +----------- +- The ``skimage.filter`` subpackage has been renamed to ``skimage.filters``. +- Some edge detectors returned values greater than 1--their results are now + appropriately scaled with a factor of ``sqrt(2)``. + +Contributors to this release +---------------------------- +(Listed alphabetically by last name) + +- Fedor Baart +- Vighnesh Birodkar +- François Boulogne +- Nelson Brown +- Alexey Buzmakov +- Julien Coste +- Phil Elson +- Adam Feuer +- Jim Fienup +- Geoffrey French +- Emmanuelle Gouillart +- Charles Harris +- Jonathan Helmus +- Alexander Iacchetta +- Ivana Kajić +- Kevin Keraudren +- Almar Klein +- Gregory R. Lee +- Jeremy Metz +- Stuart Mumford +- Damian Nadales +- Pablo Márquez Neila +- Juan Nunez-Iglesias +- Rebecca Roisin +- Jasper St. Pierre +- Jacopo Sabbatini +- Michael Sarahan +- Salvatore Scaramuzzino +- Phil Schaf +- Johannes Schönberger +- Tim Seifert +- Arve Seljebu +- Steven Silvester +- Julian Taylor +- Matěj Týč +- Alexey Umnov +- Pratap Vardhan +- Stefan van der Walt +- Joshua Warner +- Tony S Yu diff --git a/doc/release/release_dev.txt b/doc/release/release_dev.txt new file mode 100644 index 00000000..1123eb77 --- /dev/null +++ b/doc/release/release_dev.txt @@ -0,0 +1,36 @@ +Announcement: scikit-image 0.X.0 +================================ + +We're happy to announce the release of scikit-image v0.X.0! + +scikit-image is an image processing toolbox for SciPy that includes algorithms +for segmentation, geometric transformations, color space manipulation, +analysis, filtering, morphology, feature detection, and more. + +For more information, examples, and documentation, please visit our website: + +http://scikit-image.org + + +New Features +------------ + + + + +Improvements +------------ + + + + +API Changes +----------- + + + + +Deprecations +------------ + + diff --git a/doc/release/release_template.txt b/doc/release/release_template.txt new file mode 100644 index 00000000..2d200c7c --- /dev/null +++ b/doc/release/release_template.txt @@ -0,0 +1,38 @@ +Announcement: scikit-image 0.X.0 +================================ + +We're happy to announce the release of scikit-image v0.X.0! + +scikit-image is an image processing toolbox for SciPy that includes algorithms +for segmentation, geometric transformations, color space manipulation, +analysis, filtering, morphology, feature detection, and more. + +For more information, examples, and documentation, please visit our website: + +http://scikit-image.org + + +New Features +------------ + + + + +Improvements +------------ + + + + +API Changes +----------- + + + + +Deprecations +------------ + + +Contributors to this release +---------------------------- diff --git a/doc/source/_static/GitHub-Mark-32px.png b/doc/source/_static/GitHub-Mark-32px.png new file mode 100644 index 00000000..176ab33e Binary files /dev/null and b/doc/source/_static/GitHub-Mark-32px.png differ diff --git a/doc/source/_static/docversions.js b/doc/source/_static/docversions.js index cb0d3339..df01a65c 100644 --- a/doc/source/_static/docversions.js +++ b/doc/source/_static/docversions.js @@ -1,4 +1,4 @@ -var versions = ['dev', '0.10.x', '0.9.x', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; +var versions = ['dev', '0.11.x', '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/_templates/navbar.html b/doc/source/_templates/navbar.html index 267af8e7..75d6467d 100644 --- a/doc/source/_templates/navbar.html +++ b/doc/source/_templates/navbar.html @@ -2,4 +2,10 @@
  • Download
  • Gallery
  • Documentation
  • -
  • Source
  • +
  • + + Source +
  • diff --git a/doc/source/api_changes.txt b/doc/source/api_changes.txt index 0271bec0..84625732 100644 --- a/doc/source/api_changes.txt +++ b/doc/source/api_changes.txt @@ -1,3 +1,9 @@ +Version 0.11 +------------ +- The ``skimage.filter`` subpackage has been renamed to ``skimage.filters``. +- Some edge detectors returned values greater than 1--their results are now + appropriately scaled with a factor of ``sqrt(2)``. + Version 0.10 ------------ - Removed ``skimage.io.video`` functionality due to broken gstreamer bindings diff --git a/doc/source/conf.py b/doc/source/conf.py index c2179b41..94904305 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -251,6 +251,7 @@ latex_use_modindex = False # Numpy extensions # ----------------------------------------------------------------------------- numpydoc_show_class_members = False +numpydoc_class_members_toctree = False # ----------------------------------------------------------------------------- # Plots diff --git a/doc/source/contents.txt b/doc/source/contents.txt deleted file mode 100644 index 250546b0..00000000 --- a/doc/source/contents.txt +++ /dev/null @@ -1,7 +0,0 @@ -Table of Contents -================= - -.. toctree:: - - /api/api - diff --git a/doc/source/coverage_generator.py b/doc/source/coverage_generator.py index 1112c021..aa566acf 100755 --- a/doc/source/coverage_generator.py +++ b/doc/source/coverage_generator.py @@ -10,7 +10,10 @@ import csv try: import cStringIO as StringIO except ImportError: - import StringIO + try: + import StringIO + except: + import io as StringIO # Missing item value MISSING_STRING=":missing:`Not Implemented`" @@ -74,7 +77,7 @@ def read_table_titles(reader): break section_titles.append(row[0]) table_names[row[0]] = names - except csv.Error, e: + except csv.Error as e: sys.exit('line %d: %s' % (reader.line_num, e)) return section_titles,table_names @@ -106,7 +109,7 @@ def table_row(stream,data,lengths,num_columns=None): if num_columns is None: num_columns = len(data) stream.write("|") - for i in xrange(num_columns): + for i in range(num_columns): if len(data)-1 >= i: if len(data[i]) == 0: entry = MISSING_STRING @@ -145,12 +148,12 @@ def generate_table(reader,stream,table_name=None, break data.append([entry.expandtabs() for entry in row]) num_columns = max(num_columns,len(row)) - except csv.Error, e: + except csv.Error as e: sys.exit('line %d: %s' % (reader.line_num, e)) column_lengths = [len(MISSING_STRING)]*num_columns for row in data: - for i in xrange(len(row)): + for i in range(len(row)): column_lengths[i] = max(column_lengths[i],len(row[i])) # Output table header diff --git a/doc/source/gitwash/development_workflow.txt b/doc/source/gitwash/development_workflow.txt index 47bdead0..5991b61e 100644 --- a/doc/source/gitwash/development_workflow.txt +++ b/doc/source/gitwash/development_workflow.txt @@ -4,7 +4,7 @@ Development workflow #################### -You already have your own forked copy of the scikit-image_ repository, by +You already have your own forked copy of the `scikit-image`_ repository, by following :ref:`forking`. You have :ref:`set-up-fork`. You have configured git by following :ref:`configure-git`. Now you are ready for some real work. @@ -22,7 +22,7 @@ In what follows we'll refer to the upstream scikit-image ``master`` branch, as * Name your branch for the purpose of the changes - e.g. ``bugfix-for-issue-14`` or ``refactor-database-code``. * If you can possibly avoid it, avoid merging trunk or any other branches into - your feature branch while you are working. + your feature branch while you are working. * If you do find yourself merging from trunk, consider :ref:`rebase-on-trunk` * Ask on the `scikit-image mailing list`_ if you get stuck. * Ask for code review! @@ -81,7 +81,7 @@ what the changes in the branch are for. For example ``add-ability-to-fly``, or git checkout my-new-feature Generally, you will want to keep your feature branches on your public github_ -fork of scikit-image_. To do this, you `git push`_ this new branch up to your +fork of `scikit-image`_. To do this, you `git push`_ this new branch up to your github repo. Generally (if you followed the instructions in these pages, and by default), git will have a link to your github repo, called ``origin``. You push up to your own repo on github with:: diff --git a/doc/source/gitwash/forking_hell.txt b/doc/source/gitwash/forking_hell.txt index 02c14b25..2b91c0b9 100644 --- a/doc/source/gitwash/forking_hell.txt +++ b/doc/source/gitwash/forking_hell.txt @@ -1,33 +1,33 @@ .. _forking: -============================================ +====================================================== Making your own copy (fork) of scikit-image -============================================ +====================================================== You need to do this only once. The instructions here are very similar to the instructions at http://help.github.com/forking/ |emdash| please see that page for more detail. We're repeating some of it here just to give the -specifics for the scikit-image_ project, and to suggest some default names. +specifics for the `scikit-image`_ project, and to suggest some default names. Set up and configure a github account -====================================== +===================================== If you don't have a github account, go to the github page, and make one. You then need to configure your account to allow write access |emdash| see the ``Generating SSH keys`` help on `github help`_. -Create your own forked copy of scikit-image_ -============================================= +Create your own forked copy of `scikit-image`_ +====================================================== #. Log into your github account. -#. Go to the scikit-image_ github home at `scikit-image github`_. +#. Go to the `scikit-image`_ github home at `scikit-image github`_. #. Click on the *fork* button: .. image:: forking_button.png Now, after a short pause and some 'Hardcore forking action', you - should find yourself at the home page for your own forked copy of scikit-image_. + should find yourself at the home page for your own forked copy of `scikit-image`_. .. include:: links.inc diff --git a/doc/source/gitwash/git_install.txt b/doc/source/gitwash/git_install.txt index 72a6b911..d39003ec 100644 --- a/doc/source/gitwash/git_install.txt +++ b/doc/source/gitwash/git_install.txt @@ -8,7 +8,7 @@ Overview ======== ================ ============= -Debian / Ubuntu ``sudo apt-get install git-core`` +Debian / Ubuntu ``sudo apt-get install git`` Fedora ``sudo yum install git-core`` Windows Download and install msysGit_ OS X Use the git-osx-installer_ diff --git a/doc/source/gitwash/git_intro.txt b/doc/source/gitwash/git_intro.txt index 8b4d42bc..f98cb712 100644 --- a/doc/source/gitwash/git_intro.txt +++ b/doc/source/gitwash/git_intro.txt @@ -2,7 +2,7 @@ Introduction ============== -These pages describe a git_ and github_ workflow for the scikit-image_ +These pages describe a git_ and github_ workflow for the `scikit-image`_ project. There are several different workflows here, for different ways of diff --git a/doc/source/gitwash/git_links.inc b/doc/source/gitwash/git_links.inc index 3822fd9d..8e628ae1 100644 --- a/doc/source/gitwash/git_links.inc +++ b/doc/source/gitwash/git_links.inc @@ -20,27 +20,27 @@ .. _git svn crash course: http://git-scm.com/course/svn.html .. _learn.github: http://learn.github.com/ .. _network graph visualizer: http://github.com/blog/39-say-hello-to-the-network-graph-visualizer -.. _git user manual: http://www.kernel.org/pub/software/scm/git/docs/user-manual.html -.. _git tutorial: http://www.kernel.org/pub/software/scm/git/docs/gittutorial.html +.. _git user manual: http://schacon.github.com/git/user-manual.html +.. _git tutorial: http://schacon.github.com/git/gittutorial.html .. _git community book: http://book.git-scm.com/ .. _git ready: http://www.gitready.com/ .. _git casts: http://www.gitcasts.com/ .. _Fernando's git page: http://www.fperez.org/py4science/git.html .. _git magic: http://www-cs-students.stanford.edu/~blynn/gitmagic/index.html .. _git concepts: http://www.eecs.harvard.edu/~cduan/technical/git/ -.. _git clone: http://www.kernel.org/pub/software/scm/git/docs/git-clone.html -.. _git checkout: http://www.kernel.org/pub/software/scm/git/docs/git-checkout.html -.. _git commit: http://www.kernel.org/pub/software/scm/git/docs/git-commit.html -.. _git push: http://www.kernel.org/pub/software/scm/git/docs/git-push.html -.. _git pull: http://www.kernel.org/pub/software/scm/git/docs/git-pull.html -.. _git add: http://www.kernel.org/pub/software/scm/git/docs/git-add.html -.. _git status: http://www.kernel.org/pub/software/scm/git/docs/git-status.html -.. _git diff: http://www.kernel.org/pub/software/scm/git/docs/git-diff.html -.. _git log: http://www.kernel.org/pub/software/scm/git/docs/git-log.html -.. _git branch: http://www.kernel.org/pub/software/scm/git/docs/git-branch.html -.. _git remote: http://www.kernel.org/pub/software/scm/git/docs/git-remote.html -.. _git rebase: http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html -.. _git config: http://www.kernel.org/pub/software/scm/git/docs/git-config.html +.. _git clone: http://schacon.github.com/git/git-clone.html +.. _git checkout: http://schacon.github.com/git/git-checkout.html +.. _git commit: http://schacon.github.com/git/git-commit.html +.. _git push: http://schacon.github.com/git/git-push.html +.. _git pull: http://schacon.github.com/git/git-pull.html +.. _git add: http://schacon.github.com/git/git-add.html +.. _git status: http://schacon.github.com/git/git-status.html +.. _git diff: http://schacon.github.com/git/git-diff.html +.. _git log: http://schacon.github.com/git/git-log.html +.. _git branch: http://schacon.github.com/git/git-branch.html +.. _git remote: http://schacon.github.com/git/git-remote.html +.. _git rebase: http://schacon.github.com/git/git-rebase.html +.. _git config: http://schacon.github.com/git/git-config.html .. _why the -a flag?: http://www.gitready.com/beginner/2009/01/18/the-staging-area.html .. _git staging area: http://www.gitready.com/beginner/2009/01/18/the-staging-area.html .. _tangled working copy problem: http://tomayko.com/writings/the-thing-about-git @@ -50,7 +50,7 @@ .. _git foundation: http://matthew-brett.github.com/pydagogue/foundation.html .. _deleting master on github: http://matthew-brett.github.com/pydagogue/gh_delete_master.html .. _rebase without tears: http://matthew-brett.github.com/pydagogue/rebase_without_tears.html -.. _resolving a merge: http://www.kernel.org/pub/software/scm/git/docs/user-manual.html#resolving-a-merge +.. _resolving a merge: http://schacon.github.com/git/user-manual.html#resolving-a-merge .. _ipython git workflow: http://mail.scipy.org/pipermail/ipython-dev/2010-October/006746.html .. other stuff diff --git a/doc/source/gitwash/index.txt b/doc/source/gitwash/index.txt index 7d26110e..2d285df0 100644 --- a/doc/source/gitwash/index.txt +++ b/doc/source/gitwash/index.txt @@ -1,7 +1,7 @@ .. _using-git: Working with *scikit-image* source code -======================================== +================================================ Contents: diff --git a/doc/source/gitwash/known_projects.inc b/doc/source/gitwash/known_projects.inc index 29723528..c4c3c42f 100644 --- a/doc/source/gitwash/known_projects.inc +++ b/doc/source/gitwash/known_projects.inc @@ -6,7 +6,7 @@ .. _`PROJECTNAME mailing list`: http://projects.scipy.org/mailman/listinfo/nipy-devel .. numpy -.. _numpy: hhttp://numpy.scipy.org +.. _numpy: http://numpy.org .. _`numpy github`: http://github.com/numpy/numpy .. _`numpy mailing list`: http://mail.scipy.org/mailman/listinfo/numpy-discussion diff --git a/doc/source/gitwash/patching.txt b/doc/source/gitwash/patching.txt index b40118af..0a317773 100644 --- a/doc/source/gitwash/patching.txt +++ b/doc/source/gitwash/patching.txt @@ -3,7 +3,7 @@ ================ You've discovered a bug or something else you want to change -in scikit-image_ .. |emdash| excellent! +in `scikit-image`_ .. |emdash| excellent! You've worked out a way to fix it |emdash| even better! @@ -57,7 +57,7 @@ In detail git config --global user.name "Your Name Comes Here" #. If you don't already have one, clone a copy of the - scikit-image_ repository:: + `scikit-image`_ repository:: git clone git://github.com/scikit-image/scikit-image.git cd scikit-image @@ -115,7 +115,7 @@ more feature branches, you will probably want to switch to development mode. You can do this with the repository you have. -Fork the scikit-image_ repository on github |emdash| :ref:`forking`. +Fork the `scikit-image`_ repository on github |emdash| :ref:`forking`. Then:: # checkout and refresh master branch from main repo diff --git a/doc/source/gitwash/set_up_fork.txt b/doc/source/gitwash/set_up_fork.txt index 36153e7d..2679ec31 100644 --- a/doc/source/gitwash/set_up_fork.txt +++ b/doc/source/gitwash/set_up_fork.txt @@ -49,7 +49,7 @@ Linking your repository to the upstream repo git remote add upstream git://github.com/scikit-image/scikit-image.git ``upstream`` here is just the arbitrary name we're using to refer to the -main scikit-image_ repository at `scikit-image github`_. +main `scikit-image`_ repository at `scikit-image github`_. Note that we've used ``git://`` for the URL rather than ``git@``. The ``git://`` URL is read only. This means we that we can't accidentally diff --git a/doc/source/gitwash/this_project.inc b/doc/source/gitwash/this_project.inc index 349b92ba..36b74c83 100644 --- a/doc/source/gitwash/this_project.inc +++ b/doc/source/gitwash/this_project.inc @@ -1,5 +1,5 @@ .. scikit-image -.. _scikit-image: http://scikit-image.org +.. _`scikit-image`: http://scikit-image.org .. _`scikit-image github`: http://github.com/scikit-image/scikit-image .. _`scikit-image mailing list`: http://groups.google.com/group/scikit-image diff --git a/doc/source/index.txt b/doc/source/index.txt index 3c6ff75c..93fb259a 100644 --- a/doc/source/index.txt +++ b/doc/source/index.txt @@ -15,7 +15,7 @@ Sections :hidden: overview - api + api/api api_changes install user_guide diff --git a/doc/source/install.txt b/doc/source/install.txt index 81cb7340..555ec407 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -2,8 +2,9 @@ Pre-built installation ---------------------- `Windows binaries -`__ -are kindly provided by Christoph Gohlke. +`__ +are kindly provided by Christoph Gohlke (note that, when upgrading, +you should first uninstall any older versions). The latest stable release is also included as part of `Enthought Canopy `__, @@ -19,13 +20,6 @@ add Neurodebian to your system package manager, then look for On systems that support setuptools, the package can be installed from the `Python packaging index `__ using - -:: - - easy_install -U scikit-image - -or - :: pip install -U scikit-image @@ -45,13 +39,9 @@ If you do not have git installed, you can also download a zipball from `https://github.com/scikit-image/scikit-image/zipball/master `_. -The SciKit can be installed globally using:: +The scikit can be installed using:: - python setup.py install - -or locally using:: - - python setup.py install --prefix=${HOME} + pip install . If you prefer, you can use it without installing, by simply adding this path to your ``PYTHONPATH`` variable and compiling extensions @@ -80,6 +70,7 @@ From the ``scikit-image`` source directory:: bentomaker build -j # (add -i for in-place build) bentomaker install # (when not builing in-place) -Depending on file permissions, the install commands may need to be run as sudo. - +Depending on file permissions, the install commands may need to be run as +sudo. + .. include:: ../../DEPENDS.txt diff --git a/doc/source/themes/scikit-image/layout.html b/doc/source/themes/scikit-image/layout.html index 58424f2f..ca8b5801 100644 --- a/doc/source/themes/scikit-image/layout.html +++ b/doc/source/themes/scikit-image/layout.html @@ -106,7 +106,7 @@ diff --git a/doc/source/themes/scikit-image/static/css/custom.css b/doc/source/themes/scikit-image/static/css/custom.css index 0a2d3136..12eb642b 100644 --- a/doc/source/themes/scikit-image/static/css/custom.css +++ b/doc/source/themes/scikit-image/static/css/custom.css @@ -1,5 +1,7 @@ body { font-family: "Raleway"; + letter-spacing: 0.02em; + font-size: 102%; } a { color: #CE5C00; @@ -10,9 +12,6 @@ select, textarea { font-family: "Raleway"; } -pre { - font-size: 11px; -} h1, h2, h3, h4, h5, h6 { clear: left; @@ -41,12 +40,6 @@ h6 { font-size: 13px; line-height: 15px; } -blockquote { - border-left: 0; -} -dt { - font-weight: normal; -} .logo { float: left; @@ -57,18 +50,21 @@ dt { } .hero { - padding: 10px 25px; + padding: 10px 25px 15px 25px; } .gallery-random { float: right; line-height: 180px; + margin-left: 10px; } .gallery-random img { max-height: 180px; + max-width: 180px; } .coins-sample { + float: left; padding: 5px; } @@ -79,10 +75,6 @@ dt { padding-left: 15px; } -#current { - font-weight: bold; -} - .headerlink { margin-left: 10px; color: #ddd; @@ -111,7 +103,7 @@ h6:hover .headerlink { text-decoration: underline; } -.ohloh-use, .gplus-use { +.gplus-use { float: left; margin: 0 0 10px 15px; } @@ -233,7 +225,14 @@ p.admonition-title { text-align: center !important; } -/* misc */ -div.math { - text-align: center; +.summary-box { + /* Should derive width from span8 */ + width: 640px; +} + +.citation { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +/* padding: 1em;*/ } diff --git a/doc/source/user_guide.txt b/doc/source/user_guide.txt index 178e130d..197c12cb 100644 --- a/doc/source/user_guide.txt +++ b/doc/source/user_guide.txt @@ -4,8 +4,13 @@ User Guide .. toctree:: :maxdepth: 2 + user_guide/getting_started + user_guide/numpy_images user_guide/data_types + user_guide/transforming_image_data user_guide/plugins user_guide/tutorials user_guide/getting_help user_guide/viewer + user_guide/tutorial_parallelization + user_guide/tutorial_segmentation \ No newline at end of file diff --git a/doc/source/user_guide/data_types.txt b/doc/source/user_guide/data_types.txt index 258314af..640e845f 100644 --- a/doc/source/user_guide/data_types.txt +++ b/doc/source/user_guide/data_types.txt @@ -1,3 +1,4 @@ +.. _data_types: =================================== Image data types and what they mean @@ -14,16 +15,16 @@ Data type Range uint8 0 to 255 uint16 0 to 65535 uint32 0 to 2\ :sup:`32` -float -1 to 1 +float -1 to 1 or 0 to 1 int8 -128 to 127 int16 -32768 to 32767 int32 -2\ :sup:`31` to 2\ :sup:`31` - 1 ========= ================================= -Note that float images are restricted to the range -1 to 1 even though the data -type itself can exceed this range; all integer dtypes, on the other hand, have -pixel intensities that can span the entire data type range. Currently, *64-bit -(u)int images are not supported*. +Note that float images should be restricted to the range -1 to 1 even though +the data type itself can exceed this range; all integer dtypes, on the other +hand, have pixel intensities that can span the entire data type range. With a +few exceptions, *64-bit (u)int images are not supported*. Functions in ``skimage`` are designed so that they accept any of these dtypes, but, for efficiency, *may return an image of a different dtype* (see `Output @@ -43,9 +44,10 @@ violates these assumptions about the dtype range:: Input types =========== -Functions may choose to support only a subset of these data-types. In such +Although we aim to preserve the data range and type of input images, functions +may support only a subset of these data-types. In such a case, the input will be converted to the required type (if possible), and -a warning message is printed to the log if a memory copy is needed. Type +a warning message printed to the log if a memory copy is needed. Type requirements should be noted in the docstrings. The following utility functions in the main package are available to developers @@ -72,6 +74,32 @@ issued:: array([ 0, 128, 255], dtype=uint8) +Additionally, some functions take a ``preserve_range`` argument where a range +conversion is convenient but not necessary. For example, interpolation in +``transform.warp`` requires an image of type float, which should have a range +in [0, 1]. So, by default, input images will be rescaled to this range. +However, in some cases, the image values represent physical measurements, such +as temperature or rainfall values, that the user does not want rescaled. +With ``preserve_range=True``, the original range of the data will be +preserved, even though the output is a float image. Users must then ensure +this non-standard image is properly processed by downstream functions, which +may expect an image in [0, 1]. + + >>> from skimage import data + >>> from skimage.transform import rescale + >>> image = data.coins() + >>> image.dtype, image.min(), image.max(), image.shape + (dtype('uint8'), 1, 252, (303, 384)) + >>> rescaled = rescale(image, 0.5) + >>> (rescaled.dtype, np.round(rescaled.min(), 4), + ... np.round(rescaled.max(), 4), rescaled.shape) + (dtype('float64'), 0.0147, 0.9456, (152, 192)) + >>> rescaled = rescale(image, 0.5, preserve_range=True) + >>> (rescaled.dtype, np.round(rescaled.min()), + ... np.round(rescaled.max()), rescaled.shape + (dtype('float64'), 4.0, 241.0, (152, 192)) + + Output types ============ diff --git a/doc/source/user_guide/getting_started.txt b/doc/source/user_guide/getting_started.txt new file mode 100644 index 00000000..4013e2fa --- /dev/null +++ b/doc/source/user_guide/getting_started.txt @@ -0,0 +1,43 @@ +Getting started +--------------- + +``scikit-image`` is an image processing Python package that works with +:mod:`numpy` arrays. The package is imported as ``skimage``: :: + + >>> import skimage + +Most functions of ``skimage`` are found within submodules: :: + + >>> from skimage import data + >>> camera = data.camera() + +A list of submodules and functions is found on the `API reference +`_ webpage. + +Within scikit-image, images are represented as NumPy arrays, for +example 2-D arrays for grayscale 2-D images :: + + >>> type(camera) + + >>> # An image with 512 rows and 512 columns + >>> camera.shape + (512, 512) + +The :mod:`skimage.data` submodule provides a set of functions returning +example images, that can be used to get started quickly on using +scikit-image's functions: :: + + >>> coins = data.coins() + >>> from skimage import filters + >>> threshold_value = filters.threshold_otsu(coins) + >>> threshold_value + 107 + +Of course, it is also possible to load your own images as NumPy arrays +from image files, using :func:`skimage.io.imread`: :: + + >>> import os + >>> filename = os.path.join(skimage.data_dir, 'moon.png') + >>> from skimage import io + >>> moon = io.imread(filename) + diff --git a/doc/source/user_guide/numpy_images.txt b/doc/source/user_guide/numpy_images.txt new file mode 100644 index 00000000..3dab852a --- /dev/null +++ b/doc/source/user_guide/numpy_images.txt @@ -0,0 +1,258 @@ +A crash course on NumPy for images +---------------------------------- + +Images manipulated by ``scikit-image`` are simply NumPy arrays. Hence, a +large fraction of operations on images will just consist in using NumPy:: + + >>> from skimage import data + >>> camera = data.camera() + >>> type(camera) + + +Retrieving the geometry of the image and the number of pixels: :: + + >>> camera.shape + (512, 512) + >>> camera.size + 262144 + +Retrieving statistical information about gray values: :: + + >>> camera.min(), camera.max() + (0, 255) + >>> camera.mean() + 118.31400299072266 + +NumPy arrays representing images can be of different integer of float +numerical types. See :ref:`data_types` for more information about these +types and how scikit-image treats them. + + +NumPy indexing +-------------- + +NumPy indexing can be used both for looking at pixel values, and to +modify pixel values: :: + + >>> # Get the value of the pixel on the 10th row and 20th column + >>> camera[10, 20] + 153 + >>> # Set to black the pixel on the 3rd row and 10th column + >>> camera[3, 10] = 0 + +Be careful: in NumPy indexing, the first dimension (``camera.shape[0]``) +corresponds to rows, while the second (``camera.shape[1]``) corresponds +to columns, with the origin (``camera[0, 0]``) on the top-left corner. +This matches matrix/linear algebra notation, but is in contrast to +Cartesian (x, y) coordinates. See `Coordinate conventions`_ below for +more details. + +Beyond individual pixels, it is possible to access / modify values of +whole sets of pixels, using the different indexing possibilities of +NumPy. + +Slicing:: + + >>> # Set to black the ten first lines + >>> camera[:10] = 0 + +Masking (indexing with masks of booleans):: + + >>> mask = camera < 87 + >>> # Set to "white" (255) pixels where mask is True + >>> camera[mask] = 255 + +Fancy indexing (indexing with sets of indices) :: + + >>> inds_r = np.arange(len(camera)) + >>> inds_c = 4 * inds_r % len(camera) + >>> camera[inds_r, inds_c] = 0 + +Using masks, especially, is very useful to select a set of pixels on +which to perform further manipulations. The mask can be any boolean array +of same shape as the image (or a shape broadcastable to the image shape). +This can be useful to define a region of interest, such as a +disk: :: + + >>> nrows, ncols = camera.shape + >>> row, col = np.ogrid[:nrows, :ncols] + >>> cnt_row, cnt_col = nrows / 2, ncols / 2 + >>> outer_disk_mask = ((row - cnt_row)**2 + (col - cnt_col)**2 < + ... (nrows / 2)**2) + >>> camera[outer_disk_mask] = 0 + +.. image:: ../auto_examples/images/plot_camera_numpy_1.png + :width: 45% + :target: ../auto_examples/plot_camera_numpy.html + +Boolean arithmetic can be used to define more complex masks: :: + + >>> lower_half = row > cnt_row + >>> lower_half_disk = np.logical_and(lower_half, outer_disk_mask) + >>> camera = data.camera() + >>> camera[lower_half_disk] = 0 + + +Color images +------------ + +All of the above is true of color images, too: a color image is a +NumPy array, with an additional trailing dimension for the channels: + + >>> cat = data.chelsea() + >>> type(cat) + + >>> cat.shape + (300, 451, 3) + +This shows that ``cat`` is a 300-by-451 pixel image with three +channels (red, green, and blue). +As before, we can get and set pixel values: + + >>> cat[10, 20] + array([151, 129, 115], dtype=uint8) + >>> # set the pixel at row 50, column 60 to black + >>> cat[50, 60] = 0 + >>> # set the pixel at row 50, column 61 to green + >>> cat[50, 61] = [0, 255, 0] # [red, green, blue] + +We can also use 2D boolean masks for a 2D color image, as we did with +the grayscale image above: + +.. plot:: + + Using a 2D mask on a 2D color image + + >>> from skimage import data + >>> cat = data.chelsea() + >>> reddish = cat[:, :, 0] > 160 + >>> cat[reddish] = [0, 255, 0] + >>> plt.imshow(cat) + + +Coordinate conventions +---------------------- + +Because we represent images with numpy arrays, our coordinates must +match accordingly. Two-dimensional (2D) grayscale images (such as +`camera` above) are indexed by row and columns (abbreviated to either +``row, col`` or ``r, c``), with the lowest element (0, 0) at the top- +-left corner. In various parts of the library, you will +also see ``rr`` and ``cc`` refer to lists of row and column +coordinates. We distinguish this from (x, y), which commonly denote +standard Cartesian coordinates, where x is the horizontal coordinate, +y the vertical, and the origin is on the bottom right. (Matplotlib, for +example, uses this convention.) + +In the case of color (or multichannel) images, the last dimension +contains the color information and is denoted ``channel`` or ``ch``. + +Finally, for 3D images, such as videos, magnetic resonance imaging +(MRI) scans, or confocal microscopy, we refer to the leading dimension +as ``plane``, abbreviated as ``pln`` or ``p``. + +These conventions are summarized below: + +.. table:: Dimension name and order conventions in scikit-image + + ========================= ======================================== + Image type coordinates + ========================= ======================================== + 2D grayscale (row, col) + 2D multichannel (eg. RGB) (row, col, ch) + 3D grayscale (pln, row, col) + 3D multichannel (pln, row, col, ch) + ========================= ======================================== + + +Many functions in scikit-image operate on 3D images directly: + + >>> im3d = np.random.rand(100, 1000, 1000) + >>> from skimage import morphology + >>> from scipy import ndimage as nd + >>> seeds = nd.label(im3d < 0.1)[0] + >>> ws = morphology.watershed(im3d, seeds) + +In many cases, +the third imaging dimension has lower resolution than the other two. +Some scikit-image functions provide a ``spacing`` keyword argument +to process these images: + + >>> from skimage import segmentation + >>> slics = segmentation.slic(im3d, spacing=[5, 1, 1], multichannel=False) + + +Other times, processing must be done plane-wise. When planes are the +leading dimension, we can use the following syntax: + + >>> from skimage import filters + >>> edges = np.zeros_like(im3d) + >>> for pln, image in enumerate(im3d): + ... # iterate over the leading dimension (planes) + ... edges[pln] = filters.sobel(image) + + +Notes on array order +-------------------- + +Although the labeling of the axes seems arbitrary, it can have a +significant effect on speed of operations. This is because modern +processors never retrieve just one item from memory, but rather a +whole chunk of adjacent items. (This is called prefetching.) +Therefore, processing elements that are +next to each other in memory is faster than processing them +in a different order, even if the number of operations is the same: + + >>> def in_order_multiply(arr, scalar): + ... for plane in list(range(arr.shape[0])): + ... arr[plane, :, :] *= scalar + ... + >>> def out_of_order_multiply(arr, scalar): + ... for plane in list(range(arr.shape[2])): + ... arr[:, :, plane] *= scalar + ... + >>> import time + >>> im3d = np.random.rand(100, 1024, 1024) + >>> t0 = time.time(); x = in_order_multiply(im3d, 5); t1 = time.time() + >>> print("%.2f seconds" % (t1 - t0)) # doctest: +SKIP + 0.14 seconds + >>> im3d_t = np.transpose(im3d).copy() # place "planes" dimension at end + >>> im3d_t.shape + (1024, 1024, 100) + >>> s0 = time.time(); x = out_of_order_multiply(im3d, 5); s1 = time.time() + >>> print("%.2f seconds" % (s1 - s0)) doctest: +SKIP + 1.18 seconds + >>> print("Speedup: %.1fx" % ((s1 - s0) / (t1 - t0))) doctest: +SKIP + Speedup: 8.6x + + +When the dimension you are iterating over is even larger, the +speedup is even more dramatic. It is worth thinking about this +*data locality* when writing algorithms. In particular, know that +scikit-image uses C-contiguous arrays unless otherwise specified, so +one should iterate along the last/rightmost dimension in the +innermost loop of the computation. + +A note on time +-------------- + +Although scikit-image does not currently (0.11) provide functions to +work specifically with time-varying 3D data, our compatibility with +numpy arrays allows us to work quite naturally with a 5D array of the +shape (t, pln, row, col, ch): + + >>> for timepoint in image5d: # doctest: +SKIP + ... # each timepoint is a 3D multichannel image + ... do_something_with(timepoint) + + +We can then supplement the above table as follows: + +.. table:: Addendum to dimension names and orders in scikit-image + + ======================== ======================================== + Image type coordinates + ======================== ======================================== + 2D color video (t, row, col, ch) + 3D multichannel video (t, pln, row, col, ch) + ======================== ======================================== diff --git a/doc/source/user_guide/transforming_image_data.txt b/doc/source/user_guide/transforming_image_data.txt new file mode 100644 index 00000000..9dfe3139 --- /dev/null +++ b/doc/source/user_guide/transforming_image_data.txt @@ -0,0 +1,172 @@ +============================================ +Image adjustment: transforming image content +============================================ + +Color manipulation +------------------ + +.. currentmodule:: skimage.color + +Most functions for manipulating color channels are found in the submodule +:mod:`skimage.color`. + +Conversion between color models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Color images can be represented using different `color spaces +`_. One of the most common +color spaces is the `RGB space +`_, where an image has red, +green and blue channels. However, other color models are widely used, +such as the `HSV color model +`_, where hue, saturation and +value are independent channels, or the `CMYK model +`_ used for printing. + +:mod:`skimage.color` provides utility functions to convert images +to and from different color spaces. Integer-type arrays can be +transformed to floating-point type by the conversion operation:: + + >>> # bright saturated red + >>> red_pixel_rgb = np.array([[[255, 0, 0]]], dtype=np.uint8) + >>> color.rgb2hsv(red_pixel_rgb) + array([[[ 0., 1., 1.]]]) + >>> # darker saturated blue + >>> dark_blue_pixel_rgb = np.array([[[0, 0, 100]]], dtype=np.uint8) + >>> color.rgb2hsv(dark_blue_pixel_rgb) + array([[[ 0.66666667, 1. , 0.39215686]]]) + >>> # less saturated pink + >>> pink_pixel_rgb = np.array([[[255, 100, 255]]], dtype=np.uint8) + >>> color.rgb2hsv(pink_pixel_rgb) + array([[[ 0.83333333, 0.60784314, 1. ]]]) + + + +Conversion between color and gray values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Converting an RGB image to a grayscale image is realized with +:func:`rgb2gray` :: + + >>> from skimage.color import rgb2gray + >>> from skimage import data + >>> img = data.astronaut() + >>> img_gray = rgb2gray(img) + +:func:`rgb2gray` uses a non-uniform weighting of color channels, because of the +different sensitivity of the human eye to different colors. Therefore, +such a weighting ensures `luminance preservation +`_ +from RGB to grayscale:: + + >>> red_pixel = np.array([[[255, 0, 0]]], dtype=np.uint8) + >>> color.rgb2gray(red_pixel) + array([[ 0.2125]]) + >>> green_pixel = np.array([[[0, 255, 0]]], dtype=np.uint8) + >>> color.rgb2gray(green_pixel) + array([[ 0.7154]]) + + +Converting a grayscale image to RGB with :func:`gray2rgb` simply +duplicates the gray values over the three color channels. + +Painting images with labels +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:func:`label2rgb` can be used to superimpose colors on a grayscale image +using an array of labels to encode the regions to be represented with the +same color. + + +.. image: ../auto_examples/images/plot_join_segmentations_1.png + :target: ../auto_examples/plot_join_segmentations.html + :align: center + :width: 80% + + + +.. topic:: Examples: + + * :ref:`example_plot_tinting_grayscale_images.py` + * :ref:`example_plot_join_segmentations.py` + * :ref:`example_plot_rag_mean_color.py` + + +Contrast and exposure +--------------------- + +.. currentmodule:: skimage.exposure + +Image pixels can take values determined by the ``dtype`` of the image +(see :ref:`data_types`), such as 0 to 255 for ``uint8`` images or ``[0, +1]`` for floating-point images. However, most images either have a +narrower range of values (because of poor contrast), or have most pixel +values concentrated in a subrange of the accessible values. +:mod:`skimage.exposure` provides functions that spread the intensity +values over a larger range. + +A first class of methods compute a nonlinear function of the intensity, +that is independent of the pixel values of a specific image. Such methods +are often used for correcting a known non-linearity of sensors, or +receptors such as the human eye. A well-known example is `Gamma +correction `_, implemented +in :func:`adjust_gamma`. + +Other methods re-distribute pixel values according to the *histogram* of +the image. The histogram of pixel values is computed with +:func:`skimage.exposure.histogram`:: + + >>> image = np.array([[1, 3], [1, 1]]) + >>> exposure.histogram(image) + (array([3, 0, 1]), array([1, 2, 3])) + +:func:`histogram` returns the number of pixels for each value bin, and +the centers of the bins. The behavior of :func:`histogram` is therefore +slightly different from the one of :func:`np.histogram`, which returns +the boundaries of the bins. + +The simplest contrast enhancement :func:`rescale_intensity` consists in +stretching pixel values to the whole allowed range, using a linear +transformation:: + + >>> from skimage import exposure + >>> text = data.text() + >>> text.min(), text.max() + (10, 197) + >>> better_contrast = exposure.rescale_intensity(text) + >>> better_contrast.min(), better_contrast.max() + (0, 255) + +Even if an image uses the whole value range, sometimes there is very +little weight at the ends of the value range. In such a case, clipping +pixel values using percentiles of the image improves the contrast (at the +expense of some loss of information, because some pixels are saturated by +this operation):: + + >>> moon = data.moon() + >>> v_min, v_max = np.percentile(moon, (0.2, 99.8)) + >>> v_min, v_max + (10.0, 186.0) + >>> better_contrast = exposure.rescale_intensity( + ... moon, in_range=(v_min, v_max)) + +The function :func:`equalize_hist` maps the cumulative distribution +function (cdf) of pixel values onto a linear cdf, ensuring that all parts +of the value range are equally represented in the image. As a result, +details are enhanced in large regions with poor contrast. As a further +refinement, histogram equalization can be performed in subregions of the +image with :func:`equalize_adapthist`, in order to correct for exposure +gradients across the image. See the example +:ref:`example_plot_equalize.py`. + +.. image:: ../auto_examples/images/plot_equalize_1.png + :target: ../auto_examples/plot_equalize.html + :align: center + :width: 90% + + +.. topic:: Examples: + + * :ref:`example_plot_equalize.py` + + diff --git a/doc/source/user_guide/tutorial_parallelization.txt b/doc/source/user_guide/tutorial_parallelization.txt new file mode 100755 index 00000000..e083cbd2 --- /dev/null +++ b/doc/source/user_guide/tutorial_parallelization.txt @@ -0,0 +1,61 @@ +======================== +How to parallelize loops +======================== + +In image processing, we frequently apply the same algorithm +on a large batch of images. Here is an example: + +.. code-block:: python + + from skimage import data, color, util + from skimage.restoration import denoise_tv_chambolle + from skimage.feature import hog + + def task(image): + """ + Apply some functions and return an image. + """ + image = denoise_tv_chambolle(image[0][0], weight=0.1, multichannel=True) + fd, hog_image = hog(color.rgb2gray(image), orientations=8, + pixels_per_cell=(16, 16), cells_per_block=(1, 1), + visualise=True) + return hog_image + + + # Prepare images + hubble = data.hubble_deep_field() + width = 10 + pics = util.view_as_windows(hubble, (width, hubble.shape[1], hubble.shape[2]), step=width) + +To call the function ``task`` on each element of the list ``pics``, it is +usual to write a for loop. To measure the execution time of this loop, you can +use ipython and measure the execution time with ``%timeit``. + +.. code-block:: python + + def classic_loop(): + for image in pics: + task(image) + + + %timeit classic_loop() + +Another equivalent way to code this loop is to use a comprehension list which has the same efficiency. + +.. code-block:: python + + def comprehension_loop(): + [task(image) for image in pics] + + %timeit comprehension_loop() + +``joblib`` is a library providing an easy way to parallelize for loops once we have a comprehension list. +The number of jobs can be specified. + +.. code-block:: python + + from joblib import Parallel, delayed + def joblib_loop(): + Parallel(n_jobs=4)(delayed(task)(i) for i in pics) + + %timeit joblib_loop() diff --git a/doc/source/user_guide/tutorial_segmentation.txt b/doc/source/user_guide/tutorial_segmentation.txt index cff7c651..2cf45374 100644 --- a/doc/source/user_guide/tutorial_segmentation.txt +++ b/doc/source/user_guide/tutorial_segmentation.txt @@ -11,7 +11,7 @@ the coins cannot be done directly from the histogram of grey values, because the background shares enough grey levels with the coins that a thresholding segmentation is not sufficient. -.. image:: ../../_images/plot_coins_segmentation_1.png +.. image:: ../auto_examples/applications/images/plot_coins_segmentation_1.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center @@ -26,7 +26,7 @@ Simply thresholding the image leads either to missing significant parts of the coins, or to merging parts of the background with the coins. This is due to the inhomogeneous lighting of the image. -.. image:: ../../_images/plot_coins_segmentation_2.png +.. image:: ../auto_examples/applications/images/plot_coins_segmentation_2.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center @@ -38,11 +38,11 @@ Edge-based segmentation Let us first try to detect edges that enclose the coins. For edge detection, we use the `Canny detector -`_ of ``skimage.filter.canny`` +`_ of ``skimage.feature.canny`` :: - >>> from skimage.filter import canny + >>> from skimage.feature import canny >>> edges = canny(coins/255.) As the background is very smooth, almost all edges are found at the @@ -53,7 +53,7 @@ boundary of the coins, or inside the coins. >>> from scipy import ndimage >>> fill_coins = ndimage.binary_fill_holes(edges) -.. image:: ../../_images/plot_coins_segmentation_3.png +.. image:: ../auto_examples/applications/images/plot_coins_segmentation_3.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center @@ -62,7 +62,7 @@ we fill the inner part of the coins using the ``ndimage.binary_fill_holes`` function, which uses mathematical morphology to fill the holes. -.. image:: ../../_images/plot_coins_segmentation_4.png +.. image:: ../auto_examples/applications/images/plot_coins_segmentation_4.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center @@ -83,7 +83,7 @@ has not been segmented correctly at all. The reason is that the contour that we got from the Canny detector was not completely closed, therefore the filling function did not fill the inner part of the coin. -.. image:: ../../_images/plot_coins_segmentation_5.png +.. image:: ../auto_examples/applications/images/plot_coins_segmentation_5.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center @@ -117,7 +117,7 @@ The choice of the elevation map is critical for good segmentation. Here, the amplitude of the gradient provides a good elevation map. We use the Sobel operator for computing the amplitude of the gradient:: - >>> from skimage.filter import sobel + >>> from skimage.filters import sobel >>> elevation_map = sobel(coins) From the 3-D surface plot shown below, we see that high barriers effectively @@ -128,7 +128,7 @@ separate the coins from the background. and here is the corresponding 2-D plot: -.. image:: ../../_images/plot_coins_segmentation_6.png +.. image:: ../auto_examples/applications/images/plot_coins_segmentation_6.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center @@ -139,7 +139,7 @@ extreme parts of the histogram of grey values:: >>> markers[coins < 30] = 1 >>> markers[coins > 150] = 2 -.. image:: ../../_images/plot_coins_segmentation_7.png +.. image:: ../auto_examples/applications/images/plot_coins_segmentation_7.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center @@ -148,7 +148,7 @@ Let us now compute the watershed transform:: >>> from skimage.morphology import watershed >>> segmentation = watershed(elevation_map, markers) -.. image:: ../../_images/plot_coins_segmentation_8.png +.. image:: ../auto_examples/applications/images/plot_coins_segmentation_8.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center @@ -165,7 +165,7 @@ We can now label all the coins one by one using ``ndimage.label``:: >>> labeled_coins, _ = ndimage.label(segmentation) -.. image:: ../../_images/plot_coins_segmentation_9.png +.. image:: ../auto_examples/applications/images/plot_coins_segmentation_9.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center diff --git a/doc/source/user_guide/tutorials.txt b/doc/source/user_guide/tutorials.txt index be724a63..6235bcc5 100644 --- a/doc/source/user_guide/tutorials.txt +++ b/doc/source/user_guide/tutorials.txt @@ -5,3 +5,4 @@ Tutorials :maxdepth: 1 tutorial_segmentation + tutorial_parallelization diff --git a/doc/source/user_guide/viewer.txt b/doc/source/user_guide/viewer.txt index 3145effd..eac6b3c3 100644 --- a/doc/source/user_guide/viewer.txt +++ b/doc/source/user_guide/viewer.txt @@ -51,7 +51,7 @@ call the total-variation denoising function, ``denoise_tv_bregman``: .. code-block:: python - from skimage.filter import denoise_tv_bregman + from skimage.filters import denoise_tv_bregman from skimage.viewer.plugins.base import Plugin denoise_plugin = Plugin(image_filter=denoise_tv_bregman) diff --git a/doc/tools/apigen.py b/doc/tools/apigen.py index f1a94b6a..44f9e871 100644 --- a/doc/tools/apigen.py +++ b/doc/tools/apigen.py @@ -21,7 +21,7 @@ is an MIT-licensed project. import os import re -from types import BuiltinFunctionType +from types import BuiltinFunctionType, FunctionType # suppress print statements (warnings for empty files) DEBUG = True @@ -200,7 +200,7 @@ class ApiDocWriter(object): classes : list of str A list of (public) class names in the module. """ - mod = __import__(uri, fromlist=[uri]) + mod = __import__(uri, fromlist=[uri.split('.')[-1]]) # find all public objects in the module. obj_strs = [obj for obj in dir(mod) if not obj.startswith('_')] functions = [] @@ -210,9 +210,9 @@ class ApiDocWriter(object): if obj_str not in mod.__dict__: continue obj = mod.__dict__[obj_str] + # figure out if obj is a function or class - if hasattr(obj, 'func_name') or \ - isinstance(obj, BuiltinFunctionType): + if isinstance(obj, FunctionType): functions.append(obj_str) else: try: @@ -279,6 +279,20 @@ class ApiDocWriter(object): ad += '\n.. automodule:: ' + uri + '\n' ad += '\n.. currentmodule:: ' + uri + '\n' + ad += '.. autosummary::\n\n' + for f in functions: + ad += ' ' + uri + '.' + f + '\n' + ad += '\n' + for c in classes: + ad += ' ' + uri + '.' + c + '\n' + ad += '\n' + + for f in functions: + # must NOT exclude from index to keep cross-refs working + full_f = uri + '.' + f + ad += f + '\n' + ad += self.rst_section_levels[2] * len(f) + '\n' + ad += '\n.. autofunction:: ' + full_f + '\n\n' for c in classes: ad += '\n:class:`' + c + '`\n' \ + self.rst_section_levels[2] * \ @@ -290,17 +304,6 @@ class ApiDocWriter(object): ' :show-inheritance:\n' \ '\n' \ ' .. automethod:: __init__\n' - ad += '.. autosummary::\n\n' - for f in functions: - ad += ' ' + uri + '.' + f + '\n' - ad += '\n' - - for f in functions: - # must NOT exclude from index to keep cross-refs working - full_f = uri + '.' + f - ad += f + '\n' - ad += self.rst_section_levels[2] * len(f) + '\n' - ad += '\n.. autofunction:: ' + full_f + '\n\n' return ad def _survives_exclude(self, matchstr, match_type): diff --git a/doc/tools/build_modref_templates.py b/doc/tools/build_modref_templates.py index b4db115d..e83ce980 100644 --- a/doc/tools/build_modref_templates.py +++ b/doc/tools/build_modref_templates.py @@ -25,7 +25,7 @@ if __name__ == '__main__': try: __import__(package) - except ImportError, e: + except ImportError as e: abort("Can not import skimage") module = sys.modules[package] @@ -51,6 +51,7 @@ if __name__ == '__main__': docwriter = ApiDocWriter(package) docwriter.package_skip_patterns += [r'\.fixes$', r'\.externals$', + r'filter$', ] docwriter.write_api_docs(outdir) docwriter.write_index(outdir, 'api', relative_to='source/api') diff --git a/requirements.txt b/requirements.txt index 48e03b8b..12ce5fc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ -cython>=0.17 -matplotlib>=1.0 -numpy>=1.6 -six>=1.3.0 +cython>=0.21 +matplotlib>=1.1.0 +numpy>=1.6.1 +scipy>=0.9 +six>=1.3 +networkx>=1.8 +pillow>=1.7.8 diff --git a/setup.py b/setup.py index 7a0dad7a..8ba529e5 100755 --- a/setup.py +++ b/setup.py @@ -17,25 +17,28 @@ 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.10.1' -PYTHON_VERSION = (2, 5) -DEPENDENCIES = { - 'numpy': (1, 6), - 'six': (1, 3), - } - -# Only require Cython if we have a developer checkout -if VERSION.endswith('dev'): - DEPENDENCIES['Cython'] = (0, 17) - - +VERSION = '0.11.0' +PYTHON_VERSION = (2, 6) import os import sys -import re + import setuptools -from numpy.distutils.core import setup from distutils.command.build_py import build_py +from distutils.version import LooseVersion + + +# These are manually checked. +# These packages are sometimes installed outside of the setuptools scope +DEPENDENCIES = {} +with open('requirements.txt', 'rb') as fid: + data = fid.read().decode('utf-8', 'replace') +for line in data.splitlines(): + pkg, _, version_info = line.replace('==', '>=').partition('>=') + # Only require Cython if we have a developer checkout + if pkg.lower() == 'cython' and not VERSION.endswith('dev'): + continue + DEPENDENCIES[str(pkg).lower()] = str(version_info) def configuration(parent_package='', top_path=None): @@ -61,48 +64,58 @@ def write_version_py(filename='skimage/version.py'): version='%s' """ - vfile = open(os.path.join(os.path.dirname(__file__), - filename), 'w') - try: + vfile = open(os.path.join(os.path.dirname(__file__), + filename), 'w') vfile.write(template % VERSION) + + except IOError: + raise IOError("Could not open/write to skimage/version.py - did you " + "install using sudo in the past? If so, run\n" + "sudo chown -R your_username ./*\n" + "from package root to fix permissions, and try again.") + finally: vfile.close() def get_package_version(package): - version = [] - for version_attr in ('version', 'VERSION', '__version__'): - if hasattr(package, version_attr) \ - and isinstance(getattr(package, version_attr), str): - version_info = getattr(package, version_attr, '') - for part in re.split('\D+', version_info): - try: - version.append(int(part)) - except ValueError: - pass - return tuple(version) + for version_attr in ('__version__', 'VERSION', 'version'): + version_info = getattr(package, version_attr, None) + if version_info and str(version_attr) == version_attr: + return str(version_info) def check_requirements(): if sys.version_info < PYTHON_VERSION: raise SystemExit('You need Python version %d.%d or later.' \ % PYTHON_VERSION) - - for package_name, min_version in DEPENDENCIES.items(): - dep_error = False + for (package_name, min_version) in DEPENDENCIES.items(): + if package_name == 'cython': + package_name = 'Cython' + dep_error = '' + if package_name.lower() == 'pillow': + package_name = 'PIL.Image' + min_version = '1.1.7' try: - package = __import__(package_name) + package = __import__(package_name, + fromlist=[package_name.rpartition('.')[0]]) except ImportError: - dep_error = True + dep_error = ('You need `%s` version %s or later.' + % (package_name, min_version)) else: - package_version = get_package_version(package) - if min_version > package_version: - dep_error = True + if package_name == 'PIL': + package_version = package.PILLOW_VERSION + else: + package_version = get_package_version(package) + if LooseVersion(min_version) > LooseVersion(package_version): + dep_error = ('You need `%s` version %s or later,' + 'found version %s.' + % (package_name, min_version, + package_version)) if dep_error: - raise ImportError('You need `%s` version %d.%d or later.' \ - % ((package_name, ) + min_version)) + raise ImportError(dep_error) if __name__ == "__main__": @@ -111,6 +124,7 @@ if __name__ == "__main__": write_version_py() + from numpy.distutils.core import setup setup( name=DISTNAME, description=DESCRIPTION, @@ -139,10 +153,12 @@ if __name__ == "__main__": ], configuration=configuration, - + install_requires=[ + "six>=%s" % DEPENDENCIES['six'] + ], packages=setuptools.find_packages(exclude=['doc']), include_package_data=True, - zip_safe=False, # the package can run out of an .egg file + zip_safe=False, # the package can run out of an .egg file entry_points={ 'console_scripts': ['skivi = skimage.scripts.skivi:main'], diff --git a/skimage/__init__.py b/skimage/__init__.py index 59c80ed2..8f386682 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -19,7 +19,7 @@ exposure Image intensity adjustment, e.g., histogram equalization, etc. feature Feature detection and extraction, e.g., texture analysis corners, etc. -filter +filters Sharpening, edge finding, rank filters, thresholding, etc. graph Graph-theoretic operations, e.g., shortest paths. @@ -60,7 +60,6 @@ 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__)) data_dir = _osp.join(pkg_dir, 'data') @@ -69,23 +68,19 @@ try: from .version import version as __version__ except ImportError: __version__ = "unbuilt-dev" -del version +else: + del version try: _imp.find_module('nose') except ImportError: - def _test(verbose=False): + def _test(doctest=False, verbose=False): """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(doctest=False, verbose=False): """Run all unit tests.""" @@ -119,53 +114,4 @@ doctest.__doc__ = doctest.__doc__ doctest_verbose = _functools.partial(test, doctest=True, verbose=True) doctest_verbose.__doc__ = doctest.__doc__ - -class _Log(Warning): - pass - - -class _FakeLog(object): - def __init__(self, name): - """ - Parameters - ---------- - name : str - Name of the log. - repeat : bool - Whether to print repeating messages more than once (False by - default). - """ - self._name = name - - warnings.simplefilter("always", _Log) - - self._warnings = _warnings - - def _warn(self, msg, wtype): - self._warnings.warn('%s: %s' % (wtype, msg), _Log) - - def debug(self, msg): - self._warn(msg, 'DEBUG') - - def info(self, msg): - self._warn(msg, 'INFO') - - def warning(self, msg): - self._warn(msg, 'WARNING') - - warn = warning - - def error(self, msg): - self._warn(msg, 'ERROR') - - def critical(self, msg): - self._warn(msg, 'CRITICAL') - - def addHandler(*args): - pass - - def setLevel(*args): - pass - - from .util.dtype import * diff --git a/skimage/_shared/LICENSE.txt b/skimage/_shared/LICENSE.txt new file mode 100644 index 00000000..69293997 --- /dev/null +++ b/skimage/_shared/LICENSE.txt @@ -0,0 +1,12 @@ + +version_requirements._check_version: + + Copyright (C) 2013 The IPython Development Team + + Distributed under the terms of the BSD License. + + +version_requirements.is_installed: + + Original Copyright (C) 2009-2011 Pierre Raybaut + Licensed under the terms of the MIT License. diff --git a/skimage/_shared/_warnings.py b/skimage/_shared/_warnings.py index c4cd0a68..78d38fbd 100644 --- a/skimage/_shared/_warnings.py +++ b/skimage/_shared/_warnings.py @@ -1,9 +1,10 @@ -__all__ = ['all_warnings'] +__all__ = ['all_warnings', 'expected_warnings'] from contextlib import contextmanager import sys import warnings import inspect +import re @contextmanager @@ -61,3 +62,54 @@ def all_warnings(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") yield w + + +@contextmanager +def expected_warnings(matching): + """Context for use in testing to catch known warnings matching regexes + + Parameters + ---------- + matching : list of strings or compiled regexes + Regexes for the desired warning to catch + + + Examples + -------- + >>> from skimage import data, img_as_ubyte, img_as_float + >>> with expected_warnings(['precision loss']): + ... d = img_as_ubyte(img_as_float(data.coins())) + + Notes + ----- + Uses `all_warnings` to ensure all warnings are raised. + Upon exiting, it checks the recorded warnings for the desired matching + pattern(s). + Raises a ValueError if any match was not found or an unexpected + warning was raised. + Allows for three types of behaviors: "and", "or", and "optional" matches. + This is done to accomodate different build enviroments or loop conditions + that may produce different warnings. The behaviors can be combined. + If you pass multiple patterns, you get an orderless "and", where all of the + warnings must be raised. + If you use the "|" operator in a pattern, you can catch one of several warnings. + Finally, you can use "|\A\Z" in a pattern to signify it as optional. + + """ + with all_warnings() as w: + # enter context + yield w + # exited user context, check the recorded warnings + remaining = [m for m in matching if not '\A\Z' in m.split('|')] + for warn in w: + found = False + for match in matching: + if re.search(match, str(warn.message)) is not None: + found = True + if match in remaining: + remaining.remove(match) + if not found: + raise ValueError('Unexpected warning: %s' % str(warn.message)) + if len(remaining) > 0: + msg = 'No warning raised matching:\n%s' % '\n'.join(remaining) + raise ValueError(msg) diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd index 7d212969..088be1ef 100644 --- a/skimage/_shared/interpolation.pxd +++ b/skimage/_shared/interpolation.pxd @@ -9,7 +9,8 @@ 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, +cdef inline double nearest_neighbour_interpolation(double* image, + Py_ssize_t rows, Py_ssize_t cols, double r, double c, char mode, double cval): @@ -63,30 +64,33 @@ cdef inline double bilinear_interpolation(double* image, Py_ssize_t rows, """ cdef double dr, dc - cdef Py_ssize_t minr, minc, maxr, maxc + cdef long minr, minc, maxr, maxc - minr = floor(r) - minc = floor(c) - maxr = ceil(r) - maxc = ceil(c) + 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) \ + 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. + """WARNING: Do not use, not implemented correctly. + + Quadratic interpolation. Parameters ---------- x : double - Position in the interval [-1, 1]. - f : double[4] - Function values at positions [-1, 0, 1]. + Position in the interval [0, 2]. + f : double[3] + Function values at positions [0, 2]. Returns ------- @@ -94,13 +98,17 @@ cdef inline double quadratic_interpolation(double x, double[3] f): Interpolated value. """ - return f[1] - 0.25 * (f[0] - f[2]) * x + return (x * f[2] * (x - 1)) / 2 - \ + x * f[1] * (x - 2) + \ + (f[0] * (x - 1) * (x - 2)) / 2 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. + Py_ssize_t cols, double r, + double c, char mode, double cval): + """WARNING: Do not use, not implemented correctly. + + Biquadratic interpolation at a given position in the image. Parameters ---------- @@ -122,30 +130,23 @@ cdef inline double biquadratic_interpolation(double* image, Py_ssize_t rows, """ - 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 long r0 = round(r) - 1 + cdef long c0 = round(c) - 1 + + cdef double xr = r - r0 + cdef double xc = c - c0 cdef double fc[3] cdef double fr[3] - cdef Py_ssize_t pr, pc + cdef long 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) + for pr in range(3): + for pc in range(3): + fc[pc] = get_pixel2d(image, rows, cols, + r0 + pr, c0 + pc, mode, cval) + fr[pr] = quadratic_interpolation(xc, fc) # cubic interpolation for interpolated values of each row return quadratic_interpolation(xr, fr) @@ -159,7 +160,7 @@ cdef inline double cubic_interpolation(double x, double[4] f): x : double Position in the interval [0, 1]. f : double[4] - Function values at positions [0, 1/3, 2/3, 1]. + Function values at positions [-1, 0, 1, 2]. Returns ------- @@ -179,6 +180,9 @@ cdef inline double bicubic_interpolation(double* image, Py_ssize_t rows, char mode, double cval): """Bicubic interpolation at a given position in the image. + Interpolation using Catmull-Rom splines, based on the bicubic convolution + algorithm described in [1]_. + Parameters ---------- image : double array @@ -197,35 +201,42 @@ cdef inline double bicubic_interpolation(double* image, Py_ssize_t rows, value : double Interpolated value. + References + ---------- + .. [1] R. Keys, (1981). "Cubic convolution interpolation for digital image + processing". IEEE Transactions on Signal Processing, Acoustics, + Speech, and Signal Processing 29 (6): 1153–1160. + """ - cdef Py_ssize_t r0 = r - 1 - cdef Py_ssize_t c0 = c - 1 - if r < 0: - r0 -= 1 - if c < 0: - c0 -= 1 + cdef long r0 = floor(r) + cdef long c0 = floor(c) + # scale position to range [0, 1] - cdef double xr = (r - r0) / 3 - cdef double xc = (c - c0) / 3 + cdef double xr = r - r0 + cdef double xc = c - c0 + + r0 -= 1 + c0 -= 1 cdef double fc[4] cdef double fr[4] - cdef Py_ssize_t pr, pc + cdef long 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) + for pr in range(4): + for pc in range(4): + fc[pc] = get_pixel2d(image, rows, cols, pr + r0, pc + c0, mode, cval) + fr[pr] = 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): + long r, long c, char mode, + double cval): """Get a pixel from the image, taking wrapping mode into consideration. Parameters @@ -248,7 +259,7 @@ cdef inline double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, """ if mode == 'C': - if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): + if (r < 0) or (r >= rows) or (c < 0) or (c >= cols): return cval else: return image[r * cols + c] @@ -257,8 +268,8 @@ cdef inline double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, 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): + Py_ssize_t dims, long r, long c, + long d, char mode, double cval): """Get a pixel from the image, taking wrapping mode into consideration. Parameters @@ -281,19 +292,18 @@ cdef inline double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, """ if mode == 'C': - if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): + if (r < 0) or (r >= rows) or (c < 0) or (c >= cols): 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] + + coord_map(dims, d, mode)] -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. +cdef inline Py_ssize_t coord_map(Py_ssize_t dim, long coord, char mode): + """Wrap a coordinate, according to a given mode. Parameters ---------- diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py index 9b42c582..1c41221b 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -1,7 +1,16 @@ """Testing utilities.""" +import os import re +from tempfile import NamedTemporaryFile + +from numpy import testing +import numpy as np +from skimage._shared._warnings import expected_warnings +import warnings + +from .. import data, io, img_as_uint, img_as_float, img_as_int, img_as_ubyte SKIP_RE = re.compile("(\s*>>>.*?)(\s*)#\s*skip\s+if\s+(.*)$") @@ -41,7 +50,7 @@ def doctest_skip_parser(func): >>> something + else >>> something # skip if HAVE_BMODULE - This decorator will evaluate the expresssion after ``skip if``. If this + This decorator will evaluate the expression 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`. @@ -75,3 +84,125 @@ def doctest_skip_parser(func): new_lines.append(code) func.__doc__ = "\n".join(new_lines) return func + + +def roundtrip(img, plugin, suffix): + """Save and read an image using a specified plugin""" + if not '.' in suffix: + suffix = '.' + suffix + temp_file = NamedTemporaryFile(suffix=suffix, delete=False) + temp_file.close() + fname = temp_file.name + io.imsave(fname, img, plugin=plugin) + new = io.imread(fname, plugin=plugin) + try: + os.remove(fname) + except Exception: + pass + return new + + +def color_check(plugin, fmt='png'): + """Check roundtrip behavior for color images. + + All major input types should be handled as ubytes and read + back correctly. + """ + img = img_as_ubyte(data.chelsea()) + r1 = roundtrip(img, plugin, fmt) + testing.assert_allclose(img, r1) + + img2 = img > 128 + r2 = roundtrip(img2, plugin, fmt) + testing.assert_allclose(img2.astype(np.uint8), r2) + + img3 = img_as_float(img) + with expected_warnings(['precision loss|unclosed file']): + r3 = roundtrip(img3, plugin, fmt) + testing.assert_allclose(r3, img) + + with expected_warnings(['precision loss']): + img4 = img_as_int(img) + if fmt.lower() in (('tif', 'tiff')): + img4 -= 100 + with expected_warnings(['sign loss']): + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img4) + else: + with expected_warnings(['sign loss|precision loss|unclosed file']): + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img_as_ubyte(img4)) + + img5 = img_as_uint(img) + with expected_warnings(['precision loss|unclosed file']): + r5 = roundtrip(img5, plugin, fmt) + testing.assert_allclose(r5, img) + + +def mono_check(plugin, fmt='png'): + """Check the roundtrip behavior for images that support most types. + + All major input types should be handled. + """ + + img = img_as_ubyte(data.moon()) + r1 = roundtrip(img, plugin, fmt) + testing.assert_allclose(img, r1) + + img2 = img > 128 + r2 = roundtrip(img2, plugin, fmt) + testing.assert_allclose(img2.astype(np.uint8), r2) + + img3 = img_as_float(img) + with expected_warnings(['precision|unclosed file|\A\Z']): + r3 = roundtrip(img3, plugin, fmt) + if r3.dtype.kind == 'f': + testing.assert_allclose(img3, r3) + else: + testing.assert_allclose(r3, img_as_uint(img)) + + with expected_warnings(['precision loss']): + img4 = img_as_int(img) + if fmt.lower() in (('tif', 'tiff')): + img4 -= 100 + with expected_warnings(['sign loss|\A\Z']): + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img4) + else: + with expected_warnings(['precision loss|sign loss|unclosed file']): + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img_as_uint(img4)) + + img5 = img_as_uint(img) + r5 = roundtrip(img5, plugin, fmt) + testing.assert_allclose(r5, img5) + + +def setup_test(): + """Default package level setup routine for skimage tests. + + Import packages known to raise errors, and then + force warnings to raise errors. + Set a random seed + """ + warnings.simplefilter('default') + from scipy import signal, ndimage, special, optimize, linalg + from scipy.io import loadmat + from skimage import viewer, filter + np.random.seed(0) + warnings.simplefilter('error') + + +def teardown_test(): + """Default package level teardown routine for skimage tests. + + Restore warnings to default behavior + """ + warnings.simplefilter('default') + + +if __name__ == '__main__': + color_check('pil') + mono_check('pil') + mono_check('pil', 'bmp') + mono_check('pil', 'tiff') diff --git a/skimage/_shared/tests/__init__.py b/skimage/_shared/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/_shared/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/_shared/tests/test_safe_as_int.py b/skimage/_shared/tests/test_safe_as_int.py index 009fa9a4..1f1ad96a 100644 --- a/skimage/_shared/tests/test_safe_as_int.py +++ b/skimage/_shared/tests/test_safe_as_int.py @@ -8,14 +8,14 @@ def test_int_cast_not_possible(): 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))) + (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) + (2, 7.6, 289)), 0.25) def test_int_cast_possible(): @@ -25,8 +25,8 @@ def test_int_cast_possible(): 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.]]]), + [7, 2, -8.999999999], + [6, 9, -4234918347.]]]), np.r_[[[3, 4, 1], [7, 2, -9], [6, 9, -4234918347]]]) diff --git a/skimage/_shared/tests/test_testing.py b/skimage/_shared/tests/test_testing.py index f563caad..a8b662bb 100644 --- a/skimage/_shared/tests/test_testing.py +++ b/skimage/_shared/tests/test_testing.py @@ -11,6 +11,7 @@ def test_skipper(): pass class c(): + def __init__(self): self.me = "I think, therefore..." diff --git a/skimage/_shared/tests/test_version_requirements.py b/skimage/_shared/tests/test_version_requirements.py new file mode 100644 index 00000000..59cf5814 --- /dev/null +++ b/skimage/_shared/tests/test_version_requirements.py @@ -0,0 +1,42 @@ +"""Tests for the version requirement functions. + +""" +import numpy as np +from numpy.testing import assert_raises, assert_equal +import nose +from skimage._shared import version_requirements as version_req + + +def test_get_module_version(): + assert version_req.get_module_version('numpy') + assert version_req.get_module_version('scipy') + assert_raises(ImportError, + lambda: version_req.get_module_version('fakenumpy')) + + +def test_is_installed(): + assert version_req.is_installed('python', '>=2.6') + assert not version_req.is_installed('numpy', '<1.0') + + +def test_require(): + + # A function that only runs on Python >2.6 and numpy > 1.5 (should pass) + @version_req.require('python', '>2.6') + @version_req.require('numpy', '>1.5') + def foo(): + return 1 + + assert_equal(foo(), 1) + + # function that requires scipy < 0.1 (should fail) + @version_req.require('scipy', '<0.1') + def bar(): + return 0 + + assert_raises(ImportError, lambda: bar()) + + +def test_get_module(): + assert_equal(version_req.get_module('numpy'), np) + assert_equal(version_req.get_module('nose'), nose) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 612a2b63..43fda35d 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -8,7 +8,7 @@ import six from ._warnings import all_warnings __all__ = ['deprecated', 'get_bound_method_class', 'all_warnings', - 'safe_as_int'] + 'safe_as_int', 'assert_nD'] class skimage_deprecation(Warning): @@ -52,9 +52,9 @@ class deprecated(object): func_code = six.get_function_code(func) warnings.simplefilter('always', skimage_deprecation) warnings.warn_explicit(msg, - category=skimage_deprecation, - filename=func_code.co_filename, - lineno=func_code.co_firstlineno + 1) + category=skimage_deprecation, + filename=func_code.co_filename, + lineno=func_code.co_firstlineno + 1) elif self.behavior == 'raise': raise skimage_deprecation(msg) return func(*args, **kwargs) @@ -141,3 +141,25 @@ def safe_as_int(val, atol=1e-3): "{0}, check inputs.".format(val)) return np.round(val).astype(np.int64) + + +def assert_nD(array, ndim, arg_name='image'): + """ + Verify an array meets the desired ndims. + + Parameters + ---------- + array : array-like + Input array to be validated + ndim : int or iterable of ints + Allowable ndim or ndims for the array. + arg_name : str, optional + The name of the array in the original function. + + """ + array = np.asanyarray(array) + msg = "The parameter `%s` must be a %s-dimensional array" + if isinstance(ndim, int): + ndim = [ndim] + if not array.ndim in ndim: + raise ValueError(msg % (arg_name, '-or-'.join([str(n) for n in ndim]))) diff --git a/skimage/_shared/version_requirements.py b/skimage/_shared/version_requirements.py new file mode 100644 index 00000000..430f7f5e --- /dev/null +++ b/skimage/_shared/version_requirements.py @@ -0,0 +1,142 @@ +from distutils.version import LooseVersion +import functools +import re +import sys + + +def _check_version(actver, version, cmp_op): + """ + Check version string of an active module against a required version. + + If dev/prerelease tags result in TypeError for string-number comparison, + it is assumed that the dependency is satisfied. + Users on dev branches are responsible for keeping their own packages up to + date. + + Copyright (C) 2013 The IPython Development Team + + Distributed under the terms of the BSD License. + """ + try: + if cmp_op == '>': + return LooseVersion(actver) > LooseVersion(version) + elif cmp_op == '>=': + return LooseVersion(actver) >= LooseVersion(version) + elif cmp_op == '=': + return LooseVersion(actver) == LooseVersion(version) + elif cmp_op == '<': + return LooseVersion(actver) < LooseVersion(version) + else: + return False + except TypeError: + return True + + +def get_module_version(module_name): + """Return module version or None if version can't be retrieved.""" + mod = __import__(module_name, + fromlist=[module_name.rpartition('.')[-1]]) + return getattr(mod, '__version__', getattr(mod, 'VERSION', None)) + + +def is_installed(name, version=None): + """Test if *name* is installed. + + Parameters + ---------- + name : str + Name of module or "python" + version : str, optional + Version string to test against. + If version is not None, checking version + (must have an attribute named '__version__' or 'VERSION') + Version may start with =, >=, > or < to specify the exact requirement + + Returns + ------- + out : bool + True if `name` is installed matching the optional version. + + Note + ---- + Original Copyright (C) 2009-2011 Pierre Raybaut + Licensed under the terms of the MIT License. + """ + if name.lower() == 'python': + actver = sys.version[:6] + else: + try: + actver = get_module_version(name) + except ImportError: + return False + if version is None: + return True + else: + match = re.search('[0-9]', version) + assert match is not None, "Invalid version number" + symb = version[:match.start()] + if not symb: + symb = '=' + assert symb in ('>=', '>', '=', '<'),\ + "Invalid version condition '%s'" % symb + version = version[match.start():] + return _check_version(actver, version, symb) + + +def require(name, version=None): + """Return decorator that forces a requirement for a function or class. + + Parameters + ---------- + name : str + Name of module or "python". + version : str, optional + Version string to test against. + If version is not None, checking version + (must have an attribute named '__version__' or 'VERSION') + Version may start with =, >=, > or < to specify the exact requirement + + Returns + ------- + func : function + A decorator that raises an ImportError if a function is run + in the absence of the input dependency. + """ + def decorator(obj): + @functools.wraps(obj) + def func_wrapped(*args, **kwargs): + if is_installed(name, version): + return obj(*args, **kwargs) + else: + msg = '"%s" in "%s" requires "%s' + msg = msg % (obj, obj.__module__, name) + if not version is None: + msg += " %s" % version + raise ImportError(msg + '"') + return func_wrapped + return decorator + + +def get_module(module_name, version=None): + """Return a module object of name *module_name* if installed. + + Parameters + ---------- + module_name : str + Name of module. + version : str, optional + Version string to test against. + If version is not None, checking version + (must have an attribute named '__version__' or 'VERSION') + Version may start with =, >=, > or < to specify the exact requirement + + Returns + ------- + mod : module or None + Module if *module_name* is installed matching the optional version + or None otherwise. + """ + if not is_installed(module_name, version): + return None + return __import__(module_name, + fromlist=[module_name.rpartition('.')[-1]]) diff --git a/skimage/color/adapt_rgb.py b/skimage/color/adapt_rgb.py new file mode 100644 index 00000000..8dc02b34 --- /dev/null +++ b/skimage/color/adapt_rgb.py @@ -0,0 +1,78 @@ +import functools + +import numpy as np + +from .. import color +from ..util.dtype import convert + + +__all__ = ['adapt_rgb', 'hsv_value', 'each_channel'] + + +def is_rgb_like(image): + """Return True if the image *looks* like it's RGB. + + This function should not be public because it is only intended to be used + for functions that don't accept volumes as input, since checking an image's + shape is fragile. + """ + return (image.ndim == 3) and (image.shape[2] in (3, 4)) + + +def adapt_rgb(apply_to_rgb): + """Return decorator that adapts to RGB images to a gray-scale filter. + + This function is only intended to be used for functions that don't accept + volumes as input, since checking an image's shape is fragile. + + Parameters + ---------- + apply_to_rgb : function + Function that returns a filtered image from an image-filter and RGB + image. This will only be called if the image is RGB-like. + """ + def decorator(image_filter): + @functools.wraps(image_filter) + def image_filter_adapted(image, *args, **kwargs): + if is_rgb_like(image): + return apply_to_rgb(image_filter, image, *args, **kwargs) + else: + return image_filter(image, *args, **kwargs) + return image_filter_adapted + return decorator + + +def hsv_value(image_filter, image, *args, **kwargs): + """Return color image by applying `image_filter` on HSV-value of `image`. + + Note that this function is intended for use with `adapt_rgb`. + + Parameters + ---------- + image_filter : function + Function that filters a gray-scale image. + image : array + Input image. Note that RGBA images are treated as RGB. + """ + # Slice the first three channels so that we remove any alpha channels. + hsv = color.rgb2hsv(image[:, :, :3]) + value = hsv[:, :, 2].copy() + value = image_filter(value, *args, **kwargs) + hsv[:, :, 2] = convert(value, hsv.dtype) + return color.hsv2rgb(hsv) + + +def each_channel(image_filter, image, *args, **kwargs): + """Return color image by applying `image_filter` on channels of `image`. + + Note that this function is intended for use with `adapt_rgb`. + + Parameters + ---------- + image_filter : function + Function that filters a gray-scale image. + image : array + Input image. + """ + c_new = [image_filter(c, *args, **kwargs) for c in image.T] + return np.array(c_new).T diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 80f115ee..74a9a520 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -53,10 +53,10 @@ References from __future__ import division +from warnings import warn import numpy as np from scipy import linalg from ..util import dtype -from skimage._shared.utils import deprecated def guess_spatial_dimensions(image): @@ -114,13 +114,14 @@ 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 -------- >>> from skimage import data - >>> lena = data.lena() - >>> lena_hsv = convert_colorspace(lena, 'RGB', 'HSV') + >>> img = data.astronaut() + >>> img_hsv = convert_colorspace(img, 'RGB', 'HSV') """ fromdict = {'RGB': lambda im: im, 'HSV': hsv2rgb, 'RGB CIE': rgbcie2rgb, 'XYZ': xyz2rgb} @@ -129,9 +130,9 @@ def convert_colorspace(arr, fromspace, tospace): fromspace = fromspace.upper() tospace = tospace.upper() - if not fromspace in fromdict.keys(): + if fromspace not in fromdict.keys(): raise ValueError('fromspace needs to be one of %s' % fromdict.keys()) - if not tospace in todict.keys(): + if tospace not in todict.keys(): raise ValueError('tospace needs to be one of %s' % todict.keys()) return todict[tospace](fromdict[fromspace](arr)) @@ -186,8 +187,8 @@ def rgb2hsv(rgb): -------- >>> from skimage import color >>> from skimage import data - >>> lena = data.lena() - >>> lena_hsv = color.rgb2hsv(lena) + >>> img = data.astronaut() + >>> img_hsv = color.rgb2hsv(img) """ arr = _prepare_colorarray(rgb) out = np.empty_like(arr) @@ -263,9 +264,9 @@ def hsv2rgb(hsv): Examples -------- >>> from skimage import data - >>> lena = data.lena() - >>> lena_hsv = rgb2hsv(lena) - >>> lena_rgb = hsv2rgb(lena_hsv) + >>> img = data.astronaut() + >>> img_hsv = rgb2hsv(img) + >>> img_rgb = hsv2rgb(img_hsv) """ arr = _prepare_colorarray(hsv) @@ -287,20 +288,20 @@ def hsv2rgb(hsv): return out -#--------------------------------------------------------------- +# --------------------------------------------------------------- # Primaries for the coordinate systems -#--------------------------------------------------------------- +# --------------------------------------------------------------- cie_primaries = np.array([700, 546.1, 435.8]) sb_primaries = np.array([1. / 155, 1. / 190, 1. / 225]) * 1e5 -#--------------------------------------------------------------- +# --------------------------------------------------------------- # Matrices that define conversion between different color spaces -#--------------------------------------------------------------- +# --------------------------------------------------------------- # From sRGB specification xyz_from_rgb = np.array([[0.412453, 0.357580, 0.180423], - [0.212671, 0.715160, 0.072169], - [0.019334, 0.119193, 0.950227]]) + [0.212671, 0.715160, 0.072169], + [0.019334, 0.119193, 0.950227]]) rgb_from_xyz = linalg.inv(xyz_from_rgb) @@ -321,9 +322,81 @@ gray_from_rgb = np.array([[0.2125, 0.7154, 0.0721], [0, 0, 0], [0, 0, 0]]) -# CIE LAB constants for Observer= 2A, Illuminant= D65 +# CIE LAB constants for Observer=2A, Illuminant=D65 +# NOTE: this is actually the XYZ values for the illuminant above. lab_ref_white = np.array([0.95047, 1., 1.08883]) +# XYZ coordinates of the illuminants, scaled to [0, 1]. For each illuminant I +# we have: +# +# illuminant[I][0] corresponds to the XYZ coordinates for the 2 degree +# field of view. +# +# illuminant[I][1] corresponds to the XYZ coordinates for the 10 degree +# field of view. +# +# The XYZ coordinates are calculated from [1], using the formula: +# +# X = x * ( Y / y ) +# Y = Y +# Z = ( 1 - x - y ) * ( Y / y ) +# +# where Y = 1. The only exception is the illuminant "D65" with aperture angle +# 2, whose coordinates are copied from 'lab_ref_white' for +# backward-compatibility reasons. +# +# References +# ---------- +# .. [1] http://en.wikipedia.org/wiki/Standard_illuminant + +illuminants = \ + {"A": {'2': (1.098466069456375, 1, 0.3558228003436005), + '10': (1.111420406956693, 1, 0.3519978321919493)}, + "D50": {'2': (0.9642119944211994, 1, 0.8251882845188288), + '10': (0.9672062750333777, 1, 0.8142801513128616)}, + "D55": {'2': (0.956797052643698, 1, 0.9214805860173273), + '10': (0.9579665682254781, 1, 0.9092525159847462)}, + "D65": {'2': (0.95047, 1., 1.08883), # This was: `lab_ref_white` + '10': (0.94809667673716, 1, 1.0730513595166162)}, + "D75": {'2': (0.9497220898840717, 1, 1.226393520724154), + '10': (0.9441713925645873, 1, 1.2064272211720228)}, + "E": {'2': (1.0, 1.0, 1.0), + '10': (1.0, 1.0, 1.0)}} + + +def get_xyz_coords(illuminant, observer): + """Get the XYZ coordinates of the given illuminant and observer [1]_. + + Parameters + ---------- + illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10"}, optional + The aperture angle of the observer. + + Returns + ------- + (x, y, z) : tuple + A tuple with 3 elements containing the XYZ coordinates of the given + illuminant. + + Raises + ------ + ValueError + If either the illuminant or the observer angle are not supported or + unknown. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Standard_illuminant + + """ + illuminant = illuminant.upper() + try: + return illuminants[illuminant][observer] + except KeyError: + raise ValueError("Unknown illuminant/observer combination\ + (\'{0}\', \'{1}\')".format(illuminant, observer)) # Haematoxylin-Eosin-DAB colorspace # From original Ruifrok's paper: A. C. Ruifrok and D. A. Johnston, @@ -408,9 +481,9 @@ rgb_from_hpx = np.array([[0.644211, 0.716556, 0.266844], rgb_from_hpx[2, :] = np.cross(rgb_from_hpx[0, :], rgb_from_hpx[1, :]) hpx_from_rgb = linalg.inv(rgb_from_hpx) -#------------------------------------------------------------- +# ------------------------------------------------------------- # The conversion functions that make use of the matrices above -#------------------------------------------------------------- +# ------------------------------------------------------------- def _convert(matrix, arr): @@ -470,9 +543,9 @@ def xyz2rgb(xyz): -------- >>> from skimage import data >>> from skimage.color import rgb2xyz, xyz2rgb - >>> lena = data.lena() - >>> lena_xyz = rgb2xyz(lena) - >>> lena_rgb = xyz2rgb(lena_xyz) + >>> img = data.astronaut() + >>> img_xyz = rgb2xyz(img) + >>> img_rgb = xyz2rgb(img_xyz) """ # Follow the algorithm from http://www.easyrgb.com/index.php # except we don't multiply/divide by 100 in the conversion @@ -480,6 +553,8 @@ def xyz2rgb(xyz): mask = arr > 0.0031308 arr[mask] = 1.055 * np.power(arr[mask], 1 / 2.4) - 0.055 arr[~mask] *= 12.92 + arr[arr < 0] = 0 + arr[arr > 1] = 1 return arr @@ -515,8 +590,8 @@ def rgb2xyz(rgb): Examples -------- >>> from skimage import data - >>> lena = data.lena() - >>> lena_xyz = rgb2xyz(lena) + >>> img = data.astronaut() + >>> img_xyz = rgb2xyz(img) """ # Follow the algorithm from http://www.easyrgb.com/index.php # except we don't multiply/divide by 100 in the conversion @@ -553,8 +628,8 @@ def rgb2rgbcie(rgb): -------- >>> from skimage import data >>> from skimage.color import rgb2rgbcie - >>> lena = data.lena() - >>> lena_rgbcie = rgb2rgbcie(lena) + >>> img = data.astronaut() + >>> img_rgbcie = rgb2rgbcie(img) """ return _convert(rgbcie_from_rgb, rgb) @@ -585,9 +660,9 @@ def rgbcie2rgb(rgbcie): -------- >>> from skimage import data >>> from skimage.color import rgb2rgbcie, rgbcie2rgb - >>> lena = data.lena() - >>> lena_rgbcie = rgb2rgbcie(lena) - >>> lena_rgb = rgbcie2rgb(lena_rgbcie) + >>> img = data.astronaut() + >>> img_rgbcie = rgb2rgbcie(img) + >>> img_rgb = rgbcie2rgb(img_rgbcie) """ return _convert(rgb_from_rgbcie, rgbcie) @@ -629,8 +704,8 @@ def rgb2gray(rgb): -------- >>> from skimage.color import rgb2gray >>> from skimage import data - >>> lena = data.lena() - >>> lena_gray = rgb2gray(lena) + >>> img = data.astronaut() + >>> img_gray = rgb2gray(img) """ if rgb.ndim == 2: return rgb @@ -668,7 +743,7 @@ def gray2rgb(image): raise ValueError("Input image expected to be RGB, RGBA or gray.") -def xyz2lab(xyz): +def xyz2lab(xyz, illuminant="D65", observer="2"): """XYZ to CIE-LAB color space conversion. Parameters @@ -676,6 +751,10 @@ def xyz2lab(xyz): xyz : array_like The image in XYZ format, in a 3- or 4-D array of shape ``(.., ..,[ ..,] 3)``. + illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10"}, optional + The aperture angle of the observer. Returns ------- @@ -687,11 +766,15 @@ def xyz2lab(xyz): ------ ValueError If `xyz` is not a 3-D array of shape ``(.., ..,[ ..,] 3)``. + ValueError + If either the illuminant or the observer angle is unsupported or + unknown. Notes ----- - Observer= 2A, Illuminant= D65 - CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 + By default Observer= 2A, Illuminant= D65. CIE XYZ tristimulus values + x_ref=95.047, y_ref=100., z_ref=108.883. See function `get_xyz_coords` for + a list of supported illuminants. References ---------- @@ -702,14 +785,16 @@ def xyz2lab(xyz): -------- >>> from skimage import data >>> from skimage.color import rgb2xyz, xyz2lab - >>> lena = data.lena() - >>> lena_xyz = rgb2xyz(lena) - >>> lena_lab = xyz2lab(lena_xyz) + >>> img = data.astronaut() + >>> img_xyz = rgb2xyz(img) + >>> img_lab = xyz2lab(img_xyz) """ arr = _prepare_colorarray(xyz) + xyz_ref_white = get_xyz_coords(illuminant, observer) + # scale by CIE XYZ tristimulus values of the reference white point - arr = arr / lab_ref_white + arr = arr / xyz_ref_white # Nonlinear distortion and linear transformation mask = arr > 0.008856 @@ -726,13 +811,17 @@ def xyz2lab(xyz): return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1) -def lab2xyz(lab): +def lab2xyz(lab, illuminant="D65", observer="2"): """CIE-LAB to XYZcolor space conversion. Parameters ---------- lab : array_like The image in lab format, in a 3-D array of shape ``(.., .., 3)``. + illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10"}, optional + The aperture angle of the observer. Returns ------- @@ -743,11 +832,18 @@ def lab2xyz(lab): ------ ValueError If `lab` is not a 3-D array of shape ``(.., .., 3)``. + ValueError + If either the illuminant or the observer angle are not supported or + unknown. + UserWarning + If any of the pixels are invalid (Z < 0). + Notes ----- - Observer = 2A, Illuminant = D65 - CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 + By default Observer= 2A, Illuminant= D65. CIE XYZ tristimulus values x_ref + = 95.047, y_ref = 100., z_ref = 108.883. See function 'get_xyz_coords' for + a list of supported illuminants. References ---------- @@ -763,14 +859,20 @@ def lab2xyz(lab): x = (a / 500.) + y z = y - (b / 200.) + if np.any(z < 0): + invalid = np.nonzero(z < 0) + warn('Color data out of range: Z < 0 in %s pixels' % invalid[0].size) + z[invalid] = 0 + out = np.dstack([x, y, z]) mask = out > 0.2068966 out[mask] = np.power(out[mask], 3.) out[~mask] = (out[~mask] - 16.0 / 116.) / 7.787 - # rescale Observer= 2 deg, Illuminant= D65 - out *= lab_ref_white + # rescale to the reference white (illuminant) + xyz_ref_white = get_xyz_coords(illuminant, observer) + out *= xyz_ref_white return out @@ -826,7 +928,7 @@ def lab2rgb(lab): return xyz2rgb(lab2xyz(lab)) -def xyz2luv(xyz): +def xyz2luv(xyz, illuminant="D65", observer="2"): """XYZ to CIE-Luv color space conversion. Parameters @@ -834,6 +936,10 @@ def xyz2luv(xyz): xyz : (M, N, [P,] 3) array_like The 3 or 4 dimensional image in XYZ format. Final dimension denotes channels. + illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10"}, optional + The aperture angle of the observer. Returns ------- @@ -844,11 +950,16 @@ def xyz2luv(xyz): ------ ValueError If `xyz` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + ValueError + If either the illuminant or the observer angle are not supported or + unknown. Notes ----- - XYZ conversion weights use Observer = 2A. Reference whitepoint for D65 - Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. + By default XYZ conversion weights use observer=2A. Reference whitepoint + for D65 Illuminant, with XYZ tristimulus values of ``(95.047, 100., + 108.883)``. See function 'get_xyz_coords' for a list of supported + illuminants. References ---------- @@ -859,9 +970,9 @@ def xyz2luv(xyz): -------- >>> from skimage import data >>> from skimage.color import rgb2xyz, xyz2luv - >>> lena = data.lena() - >>> lena_xyz = rgb2xyz(lena) - >>> lena_luv = xyz2luv(lena_xyz) + >>> img = data.astronaut() + >>> img_xyz = rgb2xyz(img) + >>> img_luv = xyz2luv(img_xyz) """ arr = _prepare_colorarray(xyz) @@ -871,29 +982,30 @@ def xyz2luv(xyz): eps = np.finfo(np.float).eps # compute y_r and L - L = y / lab_ref_white[1] + xyz_ref_white = get_xyz_coords(illuminant, observer) + L = y / xyz_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) + u0 = 4 * xyz_ref_white[0] / np.dot([1, 15, 3], xyz_ref_white) + v0 = 9 * xyz_ref_white[1] / np.dot([1, 15, 3], xyz_ref_white) # u' and v' helper functions def fu(X, Y, Z): - return (4.*X) / (X + 15.*Y + 3.*Z + eps) + return (4. * X) / (X + 15. * Y + 3. * Z + eps) def fv(X, Y, Z): - return (9.*Y) / (X + 15.*Y + 3.*Z + eps) + 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) + 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): +def luv2xyz(luv, illuminant="D65", observer="2"): """CIE-Luv to XYZ color space conversion. Parameters @@ -901,6 +1013,10 @@ def luv2xyz(luv): luv : (M, N, [P,] 3) array_like The 3 or 4 dimensional image in CIE-Luv format. Final dimension denotes channels. + illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10"}, optional + The aperture angle of the observer. Returns ------- @@ -911,11 +1027,15 @@ def luv2xyz(luv): ------ ValueError If `luv` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + ValueError + If either the illuminant or the observer angle are not supported or + unknown. Notes ----- - XYZ conversion weights use Observer = 2A. Reference whitepoint for D65 - Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. + XYZ conversion weights use observer=2A. Reference whitepoint for D65 + Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. See + function 'get_xyz_coords' for a list of supported illuminants. References ---------- @@ -933,23 +1053,24 @@ def luv2xyz(luv): # compute y y = L.copy() mask = y > 7.999625 - y[mask] = np.power((y[mask]+16.) / 116., 3.) + y[mask] = np.power((y[mask] + 16.) / 116., 3.) y[~mask] = y[~mask] / 903.3 - y *= lab_ref_white[1] + xyz_ref_white = get_xyz_coords(illuminant, observer) + y *= xyz_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) + u0 = 4 * xyz_ref_white[0] / np.dot(uv_weights, xyz_ref_white) + v0 = 9 * xyz_ref_white[1] / np.dot(uv_weights, xyz_ref_white) # compute intermediate values - a = u0 + u / (13.*L + eps) - b = v0 + v / (13.*L + eps) - c = 3*y * (5*b-3) + 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) + 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) @@ -1048,7 +1169,8 @@ 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 ------- @@ -1091,7 +1213,8 @@ def separate_stains(rgb, conv_matrix): 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 ------ @@ -1127,7 +1250,8 @@ def separate_stains(rgb, conv_matrix): >>> ihc = data.immunohistochemistry() >>> ihc_hdx = separate_stains(ihc, hdx_from_rgb) """ - rgb = dtype.img_as_float(rgb) + 2 + rgb = dtype.img_as_float(rgb, force_copy=True) + rgb += 2 stains = np.dot(np.reshape(-np.log(rgb), (-1, 3)), conv_matrix) return np.reshape(stains, rgb.shape) @@ -1138,7 +1262,8 @@ 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]_. @@ -1189,7 +1314,8 @@ def combine_stains(stains, conv_matrix): stains = dtype.img_as_float(stains) logrgb2 = np.dot(-np.reshape(stains, (-1, 3)), conv_matrix) rgb2 = np.exp(logrgb2) - return rescale_intensity(np.reshape(rgb2 - 2, stains.shape), in_range=(-1, 1)) + return rescale_intensity(np.reshape(rgb2 - 2, stains.shape), + in_range=(-1, 1)) def lab2lch(lab): @@ -1222,9 +1348,9 @@ def lab2lch(lab): -------- >>> from skimage import data >>> from skimage.color import rgb2lab, lab2lch - >>> lena = data.lena() - >>> lena_lab = rgb2lab(lena) - >>> lena_lch = lab2lch(lena_lab) + >>> img = data.astronaut() + >>> img_lab = rgb2lab(img) + >>> img_lch = lab2lch(img_lab) """ lch = _prepare_lab_array(lab) @@ -1234,7 +1360,7 @@ def lab2lch(lab): def _cart2polar_2pi(x, y): - """convert cartesian coordiantes to polar (uses non-standard theta range!) + """convert cartesian coordinates to polar (uses non-standard theta range!) NON-STANDARD RANGE! Maps to ``(0, 2*pi)`` rather than usual ``(-pi, +pi)`` """ @@ -1269,10 +1395,10 @@ def lch2lab(lch): -------- >>> from skimage import data >>> from skimage.color import rgb2lab, lch2lab - >>> lena = data.lena() - >>> lena_lab = rgb2lab(lena) - >>> lena_lch = lab2lch(lena_lab) - >>> lena_lab2 = lch2lab(lena_lch) + >>> img = data.astronaut() + >>> img_lab = rgb2lab(img) + >>> img_lch = lab2lch(img_lab) + >>> img_lab2 = lch2lab(img_lch) """ lch = _prepare_lab_array(lch) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index ed74f3a8..76cdd6f2 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -3,9 +3,9 @@ import itertools import numpy as np -from skimage import img_as_float -from .colorconv import rgb2gray, gray2rgb +from .. import img_as_float from . import rgb_colors +from .colorconv import rgb2gray, gray2rgb import six from six.moves import zip @@ -64,28 +64,76 @@ def _match_label_with_color(label, colors, bg_label, bg_color): def label2rgb(label, image=None, colors=None, alpha=0.3, - bg_label=-1, bg_color=None, image_alpha=1): + bg_label=-1, bg_color=(0, 0, 0), image_alpha=1, kind='overlay'): """Return an RGB image where color-coded labels are painted over the image. Parameters ---------- - label : array + label : array, shape (M, N) Integer array of labels with the same shape as `image`. - image : array + image : array, shape (M, N, 3), optional Image used as underlay for labels. If the input is an RGB image, it's converted to grayscale before coloring. - colors : list + colors : list, optional List of colors. If the number of labels exceeds the number of colors, then the colors are cycled. - alpha : float [0, 1] + alpha : float [0, 1], optional Opacity of colorized labels. Ignored if image is `None`. - bg_label : int + bg_label : int, optional Label that's treated as the background. - bg_color : str or array + bg_color : str or array, optional Background color. Must be a name in `color_dict` or RGB float values between [0, 1]. - image_alpha : float [0, 1] + image_alpha : float [0, 1], optional Opacity of the image. + kind : string, one of {'overlay', 'avg'} + The kind of color image desired. 'overlay' cycles over defined colors + and overlays the colored labels over the original image. 'avg' replaces + each labeled segment with its average color, for a stained-class or + pastel painting appearance. + + Returns + ------- + result : array of float, shape (M, N, 3) + The result of blending a cycling colormap (`colors`) for each distinct + value in `label` with the image, at a certain alpha value. + """ + if kind == 'overlay': + return _label2rgb_overlay(label, image, colors, alpha, bg_label, + bg_color, image_alpha) + else: + return _label2rgb_avg(label, image, bg_label, bg_color) + + +def _label2rgb_overlay(label, image=None, colors=None, alpha=0.3, + bg_label=-1, bg_color=None, image_alpha=1): + """Return an RGB image where color-coded labels are painted over the image. + + Parameters + ---------- + label : array, shape (M, N) + Integer array of labels with the same shape as `image`. + image : array, shape (M, N, 3), optional + Image used as underlay for labels. If the input is an RGB image, it's + converted to grayscale before coloring. + colors : list, optional + List of colors. If the number of labels exceeds the number of colors, + then the colors are cycled. + alpha : float [0, 1], optional + Opacity of colorized labels. Ignored if image is `None`. + bg_label : int, optional + Label that's treated as the background. + bg_color : str or array, optional + Background color. Must be a name in `color_dict` or RGB float values + between [0, 1]. + image_alpha : float [0, 1], optional + Opacity of the image. + + Returns + ------- + result : array of float, shape (M, N, 3) + The result of blending a cycling colormap (`colors`) for each distinct + value in `label` with the image, at a certain alpha value. """ if colors is None: colors = DEFAULT_COLORS @@ -134,3 +182,35 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, result[label == bg_label] = image[label == bg_label] return result + + +def _label2rgb_avg(label_field, image, bg_label=0, bg_color=(0, 0, 0)): + """Visualise each segment in `label_field` with its mean color in `image`. + + Parameters + ---------- + label_field : array of int + A segmentation of an image. + image : array, shape ``label_field.shape + (3,)`` + A color image of the same spatial shape as `label_field`. + bg_label : int, optional + A value in `label_field` to be treated as background. + bg_color : 3-tuple of int, optional + The color for the background label + + Returns + ------- + out : array, same shape and type as `image` + The output visualization. + """ + out = np.zeros_like(image) + labels = np.unique(label_field) + bg = (labels == bg_label) + if bg.any(): + labels = labels[labels != bg_label] + out[bg] = bg_color + for label in labels: + mask = (label_field == label).nonzero() + color = image[mask].mean(axis=0) + out[mask] = color + return out diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 9119ecf4..0511e7a9 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -21,7 +21,7 @@ from __future__ import division import numpy as np -from skimage.color.colorconv import lab2lch, _cart2polar_2pi +from ..color.colorconv import lab2lch, _cart2polar_2pi def deltaE_cie76(lab1, lab2): @@ -336,4 +336,4 @@ def get_dH2(lab1, lab2): C2 = np.hypot(a2, b2) term = (C1 * C2) - (a1 * a2 + b1 * b2) - return 2*term + return 2 * term diff --git a/skimage/color/tests/__init__.py b/skimage/color/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/color/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/color/tests/data/lab_array_a_2.npy b/skimage/color/tests/data/lab_array_a_2.npy new file mode 100644 index 00000000..cd1b81d1 Binary files /dev/null and b/skimage/color/tests/data/lab_array_a_2.npy differ diff --git a/skimage/color/tests/data/lab_array_d50_10.npy b/skimage/color/tests/data/lab_array_d50_10.npy new file mode 100644 index 00000000..7eea857a Binary files /dev/null and b/skimage/color/tests/data/lab_array_d50_10.npy differ diff --git a/skimage/color/tests/data/lab_array_d50_2.npy b/skimage/color/tests/data/lab_array_d50_2.npy new file mode 100644 index 00000000..61270a05 Binary files /dev/null and b/skimage/color/tests/data/lab_array_d50_2.npy differ diff --git a/skimage/color/tests/data/lab_array_d55_10.npy b/skimage/color/tests/data/lab_array_d55_10.npy new file mode 100644 index 00000000..566bc110 Binary files /dev/null and b/skimage/color/tests/data/lab_array_d55_10.npy differ diff --git a/skimage/color/tests/data/lab_array_d55_2.npy b/skimage/color/tests/data/lab_array_d55_2.npy new file mode 100644 index 00000000..87a309b9 Binary files /dev/null and b/skimage/color/tests/data/lab_array_d55_2.npy differ diff --git a/skimage/color/tests/data/lab_array_d65_10.npy b/skimage/color/tests/data/lab_array_d65_10.npy new file mode 100644 index 00000000..4ac9c0ae Binary files /dev/null and b/skimage/color/tests/data/lab_array_d65_10.npy differ diff --git a/skimage/color/tests/data/lab_array_d65_2.npy b/skimage/color/tests/data/lab_array_d65_2.npy new file mode 100644 index 00000000..71e233d1 Binary files /dev/null and b/skimage/color/tests/data/lab_array_d65_2.npy differ diff --git a/skimage/color/tests/data/lab_array_d75_10.npy b/skimage/color/tests/data/lab_array_d75_10.npy new file mode 100644 index 00000000..642ccaf6 Binary files /dev/null and b/skimage/color/tests/data/lab_array_d75_10.npy differ diff --git a/skimage/color/tests/data/lab_array_d75_2.npy b/skimage/color/tests/data/lab_array_d75_2.npy new file mode 100644 index 00000000..1ef36855 Binary files /dev/null and b/skimage/color/tests/data/lab_array_d75_2.npy differ diff --git a/skimage/color/tests/data/lab_array_e_2.npy b/skimage/color/tests/data/lab_array_e_2.npy new file mode 100644 index 00000000..f795d3be Binary files /dev/null and b/skimage/color/tests/data/lab_array_e_2.npy differ diff --git a/skimage/color/tests/data/luv_array_a_2.npy b/skimage/color/tests/data/luv_array_a_2.npy new file mode 100644 index 00000000..ae25c119 Binary files /dev/null and b/skimage/color/tests/data/luv_array_a_2.npy differ diff --git a/skimage/color/tests/data/luv_array_d50_10.npy b/skimage/color/tests/data/luv_array_d50_10.npy new file mode 100644 index 00000000..cf05a2ad Binary files /dev/null and b/skimage/color/tests/data/luv_array_d50_10.npy differ diff --git a/skimage/color/tests/data/luv_array_d50_2.npy b/skimage/color/tests/data/luv_array_d50_2.npy new file mode 100644 index 00000000..8f3711f4 Binary files /dev/null and b/skimage/color/tests/data/luv_array_d50_2.npy differ diff --git a/skimage/color/tests/data/luv_array_d55_10.npy b/skimage/color/tests/data/luv_array_d55_10.npy new file mode 100644 index 00000000..b2ce26ac Binary files /dev/null and b/skimage/color/tests/data/luv_array_d55_10.npy differ diff --git a/skimage/color/tests/data/luv_array_d55_2.npy b/skimage/color/tests/data/luv_array_d55_2.npy new file mode 100644 index 00000000..88daa5db Binary files /dev/null and b/skimage/color/tests/data/luv_array_d55_2.npy differ diff --git a/skimage/color/tests/data/luv_array_d65_10.npy b/skimage/color/tests/data/luv_array_d65_10.npy new file mode 100644 index 00000000..43ce45ab Binary files /dev/null and b/skimage/color/tests/data/luv_array_d65_10.npy differ diff --git a/skimage/color/tests/data/luv_array_d65_2.npy b/skimage/color/tests/data/luv_array_d65_2.npy new file mode 100644 index 00000000..75ed4792 Binary files /dev/null and b/skimage/color/tests/data/luv_array_d65_2.npy differ diff --git a/skimage/color/tests/data/luv_array_d75_10.npy b/skimage/color/tests/data/luv_array_d75_10.npy new file mode 100644 index 00000000..a04d6965 Binary files /dev/null and b/skimage/color/tests/data/luv_array_d75_10.npy differ diff --git a/skimage/color/tests/data/luv_array_d75_2.npy b/skimage/color/tests/data/luv_array_d75_2.npy new file mode 100644 index 00000000..a2ef298c Binary files /dev/null and b/skimage/color/tests/data/luv_array_d75_2.npy differ diff --git a/skimage/color/tests/data/luv_array_e_2.npy b/skimage/color/tests/data/luv_array_e_2.npy new file mode 100644 index 00000000..f2f4c280 Binary files /dev/null and b/skimage/color/tests/data/luv_array_e_2.npy differ diff --git a/skimage/color/tests/test_adapt_rgb.py b/skimage/color/tests/test_adapt_rgb.py new file mode 100644 index 00000000..9e97b6b6 --- /dev/null +++ b/skimage/color/tests/test_adapt_rgb.py @@ -0,0 +1,84 @@ +from functools import partial + +import numpy as np + +from skimage import img_as_float, img_as_uint +from skimage import color, data, filters +from skimage.color.adapt_rgb import adapt_rgb, each_channel, hsv_value +from skimage._shared._warnings import expected_warnings + +# Down-sample image for quicker testing. +COLOR_IMAGE = data.astronaut()[::5, ::5] +GRAY_IMAGE = data.camera()[::5, ::5] + +SIGMA = 3 +smooth = partial(filters.gaussian_filter, sigma=SIGMA) +assert_allclose = partial(np.testing.assert_allclose, atol=1e-8) + + +@adapt_rgb(each_channel) +def edges_each(image): + return filters.sobel(image) + + +@adapt_rgb(each_channel) +def smooth_each(image, sigma): + return filters.gaussian_filter(image, sigma) + + +@adapt_rgb(hsv_value) +def edges_hsv(image): + return filters.sobel(image) + + +@adapt_rgb(hsv_value) +def smooth_hsv(image, sigma): + return filters.gaussian_filter(image, sigma) + + +@adapt_rgb(hsv_value) +def edges_hsv_uint(image): + with expected_warnings(['precision loss']): + return img_as_uint(filters.sobel(image)) + + +def test_gray_scale_image(): + # We don't need to test both `hsv_value` and `each_channel` since + # `adapt_rgb` is handling gray-scale inputs. + assert_allclose(edges_each(GRAY_IMAGE), filters.sobel(GRAY_IMAGE)) + + +def test_each_channel(): + filtered = edges_each(COLOR_IMAGE) + for i, channel in enumerate(np.rollaxis(filtered, axis=-1)): + expected = img_as_float(filters.sobel(COLOR_IMAGE[:, :, i])) + assert_allclose(channel, expected) + + +def test_each_channel_with_filter_argument(): + filtered = smooth_each(COLOR_IMAGE, SIGMA) + for i, channel in enumerate(np.rollaxis(filtered, axis=-1)): + assert_allclose(channel, smooth(COLOR_IMAGE[:, :, i])) + + +def test_hsv_value(): + filtered = edges_hsv(COLOR_IMAGE) + value = color.rgb2hsv(COLOR_IMAGE)[:, :, 2] + assert_allclose(color.rgb2hsv(filtered)[:, :, 2], filters.sobel(value)) + + +def test_hsv_value_with_filter_argument(): + filtered = smooth_hsv(COLOR_IMAGE, SIGMA) + value = color.rgb2hsv(COLOR_IMAGE)[:, :, 2] + assert_allclose(color.rgb2hsv(filtered)[:, :, 2], smooth(value)) + + +def test_hsv_value_with_non_float_output(): + # Since `rgb2hsv` returns a float image and the result of the filtered + # result is inserted into the HSV image, we want to make sure there isn't + # a dtype mismatch. + filtered = edges_hsv_uint(COLOR_IMAGE) + filtered_value = color.rgb2hsv(filtered)[:, :, 2] + value = color.rgb2hsv(COLOR_IMAGE)[:, :, 2] + # Reduce tolerance because dtype conversion. + assert_allclose(filtered_value, filters.sobel(value), rtol=1e-5, atol=1e-5) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 4bc01e78..c9febb5f 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -11,6 +11,7 @@ Authors :license: modified BSD """ +from __future__ import division import os.path import numpy as np @@ -39,7 +40,8 @@ from skimage.color import (rgb2hsv, hsv2rgb, guess_spatial_dimensions ) -from skimage import data_dir, data +from skimage import data_dir +from skimage._shared._warnings import expected_warnings import colorsys @@ -69,24 +71,24 @@ class TestColorconv(TestCase): colbars_point75 = colbars * 0.75 colbars_point75_array = np.swapaxes(colbars_point75.reshape(3, 4, 2), 0, 2) - xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], # red - [[0, 0, 0]], # black - [[.9505, 1., 1.089]], # white - [[.1805, .0722, .9505]], # blue - [[.07719, .15438, .02573]], # green + xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], # red + [[0, 0, 0]], # black + [[.9505, 1., 1.089]], # white + [[.1805, .0722, .9505]], # blue + [[.07719, .15438, .02573]], # green ]) - lab_array = np.array([[[53.233, 80.109, 67.220]], # red - [[0., 0., 0.]], # black - [[100.0, 0.005, -0.010]], # white - [[32.303, 79.197, -107.864]], # blue - [[46.229, -51.7, 49.898]], # green + 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 ]) - 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 + 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 @@ -154,7 +156,9 @@ class TestColorconv(TestCase): # RGB<->HED roundtrip with ubyte image def test_hed_rgb_roundtrip(self): img_rgb = img_as_ubyte(self.img_rgb) - assert_equal(img_as_ubyte(hed2rgb(rgb2hed(img_rgb))), img_rgb) + with expected_warnings(['precision loss']): + new = img_as_ubyte(hed2rgb(rgb2hed(img_rgb))) + assert_equal(new, img_rgb) # RGB<->HED roundtrip with float image def test_hed_rgb_float_roundtrip(self): @@ -223,18 +227,62 @@ class TestColorconv(TestCase): assert_equal(g.shape, (1, 1)) def test_rgb2grey_on_grey(self): - rgb2grey(np.random.random((5, 5))) + rgb2grey(np.random.rand(5, 5)) - # test matrices for xyz2lab and lab2xyz generated using http://www.easyrgb.com/index.php?X=CALC + # test matrices for xyz2lab and lab2xyz generated using + # http://www.easyrgb.com/index.php?X=CALC # Note: easyrgb website displays xyz*100 def test_xyz2lab(self): assert_array_almost_equal(xyz2lab(self.xyz_array), self.lab_array, decimal=3) + # Test the conversion with the rest of the illuminants. + for I in ["d50", "d55", "d65", "d75"]: + for obs in ["2", "10"]: + fname = "lab_array_{0}_{1}.npy".format(I, obs) + lab_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) + assert_array_almost_equal(lab_array_I_obs, + xyz2lab(self.xyz_array, I, obs), + decimal=2) + for I in ["a", "e"]: + fname = "lab_array_{0}_2.npy".format(I) + lab_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) + assert_array_almost_equal(lab_array_I_obs, + xyz2lab(self.xyz_array, I, "2"), + decimal=2) + def test_lab2xyz(self): assert_array_almost_equal(lab2xyz(self.lab_array), self.xyz_array, decimal=3) + # Test the conversion with the rest of the illuminants. + for I in ["d50", "d55", "d65", "d75"]: + for obs in ["2", "10"]: + fname = "lab_array_{0}_{1}.npy".format(I, obs) + lab_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) + assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, obs), + self.xyz_array, decimal=3) + for I in ["a", "e"]: + fname = "lab_array_{0}_2.npy".format(I, obs) + lab_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) + assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, "2"), + self.xyz_array, decimal=3) + + # And we include a call to test the exception handling in the code. + try: + xs = lab2xyz(lab_array_I_obs, "NaI", "2") # Not an illuminant + except ValueError: + pass + + try: + xs = lab2xyz(lab_array_I_obs, "d50", "42") # Not a degree + except ValueError: + pass + def test_rgb2lab_brucelindbloom(self): """ Test the RGB->Lab conversion by comparing to the calculator on the @@ -265,10 +313,42 @@ class TestColorconv(TestCase): assert_array_almost_equal(xyz2luv(self.xyz_array), self.luv_array, decimal=3) + # Test the conversion with the rest of the illuminants. + for I in ["d50", "d55", "d65", "d75"]: + for obs in ["2", "10"]: + fname = "luv_array_{0}_{1}.npy".format(I, obs) + luv_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) + assert_array_almost_equal(luv_array_I_obs, + xyz2luv(self.xyz_array, I, obs), + decimal=2) + for I in ["a", "e"]: + fname = "luv_array_{0}_2.npy".format(I) + luv_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) + assert_array_almost_equal(luv_array_I_obs, + xyz2luv(self.xyz_array, I, "2"), + decimal=2) + def test_luv2xyz(self): assert_array_almost_equal(luv2xyz(self.luv_array), self.xyz_array, decimal=3) + # Test the conversion with the rest of the illuminants. + for I in ["d50", "d55", "d65", "d75"]: + for obs in ["2", "10"]: + fname = "luv_array_{0}_{1}.npy".format(I, obs) + luv_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) + assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, obs), + self.xyz_array, decimal=3) + for I in ["a", "e"]: + fname = "luv_array_{0}_2.npy".format(I, obs) + luv_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) + assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, "2"), + self.xyz_array, decimal=3) + def test_rgb2luv_brucelindbloom(self): """ Test the RGB->Lab conversion by comparing to the calculator on the @@ -293,6 +373,26 @@ class TestColorconv(TestCase): img_rgb = img_as_float(self.img_rgb) assert_array_almost_equal(luv2rgb(rgb2luv(img_rgb)), img_rgb) + def test_lab_rgb_outlier(self): + lab_array = np.ones((3, 1, 3)) + lab_array[0] = [50, -12, 85] + lab_array[1] = [50, 12, -85] + lab_array[2] = [90, -4, -47] + rgb_array = np.array([[[0.501, 0.481, 0]], + [[0, 0.482, 1.]], + [[0.578, 0.914, 1.]], + ]) + assert_almost_equal(lab2rgb(lab_array), rgb_array, decimal=3) + + def test_lab_full_gamut(self): + a, b = np.meshgrid(np.arange(-100, 100), np.arange(-100, 100)) + L = np.ones(a.shape) + lab = np.dstack((L, a, b)) + for value in [0, 10, 20]: + lab[:, :, 0] = value + with expected_warnings(['Color data out of range']): + lab2xyz(lab) + def test_lab_lch_roundtrip(self): rgb = img_as_float(self.img_rgb) lab = rgb2lab(rgb) @@ -349,7 +449,7 @@ def test_gray2rgb(): def test_gray2rgb_rgb(): - x = np.random.random((5, 5, 4)) + x = np.random.rand(5, 5, 4) y = gray2rgb(x) assert_equal(x, y) diff --git a/skimage/color/tests/test_colorlabel.py b/skimage/color/tests/test_colorlabel.py index dcfbe4ea..8285a4dc 100644 --- a/skimage/color/tests/test_colorlabel.py +++ b/skimage/color/tests/test_colorlabel.py @@ -3,8 +3,9 @@ import itertools import numpy as np from numpy import testing from skimage.color.colorlabel import label2rgb +from skimage._shared._warnings import expected_warnings from numpy.testing import (assert_array_almost_equal as assert_close, - assert_array_equal) + assert_array_equal, assert_warns) def test_shape_mismatch(): @@ -89,6 +90,57 @@ def test_leave_labels_alone(): label2rgb(labels, bg_label=1) assert_array_equal(labels, labels_saved) +def test_avg(): + # label image + label_field = np.array([[1, 1, 1, 2], + [1, 2, 2, 2], + [3, 3, 3, 3]], dtype=np.uint8) + + # color image + r = np.array([[1., 1., 0., 0.], + [0., 0., 1., 1.], + [0., 0., 0., 0.]]) + g = np.array([[0., 0., 0., 1.], + [1., 1., 1., 0.], + [0., 0., 0., 0.]]) + b = np.array([[0., 0., 0., 1.], + [0., 1., 1., 1.], + [0., 0., 1., 1.]]) + image = np.dstack((r, g, b)) + + # reference label-colored image + rout = np.array([[0.5, 0.5, 0.5, 0.5], + [0.5, 0.5, 0.5, 0.5], + [0. , 0. , 0. , 0. ]]) + gout = np.array([[0.25, 0.25, 0.25, 0.75], + [0.25, 0.75, 0.75, 0.75], + [0. , 0. , 0. , 0. ]]) + bout = np.array([[0. , 0. , 0. , 1. ], + [0. , 1. , 1. , 1. ], + [0.5, 0.5, 0.5, 0.5]]) + expected_out = np.dstack((rout, gout, bout)) + + # test standard averaging + out = label2rgb(label_field, image, kind='avg') + assert_array_equal(out, expected_out) + + # test averaging with custom background value + out_bg = label2rgb(label_field, image, bg_label=2, bg_color=(0, 0, 0), + kind='avg') + expected_out_bg = expected_out.copy() + expected_out_bg[label_field == 2] = 0 + assert_array_equal(out_bg, expected_out_bg) + + # test default background color + out_bg = label2rgb(label_field, image, bg_label=2, kind='avg') + assert_array_equal(out_bg, expected_out_bg) + + +def test_negative_intensity(): + labels = np.arange(100).reshape(10, 10) + image = -1 * np.ones((10, 10)) + assert_warns(UserWarning, label2rgb, labels, image) + if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index b06c1f83..2be829ac 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -8,8 +8,8 @@ For more images, see import os as _os -from ..io import imread -from skimage import data_dir +from .. import data_dir +from ..io import imread, use_plugin __all__ = ['load', @@ -25,7 +25,8 @@ __all__ = ['load', 'immunohistochemistry', 'chelsea', 'coffee', - 'hubble_deep_field'] + 'hubble_deep_field', + 'astronaut'] def load(f): @@ -41,6 +42,7 @@ def load(f): img : ndarray Image loaded from skimage.data_dir. """ + use_plugin('pil') return imread(_os.path.join(data_dir, f)) @@ -65,6 +67,24 @@ def lena(): return load("lena.png") +def astronaut(): + """Colour image of the astronaut Eileen Collins. + + Photograph of Eileen Collins, an American astronaut. She was selected + as an astronaut in 1992 and first piloted the space shuttle STS-63 in + 1995. She retired in 2006 after spending a total of 38 days, 8 hours + and 10 minutes in outer space. + + This image was downloaded from the NASA Great Images database + `__. + + No known copyright restrictions, released into the public domain. + + """ + + return load("astronaut.png") + + def text(): """Gray-level "text" image used for corner detection. diff --git a/skimage/data/astronaut.png b/skimage/data/astronaut.png new file mode 100644 index 00000000..834cda00 Binary files /dev/null and b/skimage/data/astronaut.png differ diff --git a/skimage/data/no_time_for_that.gif b/skimage/data/no_time_for_that.gif new file mode 100644 index 00000000..45607b95 Binary files /dev/null and b/skimage/data/no_time_for_that.gif differ diff --git a/skimage/data/rank_filter_tests.npz b/skimage/data/rank_filter_tests.npz new file mode 100644 index 00000000..3142fcc1 Binary files /dev/null and b/skimage/data/rank_filter_tests.npz differ diff --git a/skimage/data/tests/__init__.py b/skimage/data/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/data/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/data/tests/test_data.py b/skimage/data/tests/test_data.py index 49cd4b5a..bfcdc571 100644 --- a/skimage/data/tests/test_data.py +++ b/skimage/data/tests/test_data.py @@ -7,6 +7,11 @@ def test_lena(): lena = data.lena() assert_equal(lena.shape, (512, 512, 3)) +def test_astronaut(): + """ Test that "astronaut" image can be loaded. """ + astronaut = data.astronaut() + assert_equal(astronaut.shape, (512, 512, 3)) + def test_camera(): """ Test that "camera" image can be loaded. """ diff --git a/skimage/data/truncated.jpg b/skimage/data/truncated.jpg new file mode 100644 index 00000000..11bb8140 Binary files /dev/null and b/skimage/data/truncated.jpg differ diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 7300a2a1..d99c10ae 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -7,7 +7,34 @@ import numpy as np cimport numpy as cnp from libc.math cimport sqrt, sin, cos, floor, ceil -from skimage._shared.geometry cimport point_in_polygon +from .._shared.geometry cimport point_in_polygon + + +def _coords_inside_image(rr, cc, shape, val=None): + """ + Return the coordinates inside an image of a given shape. + + Parameters + ---------- + rr, cc : (N,) ndarray of int + Indices of pixels. + shape : tuple + Image shape which is used to determine the maximum extent of output + pixel coordinates. + val : ndarray of float, optional + Values of pixels at coordinates [rr, cc]. + + Returns + ------- + rr, cc : (N,) array of int + Row and column indices of valid pixels (i.e. those inside `shape`). + val : (N,) array of float, optional + Values at `rr, cc`. Returned only if `val` is given as input. + """ + mask = (rr >= 0) & (rr < shape[0]) & (cc >= 0) & (cc < shape[1]) + if val is not None: + return rr[mask], cc[mask], val[mask] + return rr[mask], cc[mask] def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): @@ -196,9 +223,9 @@ def polygon(y, x, shape=None): x : (N,) ndarray X-coordinates of vertices of polygon. shape : tuple, optional - Image shape which is used to determine maximum extents of output pixel - coordinates. This is useful for polygons which exceed the image size. - By default the full extents of the polygon are used. + Image shape which is used to determine the maximum extent of output + pixel coordinates. This is useful for polygons which exceed the image + size. By default the full extent of the polygon are used. Returns ------- @@ -263,7 +290,7 @@ def polygon(y, x, shape=None): def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, - method='bresenham'): + method='bresenham', shape=None): """Generate circle perimeter coordinates. Parameters @@ -275,6 +302,10 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, method : {'bresenham', 'andres'}, optional bresenham : Bresenham method (default) andres : Andres method + shape : tuple, optional + Image shape which is used to determine the maximum extent of output pixel + coordinates. This is useful for circles which exceed the image size. + By default the full extent of the circle are used. Returns ------- @@ -361,11 +392,16 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, d = d + 2 * (y - x - 1) y = y - 1 x = x + 1 + if shape is not None: + return _coords_inside_image(np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx, + shape) return (np.array(rr, dtype=np.intp) + cy, np.array(cc, dtype=np.intp) + cx) -def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): +def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, + shape=None): """Generate anti-aliased circle perimeter coordinates. Parameters @@ -374,6 +410,10 @@ def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): Centre coordinate of circle. radius: int Radius of circle. + shape : tuple, optional + Image shape which is used to determine the maximum extent of output pixel + coordinates. This is useful for circles which exceed the image size. + By default the full extent of the circle are used. Returns ------- @@ -436,13 +476,18 @@ def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): val.extend([1 - dceil, dceil] * 8) dceil_prev = dceil + if shape is not None: + return _coords_inside_image(np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx, + shape, + val=np.array(val, dtype=np.float)) return (np.array(rr, dtype=np.intp) + cy, np.array(cc, dtype=np.intp) + cx, np.array(val, dtype=np.float)) def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, - Py_ssize_t xradius, double orientation=0): + Py_ssize_t xradius, double orientation=0, shape=None): """Generate ellipse perimeter coordinates. Parameters @@ -453,6 +498,10 @@ def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, Minor and major semi-axes. ``(x/xradius)**2 + (y/yradius)**2 = 1``. orientation : double, optional (default 0) Major axis orientation in clockwise direction as radians. + shape : tuple, optional + Image shape which is used to determine the maximum extent of output pixel + coordinates. This is useful for ellipses which exceed the image size. + By default the full extent of the ellipse are used. Returns ------- @@ -574,6 +623,9 @@ def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, py.extend(rr) px.extend(cc) + if shape is not None: + return _coords_inside_image(np.array(py, dtype=np.intp), + np.array(px, dtype=np.intp), shape) return np.array(py, dtype=np.intp), np.array(px, dtype=np.intp) @@ -708,7 +760,7 @@ def _bezier_segment(Py_ssize_t y0, Py_ssize_t x0, def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2, - double weight): + double weight, shape=None): """Generate Bezier curve coordinates. Parameters @@ -721,6 +773,10 @@ def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, Coordinates of the last control point. weight : double Middle control point weight, it describes the line tension. + shape : tuple, optional + Image shape which is used to determine the maximum extent of output + pixel coordinates. This is useful for curves which exceed the image + size. By default the full extent of the curve are used. Returns ------- @@ -833,4 +889,8 @@ def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, rr, cc = _bezier_segment(y0, x0, y1, x1, y2, x2, weight * weight) px.extend(rr) py.extend(cc) + + if shape is not None: + return _coords_inside_image(np.array(px, dtype=np.intp), + np.array(py, dtype=np.intp), shape) return np.array(px, dtype=np.intp), np.array(py, dtype=np.intp) diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py index e61df40c..dbeb6ba0 100644 --- a/skimage/draw/draw.py +++ b/skimage/draw/draw.py @@ -1,10 +1,15 @@ # coding: utf-8 import numpy as np +from ._draw import _coords_inside_image -def _coords_inside_image(rr, cc, shape): - mask = (rr >= 0) & (rr < shape[0]) & (cc >= 0) & (cc < shape[1]) - return rr[mask], cc[mask] +def _ellipse_in_shape(shape, center, radiuses): + """Generate coordinates of points within ellipse bounded by shape.""" + y, x = np.ogrid[0:float(shape[0]), 0:float(shape[1])] + cy, cx = center + ry, rx = radiuses + distances = ((y - cy) / ry) ** 2 + ((x - cx) / rx) ** 2 + return np.nonzero(distances < 1) def ellipse(cy, cx, yradius, xradius, shape=None): @@ -17,9 +22,9 @@ def ellipse(cy, cx, yradius, xradius, shape=None): yradius, xradius : double Minor and major semi-axes. ``(x/xradius)**2 + (y/yradius)**2 = 1``. shape : tuple, optional - Image shape which is used to determine maximum extents of output pixel + Image shape which is used to determine the maximum extent of output pixel coordinates. This is useful for ellipses which exceed the image size. - By default the full extents of the ellipse are used. + By default the full extent of the ellipse are used. Returns ------- @@ -48,21 +53,26 @@ def ellipse(cy, cx, yradius, xradius, shape=None): """ - dr = 1 / float(yradius) - dc = 1 / float(xradius) - - r, c = np.ogrid[-1:1:dr, -1:1:dc] - rr, cc = np.nonzero(r ** 2 + c ** 2 < 1) - - rr.flags.writeable = True - cc.flags.writeable = True - rr += cy - yradius - cc += cx - xradius - + center = np.array([cy, cx]) + radiuses = np.array([yradius, xradius]) if shape is not None: - return _coords_inside_image(rr, cc, shape) + return _ellipse_in_shape(shape, center, radiuses) + else: + # rounding here is necessary to avoid rounding issues later + upper_left = np.floor(center - radiuses).astype(int) - return rr, cc + shifted_center = center - upper_left + + # Shifted center is in interval [radiuses, radiuses + 1], so + # the ellipse must fit in [0, 2*radiuses + 1]. + bounding_shape = np.ceil(2 * radiuses + 1) + + rr, cc = _ellipse_in_shape(bounding_shape, shifted_center, radiuses) + rr.flags.writeable = True + cc.flags.writeable = True + rr += upper_left[0] + cc += upper_left[1] + return rr, cc def circle(cy, cx, radius, shape=None): @@ -75,9 +85,9 @@ def circle(cy, cx, radius, shape=None): radius: double Radius of circle. shape : tuple, optional - Image shape which is used to determine maximum extents of output pixel + Image shape which is used to determine the maximum extent of output pixel coordinates. This is useful for circles which exceed the image size. - By default the full extents of the circle are used. + By default the full extent of the circle are used. Returns ------- @@ -85,6 +95,7 @@ def circle(cy, cx, radius, shape=None): Pixel coordinates of circle. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. + Notes ----- This function is a wrapper for skimage.draw.ellipse() diff --git a/skimage/draw/tests/__init__.py b/skimage/draw/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/draw/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 3cb8c2a6..fd759b4e 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -234,6 +234,17 @@ def test_circle_perimeter_bresenham(): assert_array_equal(img, img_) +def test_circle_perimeter_bresenham_shape(): + img = np.zeros((15, 20), 'uint8') + rr, cc = circle_perimeter(7, 10, 9, method='bresenham', shape=(15, 20)) + img[rr, cc] = 1 + shift = 5 + img_ = np.zeros((15 + 2 * shift, 20), 'uint8') + rr, cc = circle_perimeter(7 + shift, 10, 9, method='bresenham', shape=None) + img_[rr, cc] = 1 + assert_array_equal(img, img_[shift:-shift, :]) + + def test_circle_perimeter_andres(): img = np.zeros((15, 15), 'uint8') rr, cc = circle_perimeter(7, 7, 0, method='andres') @@ -298,30 +309,144 @@ def test_circle_perimeter_aa(): assert_array_equal(img, img_) -def test_ellipse(): - img = np.zeros((15, 15), 'uint8') +def test_circle_perimeter_aa_shape(): + img = np.zeros((15, 20), 'uint8') + rr, cc, val = circle_perimeter_aa(7, 10, 9, shape=(15, 20)) + img[rr, cc] = val * 255 + shift = 5 + img_ = np.zeros((15 + 2 * shift, 20), 'uint8') + rr, cc, val = circle_perimeter_aa(7 + shift, 10, 9, shape=None) + img_[rr, cc] = val * 255 + assert_array_equal(img, img_[shift:-shift, :]) +def test_ellipse_trivial(): + img = np.zeros((2, 2), 'uint8') + rr, cc = ellipse(0.5, 0.5, 0.5, 0.5) + img[rr, cc] = 1 + img_correct = np.array([ + [0, 0], + [0, 0] + ]) + assert_array_equal(img, img_correct) + + img = np.zeros((2, 2), 'uint8') + rr, cc = ellipse(0.5, 0.5, 1.1, 1.1) + img[rr, cc] = 1 + img_correct = np.array([ + [1, 1], + [1, 1], + ]) + assert_array_equal(img, img_correct) + + img = np.zeros((3, 3), 'uint8') + rr, cc = ellipse(1, 1, 0.9, 0.9) + img[rr, cc] = 1 + img_correct = np.array([ + [0, 0, 0], + [0, 1, 0], + [0, 0, 0], + ]) + assert_array_equal(img, img_correct) + + img = np.zeros((3, 3), 'uint8') + rr, cc = ellipse(1, 1, 1.1, 1.1) + img[rr, cc] = 1 + img_correct = np.array([ + [0, 1, 0], + [1, 1, 1], + [0, 1, 0], + ]) + assert_array_equal(img, img_correct) + + img = np.zeros((3, 3), 'uint8') + rr, cc = ellipse(1, 1, 1.5, 1.5) + img[rr, cc] = 1 + img_correct = np.array([ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + ]) + assert_array_equal(img, img_correct) + + +def test_ellipse_generic(): + img = np.zeros((4, 4), 'uint8') + rr, cc = ellipse(1.5, 1.5, 1.1, 1.7) + img[rr, cc] = 1 + img_ = np.array([ + [0, 0, 0, 0], + [1, 1, 1, 1], + [1, 1, 1, 1], + [0, 0, 0, 0], + ]) + assert_array_equal(img, img_) + + img = np.zeros((5, 5), 'uint8') + rr, cc = ellipse(2, 2, 1.7, 1.7) + img[rr, cc] = 1 + 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], + ]) + assert_array_equal(img, img_) + + img = np.zeros((10, 10), 'uint8') + rr, cc = ellipse(5, 5, 3, 4) + 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, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ]) + assert_array_equal(img, img_) + + img = np.zeros((10, 10), 'uint8') + rr, cc = ellipse(4.5, 5, 3.5, 4) + 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, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ]) + assert_array_equal(img, img_) + + img = np.zeros((15, 15), 'uint8') rr, cc = ellipse(7, 7, 3, 7) img[rr, cc] = 1 - - img_ = np.array( - [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 0, 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, 0, 0]] - ) - + img_ = np.array([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 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, 0, 0], + ]) assert_array_equal(img, img_) @@ -352,6 +477,20 @@ def test_ellipse_with_shape(): assert_array_equal(img, img_) +def test_ellipse_negative(): + rr, cc = ellipse(-3, -3, 1.7, 1.7) + rr_, cc_ = np.nonzero(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], + ])) + + assert_array_equal(rr, rr_ - 5) + assert_array_equal(cc, cc_ - 5) + + def test_ellipse_perimeter_dot_zeroangle(): # dot, angle == 0 img = np.zeros((30, 15), 'uint8') @@ -462,6 +601,17 @@ def test_ellipse_perimeter_nzeroangle(): assert_array_equal(img, img_) +def test_ellipse_perimeter_shape(): + img = np.zeros((15, 20), 'uint8') + rr, cc = ellipse_perimeter(7, 10, 9, 9, 0, shape=(15, 20)) + img[rr, cc] = 1 + shift = 5 + img_ = np.zeros((15 + 2 * shift, 20), 'uint8') + rr, cc = ellipse_perimeter(7 + shift, 10, 9, 9, 0, shape=None) + img_[rr, cc] = 1 + assert_array_equal(img, img_[shift:-shift, :]) + + def test_bezier_segment_straight(): image = np.zeros((200, 200), dtype=int) x0 = 50 @@ -608,6 +758,24 @@ def test_bezier_curved_weight_neq_1(): ) assert_equal(img, img_) + +def test_bezier_curve_shape(): + img = np.zeros((15, 20), 'uint8') + x1, y1 = (1, 5) + x2, y2 = (6, 11) + x3, y3 = (1, 14) + rr, cc = bezier_curve(x1, y1, x2, y2, x3, y3, 2, shape=(15, 20)) + img[rr, cc] = 1 + shift = 5 + img_ = np.zeros((15 + 2 * shift, 20), 'uint8') + x1, y1 = (1 + shift, 5) + x2, y2 = (6 + shift, 11) + x3, y3 = (1 + shift, 14) + rr, cc = bezier_curve(x1, y1, x2, y2, x3, y3, 2, shape=None) + img_[rr, cc] = 1 + assert_array_equal(img, img_[shift:-shift, :]) + + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 699e9acf..b71916f5 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -14,29 +14,34 @@ responsible. Basically, don't be a jerk, and remember that anything free comes with no guarantee. """ import numpy as np -import skimage -from skimage import color -from skimage.exposure import rescale_intensity -from skimage.util import view_as_blocks +from .. import img_as_float, img_as_uint +from ..color.adapt_rgb import adapt_rgb, hsv_value +from ..exposure import rescale_intensity +from ..util import view_as_blocks, pad MAX_REG_X = 16 # max. # contextual regions in x-direction */ MAX_REG_Y = 16 # max. # contextual regions in y-direction */ -NR_OF_GREY = 16384 # number of grayscale levels to use in CLAHE algorithm +NR_OF_GREY = 2 ** 14 # number of grayscale levels to use in CLAHE algorithm +@adapt_rgb(hsv_value) def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, nbins=256): - """Contrast Limited Adaptive Histogram Equalization. + """Contrast Limited Adaptive Histogram Equalization (CLAHE). + + An algorithm for local contrast enhancement, that uses histograms computed + over different tile regions of the image. Local details can therefore be + enhanced even in regions that are darker or lighter than most of the image. Parameters ---------- image : array-like Input image. ntiles_x : int, optional - Number of tile regions in the X direction. Ranges between 2 and 16. + Number of tile regions in the X direction. Ranges between 1 and 16. ntiles_y : int, optional - Number of tile regions in the Y direction. Ranges between 2 and 16. + Number of tile regions in the Y direction. Ranges between 1 and 16. clip_limit : float: optional Clipping limit, normalized between 0 and 1 (higher values give more contrast). @@ -48,41 +53,33 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, out : ndarray Equalized image. + See Also + -------- + equalize_hist, rescale_intensity + Notes ----- - * The algorithm relies on an image whose rows and columns are even - multiples of the number of tiles, so the extra rows and columns are left - at their original values, thus preserving the input image shape. * For color images, the following steps are performed: - - The image is converted to LAB color space - - The CLAHE algorithm is run on the L channel + - The image is converted to HSV color space + - The CLAHE algorithm is run on the V (Value) channel - The image is converted back to RGB space and returned * For RGBA images, the original alpha channel is removed. + * The CLAHE algorithm relies on image blocks of equal size. This may + result in extra border pixels that would not be handled. In that case, + we pad the image with a repeat of the border pixels, apply the + algorithm, and then trim the image to original size. References ---------- .. [1] http://tog.acm.org/resources/GraphicsGems/gems.html#gemsvi .. [2] https://en.wikipedia.org/wiki/CLAHE#CLAHE """ - args = [None, ntiles_x, ntiles_y, clip_limit * nbins, nbins] - if image.ndim > 2: - lab_img = color.rgb2lab(skimage.img_as_float(image)) - l_chan = lab_img[:, :, 0] - l_chan /= np.max(np.abs(l_chan)) - l_chan = skimage.img_as_uint(l_chan) - args[0] = rescale_intensity(l_chan, out_range=(0, NR_OF_GREY - 1)) - new_l = _clahe(*args).astype(float) - new_l = rescale_intensity(new_l, out_range=(0, 100)) - lab_img[:new_l.shape[0], :new_l.shape[1], 0] = new_l - image = color.lab2rgb(lab_img) - image = rescale_intensity(image, out_range=(0, 1)) - else: - image = skimage.img_as_uint(image) - args[0] = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) - out = _clahe(*args) - image[:out.shape[0], :out.shape[1]] = out - image = rescale_intensity(image) - return image + image = img_as_uint(image) + image = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) + out = _clahe(image, ntiles_x, ntiles_y, clip_limit * nbins, nbins) + image[:out.shape[0], :out.shape[1]] = out + image = img_as_float(image) + return rescale_intensity(image) def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): @@ -114,39 +111,51 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): """ ntiles_x = min(ntiles_x, MAX_REG_X) ntiles_y = min(ntiles_y, MAX_REG_Y) - ntiles_y = max(ntiles_x, 2) - ntiles_x = max(ntiles_y, 2) if clip_limit == 1.0: return image # is OK, immediately returns original image. + h_inner = image.shape[0] - image.shape[0] % ntiles_y + w_inner = image.shape[1] - image.shape[1] % ntiles_x + + # make the tile size divisible by 2 + h_inner -= h_inner % (2 * ntiles_y) + w_inner -= w_inner % (2 * ntiles_x) + + orig_shape = image.shape + width = w_inner // ntiles_x # Actual size of contextual regions + height = h_inner // ntiles_y + + if h_inner != image.shape[0]: + ntiles_y += 1 + if w_inner != image.shape[1]: + ntiles_x += 1 + if h_inner != image.shape[1] or w_inner != image.shape[0]: + h_pad = height * ntiles_y - image.shape[0] + w_pad = width * ntiles_x - image.shape[1] + image = pad(image, ((0, h_pad), (0, w_pad)), mode='reflect') + h_inner, w_inner = image.shape + + bin_size = 1 + NR_OF_GREY // nbins + lut = np.arange(NR_OF_GREY) + lut //= bin_size + img_blocks = view_as_blocks(image, (height, width)) + map_array = np.zeros((ntiles_y, ntiles_x, nbins), dtype=int) - - y_res = image.shape[0] - image.shape[0] % ntiles_y - x_res = image.shape[1] - image.shape[1] % ntiles_x - image = image[: y_res, : x_res] - - x_size = image.shape[1] // ntiles_x # Actual size of contextual regions - y_size = image.shape[0] // ntiles_y - n_pixels = x_size * y_size + n_pixels = width * height if clip_limit > 0.0: # Calculate actual cliplimit - clip_limit = int(clip_limit * (x_size * y_size) / nbins) + clip_limit = int(clip_limit * (width * height) / nbins) if clip_limit < 1: clip_limit = 1 else: clip_limit = NR_OF_GREY # Large value, do not clip (AHE) - bin_size = 1 + NR_OF_GREY / nbins - aLUT = np.arange(NR_OF_GREY) - aLUT //= bin_size - img_blocks = view_as_blocks(image, (y_size, x_size)) - # Calculate greylevel mappings for each contextual region for y in range(ntiles_y): for x in range(ntiles_x): sub_img = img_blocks[y, x] - hist = aLUT[sub_img.ravel()] + hist = lut[sub_img.ravel()] hist = np.bincount(hist) hist = np.append(hist, np.zeros(nbins - hist.size, dtype=int)) hist = clip_histogram(hist, clip_limit) @@ -158,29 +167,29 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): for y in range(ntiles_y + 1): xstart = 0 if y == 0: # special case: top row - ystep = y_size / 2.0 + ystep = height / 2.0 yU = 0 yB = 0 elif y == ntiles_y: # special case: bottom row - ystep = y_size / 2.0 + ystep = height / 2.0 yU = ntiles_y - 1 yB = yU else: # default values - ystep = y_size + ystep = height yU = y - 1 yB = yB + 1 for x in range(ntiles_x + 1): if x == 0: # special case: left column - xstep = x_size / 2.0 + xstep = width / 2.0 xL = 0 xR = 0 elif x == ntiles_x: # special case: right column - xstep = x_size / 2.0 + xstep = width / 2.0 xL = ntiles_x - 1 xR = xL else: # default values - xstep = x_size + xstep = width xL = x - 1 xR = xL + 1 @@ -192,12 +201,15 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): xslice = np.arange(xstart, xstart + xstep) yslice = np.arange(ystart, ystart + ystep) interpolate(image, xslice, yslice, - mapLU, mapRU, mapLB, mapRB, aLUT) + mapLU, mapRU, mapLB, mapRB, lut) xstart += xstep # set pointer on next matrix */ ystart += ystep + if image.shape != orig_shape: + image = image[:orig_shape[0], :orig_shape[1]] + return image @@ -284,7 +296,7 @@ def map_histogram(hist, min_val, max_val, n_pixels): def interpolate(image, xslice, yslice, - mapLU, mapRU, mapLB, mapRB, aLUT): + mapLU, mapRU, mapLB, mapRB, lut): """Find the new grayscale level for a region using bilinear interpolation. Parameters @@ -295,7 +307,7 @@ def interpolate(image, xslice, yslice, Indices of the region. map* : ndarray Mappings of greylevels from histograms. - aLUT : ndarray + lut : ndarray Maps grayscale levels in image to histogram levels. Returns @@ -317,7 +329,7 @@ def interpolate(image, xslice, yslice, view = image[int(yslice[0]):int(yslice[-1] + 1), int(xslice[0]):int(xslice[-1] + 1)] - im_slice = aLUT[view] + im_slice = lut[view] new = ((y_inv_coef * (x_inv_coef * mapLU[im_slice] + x_coef * mapRU[im_slice]) + y_coef * (x_inv_coef * mapLB[im_slice] diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 14379f6b..8774cf9b 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -1,20 +1,19 @@ import warnings import numpy as np -from skimage import img_as_float -from skimage.util.dtype import dtype_range, dtype_limits -from skimage._shared.utils import deprecated +from .. import img_as_float +from ..util.dtype import dtype_range, dtype_limits -__all__ = ['histogram', 'cumulative_distribution', 'equalize', +__all__ = ['histogram', 'cumulative_distribution', 'equalize_hist', '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), +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]}) @@ -45,10 +44,14 @@ def histogram(image, nbins=256): bin_centers : array The values at the center of the bins. + See Also + -------- + cumulative_distribution + Examples -------- - >>> from skimage import data, exposure, util - >>> image = util.img_as_float(data.camera()) + >>> from skimage import data, exposure, img_as_float + >>> image = img_as_float(data.camera()) >>> np.histogram(image, bins=2) (array([107432, 154712]), array([ 0. , 0.5, 1. ])) >>> exposure.histogram(image, nbins=2) @@ -63,16 +66,25 @@ def histogram(image, nbins=256): # For integer types, histogramming with bincount is more efficient. if np.issubdtype(image.dtype, np.integer): offset = 0 - if np.min(image) < 0: - offset = np.min(image) - hist = np.bincount(image.ravel() - offset) + image_min = np.min(image) + if image_min < 0: + offset = image_min + image_range = np.max(image).astype(np.int64) - image_min + # get smallest dtype that can hold both minimum and offset maximum + offset_dtype = np.promote_types(np.min_scalar_type(image_range), + np.min_scalar_type(image_min)) + if image.dtype != offset_dtype: + # prevent overflow errors when offsetting + image = image.astype(offset_dtype) + image = image - offset + hist = np.bincount(image.ravel()) bin_centers = np.arange(len(hist)) + offset # clip histogram to start with a non-zero bin idx = np.nonzero(hist)[0][0] return hist[idx:], bin_centers[idx:] else: - hist, bin_edges = np.histogram(image.flat, nbins) + hist, bin_edges = np.histogram(image.flat, bins=nbins) bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2. return hist, bin_centers @@ -94,10 +106,22 @@ def cumulative_distribution(image, nbins=256): bin_centers : array Centers of bins. + See Also + -------- + histogram + References ---------- .. [1] http://en.wikipedia.org/wiki/Cumulative_distribution_function + Examples + -------- + >>> from skimage import data, exposure, img_as_float + >>> image = img_as_float(data.camera()) + >>> hi = exposure.histogram(image) + >>> cdf = exposure.cumulative_distribution(image) + >>> np.alltrue(cdf[0] == np.cumsum(hi[0])/float(image.size)) + True """ hist, bin_centers = histogram(image, nbins) img_cdf = hist.cumsum() @@ -105,15 +129,20 @@ def cumulative_distribution(image, nbins=256): return img_cdf, bin_centers -def equalize_hist(image, nbins=256): +def equalize_hist(image, nbins=256, mask=None): """Return image after histogram equalization. Parameters ---------- image : array Image array. - nbins : int - Number of bins for image histogram. + nbins : int, optional + Number of bins for image histogram. Note: this argument is + ignored for integer images, for which each integer is its own + bin. + mask: ndarray of bools or 0s and 1s, optional + Array of same shape as `image`. Only points at which mask == True + are used for the equalization, which is applied to the whole image. Returns ------- @@ -130,31 +159,82 @@ def equalize_hist(image, nbins=256): .. [2] http://en.wikipedia.org/wiki/Histogram_equalization """ - image = img_as_float(image) - cdf, bin_centers = cumulative_distribution(image, nbins) + if mask is not None: + mask = np.array(mask, dtype=bool) + cdf, bin_centers = cumulative_distribution(image[mask], nbins) + else: + cdf, bin_centers = cumulative_distribution(image, nbins) out = np.interp(image.flat, bin_centers, cdf) return out.reshape(image.shape) -def rescale_intensity(image, in_range=None, out_range=None): +def intensity_range(image, range_values='image', clip_negative=False): + """Return image intensity range (min, max) based on desired value type. + + Parameters + ---------- + image : array + Input image. + range_values : str or 2-tuple + The image intensity range is configured by this parameter. + The possible values for this parameter are enumerated below. + + 'image' + Return image min/max as the range. + 'dtype' + Return min/max of the image's dtype as the range. + dtype-name + Return intensity range based on desired `dtype`. Must be valid key + in `DTYPE_RANGE`. Note: `image` is ignored for this range type. + 2-tuple + Return `range_values` as min/max intensities. Note that there's no + reason to use this function if you just want to specify the + intensity range explicitly. This option is included for functions + that use `intensity_range` to support all desired range types. + + clip_negative : bool + If True, clip the negative range (i.e. return 0 for min intensity) + even if the image dtype allows negative values. + """ + if range_values == 'dtype': + range_values = image.dtype.type + + if range_values == 'image': + i_min = np.min(image) + i_max = np.max(image) + elif range_values in DTYPE_RANGE: + i_min, i_max = DTYPE_RANGE[range_values] + if clip_negative: + i_min = 0 + else: + i_min, i_max = range_values + return i_min, i_max + + +def rescale_intensity(image, in_range='image', out_range='dtype'): """Return image after stretching or shrinking its intensity levels. - The image intensities are uniformly rescaled such that the minimum and - maximum values given by `in_range` match those given by `out_range`. + The desired intensity range of the input and output, `in_range` and + `out_range` respectively, are used to stretch or shrink the intensity range + of the input image. See examples below. Parameters ---------- image : array Image array. - 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. 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. If string, use data limits of dtype specified by the string. + in_range, out_range : str or 2-tuple + Min and max intensity values of input and output image. + The possible values for this parameter are enumerated below. + + 'image' + Use image min/max as the intensity range. + 'dtype' + Use min/max of the image's dtype as the intensity range. + dtype-name + Use intensity range based on desired `dtype`. Must be valid key + in `DTYPE_RANGE`. + 2-tuple + Use `range_values` as explicit min/max intensities. Returns ------- @@ -162,9 +242,15 @@ def rescale_intensity(image, in_range=None, out_range=None): Image array after rescaling its intensity. This image is the same dtype as the input image. + See Also + -------- + intensity_range, equalize_hist + Examples -------- - By default, intensities are stretched to the limits allowed by the dtype: + By default, the min/max intensities of the input image are stretched to + the limits allowed by the image's dtype, since `in_range` defaults to + 'image' and `out_range` defaults to 'dtype': >>> image = np.array([51, 102, 153], dtype=np.uint8) >>> rescale_intensity(image) @@ -203,20 +289,17 @@ def rescale_intensity(image, in_range=None, out_range=None): dtype = image.dtype.type 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 + in_range = 'image' + msg = "`in_range` should not be set to None. Use {!r} instead." + warnings.warn(msg.format(in_range)) - 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: - omin, omax = out_range + if out_range is None: + out_range = 'dtype' + msg = "`out_range` should not be set to None. Use {!r} instead." + warnings.warn(msg.format(out_range)) + + imin, imax = intensity_range(image, in_range) + omin, omax = intensity_range(image, out_range, clip_negative=(imin >= 0)) image = np.clip(image, imin, imax) @@ -253,6 +336,10 @@ def adjust_gamma(image, gamma=1, gain=1): out : ndarray Gamma corrected output image. + See Also + -------- + adjust_log + Notes ----- For gamma greater than 1, the histogram will shift towards left and @@ -265,6 +352,14 @@ def adjust_gamma(image, gamma=1, gain=1): ---------- .. [1] http://en.wikipedia.org/wiki/Gamma_correction + Examples + -------- + >>> from skimage import data, exposure, img_as_float + >>> image = img_as_float(data.moon()) + >>> gamma_corrected = exposure.adjust_gamma(image, 2) + >>> # Output is darker for gamma > 1 + >>> image.mean() > gamma_corrected.mean() + True """ _assert_non_negative(image) dtype = image.dtype.type @@ -300,6 +395,10 @@ def adjust_log(image, gain=1, inv=False): out : ndarray Logarithm corrected output image. + See Also + -------- + adjust_gamma + References ---------- .. [1] http://www.ece.ucsb.edu/Faculty/Manjunath/courses/ece178W03/EnhancePart1.pdf @@ -343,6 +442,10 @@ def adjust_sigmoid(image, cutoff=0.5, gain=10, inv=False): out : ndarray Sigmoid corrected output image. + See Also + -------- + adjust_gamma + References ---------- .. [1] Gustav J. Braun, "Image Lightness Rescaling Using Sigmoidal Contrast @@ -355,8 +458,8 @@ def adjust_sigmoid(image, cutoff=0.5, gain=10, inv=False): scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0]) if inv: - out = (1 - 1 / (1 + np.exp(gain * (cutoff - image/scale)))) * scale + out = (1 - 1 / (1 + np.exp(gain * (cutoff - image / scale)))) * scale return dtype(out) - out = (1 / (1 + np.exp(gain * (cutoff - image/scale)))) * scale + out = (1 / (1 + np.exp(gain * (cutoff - image / scale)))) * scale return dtype(out) diff --git a/skimage/exposure/tests/__init__.py b/skimage/exposure/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/exposure/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 6471ff59..1316ea4e 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -2,24 +2,59 @@ import warnings import numpy as np from numpy.testing import assert_array_almost_equal as assert_close -from numpy.testing import assert_array_equal, assert_raises +from numpy.testing import (assert_array_equal, assert_raises, + assert_almost_equal) + import skimage from skimage import data from skimage import exposure +from skimage.exposure.exposure import intensity_range from skimage.color import rgb2gray from skimage.util.dtype import dtype_range +from skimage._shared._warnings import expected_warnings + + +# Test integer histograms +# ======================= + +def test_negative_overflow(): + im = np.array([-1, 127], dtype=np.int8) + frequencies, bin_centers = exposure.histogram(im) + assert_array_equal(bin_centers, np.arange(-1, 128)) + assert frequencies[0] == 1 + assert frequencies[-1] == 1 + assert_array_equal(frequencies[1:-1], 0) + + +def test_all_negative_image(): + im = np.array([-128, -1], dtype=np.int8) + frequencies, bin_centers = exposure.histogram(im) + assert_array_equal(bin_centers, np.arange(-128, 0)) + assert frequencies[0] == 1 + assert frequencies[-1] == 1 + assert_array_equal(frequencies[1:-1], 0) # Test histogram equalization # =========================== +np.random.seed(0) + +test_img_int = data.camera() # squeeze image intensities to lower image contrast -test_img = skimage.img_as_float(data.camera()) +test_img = skimage.img_as_float(test_img_int) test_img = exposure.rescale_intensity(test_img / 5. + 100) +def test_equalize_uint8_approx(): + """Check integer bins used for uint8 images.""" + img_eq0 = exposure.equalize_hist(test_img_int) + img_eq1 = exposure.equalize_hist(test_img_int, nbins=3) + np.testing.assert_allclose(img_eq0, img_eq1) + def test_equalize_ubyte(): - img = skimage.img_as_ubyte(test_img) + with expected_warnings(['precision loss']): + img = skimage.img_as_ubyte(test_img) img_eq = exposure.equalize_hist(img) cdf, bin_edges = exposure.cumulative_distribution(img_eq) @@ -34,6 +69,19 @@ def test_equalize_float(): check_cdf_slope(cdf) +def test_equalize_masked(): + img = skimage.img_as_float(test_img) + mask = np.zeros(test_img.shape) + mask[50:150, 50:250] = 1 + img_mask_eq = exposure.equalize_hist(img, mask=mask) + img_eq = exposure.equalize_hist(img) + + cdf, bin_edges = exposure.cumulative_distribution(img_mask_eq) + check_cdf_slope(cdf) + + assert not (img_eq == img_mask_eq).all() + + def check_cdf_slope(cdf): """Slope of cdf which should equal 1 for an equalized histogram.""" norm_intensity = np.linspace(0, 1, len(cdf)) @@ -41,6 +89,36 @@ def check_cdf_slope(cdf): assert 0.9 < slope < 1.1 +# Test intensity range +# ==================== + + +def test_intensity_range_uint8(): + image = np.array([0, 1], dtype=np.uint8) + input_and_expected = [('image', [0, 1]), + ('dtype', [0, 255]), + ((10, 20), [10, 20])] + for range_values, expected_values in input_and_expected: + out = intensity_range(image, range_values=range_values) + yield assert_array_equal, out, expected_values + + +def test_intensity_range_float(): + image = np.array([0.1, 0.2], dtype=np.float64) + input_and_expected = [('image', [0.1, 0.2]), + ('dtype', [-1, 1]), + ((0.3, 0.4), [0.3, 0.4])] + for range_values, expected_values in input_and_expected: + out = intensity_range(image, range_values=range_values) + yield assert_array_equal, out, expected_values + + +def test_intensity_range_clipped_float(): + image = np.array([0.1, 0.2], dtype=np.float64) + out = intensity_range(image, range_values='dtype', clip_negative=True) + assert_array_equal(out, (0, 1)) + + # Test rescale intensity # ====================== @@ -111,59 +189,78 @@ def test_rescale_uint14_limits(): # ==================================== def test_adapthist_scalar(): - '''Test a scalar uint8 image - ''' + """Test a scalar uint8 image + """ img = skimage.img_as_ubyte(data.moon()) adapted = exposure.equalize_adapthist(img, clip_limit=0.02) - assert adapted.min() == 0 - assert adapted.max() == (1 << 16) - 1 + assert adapted.min() == 0.0 + assert adapted.max() == 1.0 assert img.shape == adapted.shape - full_scale = skimage.exposure.rescale_intensity(skimage.img_as_uint(img)) + full_scale = skimage.exposure.rescale_intensity(skimage.img_as_float(img)) assert_almost_equal = np.testing.assert_almost_equal - assert_almost_equal(peak_snr(full_scale, adapted), 101.231, 3) + assert_almost_equal(peak_snr(full_scale, adapted), 101.2295, 3) assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.041, 3) return img, adapted def test_adapthist_grayscale(): - '''Test a grayscale float image - ''' - img = skimage.img_as_float(data.lena()) + """Test a grayscale float image + """ + img = skimage.img_as_float(data.astronaut()) img = rgb2gray(img) img = np.dstack((img, img, img)) - adapted = exposure.equalize_adapthist(img, 10, 9, clip_limit=0.01, - nbins=128) + with expected_warnings(['precision loss|non-contiguous input']): + adapted = exposure.equalize_adapthist(img, 10, 9, clip_limit=0.01, + nbins=128) assert_almost_equal = np.testing.assert_almost_equal assert img.shape == adapted.shape - assert_almost_equal(peak_snr(img, adapted), 97.531, 3) - assert_almost_equal(norm_brightness_err(img, adapted), 0.0313, 3) + assert_almost_equal(peak_snr(img, adapted), 97.6876, 3) + assert_almost_equal(norm_brightness_err(img, adapted), 0.0591, 3) return data, adapted def test_adapthist_color(): - '''Test an RGB color uint16 image - ''' - img = skimage.img_as_uint(data.lena()) + """Test an RGB color uint16 image + """ + img = skimage.img_as_uint(data.astronaut()) with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') hist, bin_centers = exposure.histogram(img) assert len(w) > 0 - adapted = exposure.equalize_adapthist(img, clip_limit=0.01) + with expected_warnings(['precision loss']): + adapted = exposure.equalize_adapthist(img, clip_limit=0.01) + assert_almost_equal = np.testing.assert_almost_equal assert adapted.min() == 0 assert adapted.max() == 1.0 assert img.shape == adapted.shape full_scale = skimage.exposure.rescale_intensity(img) - assert_almost_equal(peak_snr(full_scale, adapted), 102.940, 3) - assert_almost_equal(norm_brightness_err(full_scale, adapted), - 0.0110, 3) + assert_almost_equal(peak_snr(full_scale, adapted), 109.6, 1) + assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.02, 2) return data, adapted +def test_adapthist_alpha(): + """Test an RGBA color image + """ + img = skimage.img_as_float(data.astronaut()) + alpha = np.ones((img.shape[0], img.shape[1]), dtype=float) + img = np.dstack((img, alpha)) + with expected_warnings(['precision loss']): + adapted = exposure.equalize_adapthist(img) + assert adapted.shape != img.shape + img = img[:, :, :3] + full_scale = skimage.exposure.rescale_intensity(img) + assert img.shape == adapted.shape + assert_almost_equal = np.testing.assert_almost_equal + assert_almost_equal(peak_snr(full_scale, adapted), 109.60, 2) + assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.0235, 3) + + def peak_snr(img1, img2): - '''Peak signal to noise ratio of two images + """Peak signal to noise ratio of two images Parameters ---------- @@ -174,7 +271,7 @@ def peak_snr(img1, img2): ------- peak_snr : float Peak signal to noise ratio - ''' + """ if img1.ndim == 3: img1, img2 = rgb2gray(img1.copy()), rgb2gray(img2.copy()) img1 = skimage.img_as_float(img1) @@ -185,7 +282,7 @@ def peak_snr(img1, img2): def norm_brightness_err(img1, img2): - '''Normalized Absolute Mean Brightness Error between two images + """Normalized Absolute Mean Brightness Error between two images Parameters ---------- @@ -196,7 +293,7 @@ def norm_brightness_err(img1, img2): ------- norm_brightness_error : float Normalized absolute mean brightness error - ''' + """ if img1.ndim == 3: img1, img2 = rgb2gray(img1), rgb2gray(img2) ambe = np.abs(img1.mean() - img2.mean()) @@ -204,10 +301,6 @@ def norm_brightness_err(img1, img2): return nbe -if __name__ == '__main__': - from numpy import testing - testing.run_module_suite() - # Test Gamma Correction # ===================== @@ -374,6 +467,11 @@ def test_adjust_inv_sigmoid_cutoff_half(): assert_array_equal(result, expected) -def test_neggative(): +def test_negative(): image = np.arange(-10, 245, 4).reshape(8, 8).astype(np.double) assert_raises(ValueError, exposure.adjust_gamma, image) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/external/LICENSE.txt b/skimage/external/LICENSE.txt new file mode 100644 index 00000000..d9f04072 --- /dev/null +++ b/skimage/external/LICENSE.txt @@ -0,0 +1,8 @@ + +tifffile + + Copyright (c) 2008-2014, Christoph Gohlke + Copyright (c) 2008-2014, The Regents of the University of California + Produced at the Laboratory for Fluorescence Dynamics + + Distributed under the terms of the BSD License. diff --git a/skimage/external/__init__.py b/skimage/external/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/skimage/external/setup.py b/skimage/external/setup.py new file mode 100644 index 00000000..343c4d54 --- /dev/null +++ b/skimage/external/setup.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +import os.path + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs + + config = Configuration('external', parent_package, top_path) + + config.add_extension('tifffile._tifffile', + sources=['tifffile/tifffile.c'], + include_dirs=[get_numpy_include_dirs()]) + return config + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='External Libaries', + url='https://github.com/scikit-image/scikit-image', + license='Modified BSD', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/external/tifffile/__init__.py b/skimage/external/tifffile/__init__.py new file mode 100644 index 00000000..bd426a3b --- /dev/null +++ b/skimage/external/tifffile/__init__.py @@ -0,0 +1,4 @@ +try: + from tifffile import imread, imsave, TiffFile +except ImportError: + from .tifffile_local import imread, imsave, TiffFile diff --git a/skimage/external/tifffile/tifffile.c b/skimage/external/tifffile/tifffile.c new file mode 100644 index 00000000..6aa0eaa1 --- /dev/null +++ b/skimage/external/tifffile/tifffile.c @@ -0,0 +1,962 @@ + + +/* tifffile.c + +A Python C extension module for decoding PackBits and LZW encoded TIFF data. + +Refer to the tifffile.py module for documentation and tests. + +:Author: + `Christoph Gohlke `_ + +:Organization: + Laboratory for Fluorescence Dynamics, University of California, Irvine + +:Version: 2013.11.05 + +Install +------- +Use this Python distutils setup script to build the extension module:: + + # setup.py + # Usage: ``python setup.py build_ext --inplace`` + from distutils.core import setup, Extension + import numpy + setup(name='_tifffile', + ext_modules=[Extension('_tifffile', ['tifffile.c'], + include_dirs=[numpy.get_include()])]) + +License +------- +Copyright (c) 2008-2014, Christoph Gohlke +Copyright (c) 2008-2014, The Regents of the University of California +Produced at the Laboratory for Fluorescence Dynamics +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holders nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#define _VERSION_ "2013.11.05" + +#define WIN32_LEAN_AND_MEAN +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION + +#include "Python.h" +#include "string.h" +#include "numpy/arrayobject.h" + +/* little endian by default */ +#ifndef MSB +#define MSB 1 +#endif + +#if MSB +#define LSB 0 +#define BOC '<' +#else +#define LSB 1 +#define BOC '>' +#endif + +#define NO_ERROR 0 +#define VALUE_ERROR -1 + +#ifdef _MSC_VER +typedef unsigned __int8 uint8_t; +typedef unsigned __int16 uint16_t; +typedef unsigned __int32 uint32_t; +typedef unsigned __int64 uint64_t; +#ifdef _WIN64 +typedef __int64 ssize_t; +typedef signed __int64 intptr_t; +typedef unsigned __int64 uintptr_t; +#define SSIZE_MAX (9223372036854775808) +#else +typedef int ssize_t; +typedef _W64 signed int intptr_t; +typedef _W64 unsigned int uintptr_t; +#define SSIZE_MAX (2147483648) +#endif +#else +/* non MS compilers */ +#include +#include +#endif + +#define SWAP2BYTES(x) \ + ((((x) >> 8) & 0x00FF) | (((x) & 0x00FF) << 8)) + +#define SWAP4BYTES(x) \ + ((((x) >> 24) & 0x00FF) | (((x)&0x00FF) << 24) | \ + (((x) >> 8 ) & 0xFF00) | (((x)&0xFF00) << 8)) + +#define SWAP8BYTES(x) \ + ((((x) >> 56) & 0x00000000000000FF) | (((x) >> 40) & 0x000000000000FF00) | \ + (((x) >> 24) & 0x0000000000FF0000) | (((x) >> 8) & 0x00000000FF000000) | \ + (((x) << 8) & 0x000000FF00000000) | (((x) << 24) & 0x0000FF0000000000) | \ + (((x) << 40) & 0x00FF000000000000) | (((x) << 56) & 0xFF00000000000000)) + +struct BYTE_STRING { + unsigned int ref; /* reference count */ + unsigned int len; /* length of string */ + char *str; /* pointer to bytes */ +}; + +typedef union { + uint8_t b[2]; + uint16_t i; +} u_uint16; + +typedef union { + uint8_t b[4]; + uint32_t i; +} u_uint32; + +typedef union { + uint8_t b[8]; + uint64_t i; +} u_uint64; + +/*****************************************************************************/ +/* C functions */ + +/* Return mask for itemsize bits */ +unsigned char bitmask(const int itemsize) { + unsigned char result = 0; + unsigned char power = 1; + int i; + for (i = 0; i < itemsize; i++) { + result += power; + power *= 2; + } + return result << (8 - itemsize); +} + +/** Unpack sequence of tigthly packed 1-32 bit integers. + +Native byte order will be returned. + +Input data array should be padded to the next 16, 32 or 64-bit boundary +if itemsize not in (1, 2, 4, 8, 16, 24, 32, 64). + +*/ +int unpackbits( + unsigned char *data, + const ssize_t size, /** size of data in bytes */ + const int itemsize, /** number of bits in integer */ + ssize_t numitems, /** number of items to unpack */ + unsigned char *result /** buffer to store unpacked items */ + ) +{ + ssize_t i, j, k, storagesize; + unsigned char value; + /* Input validation is done in wrapper function */ + storagesize = (ssize_t)(ceil(itemsize / 8.0)); + storagesize = storagesize < 3 ? storagesize : storagesize > 4 ? 8 : 4; + switch (itemsize) { + case 8: + case 16: + case 32: + case 64: + memcpy(result, data, numitems*storagesize); + return NO_ERROR; + case 1: + for (i = 0, j = 0; i < numitems/8; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(128)) >> 7; + result[j++] = (value & (unsigned char)(64)) >> 6; + result[j++] = (value & (unsigned char)(32)) >> 5; + result[j++] = (value & (unsigned char)(16)) >> 4; + result[j++] = (value & (unsigned char)(8)) >> 3; + result[j++] = (value & (unsigned char)(4)) >> 2; + result[j++] = (value & (unsigned char)(2)) >> 1; + result[j++] = (value & (unsigned char)(1)); + } + if (numitems % 8) { + value = data[i]; + switch (numitems % 8) { + case 7: result[j+6] = (value & (unsigned char)(2)) >> 1; + case 6: result[j+5] = (value & (unsigned char)(4)) >> 2; + case 5: result[j+4] = (value & (unsigned char)(8)) >> 3; + case 4: result[j+3] = (value & (unsigned char)(16)) >> 4; + case 3: result[j+2] = (value & (unsigned char)(32)) >> 5; + case 2: result[j+1] = (value & (unsigned char)(64)) >> 6; + case 1: result[j] = (value & (unsigned char)(128)) >> 7; + } + } + return NO_ERROR; + case 2: + for (i = 0, j = 0; i < numitems/4; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(192)) >> 6; + result[j++] = (value & (unsigned char)(48)) >> 4; + result[j++] = (value & (unsigned char)(12)) >> 2; + result[j++] = (value & (unsigned char)(3)); + } + if (numitems % 4) { + value = data[i]; + switch (numitems % 4) { + case 3: result[j+2] = (value & (unsigned char)(12)) >> 2; + case 2: result[j+1] = (value & (unsigned char)(48)) >> 4; + case 1: result[j] = (value & (unsigned char)(192)) >> 6; + } + } + return NO_ERROR; + case 4: + for (i = 0, j = 0; i < numitems/2; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(240)) >> 4; + result[j++] = (value & (unsigned char)(15)); + } + if (numitems % 2) { + value = data[i]; + result[j] = (value & (unsigned char)(240)) >> 4; + } + return NO_ERROR; + case 24: + j = k = 0; + for (i = 0; i < numitems; i++) { + result[j++] = 0; + result[j++] = data[k++]; + result[j++] = data[k++]; + result[j++] = data[k++]; + } + return NO_ERROR; + } + /* 3, 5, 6, 7 */ + if (itemsize < 8) { + int shr = 16; + u_uint16 value, mask, tmp; + j = k = 0; + value.b[MSB] = data[j++]; + value.b[LSB] = data[j++]; + mask.b[MSB] = bitmask(itemsize); + mask.b[LSB] = 0; + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[LSB]; + if (shr < itemsize) { + value.b[MSB] = value.b[LSB]; + value.b[LSB] = data[j++]; + mask.i <<= 8 - itemsize; + shr += 8; + } else { + mask.i >>= itemsize; + } + } + return NO_ERROR; + } + /* 9, 10, 11, 12, 13, 14, 15 */ + if (itemsize < 16) { + int shr = 32; + u_uint32 value, mask, tmp; + mask.i = 0; + j = k = 0; +#if MSB + for (i = 3; i >= 0; i--) { + value.b[i] = data[j++]; + } + mask.b[3] = 0xFF; + mask.b[2] = bitmask(itemsize-8); + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[0]; /* swap bytes */ + result[k++] = tmp.b[1]; + if (shr < itemsize) { + value.b[3] = value.b[1]; + value.b[2] = value.b[0]; + value.b[1] = data[j++]; + value.b[0] = data[j++]; + mask.i <<= 16 - itemsize; + shr += 16; + } else { + mask.i >>= itemsize; + } + } +#else + /* not implemented */ +#endif + return NO_ERROR; + } + /* 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 29, 30, 31 */ + if (itemsize < 32) { + int shr = 64; + u_uint64 value, mask, tmp; + mask.i = 0; + j = k = 0; +#if MSB + for (i = 7; i >= 0; i--) { + value.b[i] = data[j++]; + } + mask.b[7] = 0xFF; + mask.b[6] = 0xFF; + mask.b[5] = itemsize > 23 ? 0xFF : bitmask(itemsize-16); + mask.b[4] = itemsize < 24 ? 0x00 : bitmask(itemsize-24); + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[0]; /* swap bytes */ + result[k++] = tmp.b[1]; + result[k++] = tmp.b[2]; + result[k++] = tmp.b[3]; + if (shr < itemsize) { + value.b[7] = value.b[3]; + value.b[6] = value.b[2]; + value.b[5] = value.b[1]; + value.b[4] = value.b[0]; + value.b[3] = data[j++]; + value.b[2] = data[j++]; + value.b[1] = data[j++]; + value.b[0] = data[j++]; + mask.i <<= 32 - itemsize; + shr += 32; + } else { + mask.i >>= itemsize; + } + } +#else + /* Not implemented */ +#endif + return NO_ERROR; + } + return VALUE_ERROR; +} + +/*****************************************************************************/ +/* Python functions */ + +/** Unpack tightly packed integers. */ +char py_unpackints_doc[] = "Unpack groups of bits into numpy array."; + +static PyObject* +py_unpackints(PyObject *obj, PyObject *args, PyObject *kwds) +{ + PyObject *byteobj = NULL; + PyArrayObject *result = NULL; + PyArray_Descr *dtype = NULL; + char *encoded = NULL; + char *decoded = NULL; + Py_ssize_t encoded_len = 0; + Py_ssize_t decoded_len = 0; + Py_ssize_t runlen = 0; + Py_ssize_t i; + int storagesize, bytesize; + int itemsize = 0; + int skipbits = 0; + static char *kwlist[] = {"data", "dtype", "itemsize", "runlen", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&i|i", kwlist, + &byteobj, PyArray_DescrConverter, &dtype, &itemsize, &runlen)) + return NULL; + + Py_INCREF(byteobj); + + if (((itemsize < 1) || (itemsize > 32)) && (itemsize != 64)) { + PyErr_Format(PyExc_ValueError, "itemsize out of range"); + goto _fail; + } + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = PyBytes_GET_SIZE(byteobj); + bytesize = (int)ceil(itemsize / 8.0); + storagesize = bytesize < 3 ? bytesize : bytesize > 4 ? 8 : 4; + if ((encoded_len < bytesize) || (encoded_len > SSIZE_MAX / storagesize)) { + PyErr_Format(PyExc_ValueError, "data size out of range"); + goto _fail; + } + if (dtype->elsize != storagesize) { + PyErr_Format(PyExc_TypeError, "dtype.elsize doesn't fit itemsize"); + goto _fail; + } + + if (runlen == 0) { + runlen = (Py_ssize_t)(((uint64_t)encoded_len*8) / (uint64_t)itemsize); + } + skipbits = (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize) % 8); + if (skipbits > 0) { + skipbits = 8 - skipbits; + } + decoded_len = (Py_ssize_t)((uint64_t)runlen * (((uint64_t)encoded_len*8) / + ((uint64_t)runlen*(uint64_t)itemsize + (uint64_t)skipbits))); + + result = (PyArrayObject *)PyArray_SimpleNew(1, &decoded_len, + dtype->type_num); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "unable to allocate output array"); + goto _fail; + } + decoded = (char *)PyArray_DATA(result); + + for (i = 0; i < decoded_len; i+=runlen) { + if (NO_ERROR != + unpackbits((unsigned char *) encoded, + (ssize_t) encoded_len, + (int) itemsize, + (ssize_t) runlen, + (unsigned char *) decoded)) { + PyErr_Format(PyExc_ValueError, "unpackbits() failed"); + goto _fail; + } + encoded += (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize + + (uint64_t)skipbits) / 8); + decoded += runlen * storagesize; + } + + if ((dtype->byteorder != BOC) && (itemsize % 8 == 0)) { + switch (dtype->elsize) { + case 2: { + uint16_t *d = (uint16_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP2BYTES(*d); d++; + } + break; } + case 4: { + uint32_t *d = (uint32_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP4BYTES(*d); d++; + } + break; } + case 8: { + uint64_t *d = (uint64_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP8BYTES(*d); d++; + } + break; } + } + } + Py_DECREF(byteobj); + Py_DECREF(dtype); + return PyArray_Return(result); + + _fail: + Py_XDECREF(byteobj); + Py_XDECREF(result); + Py_XDECREF(dtype); + return NULL; +} + + +/** Decode TIFF PackBits encoded string. */ +char py_decodepackbits_doc[] = "Return TIFF PackBits decoded string."; + +static PyObject * +py_decodepackbits(PyObject *obj, PyObject *args) +{ + int n; + char e; + char *decoded = NULL; + char *encoded = NULL; + char *encoded_end = NULL; + char *encoded_pos = NULL; + unsigned int encoded_len; + unsigned int decoded_len; + PyObject *byteobj = NULL; + PyObject *result = NULL; + + if (!PyArg_ParseTuple(args, "O", &byteobj)) + return NULL; + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + Py_INCREF(byteobj); + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); + + /* release GIL: byte/string objects are immutable */ + Py_BEGIN_ALLOW_THREADS + + /* determine size of decoded string */ + encoded_pos = encoded; + encoded_end = encoded + encoded_len; + decoded_len = 0; + while (encoded_pos < encoded_end) { + n = (int)*encoded_pos++; + if (n >= 0) { + n++; + if (encoded_pos+n > encoded_end) + n = (int)(encoded_end - encoded_pos); + encoded_pos += n; + decoded_len += n; + } else if (n > -128) { + encoded_pos++; + decoded_len += 1-n; + } + } + Py_END_ALLOW_THREADS + + result = PyBytes_FromStringAndSize(0, decoded_len); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); + goto _fail; + } + decoded = PyBytes_AS_STRING(result); + + Py_BEGIN_ALLOW_THREADS + + /* decode string */ + encoded_end = encoded + encoded_len; + while (encoded < encoded_end) { + n = (int)*encoded++; + if (n >= 0) { + n++; + if (encoded+n > encoded_end) + n = (int)(encoded_end - encoded); + /* memmove(decoded, encoded, n); decoded += n; encoded += n; */ + while (n--) + *decoded++ = *encoded++; + } else if (n > -128) { + n = 1 - n; + e = *encoded++; + /* memset(decoded, e, n); decoded += n; */ + while (n--) + *decoded++ = e; + } + } + Py_END_ALLOW_THREADS + + Py_DECREF(byteobj); + return result; + + _fail: + Py_XDECREF(byteobj); + Py_XDECREF(result); + return NULL; +} + + +/** Decode TIFF LZW encoded string. */ +char py_decodelzw_doc[] = "Return TIFF LZW decoded string."; + +static PyObject * +py_decodelzw(PyObject *obj, PyObject *args) +{ + PyThreadState *_save = NULL; + PyObject *byteobj = NULL; + PyObject *result = NULL; + int i, j; + unsigned int encoded_len = 0; + unsigned int decoded_len = 0; + unsigned int result_len = 0; + unsigned int table_len = 0; + unsigned int len; + unsigned int code, c, oldcode, mask, shr; + uint64_t bitcount, bitw; + char *encoded = NULL; + char *result_ptr = NULL; + char *table2 = NULL; + char *cptr; + struct BYTE_STRING *decoded = NULL; + struct BYTE_STRING *decoded_ptr = NULL; + struct BYTE_STRING *table[4096]; + struct BYTE_STRING *newentry, *newresult, *t; + int little_endian = 0; + + if (!PyArg_ParseTuple(args, "O", &byteobj)) + return NULL; + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + Py_INCREF(byteobj); + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); + /* + if (encoded_len >= 512 * 1024 * 1024) { + PyErr_Format(PyExc_ValueError, "encoded data > 512 MB not supported"); + goto _fail; + } + */ + /* release GIL: byte/string objects are immutable */ + _save = PyEval_SaveThread(); + + if ((*encoded != -128) || ((*(encoded+1) & 128))) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_ValueError, + "strip must begin with CLEAR code"); + goto _fail; + } + little_endian = (*(unsigned short *)encoded) & 128; + + /* allocate buffer for codes and pointers */ + decoded_len = 0; + len = (encoded_len + encoded_len/9) * sizeof(decoded); + decoded = PyMem_Malloc(len * sizeof(void *)); + if (decoded == NULL) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded"); + goto _fail; + } + memset((void *)decoded, 0, len * sizeof(void *)); + decoded_ptr = decoded; + + /* cache strings of length 2 */ + cptr = table2 = PyMem_Malloc(256*256*2 * sizeof(char)); + if (table2 == NULL) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_MemoryError, "failed to allocate table2"); + goto _fail; + } + for (i = 0; i < 256; i++) { + for (j = 0; j < 256; j++) { + *cptr++ = (char)i; + *cptr++ = (char)j; + } + } + + memset(table, 0, sizeof(table)); + table_len = 258; + bitw = 9; + shr = 23; + mask = 4286578688; + bitcount = 0; + result_len = 0; + code = 0; + oldcode = 0; + + while ((unsigned int)((bitcount + bitw) / 8) <= encoded_len) { + /* read next code */ + code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); + if (little_endian) + code = SWAP4BYTES(code); + code <<= (unsigned int)(bitcount % 8); + code &= mask; + code >>= shr; + bitcount += bitw; + + if (code == 257) /* end of information */ + break; + + if (code == 256) { /* clearcode */ + /* initialize table and switch to 9 bit */ + while (table_len > 258) { + t = table[--table_len]; + t->ref--; + if (t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + bitw = 9; + shr = 23; + mask = 4286578688; + + /* read next code */ + code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); + if (little_endian) + code = SWAP4BYTES(code); + code <<= bitcount % 8; + code &= mask; + code >>= shr; + bitcount += bitw; + + if (code == 257) /* end of information */ + break; + + /* decoded.append(table[code]) */ + if (code < 256) { + result_len++; + *((int *)decoded_ptr++) = code; + } else { + newresult = table[code]; + newresult->ref++; + result_len += newresult->len; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + } + } else { + if (code < table_len) { + /* code is in table */ + /* newresult = table[code]; */ + /* newentry = table[oldcode] + table[code][0] */ + /* decoded.append(newresult); table.append(newentry) */ + if (code < 256) { + c = code; + *((unsigned int *)decoded_ptr++) = code; + result_len++; + } else { + newresult = table[code]; + newresult->ref++; + c = (unsigned int) *newresult->str; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + result_len += newresult->len; + } + newentry = PyMem_Malloc(sizeof(struct BYTE_STRING)); + newentry->ref = 1; + if (oldcode < 256) { + newentry->len = 2; + newentry->str = table2 + (oldcode << 9) + + ((unsigned char)c << 1); + } else { + len = table[oldcode]->len; + newentry->len = len + 1; + newentry->str = PyMem_Malloc(newentry->len); + if (newentry->str == NULL) + break; + memmove(newentry->str, table[oldcode]->str, len); + newentry->str[len] = c; + } + table[table_len++] = newentry; + } else { + /* code is not in table */ + /* newentry = newresult = table[oldcode] + table[oldcode][0] */ + /* decoded.append(newresult); table.append(newentry) */ + newresult = PyMem_Malloc(sizeof(struct BYTE_STRING)); + newentry = newresult; + newentry->ref = 2; + if (oldcode < 256) { + newentry->len = 2; + newentry->str = table2 + 514*oldcode; + } else { + len = table[oldcode]->len; + newentry->len = len + 1; + newentry->str = PyMem_Malloc(newentry->len); + if (newentry->str == NULL) + break; + memmove(newentry->str, table[oldcode]->str, len); + newentry->str[len] = *table[oldcode]->str; + } + table[table_len++] = newentry; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + result_len += newresult->len; + } + } + oldcode = code; + /* increase bit-width if necessary */ + switch (table_len) { + case 511: + bitw = 10; + shr = 22; + mask = 4290772992; + break; + case 1023: + bitw = 11; + shr = 21; + mask = 4292870144; + break; + case 2047: + bitw = 12; + shr = 20; + mask = 4293918720; + } + } + + PyEval_RestoreThread(_save); + + if (code != 257) { + PyErr_WarnEx(NULL, + "py_decodelzw encountered unexpected end of stream", 1); + } + + /* result = ''.join(decoded) */ + decoded_len = (unsigned int)(decoded_ptr - decoded); + decoded_ptr = decoded; + result = PyBytes_FromStringAndSize(0, result_len); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); + goto _fail; + } + result_ptr = PyBytes_AS_STRING(result); + + _save = PyEval_SaveThread(); + + while (decoded_len--) { + code = *((unsigned int *)decoded_ptr); + if (code < 256) { + *result_ptr++ = (char)code; + } else { + t = *((struct BYTE_STRING **)decoded_ptr); + memmove(result_ptr, t->str, t->len); + result_ptr += t->len; + if (--t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + decoded_ptr++; + } + PyMem_Free(decoded); + + while (table_len-- > 258) { + t = table[table_len]; + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + PyMem_Free(table2); + + PyEval_RestoreThread(_save); + + Py_DECREF(byteobj); + return result; + + _fail: + if (table2 != NULL) + PyMem_Free(table2); + if (decoded != NULL) { + /* Bug? are decoded_ptr and decoded_len correct? */ + while (decoded_len--) { + code = *((unsigned int *) decoded_ptr); + if (code > 258) { + t = *((struct BYTE_STRING **) decoded_ptr); + if (--t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + } + PyMem_Free(decoded); + } + while (table_len-- > 258) { + t = table[table_len]; + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + + Py_XDECREF(byteobj); + Py_XDECREF(result); + + return NULL; +} + +/*****************************************************************************/ +/* Create Python module */ + +char module_doc[] = + "A Python C extension module for decoding PackBits and LZW encoded " + "TIFF data.\n\n" + "Refer to the tifffile.py module for documentation and tests.\n\n" + "Authors:\n Christoph Gohlke \n" + " Laboratory for Fluorescence Dynamics, University of California, Irvine." + "\n\nVersion: %s\n"; + +static PyMethodDef module_methods[] = { +#if MSB + {"unpackints", (PyCFunction)py_unpackints, METH_VARARGS|METH_KEYWORDS, + py_unpackints_doc}, +#endif + {"decodelzw", (PyCFunction)py_decodelzw, METH_VARARGS, + py_decodelzw_doc}, + {"decodepackbits", (PyCFunction)py_decodepackbits, METH_VARARGS, + py_decodepackbits_doc}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#if PY_MAJOR_VERSION >= 3 + +struct module_state { + PyObject *error; +}; + +#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) + +static int module_traverse(PyObject *m, visitproc visit, void *arg) { + Py_VISIT(GETSTATE(m)->error); + return 0; +} + +static int module_clear(PyObject *m) { + Py_CLEAR(GETSTATE(m)->error); + return 0; +} + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_tifffile", + NULL, + sizeof(struct module_state), + module_methods, + NULL, + module_traverse, + module_clear, + NULL +}; + +#define INITERROR return NULL + +PyMODINIT_FUNC +PyInit__tifffile(void) + +#else + +#define INITERROR return + +PyMODINIT_FUNC +init_tifffile(void) + +#endif +{ + PyObject *module; + + char *doc = (char *)PyMem_Malloc(sizeof(module_doc) + sizeof(_VERSION_)); + PyOS_snprintf(doc, sizeof(doc), module_doc, _VERSION_); + +#if PY_MAJOR_VERSION >= 3 + moduledef.m_doc = doc; + module = PyModule_Create(&moduledef); +#else + module = Py_InitModule3("_tifffile", module_methods, doc); +#endif + + PyMem_Free(doc); + + if (module == NULL) + INITERROR; + + if (_import_array() < 0) { + Py_DECREF(module); + INITERROR; + } + + { +#if PY_MAJOR_VERSION < 3 + PyObject *s = PyString_FromString(_VERSION_); +#else + PyObject *s = PyUnicode_FromString(_VERSION_); +#endif + PyObject *dict = PyModule_GetDict(module); + PyDict_SetItemString(dict, "__version__", s); + Py_DECREF(s); + } + +#if PY_MAJOR_VERSION >= 3 + return module; +#endif +} + diff --git a/skimage/external/tifffile/tifffile_local.py b/skimage/external/tifffile/tifffile_local.py new file mode 100644 index 00000000..4ddd6763 --- /dev/null +++ b/skimage/external/tifffile/tifffile_local.py @@ -0,0 +1,4861 @@ + + +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# tifffile.py + +# Copyright (c) 2008-2014, Christoph Gohlke +# Copyright (c) 2008-2014, The Regents of the University of California +# Produced at the Laboratory for Fluorescence Dynamics +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the copyright holders nor the names of any +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Read and write image data from and to TIFF files. + +Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH, +SGI, ImageJ, MicroManager, FluoView, SEQ and GEL files. +Only a subset of the TIFF specification is supported, mainly uncompressed +and losslessly compressed 2**(0 to 6) bit integer, 16, 32 and 64-bit float, +grayscale and RGB(A) images, which are commonly used in bio-scientific imaging. +Specifically, reading JPEG and CCITT compressed image data or EXIF, IPTC, GPS, +and XMP metadata is not implemented. +Only primary info records are read for STK, FluoView, MicroManager, and +NIH image formats. + +TIFF, the Tagged Image File Format, is under the control of Adobe Systems. +BigTIFF allows for files greater than 4 GB. STK, LSM, FluoView, SGI, SEQ, GEL, +and OME-TIFF, are custom extensions defined by Molecular Devices (Universal +Imaging Corporation), Carl Zeiss MicroImaging, Olympus, Silicon Graphics +International, Media Cybernetics, Molecular Dynamics, and the Open Microscopy +Environment consortium respectively. + +For command line usage run ``python tifffile.py --help`` + +:Author: + `Christoph Gohlke `_ + +:Organization: + Laboratory for Fluorescence Dynamics, University of California, Irvine + +:Version: 2014.08.24 + +Requirements +------------ +* `CPython 2.7 or 3.4 `_ +* `Numpy 1.8.2 `_ +* `Matplotlib 1.4 `_ (optional for plotting) +* `Tifffile.c 2013.11.05 `_ + (recommended for faster decoding of PackBits and LZW encoded strings) + +Notes +----- +The API is not stable yet and might change between revisions. + +Tested on little-endian platforms only. + +Other Python packages and modules for reading bio-scientific TIFF files: + +* `Imread `_ +* `PyLibTiff `_ +* `SimpleITK `_ +* `PyLSM `_ +* `PyMca.TiffIO.py `_ (same as fabio.TiffIO) +* `BioImageXD.Readers `_ +* `Cellcognition.io `_ +* `CellProfiler.bioformats + `_ + +Acknowledgements +---------------- +* Egor Zindy, University of Manchester, for cz_lsm_scan_info specifics. +* Wim Lewis for a bug fix and some read_cz_lsm functions. +* Hadrien Mary for help on reading MicroManager files. + +References +---------- +(1) TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated. + http://partners.adobe.com/public/developer/tiff/ +(2) TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html +(3) MetaMorph Stack (STK) Image File Format. + http://support.meta.moleculardevices.com/docs/t10243.pdf +(4) Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010). + Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011 +(5) File Format Description - LSM 5xx Release 2.0. + http://ibb.gsf.de/homepage/karsten.rodenacker/IDL/Lsmfile.doc +(6) The OME-TIFF format. + http://www.openmicroscopy.org/site/support/file-formats/ome-tiff +(7) UltraQuant(r) Version 6.0 for Windows Start-Up Guide. + http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf +(8) Micro-Manager File Formats. + http://www.micro-manager.org/wiki/Micro-Manager_File_Formats +(9) Tags for TIFF and Related Specifications. Digital Preservation. + http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml + +Examples +-------- +>>> data = numpy.random.rand(5, 301, 219) +>>> imsave('temp.tif', data) + +>>> image = imread('temp.tif') +>>> numpy.testing.assert_array_equal(image, data) + +>>> with TiffFile('temp.tif') as tif: +... images = tif.asarray() +... for page in tif: +... for tag in page.tags.values(): +... t = tag.name, tag.value +... image = page.asarray() + +""" + +from __future__ import division, print_function + +import sys +import os +import re +import glob +import math +import zlib +import time +import json +import struct +import warnings +import tempfile +import datetime +import collections +from fractions import Fraction +from xml.etree import cElementTree as etree + +import numpy + +from . import _tifffile + +__version__ = '0.3.3' +__docformat__ = 'restructuredtext en' +__all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', + 'TiffSequence') + + +def imsave(filename, data, **kwargs): + """Write image data to TIFF file. + + Refer to the TiffWriter class and member functions for documentation. + + Parameters + ---------- + filename : str + Name of file to write. + data : array_like + Input image. The last dimensions are assumed to be image depth, + height, width, and samples. + kwargs : dict + Parameters 'byteorder', 'bigtiff', and 'software' are passed to + the TiffWriter class. + Parameters 'photometric', 'planarconfig', 'resolution', + 'description', 'compress', 'volume', and 'extratags' are passed to + the TiffWriter.save function. + + Examples + -------- + >>> data = numpy.random.rand(2, 5, 3, 301, 219) + >>> description = '{"shape": %s}' % str(list(data.shape)) + >>> imsave('temp.tif', data, compress=6, + ... extratags=[(270, 's', 0, description, True)]) + + """ + tifargs = {} + for key in ('byteorder', 'bigtiff', 'software', 'writeshape'): + if key in kwargs: + tifargs[key] = kwargs[key] + del kwargs[key] + + if 'writeshape' not in kwargs: + kwargs['writeshape'] = True + if 'bigtiff' not in tifargs and data.size*data.dtype.itemsize > 2000*2**20: + tifargs['bigtiff'] = True + + with TiffWriter(filename, **tifargs) as tif: + tif.save(data, **kwargs) + + +class TiffWriter(object): + """Write image data to TIFF file. + + TiffWriter instances must be closed using the close method, which is + automatically called when using the 'with' statement. + + Examples + -------- + >>> data = numpy.random.rand(2, 5, 3, 301, 219) + >>> with TiffWriter('temp.tif', bigtiff=True) as tif: + ... for i in range(data.shape[0]): + ... tif.save(data[i], compress=6) + + """ + TYPES = {'B': 1, 's': 2, 'H': 3, 'I': 4, '2I': 5, 'b': 6, + 'h': 8, 'i': 9, 'f': 11, 'd': 12, 'Q': 16, 'q': 17} + TAGS = { + 'new_subfile_type': 254, 'subfile_type': 255, + 'image_width': 256, 'image_length': 257, 'bits_per_sample': 258, + 'compression': 259, 'photometric': 262, 'fill_order': 266, + 'document_name': 269, 'image_description': 270, 'strip_offsets': 273, + 'orientation': 274, 'samples_per_pixel': 277, 'rows_per_strip': 278, + 'strip_byte_counts': 279, 'x_resolution': 282, 'y_resolution': 283, + 'planar_configuration': 284, 'page_name': 285, 'resolution_unit': 296, + 'software': 305, 'datetime': 306, 'predictor': 317, 'color_map': 320, + 'tile_width': 322, 'tile_length': 323, 'tile_offsets': 324, + 'tile_byte_counts': 325, 'extra_samples': 338, 'sample_format': 339, + 'image_depth': 32997, 'tile_depth': 32998} + + def __init__(self, filename, bigtiff=False, byteorder=None, + software='tifffile.py'): + """Create a new TIFF file for writing. + + Use bigtiff=True when creating files greater than 2 GB. + + Parameters + ---------- + filename : str + Name of file to write. + bigtiff : bool + If True, the BigTIFF format is used. + byteorder : {'<', '>'} + The endianness of the data in the file. + By default this is the system's native byte order. + software : str + Name of the software used to create the image. + Saved with the first page only. + + """ + if byteorder not in (None, '<', '>'): + raise ValueError("invalid byteorder %s" % byteorder) + if byteorder is None: + byteorder = '<' if sys.byteorder == 'little' else '>' + + self._byteorder = byteorder + self._software = software + + self._fh = open(filename, 'wb') + self._fh.write({'<': b'II', '>': b'MM'}[byteorder]) + + if bigtiff: + self._bigtiff = True + self._offset_size = 8 + self._tag_size = 20 + self._numtag_format = 'Q' + self._offset_format = 'Q' + self._val_format = '8s' + self._fh.write(struct.pack(byteorder+'HHH', 43, 8, 0)) + else: + self._bigtiff = False + self._offset_size = 4 + self._tag_size = 12 + self._numtag_format = 'H' + self._offset_format = 'I' + self._val_format = '4s' + self._fh.write(struct.pack(byteorder+'H', 42)) + + # first IFD + self._ifd_offset = self._fh.tell() + self._fh.write(struct.pack(byteorder+self._offset_format, 0)) + + def save(self, data, photometric=None, planarconfig=None, resolution=None, + description=None, volume=False, writeshape=False, compress=0, + extratags=()): + """Write image data to TIFF file. + + Image data are written in one stripe per plane. + Dimensions larger than 2 to 4 (depending on photometric mode, planar + configuration, and SGI mode) are flattened and saved as separate pages. + The 'sample_format' and 'bits_per_sample' TIFF tags are derived from + the data type. + + Parameters + ---------- + data : array_like + Input image. The last dimensions are assumed to be image depth, + height, width, and samples. + photometric : {'minisblack', 'miniswhite', 'rgb'} + The color space of the image data. + By default this setting is inferred from the data shape. + planarconfig : {'contig', 'planar'} + Specifies if samples are stored contiguous or in separate planes. + By default this setting is inferred from the data shape. + 'contig': last dimension contains samples. + 'planar': third last dimension contains samples. + resolution : (float, float) or ((int, int), (int, int)) + X and Y resolution in dots per inch as float or rational numbers. + description : str + The subject of the image. Saved with the first page only. + compress : int + Values from 0 to 9 controlling the level of zlib compression. + If 0, data are written uncompressed (default). + volume : bool + If True, volume data are stored in one tile (if applicable) using + the SGI image_depth and tile_depth tags. + Image width and depth must be multiple of 16. + Few software can read this format, e.g. MeVisLab. + writeshape : bool + If True, write the data shape to the image_description tag + if necessary and no other description is given. + extratags: sequence of tuples + Additional tags as [(code, dtype, count, value, writeonce)]. + + code : int + The TIFF tag Id. + dtype : str + Data type of items in 'value' in Python struct format. + One of B, s, H, I, 2I, b, h, i, f, d, Q, or q. + count : int + Number of data values. Not used for string values. + value : sequence + 'Count' values compatible with 'dtype'. + writeonce : bool + If True, the tag is written to the first page only. + + """ + if photometric not in (None, 'minisblack', 'miniswhite', 'rgb'): + raise ValueError("invalid photometric %s" % photometric) + if planarconfig not in (None, 'contig', 'planar'): + raise ValueError("invalid planarconfig %s" % planarconfig) + if not 0 <= compress <= 9: + raise ValueError("invalid compression level %s" % compress) + + fh = self._fh + byteorder = self._byteorder + numtag_format = self._numtag_format + val_format = self._val_format + offset_format = self._offset_format + offset_size = self._offset_size + tag_size = self._tag_size + + data = numpy.asarray(data, dtype=byteorder+data.dtype.char, order='C') + data_shape = shape = data.shape + data = numpy.atleast_2d(data) + + # normalize shape of data + samplesperpixel = 1 + extrasamples = 0 + if volume and data.ndim < 3: + volume = False + if photometric is None: + if planarconfig: + photometric = 'rgb' + elif data.ndim > 2 and shape[-1] in (3, 4): + photometric = 'rgb' + elif volume and data.ndim > 3 and shape[-4] in (3, 4): + photometric = 'rgb' + elif data.ndim > 2 and shape[-3] in (3, 4): + photometric = 'rgb' + else: + photometric = 'minisblack' + if planarconfig and len(shape) <= (3 if volume else 2): + planarconfig = None + photometric = 'minisblack' + if photometric == 'rgb': + if len(shape) < 3: + raise ValueError("not a RGB(A) image") + if len(shape) < 4: + volume = False + if planarconfig is None: + if shape[-1] in (3, 4): + planarconfig = 'contig' + elif shape[-4 if volume else -3] in (3, 4): + planarconfig = 'planar' + elif shape[-1] > shape[-4 if volume else -3]: + planarconfig = 'planar' + else: + planarconfig = 'contig' + if planarconfig == 'contig': + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + samplesperpixel = data.shape[-1] + else: + data = data.reshape( + (-1,) + shape[(-4 if volume else -3):] + (1,)) + samplesperpixel = data.shape[1] + if samplesperpixel > 3: + extrasamples = samplesperpixel - 3 + elif planarconfig and len(shape) > (3 if volume else 2): + if planarconfig == 'contig': + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + samplesperpixel = data.shape[-1] + else: + data = data.reshape( + (-1,) + shape[(-4 if volume else -3):] + (1,)) + samplesperpixel = data.shape[1] + extrasamples = samplesperpixel - 1 + else: + planarconfig = None + # remove trailing 1s + while len(shape) > 2 and shape[-1] == 1: + shape = shape[:-1] + if len(shape) < 3: + volume = False + if False and ( + len(shape) > (3 if volume else 2) and shape[-1] < 5 and + all(shape[-1] < i + for i in shape[(-4 if volume else -3):-1])): + # DISABLED: non-standard TIFF, e.g. (220, 320, 2) + planarconfig = 'contig' + samplesperpixel = shape[-1] + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + else: + data = data.reshape( + (-1, 1) + shape[(-3 if volume else -2):] + (1,)) + + if samplesperpixel == 2: + warnings.warn("writing non-standard TIFF (samplesperpixel 2)") + + if volume and (data.shape[-2] % 16 or data.shape[-3] % 16): + warnings.warn("volume width or length are not multiple of 16") + volume = False + data = numpy.swapaxes(data, 1, 2) + data = data.reshape( + (data.shape[0] * data.shape[1],) + data.shape[2:]) + + # data.shape is now normalized 5D or 6D, depending on volume + # (pages, planar_samples, (depth,) height, width, contig_samples) + assert len(data.shape) in (5, 6) + shape = data.shape + + bytestr = bytes if sys.version[0] == '2' else ( + lambda x: bytes(x, 'utf-8') if isinstance(x, str) else x) + tags = [] # list of (code, ifdentry, ifdvalue, writeonce) + + if volume: + # use tiles to save volume data + tag_byte_counts = TiffWriter.TAGS['tile_byte_counts'] + tag_offsets = TiffWriter.TAGS['tile_offsets'] + else: + # else use strips + tag_byte_counts = TiffWriter.TAGS['strip_byte_counts'] + tag_offsets = TiffWriter.TAGS['strip_offsets'] + + def pack(fmt, *val): + return struct.pack(byteorder+fmt, *val) + + def addtag(code, dtype, count, value, writeonce=False): + # Compute ifdentry & ifdvalue bytes from code, dtype, count, value. + # Append (code, ifdentry, ifdvalue, writeonce) to tags list. + code = int(TiffWriter.TAGS.get(code, code)) + try: + tifftype = TiffWriter.TYPES[dtype] + except KeyError: + raise ValueError("unknown dtype %s" % dtype) + rawcount = count + if dtype == 's': + value = bytestr(value) + b'\0' + count = rawcount = len(value) + value = (value, ) + if len(dtype) > 1: + count *= int(dtype[:-1]) + dtype = dtype[-1] + ifdentry = [pack('HH', code, tifftype), + pack(offset_format, rawcount)] + ifdvalue = None + if count == 1: + if isinstance(value, (tuple, list)): + value = value[0] + ifdentry.append(pack(val_format, pack(dtype, value))) + elif struct.calcsize(dtype) * count <= offset_size: + ifdentry.append(pack(val_format, + pack(str(count)+dtype, *value))) + else: + ifdentry.append(pack(offset_format, 0)) + ifdvalue = pack(str(count)+dtype, *value) + tags.append((code, b''.join(ifdentry), ifdvalue, writeonce)) + + def rational(arg, max_denominator=1000000): + # return nominator and denominator from float or two integers + try: + f = Fraction.from_float(arg) + except TypeError: + f = Fraction(arg[0], arg[1]) + f = f.limit_denominator(max_denominator) + return f.numerator, f.denominator + + if self._software: + addtag('software', 's', 0, self._software, writeonce=True) + self._software = None # only save to first page + if description: + addtag('image_description', 's', 0, description, writeonce=True) + elif writeshape and shape[0] > 1 and shape != data_shape: + addtag('image_description', 's', 0, + "shape=(%s)" % (",".join('%i' % i for i in data_shape)), + writeonce=True) + addtag('datetime', 's', 0, + datetime.datetime.now().strftime("%Y:%m:%d %H:%M:%S"), + writeonce=True) + addtag('compression', 'H', 1, 32946 if compress else 1) + addtag('orientation', 'H', 1, 1) + addtag('image_width', 'I', 1, shape[-2]) + addtag('image_length', 'I', 1, shape[-3]) + if volume: + addtag('image_depth', 'I', 1, shape[-4]) + addtag('tile_depth', 'I', 1, shape[-4]) + addtag('tile_width', 'I', 1, shape[-2]) + addtag('tile_length', 'I', 1, shape[-3]) + addtag('new_subfile_type', 'I', 1, 0 if shape[0] == 1 else 2) + addtag('sample_format', 'H', 1, + {'u': 1, 'i': 2, 'f': 3, 'c': 6}[data.dtype.kind]) + addtag('photometric', 'H', 1, + {'miniswhite': 0, 'minisblack': 1, 'rgb': 2}[photometric]) + addtag('samples_per_pixel', 'H', 1, samplesperpixel) + if planarconfig and samplesperpixel > 1: + addtag('planar_configuration', 'H', 1, 1 + if planarconfig == 'contig' else 2) + addtag('bits_per_sample', 'H', samplesperpixel, + (data.dtype.itemsize * 8, ) * samplesperpixel) + else: + addtag('bits_per_sample', 'H', 1, data.dtype.itemsize * 8) + if extrasamples: + if photometric == 'rgb' and extrasamples == 1: + addtag('extra_samples', 'H', 1, 1) # associated alpha channel + else: + addtag('extra_samples', 'H', extrasamples, (0,) * extrasamples) + if resolution: + addtag('x_resolution', '2I', 1, rational(resolution[0])) + addtag('y_resolution', '2I', 1, rational(resolution[1])) + addtag('resolution_unit', 'H', 1, 2) + addtag('rows_per_strip', 'I', 1, + shape[-3] * (shape[-4] if volume else 1)) + + # use one strip or tile per plane + strip_byte_counts = (data[0, 0].size * data.dtype.itemsize,) * shape[1] + addtag(tag_byte_counts, offset_format, shape[1], strip_byte_counts) + addtag(tag_offsets, offset_format, shape[1], (0, ) * shape[1]) + + # add extra tags from users + for t in extratags: + addtag(*t) + # the entries in an IFD must be sorted in ascending order by tag code + tags = sorted(tags, key=lambda x: x[0]) + + if not self._bigtiff and (fh.tell() + data.size*data.dtype.itemsize + > 2**31-1): + raise ValueError("data too large for non-bigtiff file") + + for pageindex in range(shape[0]): + # update pointer at ifd_offset + pos = fh.tell() + fh.seek(self._ifd_offset) + fh.write(pack(offset_format, pos)) + fh.seek(pos) + + # write ifdentries + fh.write(pack(numtag_format, len(tags))) + tag_offset = fh.tell() + fh.write(b''.join(t[1] for t in tags)) + self._ifd_offset = fh.tell() + fh.write(pack(offset_format, 0)) # offset to next IFD + + # write tag values and patch offsets in ifdentries, if necessary + for tagindex, tag in enumerate(tags): + if tag[2]: + pos = fh.tell() + fh.seek(tag_offset + tagindex*tag_size + offset_size + 4) + fh.write(pack(offset_format, pos)) + fh.seek(pos) + if tag[0] == tag_offsets: + strip_offsets_offset = pos + elif tag[0] == tag_byte_counts: + strip_byte_counts_offset = pos + fh.write(tag[2]) + + # write image data + data_offset = fh.tell() + if compress: + strip_byte_counts = [] + for plane in data[pageindex]: + plane = zlib.compress(plane, compress) + strip_byte_counts.append(len(plane)) + fh.write(plane) + else: + # if this fails try update Python/numpy + data[pageindex].tofile(fh) + fh.flush() + + # update strip and tile offsets and byte_counts if necessary + pos = fh.tell() + for tagindex, tag in enumerate(tags): + if tag[0] == tag_offsets: # strip or tile offsets + if tag[2]: + fh.seek(strip_offsets_offset) + strip_offset = data_offset + for size in strip_byte_counts: + fh.write(pack(offset_format, strip_offset)) + strip_offset += size + else: + fh.seek(tag_offset + tagindex*tag_size + + offset_size + 4) + fh.write(pack(offset_format, data_offset)) + elif tag[0] == tag_byte_counts: # strip or tile byte_counts + if compress: + if tag[2]: + fh.seek(strip_byte_counts_offset) + for size in strip_byte_counts: + fh.write(pack(offset_format, size)) + else: + fh.seek(tag_offset + tagindex*tag_size + + offset_size + 4) + fh.write(pack(offset_format, strip_byte_counts[0])) + break + fh.seek(pos) + fh.flush() + # remove tags that should be written only once + if pageindex == 0: + tags = [t for t in tags if not t[-1]] + + def close(self): + self._fh.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +def imread(files, **kwargs): + """Return image data from TIFF file(s) as numpy array. + + The first image series is returned if no arguments are provided. + + Parameters + ---------- + files : str or list + File name, glob pattern, or list of file names. + key : int, slice, or sequence of page indices + Defines which pages to return as array. + series : int + Defines which series of pages in file to return as array. + multifile : bool + If True (default), OME-TIFF data may include pages from multiple files. + pattern : str + Regular expression pattern that matches axes names and indices in + file names. + kwargs : dict + Additional parameters passed to the TiffFile or TiffSequence asarray + function. + + Examples + -------- + >>> im = imread('temp.tif', key=0) + >>> im.shape + (3, 301, 219) + >>> ims = imread(['temp.tif', 'temp.tif']) + >>> ims.shape + (2, 10, 3, 301, 219) + + """ + kwargs_file = {} + if 'multifile' in kwargs: + kwargs_file['multifile'] = kwargs['multifile'] + del kwargs['multifile'] + else: + kwargs_file['multifile'] = True + kwargs_seq = {} + if 'pattern' in kwargs: + kwargs_seq['pattern'] = kwargs['pattern'] + del kwargs['pattern'] + + if isinstance(files, basestring) and any(i in files for i in '?*'): + files = glob.glob(files) + if not files: + raise ValueError('no files found') + if len(files) == 1: + files = files[0] + + if isinstance(files, basestring): + with TiffFile(files, **kwargs_file) as tif: + return tif.asarray(**kwargs) + else: + with TiffSequence(files, **kwargs_seq) as imseq: + return imseq.asarray(**kwargs) + + +class lazyattr(object): + """Lazy object attribute whose value is computed on first access.""" + __slots__ = ('func', ) + + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + if instance is None: + return self + value = self.func(instance) + if value is NotImplemented: + return getattr(super(owner, instance), self.func.__name__) + setattr(instance, self.func.__name__, value) + return value + + +class TiffFile(object): + """Read image and metadata from TIFF, STK, LSM, and FluoView files. + + TiffFile instances must be closed using the close method, which is + automatically called when using the 'with' statement. + + Attributes + ---------- + pages : list + All TIFF pages in file. + series : list of Records(shape, dtype, axes, TiffPages) + TIFF pages with compatible shapes and types. + micromanager_metadata: dict + Extra MicroManager non-TIFF metadata in the file, if exists. + + All attributes are read-only. + + Examples + -------- + >>> with TiffFile('temp.tif') as tif: + ... data = tif.asarray() + ... data.shape + (5, 301, 219) + + """ + def __init__(self, arg, name=None, offset=None, size=None, + multifile=True, multifile_close=True): + """Initialize instance from file. + + Parameters + ---------- + arg : str or open file + Name of file or open file object. + The file objects are closed in TiffFile.close(). + name : str + Optional name of file in case 'arg' is a file handle. + offset : int + Optional start position of embedded file. By default this is + the current file position. + size : int + Optional size of embedded file. By default this is the number + of bytes from the 'offset' to the end of the file. + multifile : bool + If True (default), series may include pages from multiple files. + Currently applies to OME-TIFF only. + multifile_close : bool + If True (default), keep the handles of other files in multifile + series closed. This is inefficient when few files refer to + many pages. If False, the C runtime may run out of resources. + + """ + self._fh = FileHandle(arg, name=name, offset=offset, size=size) + self.offset_size = None + self.pages = [] + self._multifile = bool(multifile) + self._multifile_close = bool(multifile_close) + self._files = {self._fh.name: self} # cache of TiffFiles + try: + self._fromfile() + except Exception: + self._fh.close() + raise + + @property + def filehandle(self): + """Return file handle.""" + return self._fh + + @property + def filename(self): + """Return name of file handle.""" + return self._fh.name + + def close(self): + """Close open file handle(s).""" + for tif in self._files.values(): + tif._fh.close() + self._files = {} + + def _fromfile(self): + """Read TIFF header and all page records from file.""" + self._fh.seek(0) + try: + self.byteorder = {b'II': '<', b'MM': '>'}[self._fh.read(2)] + except KeyError: + raise ValueError("not a valid TIFF file") + version = struct.unpack(self.byteorder+'H', self._fh.read(2))[0] + if version == 43: # BigTiff + self.offset_size, zero = struct.unpack(self.byteorder+'HH', + self._fh.read(4)) + if zero or self.offset_size != 8: + raise ValueError("not a valid BigTIFF file") + elif version == 42: + self.offset_size = 4 + else: + raise ValueError("not a TIFF file") + self.pages = [] + while True: + try: + page = TiffPage(self) + self.pages.append(page) + except StopIteration: + break + if not self.pages: + raise ValueError("empty TIFF file") + + if self.is_micromanager: + # MicroManager files contain metadata not stored in TIFF tags. + self.micromanager_metadata = read_micromanager_metadata(self._fh) + + if self.is_lsm: + self._fix_lsm_strip_offsets() + self._fix_lsm_strip_byte_counts() + + def _fix_lsm_strip_offsets(self): + """Unwrap strip offsets for LSM files greater than 4 GB.""" + for series in self.series: + wrap = 0 + previous_offset = 0 + for page in series.pages: + strip_offsets = [] + for current_offset in page.strip_offsets: + if current_offset < previous_offset: + wrap += 2**32 + strip_offsets.append(current_offset + wrap) + previous_offset = current_offset + page.strip_offsets = tuple(strip_offsets) + + def _fix_lsm_strip_byte_counts(self): + """Set strip_byte_counts to size of compressed data. + + The strip_byte_counts tag in LSM files contains the number of bytes + for the uncompressed data. + + """ + if not self.pages: + return + strips = {} + for page in self.pages: + assert len(page.strip_offsets) == len(page.strip_byte_counts) + for offset, bytecount in zip(page.strip_offsets, + page.strip_byte_counts): + strips[offset] = bytecount + offsets = sorted(strips.keys()) + offsets.append(min(offsets[-1] + strips[offsets[-1]], self._fh.size)) + for i, offset in enumerate(offsets[:-1]): + strips[offset] = min(strips[offset], offsets[i+1] - offset) + for page in self.pages: + if page.compression: + page.strip_byte_counts = tuple( + strips[offset] for offset in page.strip_offsets) + + @lazyattr + def series(self): + """Return series of TiffPage with compatible shape and properties.""" + if not self.pages: + return [] + + series = [] + page0 = self.pages[0] + + if self.is_ome: + series = self._omeseries() + elif self.is_fluoview: + dims = {b'X': 'X', b'Y': 'Y', b'Z': 'Z', b'T': 'T', + b'WAVELENGTH': 'C', b'TIME': 'T', b'XY': 'R', + b'EVENT': 'V', b'EXPOSURE': 'L'} + mmhd = list(reversed(page0.mm_header.dimensions)) + series = [Record( + axes=''.join(dims.get(i[0].strip().upper(), 'Q') + for i in mmhd if i[1] > 1), + shape=tuple(int(i[1]) for i in mmhd if i[1] > 1), + pages=self.pages, dtype=numpy.dtype(page0.dtype))] + elif self.is_lsm: + lsmi = page0.cz_lsm_info + axes = CZ_SCAN_TYPES[lsmi.scan_type] + if page0.is_rgb: + axes = axes.replace('C', '').replace('XY', 'XYC') + axes = axes[::-1] + shape = tuple(getattr(lsmi, CZ_DIMENSIONS[i]) for i in axes) + pages = [p for p in self.pages if not p.is_reduced] + series = [Record(axes=axes, shape=shape, pages=pages, + dtype=numpy.dtype(pages[0].dtype))] + if len(pages) != len(self.pages): # reduced RGB pages + pages = [p for p in self.pages if p.is_reduced] + cp = 1 + i = 0 + while cp < len(pages) and i < len(shape)-2: + cp *= shape[i] + i += 1 + shape = shape[:i] + pages[0].shape + axes = axes[:i] + 'CYX' + series.append(Record(axes=axes, shape=shape, pages=pages, + dtype=numpy.dtype(pages[0].dtype))) + elif self.is_imagej: + shape = [] + axes = [] + ij = page0.imagej_tags + if 'frames' in ij: + shape.append(ij['frames']) + axes.append('T') + if 'slices' in ij: + shape.append(ij['slices']) + axes.append('Z') + if 'channels' in ij and not self.is_rgb: + shape.append(ij['channels']) + axes.append('C') + remain = len(self.pages) // (product(shape) if shape else 1) + if remain > 1: + shape.append(remain) + axes.append('I') + shape.extend(page0.shape) + axes.extend(page0.axes) + axes = ''.join(axes) + series = [Record(pages=self.pages, shape=tuple(shape), axes=axes, + dtype=numpy.dtype(page0.dtype))] + elif self.is_nih: + if len(self.pages) == 1: + shape = page0.shape + axes = page0.axes + else: + shape = (len(self.pages),) + page0.shape + axes = 'I' + page0.axes + series = [Record(pages=self.pages, shape=shape, axes=axes, + dtype=numpy.dtype(page0.dtype))] + elif page0.is_shaped: + # TODO: shaped files can contain multiple series + shape = page0.tags['image_description'].value[7:-1] + shape = tuple(int(i) for i in shape.split(b',')) + series = [Record(pages=self.pages, shape=shape, + axes='Q' * len(shape), + dtype=numpy.dtype(page0.dtype))] + + # generic detection of series + if not series: + shapes = [] + pages = {} + for page in self.pages: + if not page.shape: + continue + shape = page.shape + (page.axes, + page.compression in TIFF_DECOMPESSORS) + if shape not in pages: + shapes.append(shape) + pages[shape] = [page] + else: + pages[shape].append(page) + series = [Record(pages=pages[s], + axes=(('I' + s[-2]) + if len(pages[s]) > 1 else s[-2]), + dtype=numpy.dtype(pages[s][0].dtype), + shape=((len(pages[s]), ) + s[:-2] + if len(pages[s]) > 1 else s[:-2])) + for s in shapes] + + # remove empty series, e.g. in MD Gel files + series = [s for s in series if sum(s.shape) > 0] + + return series + + def asarray(self, key=None, series=None, memmap=False): + """Return image data from multiple TIFF pages as numpy array. + + By default the first image series is returned. + + Parameters + ---------- + key : int, slice, or sequence of page indices + Defines which pages to return as array. + series : int + Defines which series of pages to return as array. + memmap : bool + If True, return an array stored in a binary file on disk + if possible. + + """ + if key is None and series is None: + series = 0 + if series is not None: + pages = self.series[series].pages + else: + pages = self.pages + + if key is None: + pass + elif isinstance(key, int): + pages = [pages[key]] + elif isinstance(key, slice): + pages = pages[key] + elif isinstance(key, collections.Iterable): + pages = [pages[k] for k in key] + else: + raise TypeError("key must be an int, slice, or sequence") + + if not len(pages): + raise ValueError("no pages selected") + + if self.is_nih: + if pages[0].is_palette: + result = stack_pages(pages, colormapped=False, squeeze=False) + result = numpy.take(pages[0].color_map, result, axis=1) + result = numpy.swapaxes(result, 0, 1) + else: + result = stack_pages(pages, memmap=memmap, + colormapped=False, squeeze=False) + elif len(pages) == 1: + return pages[0].asarray(memmap=memmap) + elif self.is_ome: + assert not self.is_palette, "color mapping disabled for ome-tiff" + if any(p is None for p in pages): + # zero out missing pages + firstpage = next(p for p in pages if p) + nopage = numpy.zeros_like( + firstpage.asarray(memmap=False)) + s = self.series[series] + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=s.dtype, shape=s.shape) + result = result.reshape(-1) + else: + result = numpy.empty(s.shape, s.dtype).reshape(-1) + index = 0 + + class KeepOpen: + # keep Tiff files open between consecutive pages + def __init__(self, parent, close): + self.master = parent + self.parent = parent + self._close = close + + def open(self, page): + if self._close and page and page.parent != self.parent: + if self.parent != self.master: + self.parent.filehandle.close() + self.parent = page.parent + self.parent.filehandle.open() + + def close(self): + if self._close and self.parent != self.master: + self.parent.filehandle.close() + + keep = KeepOpen(self, self._multifile_close) + for page in pages: + keep.open(page) + if page: + a = page.asarray(memmap=False, colormapped=False, + reopen=False) + else: + a = nopage + try: + result[index:index + a.size] = a.reshape(-1) + except ValueError as e: + warnings.warn("ome-tiff: %s" % e) + break + index += a.size + keep.close() + else: + result = stack_pages(pages, memmap=memmap) + + if key is None: + try: + result.shape = self.series[series].shape + except ValueError: + try: + warnings.warn("failed to reshape %s to %s" % ( + result.shape, self.series[series].shape)) + # try series of expected shapes + result.shape = (-1,) + self.series[series].shape + except ValueError: + # revert to generic shape + result.shape = (-1,) + pages[0].shape + else: + result.shape = (-1,) + pages[0].shape + return result + + def _omeseries(self): + """Return image series in OME-TIFF file(s).""" + root = etree.fromstring(self.pages[0].tags['image_description'].value) + uuid = root.attrib.get('UUID', None) + self._files = {uuid: self} + dirname = self._fh.dirname + modulo = {} + result = [] + for element in root: + if element.tag.endswith('BinaryOnly'): + warnings.warn("ome-xml: not an ome-tiff master file") + break + if element.tag.endswith('StructuredAnnotations'): + for annot in element: + if not annot.attrib.get('Namespace', + '').endswith('modulo'): + continue + for value in annot: + for modul in value: + for along in modul: + if not along.tag[:-1].endswith('Along'): + continue + axis = along.tag[-1] + newaxis = along.attrib.get('Type', 'other') + newaxis = AXES_LABELS[newaxis] + if 'Start' in along.attrib: + labels = range( + int(along.attrib['Start']), + int(along.attrib['End']) + 1, + int(along.attrib.get('Step', 1))) + else: + labels = [label.text for label in along + if label.tag.endswith('Label')] + modulo[axis] = (newaxis, labels) + if not element.tag.endswith('Image'): + continue + for pixels in element: + if not pixels.tag.endswith('Pixels'): + continue + atr = pixels.attrib + dtype = atr.get('Type', None) + axes = ''.join(reversed(atr['DimensionOrder'])) + shape = list(int(atr['Size'+ax]) for ax in axes) + size = product(shape[:-2]) + ifds = [None] * size + for data in pixels: + if not data.tag.endswith('TiffData'): + continue + atr = data.attrib + ifd = int(atr.get('IFD', 0)) + num = int(atr.get('NumPlanes', 1 if 'IFD' in atr else 0)) + num = int(atr.get('PlaneCount', num)) + idx = [int(atr.get('First'+ax, 0)) for ax in axes[:-2]] + try: + idx = numpy.ravel_multi_index(idx, shape[:-2]) + except ValueError: + # ImageJ produces invalid ome-xml when cropping + warnings.warn("ome-xml: invalid TiffData index") + continue + for uuid in data: + if not uuid.tag.endswith('UUID'): + continue + if uuid.text not in self._files: + if not self._multifile: + # abort reading multifile OME series + # and fall back to generic series + return [] + fname = uuid.attrib['FileName'] + try: + tif = TiffFile(os.path.join(dirname, fname)) + except (IOError, ValueError): + tif.close() + warnings.warn( + "ome-xml: failed to read '%s'" % fname) + break + self._files[uuid.text] = tif + if self._multifile_close: + tif.close() + pages = self._files[uuid.text].pages + try: + for i in range(num if num else len(pages)): + ifds[idx + i] = pages[ifd + i] + except IndexError: + warnings.warn("ome-xml: index out of range") + # only process first uuid + break + else: + pages = self.pages + try: + for i in range(num if num else len(pages)): + ifds[idx + i] = pages[ifd + i] + except IndexError: + warnings.warn("ome-xml: index out of range") + if all(i is None for i in ifds): + # skip images without data + continue + dtype = next(i for i in ifds if i).dtype + result.append(Record(axes=axes, shape=shape, pages=ifds, + dtype=numpy.dtype(dtype))) + + for record in result: + for axis, (newaxis, labels) in modulo.items(): + i = record.axes.index(axis) + size = len(labels) + if record.shape[i] == size: + record.axes = record.axes.replace(axis, newaxis, 1) + else: + record.shape[i] //= size + record.shape.insert(i+1, size) + record.axes = record.axes.replace(axis, axis+newaxis, 1) + record.shape = tuple(record.shape) + + # squeeze dimensions + for record in result: + record.shape, record.axes = squeeze_axes(record.shape, record.axes) + + return result + + def __len__(self): + """Return number of image pages in file.""" + return len(self.pages) + + def __getitem__(self, key): + """Return specified page.""" + return self.pages[key] + + def __iter__(self): + """Return iterator over pages.""" + return iter(self.pages) + + def __str__(self): + """Return string containing information about file.""" + result = [ + self._fh.name.capitalize(), + format_size(self._fh.size), + {'<': 'little endian', '>': 'big endian'}[self.byteorder]] + if self.is_bigtiff: + result.append("bigtiff") + if len(self.pages) > 1: + result.append("%i pages" % len(self.pages)) + if len(self.series) > 1: + result.append("%i series" % len(self.series)) + if len(self._files) > 1: + result.append("%i files" % (len(self._files))) + return ", ".join(result) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + @lazyattr + def fstat(self): + try: + return os.fstat(self._fh.fileno()) + except Exception: # io.UnsupportedOperation + return None + + @lazyattr + def is_bigtiff(self): + return self.offset_size != 4 + + @lazyattr + def is_rgb(self): + return all(p.is_rgb for p in self.pages) + + @lazyattr + def is_palette(self): + return all(p.is_palette for p in self.pages) + + @lazyattr + def is_mdgel(self): + return any(p.is_mdgel for p in self.pages) + + @lazyattr + def is_mediacy(self): + return any(p.is_mediacy for p in self.pages) + + @lazyattr + def is_stk(self): + return all(p.is_stk for p in self.pages) + + @lazyattr + def is_lsm(self): + return self.pages[0].is_lsm + + @lazyattr + def is_imagej(self): + return self.pages[0].is_imagej + + @lazyattr + def is_micromanager(self): + return self.pages[0].is_micromanager + + @lazyattr + def is_nih(self): + return self.pages[0].is_nih + + @lazyattr + def is_fluoview(self): + return self.pages[0].is_fluoview + + @lazyattr + def is_ome(self): + return self.pages[0].is_ome + + +class TiffPage(object): + """A TIFF image file directory (IFD). + + Attributes + ---------- + index : int + Index of page in file. + dtype : str {TIFF_SAMPLE_DTYPES} + Data type of image, colormapped if applicable. + shape : tuple + Dimensions of the image array in TIFF page, + colormapped and with one alpha channel if applicable. + axes : str + Axes label codes: + 'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane, + 'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda, + 'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime, + 'L' exposure, 'V' event, 'Q' unknown, '_' missing + tags : TiffTags + Dictionary of tags in page. + Tag values are also directly accessible as attributes. + color_map : numpy array + Color look up table, if exists. + cz_lsm_scan_info: Record(dict) + LSM scan info attributes, if exists. + imagej_tags: Record(dict) + Consolidated ImageJ description and metadata tags, if exists. + uic_tags: Record(dict) + Consolidated MetaMorph STK/UIC tags, if exists. + + All attributes are read-only. + + Notes + ----- + The internal, normalized '_shape' attribute is 6 dimensional: + + 0. number planes (stk) + 1. planar samples_per_pixel + 2. image_depth Z (sgi) + 3. image_length Y + 4. image_width X + 5. contig samples_per_pixel + + """ + def __init__(self, parent): + """Initialize instance from file.""" + self.parent = parent + self.index = len(parent.pages) + self.shape = self._shape = () + self.dtype = self._dtype = None + self.axes = "" + self.tags = TiffTags() + + self._fromfile() + self._process_tags() + + def _fromfile(self): + """Read TIFF IFD structure and its tags from file. + + File cursor must be at storage position of IFD offset and is left at + offset to next IFD. + + Raises StopIteration if offset (first bytes read) is 0. + + """ + fh = self.parent.filehandle + byteorder = self.parent.byteorder + offset_size = self.parent.offset_size + + fmt = {4: 'I', 8: 'Q'}[offset_size] + offset = struct.unpack(byteorder + fmt, fh.read(offset_size))[0] + if not offset: + raise StopIteration() + + # read standard tags + tags = self.tags + fh.seek(offset) + fmt, size = {4: ('H', 2), 8: ('Q', 8)}[offset_size] + try: + numtags = struct.unpack(byteorder + fmt, fh.read(size))[0] + except Exception: + warnings.warn("corrupted page list") + raise StopIteration() + + tagcode = 0 + for _ in range(numtags): + try: + tag = TiffTag(self.parent) + # print(tag) + except TiffTag.Error as e: + warnings.warn(str(e)) + continue + if tagcode > tag.code: + # expected for early LSM and tifffile versions + warnings.warn("tags are not ordered by code") + tagcode = tag.code + if tag.name not in tags: + tags[tag.name] = tag + else: + # some files contain multiple IFD with same code + # e.g. MicroManager files contain two image_description + i = 1 + while True: + name = "%s_%i" % (tag.name, i) + if name not in tags: + tags[name] = tag + break + + pos = fh.tell() + + if self.is_lsm or (self.index and self.parent.is_lsm): + # correct non standard LSM bitspersample tags + self.tags['bits_per_sample']._correct_lsm_bitspersample(self) + + if self.is_lsm: + # read LSM info subrecords + for name, reader in CZ_LSM_INFO_READERS.items(): + try: + offset = self.cz_lsm_info['offset_'+name] + except KeyError: + continue + if offset < 8: + # older LSM revision + continue + fh.seek(offset) + try: + setattr(self, 'cz_lsm_'+name, reader(fh)) + except ValueError: + pass + + elif self.is_stk and 'uic1tag' in tags and not tags['uic1tag'].value: + # read uic1tag now that plane count is known + uic1tag = tags['uic1tag'] + fh.seek(uic1tag.value_offset) + tags['uic1tag'].value = Record( + read_uic1tag(fh, byteorder, uic1tag.dtype, uic1tag.count, + tags['uic2tag'].count)) + fh.seek(pos) + + def _process_tags(self): + """Validate standard tags and initialize attributes. + + Raise ValueError if tag values are not supported. + + """ + tags = self.tags + for code, (name, default, dtype, count, validate) in TIFF_TAGS.items(): + if not (name in tags or default is None): + tags[name] = TiffTag(code, dtype=dtype, count=count, + value=default, name=name) + if name in tags and validate: + try: + if tags[name].count == 1: + setattr(self, name, validate[tags[name].value]) + else: + setattr(self, name, tuple( + validate[value] for value in tags[name].value)) + except KeyError: + raise ValueError("%s.value (%s) not supported" % + (name, tags[name].value)) + + tag = tags['bits_per_sample'] + if tag.count == 1: + self.bits_per_sample = tag.value + else: + # LSM might list more items than samples_per_pixel + value = tag.value[:self.samples_per_pixel] + if any((v-value[0] for v in value)): + self.bits_per_sample = value + else: + self.bits_per_sample = value[0] + + tag = tags['sample_format'] + if tag.count == 1: + self.sample_format = TIFF_SAMPLE_FORMATS[tag.value] + else: + value = tag.value[:self.samples_per_pixel] + if any((v-value[0] for v in value)): + self.sample_format = [TIFF_SAMPLE_FORMATS[v] for v in value] + else: + self.sample_format = TIFF_SAMPLE_FORMATS[value[0]] + + if 'photometric' not in tags: + self.photometric = None + + if 'image_depth' not in tags: + self.image_depth = 1 + + if 'image_length' in tags: + self.strips_per_image = int(math.floor( + float(self.image_length + self.rows_per_strip - 1) / + self.rows_per_strip)) + else: + self.strips_per_image = 0 + + key = (self.sample_format, self.bits_per_sample) + self.dtype = self._dtype = TIFF_SAMPLE_DTYPES.get(key, None) + + if 'image_length' not in self.tags or 'image_width' not in self.tags: + # some GEL file pages are missing image data + self.image_length = 0 + self.image_width = 0 + self.image_depth = 0 + self.strip_offsets = 0 + self._shape = () + self.shape = () + self.axes = '' + + if self.is_palette: + self.dtype = self.tags['color_map'].dtype[1] + self.color_map = numpy.array(self.color_map, self.dtype) + dmax = self.color_map.max() + if dmax < 256: + self.dtype = numpy.uint8 + self.color_map = self.color_map.astype(self.dtype) + #else: + # self.dtype = numpy.uint8 + # self.color_map >>= 8 + # self.color_map = self.color_map.astype(self.dtype) + self.color_map.shape = (3, -1) + + # determine shape of data + image_length = self.image_length + image_width = self.image_width + image_depth = self.image_depth + samples_per_pixel = self.samples_per_pixel + + if self.is_stk: + assert self.image_depth == 1 + planes = self.tags['uic2tag'].count + if self.is_contig: + self._shape = (planes, 1, 1, image_length, image_width, + samples_per_pixel) + if samples_per_pixel == 1: + self.shape = (planes, image_length, image_width) + self.axes = 'YX' + else: + self.shape = (planes, image_length, image_width, + samples_per_pixel) + self.axes = 'YXS' + else: + self._shape = (planes, samples_per_pixel, 1, image_length, + image_width, 1) + if samples_per_pixel == 1: + self.shape = (planes, image_length, image_width) + self.axes = 'YX' + else: + self.shape = (planes, samples_per_pixel, image_length, + image_width) + self.axes = 'SYX' + # detect type of series + if planes == 1: + self.shape = self.shape[1:] + elif numpy.all(self.uic2tag.z_distance != 0): + self.axes = 'Z' + self.axes + elif numpy.all(numpy.diff(self.uic2tag.time_created) != 0): + self.axes = 'T' + self.axes + else: + self.axes = 'I' + self.axes + # DISABLED + if self.is_palette: + assert False, "color mapping disabled for stk" + if self.color_map.shape[1] >= 2**self.bits_per_sample: + if image_depth == 1: + self.shape = (3, planes, image_length, image_width) + else: + self.shape = (3, planes, image_depth, image_length, + image_width) + self.axes = 'C' + self.axes + else: + warnings.warn("palette cannot be applied") + self.is_palette = False + elif self.is_palette: + samples = 1 + if 'extra_samples' in self.tags: + samples += len(self.extra_samples) + if self.is_contig: + self._shape = (1, 1, image_depth, image_length, image_width, + samples) + else: + self._shape = (1, samples, image_depth, image_length, + image_width, 1) + if self.color_map.shape[1] >= 2**self.bits_per_sample: + if image_depth == 1: + self.shape = (3, image_length, image_width) + self.axes = 'CYX' + else: + self.shape = (3, image_depth, image_length, image_width) + self.axes = 'CZYX' + else: + warnings.warn("palette cannot be applied") + self.is_palette = False + if image_depth == 1: + self.shape = (image_length, image_width) + self.axes = 'YX' + else: + self.shape = (image_depth, image_length, image_width) + self.axes = 'ZYX' + elif self.is_rgb or samples_per_pixel > 1: + if self.is_contig: + self._shape = (1, 1, image_depth, image_length, image_width, + samples_per_pixel) + if image_depth == 1: + self.shape = (image_length, image_width, samples_per_pixel) + self.axes = 'YXS' + else: + self.shape = (image_depth, image_length, image_width, + samples_per_pixel) + self.axes = 'ZYXS' + else: + self._shape = (1, samples_per_pixel, image_depth, + image_length, image_width, 1) + if image_depth == 1: + self.shape = (samples_per_pixel, image_length, image_width) + self.axes = 'SYX' + else: + self.shape = (samples_per_pixel, image_depth, + image_length, image_width) + self.axes = 'SZYX' + if False and self.is_rgb and 'extra_samples' in self.tags: + # DISABLED: only use RGB and first alpha channel if exists + extra_samples = self.extra_samples + if self.tags['extra_samples'].count == 1: + extra_samples = (extra_samples, ) + for exs in extra_samples: + if exs in ('unassalpha', 'assocalpha', 'unspecified'): + if self.is_contig: + self.shape = self.shape[:-1] + (4,) + else: + self.shape = (4,) + self.shape[1:] + break + else: + self._shape = (1, 1, image_depth, image_length, image_width, 1) + if image_depth == 1: + self.shape = (image_length, image_width) + self.axes = 'YX' + else: + self.shape = (image_depth, image_length, image_width) + self.axes = 'ZYX' + if not self.compression and 'strip_byte_counts' not in tags: + self.strip_byte_counts = ( + product(self.shape) * (self.bits_per_sample // 8), ) + + assert len(self.shape) == len(self.axes) + + def asarray(self, squeeze=True, colormapped=True, rgbonly=False, + scale_mdgel=False, memmap=False, reopen=True): + """Read image data from file and return as numpy array. + + Raise ValueError if format is unsupported. + If any of 'squeeze', 'colormapped', or 'rgbonly' are not the default, + the shape of the returned array might be different from the page shape. + + Parameters + ---------- + squeeze : bool + If True, all length-1 dimensions (except X and Y) are + squeezed out from result. + colormapped : bool + If True, color mapping is applied for palette-indexed images. + rgbonly : bool + If True, return RGB(A) image without additional extra samples. + memmap : bool + If True, use numpy.memmap to read arrays from file if possible. + For use on 64 bit systems and files with few huge contiguous data. + reopen : bool + If True and the parent file handle is closed, the file is + temporarily re-opened (and closed if no exception occurs). + scale_mdgel : bool + If True, MD Gel data will be scaled according to the private + metadata in the second TIFF page. The dtype will be float32. + + """ + if not self._shape: + return + + if self.dtype is None: + raise ValueError("data type not supported: %s%i" % ( + self.sample_format, self.bits_per_sample)) + if self.compression not in TIFF_DECOMPESSORS: + raise ValueError("cannot decompress %s" % self.compression) + tag = self.tags['sample_format'] + if tag.count != 1 and any((i-tag.value[0] for i in tag.value)): + raise ValueError("sample formats don't match %s" % str(tag.value)) + + fh = self.parent.filehandle + closed = fh.closed + if closed: + if reopen: + fh.open() + else: + raise IOError("file handle is closed") + + dtype = self._dtype + shape = self._shape + image_width = self.image_width + image_length = self.image_length + image_depth = self.image_depth + typecode = self.parent.byteorder + dtype + bits_per_sample = self.bits_per_sample + + if self.is_tiled: + if 'tile_offsets' in self.tags: + byte_counts = self.tile_byte_counts + offsets = self.tile_offsets + else: + byte_counts = self.strip_byte_counts + offsets = self.strip_offsets + tile_width = self.tile_width + tile_length = self.tile_length + tile_depth = self.tile_depth if 'tile_depth' in self.tags else 1 + tw = (image_width + tile_width - 1) // tile_width + tl = (image_length + tile_length - 1) // tile_length + td = (image_depth + tile_depth - 1) // tile_depth + shape = (shape[0], shape[1], + td*tile_depth, tl*tile_length, tw*tile_width, shape[-1]) + tile_shape = (tile_depth, tile_length, tile_width, shape[-1]) + runlen = tile_width + else: + byte_counts = self.strip_byte_counts + offsets = self.strip_offsets + runlen = image_width + + if any(o < 2 for o in offsets): + raise ValueError("corrupted page") + + if memmap and self._is_memmappable(rgbonly, colormapped): + result = fh.memmap_array(typecode, shape, offset=offsets[0]) + elif self.is_contiguous: + fh.seek(offsets[0]) + result = fh.read_array(typecode, product(shape)) + result = result.astype('=' + dtype) + else: + if self.is_contig: + runlen *= self.samples_per_pixel + if bits_per_sample in (8, 16, 32, 64, 128): + if (bits_per_sample * runlen) % 8: + raise ValueError("data and sample size mismatch") + + def unpack(x): + try: + return numpy.fromstring(x, typecode) + except ValueError as e: + # strips may be missing EOI + warnings.warn("unpack: %s" % e) + xlen = ((len(x) // (bits_per_sample // 8)) + * (bits_per_sample // 8)) + return numpy.fromstring(x[:xlen], typecode) + + elif isinstance(bits_per_sample, tuple): + def unpack(x): + return unpackrgb(x, typecode, bits_per_sample) + else: + def unpack(x): + return unpackints(x, typecode, bits_per_sample, runlen) + + decompress = TIFF_DECOMPESSORS[self.compression] + if self.compression == 'jpeg': + table = self.jpeg_tables if 'jpeg_tables' in self.tags else b'' + decompress = lambda x: decodejpg(x, table, self.photometric) + + if self.is_tiled: + result = numpy.empty(shape, dtype) + tw, tl, td, pl = 0, 0, 0, 0 + for offset, bytecount in zip(offsets, byte_counts): + fh.seek(offset) + tile = unpack(decompress(fh.read(bytecount))) + tile.shape = tile_shape + if self.predictor == 'horizontal': + numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile) + result[0, pl, td:td+tile_depth, + tl:tl+tile_length, tw:tw+tile_width, :] = tile + del tile + tw += tile_width + if tw >= shape[4]: + tw, tl = 0, tl + tile_length + if tl >= shape[3]: + tl, td = 0, td + tile_depth + if td >= shape[2]: + td, pl = 0, pl + 1 + result = result[..., + :image_depth, :image_length, :image_width, :] + else: + strip_size = (self.rows_per_strip * self.image_width * + self.samples_per_pixel) + result = numpy.empty(shape, dtype).reshape(-1) + index = 0 + for offset, bytecount in zip(offsets, byte_counts): + fh.seek(offset) + strip = fh.read(bytecount) + strip = decompress(strip) + strip = unpack(strip) + size = min(result.size, strip.size, strip_size, + result.size - index) + result[index:index+size] = strip[:size] + del strip + index += size + + result.shape = self._shape + + if self.predictor == 'horizontal' and not (self.is_tiled and not + self.is_contiguous): + # work around bug in LSM510 software + if not (self.parent.is_lsm and not self.compression): + numpy.cumsum(result, axis=-2, dtype=dtype, out=result) + + if colormapped and self.is_palette: + if self.color_map.shape[1] >= 2**bits_per_sample: + # FluoView and LSM might fail here + result = numpy.take(self.color_map, + result[:, 0, :, :, :, 0], axis=1) + elif rgbonly and self.is_rgb and 'extra_samples' in self.tags: + # return only RGB and first alpha channel if exists + extra_samples = self.extra_samples + if self.tags['extra_samples'].count == 1: + extra_samples = (extra_samples, ) + for i, exs in enumerate(extra_samples): + if exs in ('unassalpha', 'assocalpha', 'unspecified'): + if self.is_contig: + result = result[..., [0, 1, 2, 3+i]] + else: + result = result[:, [0, 1, 2, 3+i]] + break + else: + if self.is_contig: + result = result[..., :3] + else: + result = result[:, :3] + + if squeeze: + try: + result.shape = self.shape + except ValueError: + warnings.warn("failed to reshape from %s to %s" % ( + str(result.shape), str(self.shape))) + + if scale_mdgel and self.parent.is_mdgel: + # MD Gel stores private metadata in the second page + tags = self.parent.pages[1] + if tags.md_file_tag in (2, 128): + scale = tags.md_scale_pixel + scale = scale[0] / scale[1] # rational + result = result.astype('float32') + if tags.md_file_tag == 2: + result **= 2 # squary root data format + result *= scale + + if closed: + # TODO: file remains open if an exception occurred above + fh.close() + return result + + def _is_memmappable(self, rgbonly, colormapped): + """Return if image data in file can be memory mapped.""" + if not self.parent.filehandle.is_file or not self.is_contiguous: + return False + return not (self.predictor or + (rgbonly and 'extra_samples' in self.tags) or + (colormapped and self.is_palette) or + ({'big': '>', 'little': '<'}[sys.byteorder] != + self.parent.byteorder)) + + @lazyattr + def is_contiguous(self): + """Return offset and size of contiguous data, else None. + + Excludes prediction and colormapping. + + """ + if self.compression or self.bits_per_sample not in (8, 16, 32, 64): + return + if self.is_tiled: + if (self.image_width != self.tile_width or + self.image_length % self.tile_length or + self.tile_width % 16 or self.tile_length % 16): + return + if ('image_depth' in self.tags and 'tile_depth' in self.tags and + (self.image_length != self.tile_length or + self.image_depth % self.tile_depth)): + return + offsets = self.tile_offsets + byte_counts = self.tile_byte_counts + else: + offsets = self.strip_offsets + byte_counts = self.strip_byte_counts + if len(offsets) == 1: + return offsets[0], byte_counts[0] + if self.is_stk or all(offsets[i] + byte_counts[i] == offsets[i+1] + or byte_counts[i+1] == 0 # no data/ignore offset + for i in range(len(offsets)-1)): + return offsets[0], sum(byte_counts) + + def __str__(self): + """Return string containing information about page.""" + s = ', '.join(s for s in ( + ' x '.join(str(i) for i in self.shape), + str(numpy.dtype(self.dtype)), + '%s bit' % str(self.bits_per_sample), + self.photometric if 'photometric' in self.tags else '', + self.compression if self.compression else 'raw', + '|'.join(t[3:] for t in ( + 'is_stk', 'is_lsm', 'is_nih', 'is_ome', 'is_imagej', + 'is_micromanager', 'is_fluoview', 'is_mdgel', 'is_mediacy', + 'is_sgi', 'is_reduced', 'is_tiled', + 'is_contiguous') if getattr(self, t))) if s) + return "Page %i: %s" % (self.index, s) + + def __getattr__(self, name): + """Return tag value.""" + if name in self.tags: + value = self.tags[name].value + setattr(self, name, value) + return value + raise AttributeError(name) + + @lazyattr + def uic_tags(self): + """Consolidate UIC tags.""" + if not self.is_stk: + raise AttributeError("uic_tags") + tags = self.tags + result = Record() + result.number_planes = tags['uic2tag'].count + if 'image_description' in tags: + result.plane_descriptions = self.image_description.split(b'\x00') + if 'uic1tag' in tags: + result.update(tags['uic1tag'].value) + if 'uic3tag' in tags: + result.update(tags['uic3tag'].value) # wavelengths + if 'uic4tag' in tags: + result.update(tags['uic4tag'].value) # override uic1 tags + uic2tag = tags['uic2tag'].value + result.z_distance = uic2tag.z_distance + result.time_created = uic2tag.time_created + result.time_modified = uic2tag.time_modified + try: + result.datetime_created = [ + julian_datetime(*dt) for dt in + zip(uic2tag.date_created, uic2tag.time_created)] + result.datetime_modified = [ + julian_datetime(*dt) for dt in + zip(uic2tag.date_modified, uic2tag.time_modified)] + except ValueError as e: + warnings.warn("uic_tags: %s" % e) + return result + + @lazyattr + def imagej_tags(self): + """Consolidate ImageJ metadata.""" + if not self.is_imagej: + raise AttributeError("imagej_tags") + tags = self.tags + if 'image_description_1' in tags: + # MicroManager + result = imagej_description(tags['image_description_1'].value) + else: + result = imagej_description(tags['image_description'].value) + if 'imagej_metadata' in tags: + try: + result.update(imagej_metadata( + tags['imagej_metadata'].value, + tags['imagej_byte_counts'].value, + self.parent.byteorder)) + except Exception as e: + warnings.warn(str(e)) + return Record(result) + + @lazyattr + def is_rgb(self): + """True if page contains a RGB image.""" + return ('photometric' in self.tags and + self.tags['photometric'].value == 2) + + @lazyattr + def is_contig(self): + """True if page contains a contiguous image.""" + return ('planar_configuration' in self.tags and + self.tags['planar_configuration'].value == 1) + + @lazyattr + def is_palette(self): + """True if page contains a palette-colored image and not OME or STK.""" + try: + # turn off color mapping for OME-TIFF and STK + if self.is_stk or self.is_ome or self.parent.is_ome: + return False + except IndexError: + pass # OME-XML not found in first page + return ('photometric' in self.tags and + self.tags['photometric'].value == 3) + + @lazyattr + def is_tiled(self): + """True if page contains tiled image.""" + return 'tile_width' in self.tags + + @lazyattr + def is_reduced(self): + """True if page is a reduced image of another image.""" + return bool(self.tags['new_subfile_type'].value & 1) + + @lazyattr + def is_mdgel(self): + """True if page contains md_file_tag tag.""" + return 'md_file_tag' in self.tags + + @lazyattr + def is_mediacy(self): + """True if page contains Media Cybernetics Id tag.""" + return ('mc_id' in self.tags and + self.tags['mc_id'].value.startswith(b'MC TIFF')) + + @lazyattr + def is_stk(self): + """True if page contains UIC2Tag tag.""" + return 'uic2tag' in self.tags + + @lazyattr + def is_lsm(self): + """True if page contains LSM CZ_LSM_INFO tag.""" + return 'cz_lsm_info' in self.tags + + @lazyattr + def is_fluoview(self): + """True if page contains FluoView MM_STAMP tag.""" + return 'mm_stamp' in self.tags + + @lazyattr + def is_nih(self): + """True if page contains NIH image header.""" + return 'nih_image_header' in self.tags + + @lazyattr + def is_sgi(self): + """True if page contains SGI image and tile depth tags.""" + return 'image_depth' in self.tags and 'tile_depth' in self.tags + + @lazyattr + def is_ome(self): + """True if page contains OME-XML in image_description tag.""" + return ('image_description' in self.tags and self.tags[ + 'image_description'].value.startswith(b' parent.offset_size or code in CUSTOM_TAGS: + pos = fh.tell() + tof = {4: 'I', 8: 'Q'}[parent.offset_size] + self.value_offset = offset = struct.unpack(byteorder+tof, value)[0] + if offset < 0 or offset > parent.filehandle.size: + raise TiffTag.Error("corrupt file - invalid tag value offset") + elif offset < 4: + raise TiffTag.Error("corrupt value offset for tag %i" % code) + fh.seek(offset) + if code in CUSTOM_TAGS: + readfunc = CUSTOM_TAGS[code][1] + value = readfunc(fh, byteorder, dtype, count) + if isinstance(value, dict): # numpy.core.records.record + value = Record(value) + elif code in TIFF_TAGS or dtype[-1] == 's': + value = struct.unpack(fmt, fh.read(size)) + else: + value = read_numpy(fh, byteorder, dtype, count) + fh.seek(pos) + else: + value = struct.unpack(fmt, value[:size]) + + if code not in CUSTOM_TAGS and code not in (273, 279, 324, 325): + # scalar value if not strip/tile offsets/byte_counts + if len(value) == 1: + value = value[0] + + if (dtype.endswith('s') and isinstance(value, bytes) + and self._type != 7): + # TIFF ASCII fields can contain multiple strings, + # each terminated with a NUL + value = stripascii(value) + + self.code = code + self.name = name + self.dtype = dtype + self.count = count + self.value = value + + def _correct_lsm_bitspersample(self, parent): + """Correct LSM bitspersample tag. + + Old LSM writers may use a separate region for two 16-bit values, + although they fit into the tag value element of the tag. + + """ + if self.code == 258 and self.count == 2: + # TODO: test this. Need example file. + warnings.warn("correcting LSM bitspersample tag") + fh = parent.filehandle + tof = {4: '') + + def __str__(self): + """Return string containing information about tag.""" + return ' '.join(str(getattr(self, s)) for s in self.__slots__) + + +class TiffSequence(object): + """Sequence of image files. + + The data shape and dtype of all files must match. + + Properties + ---------- + files : list + List of file names. + shape : tuple + Shape of image sequence. + axes : str + Labels of axes in shape. + + Examples + -------- + >>> tifs = TiffSequence("test.oif.files/*.tif") # doctest: +SKIP + >>> tifs.shape, tifs.axes # doctest: +SKIP + ((2, 100), 'CT') + >>> data = tifs.asarray() # doctest: +SKIP + >>> data.shape # doctest: +SKIP + (2, 100, 256, 256) + + """ + _patterns = { + 'axes': r""" + # matches Olympus OIF and Leica TIFF series + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4})) + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + """} + + class ParseError(Exception): + pass + + def __init__(self, files, imread=TiffFile, pattern='axes', + *args, **kwargs): + """Initialize instance from multiple files. + + Parameters + ---------- + files : str, or sequence of str + Glob pattern or sequence of file names. + imread : function or class + Image read function or class with asarray function returning numpy + array from single file. + pattern : str + Regular expression pattern that matches axes names and sequence + indices in file names. + By default this matches Olympus OIF and Leica TIFF series. + + """ + if isinstance(files, basestring): + files = natural_sorted(glob.glob(files)) + files = list(files) + if not files: + raise ValueError("no files found") + #if not os.path.isfile(files[0]): + # raise ValueError("file not found") + self.files = files + + if hasattr(imread, 'asarray'): + # redefine imread + _imread = imread + + def imread(fname, *args, **kwargs): + with _imread(fname) as im: + return im.asarray(*args, **kwargs) + + self.imread = imread + + self.pattern = self._patterns.get(pattern, pattern) + try: + self._parse() + if not self.axes: + self.axes = 'I' + except self.ParseError: + self.axes = 'I' + self.shape = (len(files),) + self._start_index = (0,) + self._indices = tuple((i,) for i in range(len(files))) + + def __str__(self): + """Return string with information about image sequence.""" + return "\n".join([ + self.files[0], + '* files: %i' % len(self.files), + '* axes: %s' % self.axes, + '* shape: %s' % str(self.shape)]) + + def __len__(self): + return len(self.files) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + pass + + def asarray(self, memmap=False, *args, **kwargs): + """Read image data from all files and return as single numpy array. + + If memmap is True, return an array stored in a binary file on disk. + The args and kwargs parameters are passed to the imread function. + + Raise IndexError or ValueError if image shapes don't match. + + """ + im = self.imread(self.files[0], *args, **kwargs) + shape = self.shape + im.shape + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=im.dtype, shape=shape) + else: + result = numpy.zeros(shape, dtype=im.dtype) + result = result.reshape(-1, *im.shape) + for index, fname in zip(self._indices, self.files): + index = [i-j for i, j in zip(index, self._start_index)] + index = numpy.ravel_multi_index(index, self.shape) + im = self.imread(fname, *args, **kwargs) + result[index] = im + result.shape = shape + return result + + def _parse(self): + """Get axes and shape from file names.""" + if not self.pattern: + raise self.ParseError("invalid pattern") + pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE) + matches = pattern.findall(self.files[0]) + if not matches: + raise self.ParseError("pattern doesn't match file names") + matches = matches[-1] + if len(matches) % 2: + raise self.ParseError("pattern doesn't match axis name and index") + axes = ''.join(m for m in matches[::2] if m) + if not axes: + raise self.ParseError("pattern doesn't match file names") + + indices = [] + for fname in self.files: + matches = pattern.findall(fname)[-1] + if axes != ''.join(m for m in matches[::2] if m): + raise ValueError("axes don't match within the image sequence") + indices.append([int(m) for m in matches[1::2] if m]) + shape = tuple(numpy.max(indices, axis=0)) + start_index = tuple(numpy.min(indices, axis=0)) + shape = tuple(i-j+1 for i, j in zip(shape, start_index)) + if product(shape) != len(self.files): + warnings.warn("files are missing. Missing data are zeroed") + + self.axes = axes.upper() + self.shape = shape + self._indices = indices + self._start_index = start_index + + +class Record(dict): + """Dictionary with attribute access. + + Can also be initialized with numpy.core.records.record. + + """ + __slots__ = () + + def __init__(self, arg=None, **kwargs): + if kwargs: + arg = kwargs + elif arg is None: + arg = {} + try: + dict.__init__(self, arg) + except (TypeError, ValueError): + for i, name in enumerate(arg.dtype.names): + v = arg[i] + self[name] = v if v.dtype.char != 'S' else stripnull(v) + + def __getattr__(self, name): + return self[name] + + def __setattr__(self, name, value): + self.__setitem__(name, value) + + def __str__(self): + """Pretty print Record.""" + s = [] + lists = [] + for k in sorted(self): + try: + if k.startswith('_'): # does not work with byte + continue + except AttributeError: + pass + v = self[k] + if isinstance(v, (list, tuple)) and len(v): + if isinstance(v[0], Record): + lists.append((k, v)) + continue + elif isinstance(v[0], TiffPage): + v = [i.index for i in v if i] + s.append( + ("* %s: %s" % (k, str(v))).split("\n", 1)[0] + [:PRINT_LINE_LEN].rstrip()) + for k, v in lists: + l = [] + for i, w in enumerate(v): + l.append("* %s[%i]\n %s" % (k, i, + str(w).replace("\n", "\n "))) + s.append('\n'.join(l)) + return '\n'.join(s) + + +class TiffTags(Record): + """Dictionary of TiffTag with attribute access.""" + + def __str__(self): + """Return string with information about all tags.""" + s = [] + for tag in sorted(self.values(), key=lambda x: x.code): + typecode = "%i%s" % (tag.count * int(tag.dtype[0]), tag.dtype[1]) + line = "* %i %s (%s) %s" % ( + tag.code, tag.name, typecode, tag.as_str()) + s.append(line[:PRINT_LINE_LEN].lstrip()) + return '\n'.join(s) + + +class FileHandle(object): + """Binary file handle. + + * Handle embedded files (for CZI within CZI files). + * Allow to re-open closed files (for multi file formats such as OME-TIFF). + * Read numpy arrays and records from file like objects. + + Only binary read, seek, tell, and close are supported on embedded files. + When initialized from another file handle, do not use it unless this + FileHandle is closed. + + Attributes + ---------- + name : str + Name of the file. + path : str + Absolute path to file. + size : int + Size of file in bytes. + is_file : bool + If True, file has a filno and can be memory mapped. + + All attributes are read-only. + + """ + __slots__ = ('_fh', '_arg', '_mode', '_name', '_dir', + '_offset', '_size', '_close', 'is_file') + + def __init__(self, arg, mode='rb', name=None, offset=None, size=None): + """Initialize file handle from file name or another file handle. + + Parameters + ---------- + arg : str, File, or FileHandle + File name or open file handle. + mode : str + File open mode in case 'arg' is a file name. + name : str + Optional name of file in case 'arg' is a file handle. + offset : int + Optional start position of embedded file. By default this is + the current file position. + size : int + Optional size of embedded file. By default this is the number + of bytes from the 'offset' to the end of the file. + + """ + self._fh = None + self._arg = arg + self._mode = mode + self._name = name + self._dir = '' + self._offset = offset + self._size = size + self._close = True + self.is_file = False + self.open() + + def open(self): + """Open or re-open file.""" + if self._fh: + return # file is open + + if isinstance(self._arg, basestring): + # file name + self._arg = os.path.abspath(self._arg) + self._dir, self._name = os.path.split(self._arg) + self._fh = open(self._arg, self._mode) + self._close = True + if self._offset is None: + self._offset = 0 + elif isinstance(self._arg, FileHandle): + # FileHandle + self._fh = self._arg._fh + if self._offset is None: + self._offset = 0 + self._offset += self._arg._offset + self._close = False + if not self._name: + if self._offset: + name, ext = os.path.splitext(self._arg._name) + self._name = "%s@%i%s" % (name, self._offset, ext) + else: + self._name = self._arg._name + self._dir = self._arg._dir + else: + # open file object + self._fh = self._arg + if self._offset is None: + self._offset = self._arg.tell() + self._close = False + if not self._name: + try: + self._dir, self._name = os.path.split(self._fh.name) + except AttributeError: + self._name = "Unnamed stream" + + if self._offset: + self._fh.seek(self._offset) + + if self._size is None: + pos = self._fh.tell() + self._fh.seek(self._offset, 2) + self._size = self._fh.tell() + self._fh.seek(pos) + + try: + self._fh.fileno() + self.is_file = True + except Exception: + self.is_file = False + + def read(self, size=-1): + """Read 'size' bytes from file, or until EOF is reached.""" + if size < 0 and self._offset: + size = self._size + return self._fh.read(size) + + def memmap_array(self, dtype, shape, offset=0, mode='r', order='C'): + """Return numpy.memmap of data stored in file.""" + if not self.is_file: + raise ValueError("Can not memory map file without fileno.") + return numpy.memmap(self._fh, dtype=dtype, mode=mode, + offset=self._offset + offset, + shape=shape, order=order) + + def read_array(self, dtype, count=-1, sep=""): + """Return numpy array from file. + + Work around numpy issue #2230, "numpy.fromfile does not accept + StringIO object" https://github.com/numpy/numpy/issues/2230. + + """ + try: + return numpy.fromfile(self._fh, dtype, count, sep) + except IOError: + if count < 0: + size = self._size + else: + size = count * numpy.dtype(dtype).itemsize + data = self._fh.read(size) + return numpy.fromstring(data, dtype, count, sep) + + def read_record(self, dtype, shape=1, byteorder=None): + """Return numpy record from file.""" + try: + rec = numpy.rec.fromfile(self._fh, dtype, shape, + byteorder=byteorder) + except Exception: + dtype = numpy.dtype(dtype) + if shape is None: + shape = self._size // dtype.itemsize + size = product(sequence(shape)) * dtype.itemsize + data = self._fh.read(size) + return numpy.rec.fromstring(data, dtype, shape, + byteorder=byteorder) + return rec[0] if shape == 1 else rec + + def tell(self): + """Return file's current position.""" + return self._fh.tell() - self._offset + + def seek(self, offset, whence=0): + """Set file's current position.""" + if self._offset: + if whence == 0: + self._fh.seek(self._offset + offset, whence) + return + elif whence == 2: + self._fh.seek(self._offset + self._size + offset, 0) + return + self._fh.seek(offset, whence) + + def close(self): + """Close file.""" + if self._close and self._fh: + self._fh.close() + self._fh = None + self.is_file = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def __getattr__(self, name): + """Return attribute from underlying file object.""" + if self._offset: + warnings.warn( + "FileHandle: '%s' not implemented for embedded files" % name) + return getattr(self._fh, name) + + @property + def name(self): + return self._name + + @property + def dirname(self): + return self._dir + + @property + def path(self): + return os.path.join(self._dir, self._name) + + @property + def size(self): + return self._size + + @property + def closed(self): + return self._fh is None + + +def read_bytes(fh, byteorder, dtype, count): + """Read tag data from file and return as byte string.""" + dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] + return fh.read_array(dtype, count).tostring() + + +def read_numpy(fh, byteorder, dtype, count): + """Read tag data from file and return as numpy array.""" + dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] + return fh.read_array(dtype, count) + + +def read_json(fh, byteorder, dtype, count): + """Read JSON tag data from file and return as object.""" + data = fh.read(count) + try: + return json.loads(unicode(stripnull(data), 'utf-8')) + except ValueError: + warnings.warn("invalid JSON `%s`" % data) + + +def read_mm_header(fh, byteorder, dtype, count): + """Read MM_HEADER tag from file and return as numpy.rec.array.""" + return fh.read_record(MM_HEADER, byteorder=byteorder) + + +def read_mm_stamp(fh, byteorder, dtype, count): + """Read MM_STAMP tag from file and return as numpy.array.""" + return fh.read_array(byteorder+'f8', 8) + + +def read_uic1tag(fh, byteorder, dtype, count, plane_count=None): + """Read MetaMorph STK UIC1Tag from file and return as dictionary. + + Return empty dictionary if plane_count is unknown. + + """ + assert dtype in ('2I', '1I') and byteorder == '<' + result = {} + if dtype == '2I': + # pre MetaMorph 2.5 (not tested) + values = fh.read_array(' structure_size: + break + cz_lsm_info.append((name, dtype)) + else: + cz_lsm_info = CZ_LSM_INFO + + return fh.read_record(cz_lsm_info, byteorder=byteorder) + + +def read_cz_lsm_floatpairs(fh): + """Read LSM sequence of float pairs from file and return as list.""" + size = struct.unpack(' 0: + esize, etime, etype = struct.unpack(''}[fh.read(2)] + except IndexError: + raise ValueError("not a MicroManager TIFF file") + + results = {} + fh.seek(8) + (index_header, index_offset, display_header, display_offset, + comments_header, comments_offset, summary_header, summary_length + ) = struct.unpack(byteorder + "IIIIIIII", fh.read(32)) + + if summary_header != 2355492: + raise ValueError("invalid MicroManager summary_header") + results['summary'] = read_json(fh, byteorder, None, summary_length) + + if index_header != 54773648: + raise ValueError("invalid MicroManager index_header") + fh.seek(index_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 3453623: + raise ValueError("invalid MicroManager index_header") + data = struct.unpack(byteorder + "IIIII"*count, fh.read(20*count)) + results['index_map'] = { + 'channel': data[::5], 'slice': data[1::5], 'frame': data[2::5], + 'position': data[3::5], 'offset': data[4::5]} + + if display_header != 483765892: + raise ValueError("invalid MicroManager display_header") + fh.seek(display_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 347834724: + raise ValueError("invalid MicroManager display_header") + results['display_settings'] = read_json(fh, byteorder, None, count) + + if comments_header != 99384722: + raise ValueError("invalid MicroManager comments_header") + fh.seek(comments_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 84720485: + raise ValueError("invalid MicroManager comments_header") + results['comments'] = read_json(fh, byteorder, None, count) + + return results + + +def imagej_metadata(data, bytecounts, byteorder): + """Return dict from ImageJ metadata tag value.""" + _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') + + def read_string(data, byteorder): + return _str(stripnull(data[0 if byteorder == '<' else 1::2])) + + def read_double(data, byteorder): + return struct.unpack(byteorder+('d' * (len(data) // 8)), data) + + def read_bytes(data, byteorder): + #return struct.unpack('b' * len(data), data) + return numpy.fromstring(data, 'uint8') + + metadata_types = { # big endian + b'info': ('info', read_string), + b'labl': ('labels', read_string), + b'rang': ('ranges', read_double), + b'luts': ('luts', read_bytes), + b'roi ': ('roi', read_bytes), + b'over': ('overlays', read_bytes)} + metadata_types.update( # little endian + dict((k[::-1], v) for k, v in metadata_types.items())) + + if not bytecounts: + raise ValueError("no ImageJ metadata") + + if not data[:4] in (b'IJIJ', b'JIJI'): + raise ValueError("invalid ImageJ metadata") + + header_size = bytecounts[0] + if header_size < 12 or header_size > 804: + raise ValueError("invalid ImageJ metadata header size") + + ntypes = (header_size - 4) // 8 + header = struct.unpack(byteorder+'4sI'*ntypes, data[4:4+ntypes*8]) + pos = 4 + ntypes * 8 + counter = 0 + result = {} + for mtype, count in zip(header[::2], header[1::2]): + values = [] + name, func = metadata_types.get(mtype, (_str(mtype), read_bytes)) + for _ in range(count): + counter += 1 + pos1 = pos + bytecounts[counter] + values.append(func(data[pos:pos1], byteorder)) + pos = pos1 + result[name.strip()] = values[0] if count == 1 else values + return result + + +def imagej_description(description): + """Return dict from ImageJ image_description tag.""" + def _bool(val): + return {b'true': True, b'false': False}[val.lower()] + + _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') + result = {} + for line in description.splitlines(): + try: + key, val = line.split(b'=') + except Exception: + continue + key = key.strip() + val = val.strip() + for dtype in (int, float, _bool, _str): + try: + val = dtype(val) + break + except Exception: + pass + result[_str(key)] = val + return result + + +def _replace_by(module_function, package=None, warn=False): + """Try replace decorated function by module.function. + + This is used to replace local functions with functions from another + (usually compiled) module, if available. + + Parameters + ---------- + module_function : str + Module and function path string (e.g. numpy.ones) + package : str, optional + The parent package of the module + warn : bool, optional + Whether to warn when wrapping fails + + Returns + ------- + func : function + Wrapped function, hopefully calling a function in another module. + + Example + ------- + >>> @_replace_by('_tifffile.decodepackbits') + ... def decodepackbits(encoded): + ... raise NotImplementedError + + """ + def decorate(func, module_function=module_function, warn=warn): + try: + modname, function = module_function.split('.') + if package is None: + full_name = modname + else: + full_name = package + '.' + modname + module = __import__(full_name, romlist=[modname]) + func, oldfunc = getattr(module, function), func + globals()['__old_' + func.__name__] = oldfunc + except Exception: + if warn: + warnings.warn("failed to import %s" % module_function) + return func + + return decorate + + +def decodejpg(encoded, tables=b'', photometric=None, + ycbcr_subsampling=None, ycbcr_positioning=None): + """Decode JPEG encoded byte string (using _czifile extension module).""" + import _czifile + image = _czifile.decodejpg(encoded, tables) + if photometric == 'rgb' and ycbcr_subsampling and ycbcr_positioning: + # TODO: convert YCbCr to RGB + pass + return image.tostring() + + +@_replace_by('_tifffile.decodepackbits') +def decodepackbits(encoded): + """Decompress PackBits encoded byte string. + + PackBits is a simple byte-oriented run-length compression scheme. + + """ + func = ord if sys.version[0] == '2' else lambda x: x + result = [] + result_extend = result.extend + i = 0 + try: + while True: + n = func(encoded[i]) + 1 + i += 1 + if n < 129: + result_extend(encoded[i:i+n]) + i += n + elif n > 129: + result_extend(encoded[i:i+1] * (258-n)) + i += 1 + except IndexError: + pass + return b''.join(result) if sys.version[0] == '2' else bytes(result) + + +@_replace_by('_tifffile.decodelzw') +def decodelzw(encoded): + """Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string). + + The strip must begin with a CLEAR code and end with an EOI code. + + This is an implementation of the LZW decoding algorithm described in (1). + It is not compatible with old style LZW compressed files like quad-lzw.tif. + + """ + len_encoded = len(encoded) + bitcount_max = len_encoded * 8 + unpack = struct.unpack + + if sys.version[0] == '2': + newtable = [chr(i) for i in range(256)] + else: + newtable = [bytes([i]) for i in range(256)] + newtable.extend((0, 0)) + + def next_code(): + """Return integer of `bitw` bits at `bitcount` position in encoded.""" + start = bitcount // 8 + s = encoded[start:start+4] + try: + code = unpack('>I', s)[0] + except Exception: + code = unpack('>I', s + b'\x00'*(4-len(s)))[0] + code <<= bitcount % 8 + code &= mask + return code >> shr + + switchbitch = { # code: bit-width, shr-bits, bit-mask + 255: (9, 23, int(9*'1'+'0'*23, 2)), + 511: (10, 22, int(10*'1'+'0'*22, 2)), + 1023: (11, 21, int(11*'1'+'0'*21, 2)), + 2047: (12, 20, int(12*'1'+'0'*20, 2)), } + bitw, shr, mask = switchbitch[255] + bitcount = 0 + + if len_encoded < 4: + raise ValueError("strip must be at least 4 characters long") + + if next_code() != 256: + raise ValueError("strip must begin with CLEAR code") + + code = 0 + oldcode = 0 + result = [] + result_append = result.append + while True: + code = next_code() # ~5% faster when inlining this function + bitcount += bitw + if code == 257 or bitcount >= bitcount_max: # EOI + break + if code == 256: # CLEAR + table = newtable[:] + table_append = table.append + lentable = 258 + bitw, shr, mask = switchbitch[255] + code = next_code() + bitcount += bitw + if code == 257: # EOI + break + result_append(table[code]) + else: + if code < lentable: + decoded = table[code] + newcode = table[oldcode] + decoded[:1] + else: + newcode = table[oldcode] + newcode += newcode[:1] + decoded = newcode + result_append(decoded) + table_append(newcode) + lentable += 1 + oldcode = code + if lentable in switchbitch: + bitw, shr, mask = switchbitch[lentable] + + if code != 257: + warnings.warn("unexpected end of lzw stream (code %i)" % code) + + return b''.join(result) + + +@_replace_by('_tifffile.unpackints') +def unpackints(data, dtype, itemsize, runlen=0): + """Decompress byte string to array of integers of any bit size <= 32. + + Parameters + ---------- + data : byte str + Data to decompress. + dtype : numpy.dtype or str + A numpy boolean or integer type. + itemsize : int + Number of bits per integer. + runlen : int + Number of consecutive integers, after which to start at next byte. + + """ + if itemsize == 1: # bitarray + data = numpy.fromstring(data, '|B') + data = numpy.unpackbits(data) + if runlen % 8: + data = data.reshape(-1, runlen + (8 - runlen % 8)) + data = data[:, :runlen].reshape(-1) + return data.astype(dtype) + + dtype = numpy.dtype(dtype) + if itemsize in (8, 16, 32, 64): + return numpy.fromstring(data, dtype) + if itemsize < 1 or itemsize > 32: + raise ValueError("itemsize out of range: %i" % itemsize) + if dtype.kind not in "biu": + raise ValueError("invalid dtype") + + itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize) + if itembytes != dtype.itemsize: + raise ValueError("dtype.itemsize too small") + if runlen == 0: + runlen = len(data) // itembytes + skipbits = runlen*itemsize % 8 + if skipbits: + skipbits = 8 - skipbits + shrbits = itembytes*8 - itemsize + bitmask = int(itemsize*'1'+'0'*shrbits, 2) + dtypestr = '>' + dtype.char # dtype always big endian? + + unpack = struct.unpack + l = runlen * (len(data)*8 // (runlen*itemsize + skipbits)) + result = numpy.empty((l, ), dtype) + bitcount = 0 + for i in range(len(result)): + start = bitcount // 8 + s = data[start:start+itembytes] + try: + code = unpack(dtypestr, s)[0] + except Exception: + code = unpack(dtypestr, s + b'\x00'*(itembytes-len(s)))[0] + code <<= bitcount % 8 + code &= bitmask + result[i] = code >> shrbits + bitcount += itemsize + if (i+1) % runlen == 0: + bitcount += skipbits + return result + + +def unpackrgb(data, dtype='>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) + >>> print(unpackrgb(data, '>> print(unpackrgb(data, '>> print(unpackrgb(data, '= bits) + data = numpy.fromstring(data, dtype.byteorder+dt) + result = numpy.empty((data.size, len(bitspersample)), dtype.char) + for i, bps in enumerate(bitspersample): + t = data >> int(numpy.sum(bitspersample[i+1:])) + t &= int('0b'+'1'*bps, 2) + if rescale: + o = ((dtype.itemsize * 8) // bps + 1) * bps + if o > data.dtype.itemsize * 8: + t = t.astype('I') + t *= (2**o - 1) // (2**bps - 1) + t //= 2**(o - (dtype.itemsize * 8)) + result[:, i] = t + return result.reshape(-1) + + +def reorient(image, orientation): + """Return reoriented view of image array. + + Parameters + ---------- + image : numpy array + Non-squeezed output of asarray() functions. + Axes -3 and -2 must be image length and width respectively. + orientation : int or str + One of TIFF_ORIENTATIONS keys or values. + + """ + o = TIFF_ORIENTATIONS.get(orientation, orientation) + if o == 'top_left': + return image + elif o == 'top_right': + return image[..., ::-1, :] + elif o == 'bottom_left': + return image[..., ::-1, :, :] + elif o == 'bottom_right': + return image[..., ::-1, ::-1, :] + elif o == 'left_top': + return numpy.swapaxes(image, -3, -2) + elif o == 'right_top': + return numpy.swapaxes(image, -3, -2)[..., ::-1, :] + elif o == 'left_bottom': + return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :] + elif o == 'right_bottom': + return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :] + + +def squeeze_axes(shape, axes, skip='XY'): + """Return shape and axes with single-dimensional entries removed. + + Remove unused dimensions unless their axes are listed in 'skip'. + + >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') + ((5, 2, 1), 'TYX') + + """ + if len(shape) != len(axes): + raise ValueError("dimensions of axes and shape don't match") + shape, axes = zip(*(i for i in zip(shape, axes) + if i[0] > 1 or i[1] in skip)) + return shape, ''.join(axes) + + +def transpose_axes(data, axes, asaxes='CTZYX'): + """Return data with its axes permuted to match specified axes. + + A view is returned if possible. + + >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape + (5, 2, 1, 3, 4) + + """ + for ax in axes: + if ax not in asaxes: + raise ValueError("unknown axis %s" % ax) + # add missing axes to data + shape = data.shape + for ax in reversed(asaxes): + if ax not in axes: + axes = ax + axes + shape = (1,) + shape + data = data.reshape(shape) + # transpose axes + data = data.transpose([axes.index(ax) for ax in asaxes]) + return data + + +def stack_pages(pages, memmap=False, *args, **kwargs): + """Read data from sequence of TiffPage and stack them vertically. + + If memmap is True, return an array stored in a binary file on disk. + Additional parameters are passsed to the page asarray function. + + """ + if len(pages) == 0: + raise ValueError("no pages") + + if len(pages) == 1: + return pages[0].asarray(memmap=memmap, *args, **kwargs) + + result = pages[0].asarray(*args, **kwargs) + shape = (len(pages),) + result.shape + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=result.dtype, shape=shape) + else: + result = numpy.empty(shape, dtype=result.dtype) + + for i, page in enumerate(pages): + result[i] = page.asarray(*args, **kwargs) + + return result + + +def stripnull(string): + """Return string truncated at first null character. + + Clean NULL terminated C strings. + + >>> stripnull(b'string\\x00') # doctest: +SKIP + b'string' + + """ + i = string.find(b'\x00') + return string if (i < 0) else string[:i] + + +def stripascii(string): + """Return string truncated at last byte that is 7bit ASCII. + + Clean NULL separated and terminated TIFF strings. + + >>> stripascii(b'string\\x00string\\n\\x01\\x00') # doctest: +SKIP + b'string\\x00string\\n' + >>> stripascii(b'\\x00') # doctest: +SKIP + b'' + + """ + # TODO: pythonize this + ord_ = ord if sys.version_info[0] < 3 else lambda x: x + i = len(string) + while i: + i -= 1 + if 8 < ord_(string[i]) < 127: + break + else: + i = -1 + return string[:i+1] + + +def format_size(size): + """Return file size as string from byte size.""" + for unit in ('B', 'KB', 'MB', 'GB', 'TB'): + if size < 2048: + return "%.f %s" % (size, unit) + size /= 1024.0 + + +def sequence(value): + """Return tuple containing value if value is not a sequence. + + >>> sequence(1) + (1,) + >>> sequence([1]) + [1] + + """ + try: + len(value) + return value + except TypeError: + return (value, ) + + +def product(iterable): + """Return product of sequence of numbers. + + Equivalent of functools.reduce(operator.mul, iterable, 1). + + >>> product([2**8, 2**30]) + 274877906944 + >>> product([]) + 1 + + """ + prod = 1 + for i in iterable: + prod *= i + return prod + + +def natural_sorted(iterable): + """Return human sorted list of strings. + + E.g. for sorting file names. + + >>> natural_sorted(['f1', 'f2', 'f10']) + ['f1', 'f2', 'f10'] + + """ + def sortkey(x): + return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)] + numbers = re.compile(r'(\d+)') + return sorted(iterable, key=sortkey) + + +def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)): + """Return datetime object from timestamp in Excel serial format. + + Convert LSM time stamps. + + >>> excel_datetime(40237.029999999795) + datetime.datetime(2010, 2, 28, 0, 43, 11, 999982) + + """ + return epoch + datetime.timedelta(timestamp) + + +def julian_datetime(julianday, milisecond=0): + """Return datetime from days since 1/1/4713 BC and ms since midnight. + + Convert Julian dates according to MetaMorph. + + >>> julian_datetime(2451576, 54362783) + datetime.datetime(2000, 2, 2, 15, 6, 2, 783) + + """ + if julianday <= 1721423: + # no datetime before year 1 + return None + + a = julianday + 1 + if a > 2299160: + alpha = math.trunc((a - 1867216.25) / 36524.25) + a += 1 + alpha - alpha // 4 + b = a + (1524 if a > 1721423 else 1158) + c = math.trunc((b - 122.1) / 365.25) + d = math.trunc(365.25 * c) + e = math.trunc((b - d) / 30.6001) + + day = b - d - math.trunc(30.6001 * e) + month = e - (1 if e < 13.5 else 13) + year = c - (4716 if month > 2.5 else 4715) + + hour, milisecond = divmod(milisecond, 1000 * 60 * 60) + minute, milisecond = divmod(milisecond, 1000 * 60) + second, milisecond = divmod(milisecond, 1000) + + return datetime.datetime(year, month, day, + hour, minute, second, milisecond) + + +def test_tifffile(directory='testimages', verbose=True): + """Read all images in directory. + + Print error message on failure. + + >>> test_tifffile(verbose=False) + + """ + successful = 0 + failed = 0 + start = time.time() + for f in glob.glob(os.path.join(directory, '*.*')): + if verbose: + print("\n%s>\n" % f.lower(), end='') + t0 = time.time() + try: + tif = TiffFile(f, multifile=True) + except Exception as e: + if not verbose: + print(f, end=' ') + print("ERROR:", e) + failed += 1 + continue + try: + img = tif.asarray() + except ValueError: + try: + img = tif[0].asarray() + except Exception as e: + if not verbose: + print(f, end=' ') + print("ERROR:", e) + failed += 1 + continue + finally: + tif.close() + successful += 1 + if verbose: + print("%s, %s %s, %s, %.0f ms" % ( + str(tif), str(img.shape), img.dtype, tif[0].compression, + (time.time()-t0) * 1e3)) + if verbose: + print("\nSuccessfully read %i of %i files in %.3f s\n" % ( + successful, successful+failed, time.time()-start)) + + +class TIFF_SUBFILE_TYPES(object): + def __getitem__(self, key): + result = [] + if key & 1: + result.append('reduced_image') + if key & 2: + result.append('page') + if key & 4: + result.append('mask') + return tuple(result) + + +TIFF_PHOTOMETRICS = { + 0: 'miniswhite', + 1: 'minisblack', + 2: 'rgb', + 3: 'palette', + 4: 'mask', + 5: 'separated', # CMYK + 6: 'ycbcr', + 8: 'cielab', + 9: 'icclab', + 10: 'itulab', + 32803: 'cfa', # Color Filter Array + 32844: 'logl', + 32845: 'logluv', + 34892: 'linear_raw' +} + +TIFF_COMPESSIONS = { + 1: None, + 2: 'ccittrle', + 3: 'ccittfax3', + 4: 'ccittfax4', + 5: 'lzw', + 6: 'ojpeg', + 7: 'jpeg', + 8: 'adobe_deflate', + 9: 't85', + 10: 't43', + 32766: 'next', + 32771: 'ccittrlew', + 32773: 'packbits', + 32809: 'thunderscan', + 32895: 'it8ctpad', + 32896: 'it8lw', + 32897: 'it8mp', + 32898: 'it8bl', + 32908: 'pixarfilm', + 32909: 'pixarlog', + 32946: 'deflate', + 32947: 'dcs', + 34661: 'jbig', + 34676: 'sgilog', + 34677: 'sgilog24', + 34712: 'jp2000', + 34713: 'nef', +} + +TIFF_DECOMPESSORS = { + None: lambda x: x, + 'adobe_deflate': zlib.decompress, + 'deflate': zlib.decompress, + 'packbits': decodepackbits, + 'lzw': decodelzw, + # 'jpeg': decodejpg +} + +TIFF_DATA_TYPES = { + 1: '1B', # BYTE 8-bit unsigned integer. + 2: '1s', # ASCII 8-bit byte that contains a 7-bit ASCII code; + # the last byte must be NULL (binary zero). + 3: '1H', # SHORT 16-bit (2-byte) unsigned integer + 4: '1I', # LONG 32-bit (4-byte) unsigned integer. + 5: '2I', # RATIONAL Two LONGs: the first represents the numerator of + # a fraction; the second, the denominator. + 6: '1b', # SBYTE An 8-bit signed (twos-complement) integer. + 7: '1s', # UNDEFINED An 8-bit byte that may contain anything, + # depending on the definition of the field. + 8: '1h', # SSHORT A 16-bit (2-byte) signed (twos-complement) integer. + 9: '1i', # SLONG A 32-bit (4-byte) signed (twos-complement) integer. + 10: '2i', # SRATIONAL Two SLONGs: the first represents the numerator + # of a fraction, the second the denominator. + 11: '1f', # FLOAT Single precision (4-byte) IEEE format. + 12: '1d', # DOUBLE Double precision (8-byte) IEEE format. + 13: '1I', # IFD unsigned 4 byte IFD offset. + #14: '', # UNICODE + #15: '', # COMPLEX + 16: '1Q', # LONG8 unsigned 8 byte integer (BigTiff) + 17: '1q', # SLONG8 signed 8 byte integer (BigTiff) + 18: '1Q', # IFD8 unsigned 8 byte IFD offset (BigTiff) +} + +TIFF_SAMPLE_FORMATS = { + 1: 'uint', + 2: 'int', + 3: 'float', + #4: 'void', + #5: 'complex_int', + 6: 'complex', +} + +TIFF_SAMPLE_DTYPES = { + ('uint', 1): '?', # bitmap + ('uint', 2): 'B', + ('uint', 3): 'B', + ('uint', 4): 'B', + ('uint', 5): 'B', + ('uint', 6): 'B', + ('uint', 7): 'B', + ('uint', 8): 'B', + ('uint', 9): 'H', + ('uint', 10): 'H', + ('uint', 11): 'H', + ('uint', 12): 'H', + ('uint', 13): 'H', + ('uint', 14): 'H', + ('uint', 15): 'H', + ('uint', 16): 'H', + ('uint', 17): 'I', + ('uint', 18): 'I', + ('uint', 19): 'I', + ('uint', 20): 'I', + ('uint', 21): 'I', + ('uint', 22): 'I', + ('uint', 23): 'I', + ('uint', 24): 'I', + ('uint', 25): 'I', + ('uint', 26): 'I', + ('uint', 27): 'I', + ('uint', 28): 'I', + ('uint', 29): 'I', + ('uint', 30): 'I', + ('uint', 31): 'I', + ('uint', 32): 'I', + ('uint', 64): 'Q', + ('int', 8): 'b', + ('int', 16): 'h', + ('int', 32): 'i', + ('int', 64): 'q', + ('float', 16): 'e', + ('float', 32): 'f', + ('float', 64): 'd', + ('complex', 64): 'F', + ('complex', 128): 'D', + ('uint', (5, 6, 5)): 'B', +} + +TIFF_ORIENTATIONS = { + 1: 'top_left', + 2: 'top_right', + 3: 'bottom_right', + 4: 'bottom_left', + 5: 'left_top', + 6: 'right_top', + 7: 'right_bottom', + 8: 'left_bottom', +} + +# TODO: is there a standard for character axes labels? +AXES_LABELS = { + 'X': 'width', + 'Y': 'height', + 'Z': 'depth', + 'S': 'sample', # rgb(a) + 'I': 'series', # general sequence, plane, page, IFD + 'T': 'time', + 'C': 'channel', # color, emission wavelength + 'A': 'angle', + 'P': 'phase', # formerly F # P is Position in LSM! + 'R': 'tile', # region, point, mosaic + 'H': 'lifetime', # histogram + 'E': 'lambda', # excitation wavelength + 'L': 'exposure', # lux + 'V': 'event', + 'Q': 'other', + #'M': 'mosaic', # LSM 6 +} + +AXES_LABELS.update(dict((v, k) for k, v in AXES_LABELS.items())) + +# Map OME pixel types to numpy dtype +OME_PIXEL_TYPES = { + 'int8': 'i1', + 'int16': 'i2', + 'int32': 'i4', + 'uint8': 'u1', + 'uint16': 'u2', + 'uint32': 'u4', + 'float': 'f4', + # 'bit': 'bit', + 'double': 'f8', + 'complex': 'c8', + 'double-complex': 'c16', +} + +# NIH Image PicHeader v1.63 +NIH_IMAGE_HEADER = [ + ('fileid', 'a8'), + ('nlines', 'i2'), + ('pixelsperline', 'i2'), + ('version', 'i2'), + ('oldlutmode', 'i2'), + ('oldncolors', 'i2'), + ('colors', 'u1', (3, 32)), + ('oldcolorstart', 'i2'), + ('colorwidth', 'i2'), + ('extracolors', 'u2', (6, 3)), + ('nextracolors', 'i2'), + ('foregroundindex', 'i2'), + ('backgroundindex', 'i2'), + ('xscale', 'f8'), + ('_x0', 'i2'), + ('_x1', 'i2'), + ('units_t', 'i2'), # NIH_UNITS_TYPE + ('p1', [('x', 'i2'), ('y', 'i2')]), + ('p2', [('x', 'i2'), ('y', 'i2')]), + ('curvefit_t', 'i2'), # NIH_CURVEFIT_TYPE + ('ncoefficients', 'i2'), + ('coeff', 'f8', 6), + ('_um_len', 'u1'), + ('um', 'a15'), + ('_x2', 'u1'), + ('binarypic', 'b1'), + ('slicestart', 'i2'), + ('sliceend', 'i2'), + ('scalemagnification', 'f4'), + ('nslices', 'i2'), + ('slicespacing', 'f4'), + ('currentslice', 'i2'), + ('frameinterval', 'f4'), + ('pixelaspectratio', 'f4'), + ('colorstart', 'i2'), + ('colorend', 'i2'), + ('ncolors', 'i2'), + ('fill1', '3u2'), + ('fill2', '3u2'), + ('colortable_t', 'u1'), # NIH_COLORTABLE_TYPE + ('lutmode_t', 'u1'), # NIH_LUTMODE_TYPE + ('invertedtable', 'b1'), + ('zeroclip', 'b1'), + ('_xunit_len', 'u1'), + ('xunit', 'a11'), + ('stacktype_t', 'i2'), # NIH_STACKTYPE_TYPE +] + +NIH_COLORTABLE_TYPE = ( + 'CustomTable', 'AppleDefault', 'Pseudo20', 'Pseudo32', 'Rainbow', + 'Fire1', 'Fire2', 'Ice', 'Grays', 'Spectrum') + +NIH_LUTMODE_TYPE = ( + 'PseudoColor', 'OldAppleDefault', 'OldSpectrum', 'GrayScale', + 'ColorLut', 'CustomGrayscale') + +NIH_CURVEFIT_TYPE = ( + 'StraightLine', 'Poly2', 'Poly3', 'Poly4', 'Poly5', 'ExpoFit', + 'PowerFit', 'LogFit', 'RodbardFit', 'SpareFit1', 'Uncalibrated', + 'UncalibratedOD') + +NIH_UNITS_TYPE = ( + 'Nanometers', 'Micrometers', 'Millimeters', 'Centimeters', 'Meters', + 'Kilometers', 'Inches', 'Feet', 'Miles', 'Pixels', 'OtherUnits') + +NIH_STACKTYPE_TYPE = ( + 'VolumeStack', 'RGBStack', 'MovieStack', 'HSVStack') + +# Map Universal Imaging Corporation MetaMorph internal tag ids to name and type +UIC_TAGS = { + 0: ('auto_scale', int), + 1: ('min_scale', int), + 2: ('max_scale', int), + 3: ('spatial_calibration', int), + 4: ('x_calibration', Fraction), + 5: ('y_calibration', Fraction), + 6: ('calibration_units', str), + 7: ('name', str), + 8: ('thresh_state', int), + 9: ('thresh_state_red', int), + 10: ('tagid_10', None), # undefined + 11: ('thresh_state_green', int), + 12: ('thresh_state_blue', int), + 13: ('thresh_state_lo', int), + 14: ('thresh_state_hi', int), + 15: ('zoom', int), + 16: ('create_time', julian_datetime), + 17: ('last_saved_time', julian_datetime), + 18: ('current_buffer', int), + 19: ('gray_fit', None), + 20: ('gray_point_count', None), + 21: ('gray_x', Fraction), + 22: ('gray_y', Fraction), + 23: ('gray_min', Fraction), + 24: ('gray_max', Fraction), + 25: ('gray_unit_name', str), + 26: ('standard_lut', int), + 27: ('wavelength', int), + 28: ('stage_position', '(%i,2,2)u4'), # N xy positions as fractions + 29: ('camera_chip_offset', '(%i,2,2)u4'), # N xy offsets as fractions + 30: ('overlay_mask', None), + 31: ('overlay_compress', None), + 32: ('overlay', None), + 33: ('special_overlay_mask', None), + 34: ('special_overlay_compress', None), + 35: ('special_overlay', None), + 36: ('image_property', read_uic_image_property), + 37: ('stage_label', '%ip'), # N str + 38: ('autoscale_lo_info', Fraction), + 39: ('autoscale_hi_info', Fraction), + 40: ('absolute_z', '(%i,2)u4'), # N fractions + 41: ('absolute_z_valid', '(%i,)u4'), # N long + 42: ('gamma', int), + 43: ('gamma_red', int), + 44: ('gamma_green', int), + 45: ('gamma_blue', int), + 46: ('camera_bin', int), + 47: ('new_lut', int), + 48: ('image_property_ex', None), + 49: ('plane_property', int), + 50: ('user_lut_table', '(256,3)u1'), + 51: ('red_autoscale_info', int), + 52: ('red_autoscale_lo_info', Fraction), + 53: ('red_autoscale_hi_info', Fraction), + 54: ('red_minscale_info', int), + 55: ('red_maxscale_info', int), + 56: ('green_autoscale_info', int), + 57: ('green_autoscale_lo_info', Fraction), + 58: ('green_autoscale_hi_info', Fraction), + 59: ('green_minscale_info', int), + 60: ('green_maxscale_info', int), + 61: ('blue_autoscale_info', int), + 62: ('blue_autoscale_lo_info', Fraction), + 63: ('blue_autoscale_hi_info', Fraction), + 64: ('blue_min_scale_info', int), + 65: ('blue_max_scale_info', int), + #66: ('overlay_plane_color', read_uic_overlay_plane_color), +} + + +# Olympus FluoView +MM_DIMENSION = [ + ('name', 'a16'), + ('size', 'i4'), + ('origin', 'f8'), + ('resolution', 'f8'), + ('unit', 'a64'), +] + +MM_HEADER = [ + ('header_flag', 'i2'), + ('image_type', 'u1'), + ('image_name', 'a257'), + ('offset_data', 'u4'), + ('palette_size', 'i4'), + ('offset_palette0', 'u4'), + ('offset_palette1', 'u4'), + ('comment_size', 'i4'), + ('offset_comment', 'u4'), + ('dimensions', MM_DIMENSION, 10), + ('offset_position', 'u4'), + ('map_type', 'i2'), + ('map_min', 'f8'), + ('map_max', 'f8'), + ('min_value', 'f8'), + ('max_value', 'f8'), + ('offset_map', 'u4'), + ('gamma', 'f8'), + ('offset', 'f8'), + ('gray_channel', MM_DIMENSION), + ('offset_thumbnail', 'u4'), + ('voice_field', 'i4'), + ('offset_voice_field', 'u4'), +] + +# Carl Zeiss LSM +CZ_LSM_INFO = [ + ('magic_number', 'u4'), + ('structure_size', 'i4'), + ('dimension_x', 'i4'), + ('dimension_y', 'i4'), + ('dimension_z', 'i4'), + ('dimension_channels', 'i4'), + ('dimension_time', 'i4'), + ('data_type', 'i4'), # CZ_DATA_TYPES + ('thumbnail_x', 'i4'), + ('thumbnail_y', 'i4'), + ('voxel_size_x', 'f8'), + ('voxel_size_y', 'f8'), + ('voxel_size_z', 'f8'), + ('origin_x', 'f8'), + ('origin_y', 'f8'), + ('origin_z', 'f8'), + ('scan_type', 'u2'), + ('spectral_scan', 'u2'), + ('type_of_data', 'u4'), # CZ_TYPE_OF_DATA + ('offset_vector_overlay', 'u4'), + ('offset_input_lut', 'u4'), + ('offset_output_lut', 'u4'), + ('offset_channel_colors', 'u4'), + ('time_interval', 'f8'), + ('offset_channel_data_types', 'u4'), + ('offset_scan_info', 'u4'), # CZ_LSM_SCAN_INFO + ('offset_ks_data', 'u4'), + ('offset_time_stamps', 'u4'), + ('offset_event_list', 'u4'), + ('offset_roi', 'u4'), + ('offset_bleach_roi', 'u4'), + ('offset_next_recording', 'u4'), + # LSM 2.0 ends here + ('display_aspect_x', 'f8'), + ('display_aspect_y', 'f8'), + ('display_aspect_z', 'f8'), + ('display_aspect_time', 'f8'), + ('offset_mean_of_roi_overlay', 'u4'), + ('offset_topo_isoline_overlay', 'u4'), + ('offset_topo_profile_overlay', 'u4'), + ('offset_linescan_overlay', 'u4'), + ('offset_toolbar_flags', 'u4'), + ('offset_channel_wavelength', 'u4'), + ('offset_channel_factors', 'u4'), + ('objective_sphere_correction', 'f8'), + ('offset_unmix_parameters', 'u4'), + # LSM 3.2, 4.0 end here + ('offset_acquisition_parameters', 'u4'), + ('offset_characteristics', 'u4'), + ('offset_palette', 'u4'), + ('time_difference_x', 'f8'), + ('time_difference_y', 'f8'), + ('time_difference_z', 'f8'), + ('internal_use_1', 'u4'), + ('dimension_p', 'i4'), + ('dimension_m', 'i4'), + ('dimensions_reserved', '16i4'), + ('offset_tile_positions', 'u4'), + ('reserved_1', '9u4'), + ('offset_positions', 'u4'), + ('reserved_2', '21u4'), # must be 0 +] + +# Import functions for LSM_INFO sub-records +CZ_LSM_INFO_READERS = { + 'scan_info': read_cz_lsm_scan_info, + 'time_stamps': read_cz_lsm_time_stamps, + 'event_list': read_cz_lsm_event_list, + 'channel_colors': read_cz_lsm_floatpairs, + 'positions': read_cz_lsm_floatpairs, + 'tile_positions': read_cz_lsm_floatpairs, +} + +# Map cz_lsm_info.scan_type to dimension order +CZ_SCAN_TYPES = { + 0: 'XYZCT', # x-y-z scan + 1: 'XYZCT', # z scan (x-z plane) + 2: 'XYZCT', # line scan + 3: 'XYTCZ', # time series x-y + 4: 'XYZTC', # time series x-z + 5: 'XYTCZ', # time series 'Mean of ROIs' + 6: 'XYZTC', # time series x-y-z + 7: 'XYCTZ', # spline scan + 8: 'XYCZT', # spline scan x-z + 9: 'XYTCZ', # time series spline plane x-z + 10: 'XYZCT', # point mode +} + +# Map dimension codes to cz_lsm_info attribute +CZ_DIMENSIONS = { + 'X': 'dimension_x', + 'Y': 'dimension_y', + 'Z': 'dimension_z', + 'C': 'dimension_channels', + 'T': 'dimension_time', +} + +# Description of cz_lsm_info.data_type +CZ_DATA_TYPES = { + 0: 'varying data types', + 1: '8 bit unsigned integer', + 2: '12 bit unsigned integer', + 5: '32 bit float', +} + +# Description of cz_lsm_info.type_of_data +CZ_TYPE_OF_DATA = { + 0: 'Original scan data', + 1: 'Calculated data', + 2: '3D reconstruction', + 3: 'Topography height map', +} + +CZ_LSM_SCAN_INFO_ARRAYS = { + 0x20000000: "tracks", + 0x30000000: "lasers", + 0x60000000: "detection_channels", + 0x80000000: "illumination_channels", + 0xa0000000: "beam_splitters", + 0xc0000000: "data_channels", + 0x11000000: "timers", + 0x13000000: "markers", +} + +CZ_LSM_SCAN_INFO_STRUCTS = { + # 0x10000000: "recording", + 0x40000000: "track", + 0x50000000: "laser", + 0x70000000: "detection_channel", + 0x90000000: "illumination_channel", + 0xb0000000: "beam_splitter", + 0xd0000000: "data_channel", + 0x12000000: "timer", + 0x14000000: "marker", +} + +CZ_LSM_SCAN_INFO_ATTRIBUTES = { + # recording + 0x10000001: "name", + 0x10000002: "description", + 0x10000003: "notes", + 0x10000004: "objective", + 0x10000005: "processing_summary", + 0x10000006: "special_scan_mode", + 0x10000007: "scan_type", + 0x10000008: "scan_mode", + 0x10000009: "number_of_stacks", + 0x1000000a: "lines_per_plane", + 0x1000000b: "samples_per_line", + 0x1000000c: "planes_per_volume", + 0x1000000d: "images_width", + 0x1000000e: "images_height", + 0x1000000f: "images_number_planes", + 0x10000010: "images_number_stacks", + 0x10000011: "images_number_channels", + 0x10000012: "linscan_xy_size", + 0x10000013: "scan_direction", + 0x10000014: "time_series", + 0x10000015: "original_scan_data", + 0x10000016: "zoom_x", + 0x10000017: "zoom_y", + 0x10000018: "zoom_z", + 0x10000019: "sample_0x", + 0x1000001a: "sample_0y", + 0x1000001b: "sample_0z", + 0x1000001c: "sample_spacing", + 0x1000001d: "line_spacing", + 0x1000001e: "plane_spacing", + 0x1000001f: "plane_width", + 0x10000020: "plane_height", + 0x10000021: "volume_depth", + 0x10000023: "nutation", + 0x10000034: "rotation", + 0x10000035: "precession", + 0x10000036: "sample_0time", + 0x10000037: "start_scan_trigger_in", + 0x10000038: "start_scan_trigger_out", + 0x10000039: "start_scan_event", + 0x10000040: "start_scan_time", + 0x10000041: "stop_scan_trigger_in", + 0x10000042: "stop_scan_trigger_out", + 0x10000043: "stop_scan_event", + 0x10000044: "stop_scan_time", + 0x10000045: "use_rois", + 0x10000046: "use_reduced_memory_rois", + 0x10000047: "user", + 0x10000048: "use_bc_correction", + 0x10000049: "position_bc_correction1", + 0x10000050: "position_bc_correction2", + 0x10000051: "interpolation_y", + 0x10000052: "camera_binning", + 0x10000053: "camera_supersampling", + 0x10000054: "camera_frame_width", + 0x10000055: "camera_frame_height", + 0x10000056: "camera_offset_x", + 0x10000057: "camera_offset_y", + 0x10000059: "rt_binning", + 0x1000005a: "rt_frame_width", + 0x1000005b: "rt_frame_height", + 0x1000005c: "rt_region_width", + 0x1000005d: "rt_region_height", + 0x1000005e: "rt_offset_x", + 0x1000005f: "rt_offset_y", + 0x10000060: "rt_zoom", + 0x10000061: "rt_line_period", + 0x10000062: "prescan", + 0x10000063: "scan_direction_z", + # track + 0x40000001: "multiplex_type", # 0 after line; 1 after frame + 0x40000002: "multiplex_order", + 0x40000003: "sampling_mode", # 0 sample; 1 line average; 2 frame average + 0x40000004: "sampling_method", # 1 mean; 2 sum + 0x40000005: "sampling_number", + 0x40000006: "acquire", + 0x40000007: "sample_observation_time", + 0x4000000b: "time_between_stacks", + 0x4000000c: "name", + 0x4000000d: "collimator1_name", + 0x4000000e: "collimator1_position", + 0x4000000f: "collimator2_name", + 0x40000010: "collimator2_position", + 0x40000011: "is_bleach_track", + 0x40000012: "is_bleach_after_scan_number", + 0x40000013: "bleach_scan_number", + 0x40000014: "trigger_in", + 0x40000015: "trigger_out", + 0x40000016: "is_ratio_track", + 0x40000017: "bleach_count", + 0x40000018: "spi_center_wavelength", + 0x40000019: "pixel_time", + 0x40000021: "condensor_frontlens", + 0x40000023: "field_stop_value", + 0x40000024: "id_condensor_aperture", + 0x40000025: "condensor_aperture", + 0x40000026: "id_condensor_revolver", + 0x40000027: "condensor_filter", + 0x40000028: "id_transmission_filter1", + 0x40000029: "id_transmission1", + 0x40000030: "id_transmission_filter2", + 0x40000031: "id_transmission2", + 0x40000032: "repeat_bleach", + 0x40000033: "enable_spot_bleach_pos", + 0x40000034: "spot_bleach_posx", + 0x40000035: "spot_bleach_posy", + 0x40000036: "spot_bleach_posz", + 0x40000037: "id_tubelens", + 0x40000038: "id_tubelens_position", + 0x40000039: "transmitted_light", + 0x4000003a: "reflected_light", + 0x4000003b: "simultan_grab_and_bleach", + 0x4000003c: "bleach_pixel_time", + # laser + 0x50000001: "name", + 0x50000002: "acquire", + 0x50000003: "power", + # detection_channel + 0x70000001: "integration_mode", + 0x70000002: "special_mode", + 0x70000003: "detector_gain_first", + 0x70000004: "detector_gain_last", + 0x70000005: "amplifier_gain_first", + 0x70000006: "amplifier_gain_last", + 0x70000007: "amplifier_offs_first", + 0x70000008: "amplifier_offs_last", + 0x70000009: "pinhole_diameter", + 0x7000000a: "counting_trigger", + 0x7000000b: "acquire", + 0x7000000c: "point_detector_name", + 0x7000000d: "amplifier_name", + 0x7000000e: "pinhole_name", + 0x7000000f: "filter_set_name", + 0x70000010: "filter_name", + 0x70000013: "integrator_name", + 0x70000014: "channel_name", + 0x70000015: "detector_gain_bc1", + 0x70000016: "detector_gain_bc2", + 0x70000017: "amplifier_gain_bc1", + 0x70000018: "amplifier_gain_bc2", + 0x70000019: "amplifier_offset_bc1", + 0x70000020: "amplifier_offset_bc2", + 0x70000021: "spectral_scan_channels", + 0x70000022: "spi_wavelength_start", + 0x70000023: "spi_wavelength_stop", + 0x70000026: "dye_name", + 0x70000027: "dye_folder", + # illumination_channel + 0x90000001: "name", + 0x90000002: "power", + 0x90000003: "wavelength", + 0x90000004: "aquire", + 0x90000005: "detchannel_name", + 0x90000006: "power_bc1", + 0x90000007: "power_bc2", + # beam_splitter + 0xb0000001: "filter_set", + 0xb0000002: "filter", + 0xb0000003: "name", + # data_channel + 0xd0000001: "name", + 0xd0000003: "acquire", + 0xd0000004: "color", + 0xd0000005: "sample_type", + 0xd0000006: "bits_per_sample", + 0xd0000007: "ratio_type", + 0xd0000008: "ratio_track1", + 0xd0000009: "ratio_track2", + 0xd000000a: "ratio_channel1", + 0xd000000b: "ratio_channel2", + 0xd000000c: "ratio_const1", + 0xd000000d: "ratio_const2", + 0xd000000e: "ratio_const3", + 0xd000000f: "ratio_const4", + 0xd0000010: "ratio_const5", + 0xd0000011: "ratio_const6", + 0xd0000012: "ratio_first_images1", + 0xd0000013: "ratio_first_images2", + 0xd0000014: "dye_name", + 0xd0000015: "dye_folder", + 0xd0000016: "spectrum", + 0xd0000017: "acquire", + # timer + 0x12000001: "name", + 0x12000002: "description", + 0x12000003: "interval", + 0x12000004: "trigger_in", + 0x12000005: "trigger_out", + 0x12000006: "activation_time", + 0x12000007: "activation_number", + # marker + 0x14000001: "name", + 0x14000002: "description", + 0x14000003: "trigger_in", + 0x14000004: "trigger_out", +} + +# Map TIFF tag code to attribute name, default value, type, count, validator +TIFF_TAGS = { + 254: ('new_subfile_type', 0, 4, 1, TIFF_SUBFILE_TYPES()), + 255: ('subfile_type', None, 3, 1, + {0: 'undefined', 1: 'image', 2: 'reduced_image', 3: 'page'}), + 256: ('image_width', None, 4, 1, None), + 257: ('image_length', None, 4, 1, None), + 258: ('bits_per_sample', 1, 3, 1, None), + 259: ('compression', 1, 3, 1, TIFF_COMPESSIONS), + 262: ('photometric', None, 3, 1, TIFF_PHOTOMETRICS), + 266: ('fill_order', 1, 3, 1, {1: 'msb2lsb', 2: 'lsb2msb'}), + 269: ('document_name', None, 2, None, None), + 270: ('image_description', None, 2, None, None), + 271: ('make', None, 2, None, None), + 272: ('model', None, 2, None, None), + 273: ('strip_offsets', None, 4, None, None), + 274: ('orientation', 1, 3, 1, TIFF_ORIENTATIONS), + 277: ('samples_per_pixel', 1, 3, 1, None), + 278: ('rows_per_strip', 2**32-1, 4, 1, None), + 279: ('strip_byte_counts', None, 4, None, None), + 280: ('min_sample_value', None, 3, None, None), + 281: ('max_sample_value', None, 3, None, None), # 2**bits_per_sample + 282: ('x_resolution', None, 5, 1, None), + 283: ('y_resolution', None, 5, 1, None), + 284: ('planar_configuration', 1, 3, 1, {1: 'contig', 2: 'separate'}), + 285: ('page_name', None, 2, None, None), + 286: ('x_position', None, 5, 1, None), + 287: ('y_position', None, 5, 1, None), + 296: ('resolution_unit', 2, 4, 1, {1: 'none', 2: 'inch', 3: 'centimeter'}), + 297: ('page_number', None, 3, 2, None), + 305: ('software', None, 2, None, None), + 306: ('datetime', None, 2, None, None), + 315: ('artist', None, 2, None, None), + 316: ('host_computer', None, 2, None, None), + 317: ('predictor', 1, 3, 1, {1: None, 2: 'horizontal'}), + 318: ('white_point', None, 5, 2, None), + 319: ('primary_chromaticities', None, 5, 6, None), + 320: ('color_map', None, 3, None, None), + 322: ('tile_width', None, 4, 1, None), + 323: ('tile_length', None, 4, 1, None), + 324: ('tile_offsets', None, 4, None, None), + 325: ('tile_byte_counts', None, 4, None, None), + 338: ('extra_samples', None, 3, None, + {0: 'unspecified', 1: 'assocalpha', 2: 'unassalpha'}), + 339: ('sample_format', 1, 3, 1, TIFF_SAMPLE_FORMATS), + 340: ('smin_sample_value', None, None, None, None), + 341: ('smax_sample_value', None, None, None, None), + 347: ('jpeg_tables', None, 7, None, None), + 530: ('ycbcr_subsampling', 1, 3, 2, None), + 531: ('ycbcr_positioning', 1, 3, 1, None), + 32996: ('sgi_matteing', None, None, 1, None), # use extra_samples + 32996: ('sgi_datatype', None, None, 1, None), # use sample_format + 32997: ('image_depth', None, 4, 1, None), + 32998: ('tile_depth', None, 4, 1, None), + 33432: ('copyright', None, 1, None, None), + 33445: ('md_file_tag', None, 4, 1, None), + 33446: ('md_scale_pixel', None, 5, 1, None), + 33447: ('md_color_table', None, 3, None, None), + 33448: ('md_lab_name', None, 2, None, None), + 33449: ('md_sample_info', None, 2, None, None), + 33450: ('md_prep_date', None, 2, None, None), + 33451: ('md_prep_time', None, 2, None, None), + 33452: ('md_file_units', None, 2, None, None), + 33550: ('model_pixel_scale', None, 12, 3, None), + 33922: ('model_tie_point', None, 12, None, None), + 34665: ('exif_ifd', None, None, 1, None), + 34735: ('geo_key_directory', None, 3, None, None), + 34736: ('geo_double_params', None, 12, None, None), + 34737: ('geo_ascii_params', None, 2, None, None), + 34853: ('gps_ifd', None, None, 1, None), + 37510: ('user_comment', None, None, None, None), + 42112: ('gdal_metadata', None, 2, None, None), + 42113: ('gdal_nodata', None, 2, None, None), + 50289: ('mc_xy_position', None, 12, 2, None), + 50290: ('mc_z_position', None, 12, 1, None), + 50291: ('mc_xy_calibration', None, 12, 3, None), + 50292: ('mc_lens_lem_na_n', None, 12, 3, None), + 50293: ('mc_channel_name', None, 1, None, None), + 50294: ('mc_ex_wavelength', None, 12, 1, None), + 50295: ('mc_time_stamp', None, 12, 1, None), + 50838: ('imagej_byte_counts', None, None, None, None), + 65200: ('flex_xml', None, 2, None, None), + # code: (attribute name, default value, type, count, validator) +} + +# Map custom TIFF tag codes to attribute names and import functions +CUSTOM_TAGS = { + 700: ('xmp', read_bytes), + 34377: ('photoshop', read_numpy), + 33723: ('iptc', read_bytes), + 34675: ('icc_profile', read_bytes), + 33628: ('uic1tag', read_uic1tag), # Universal Imaging Corporation STK + 33629: ('uic2tag', read_uic2tag), + 33630: ('uic3tag', read_uic3tag), + 33631: ('uic4tag', read_uic4tag), + 34361: ('mm_header', read_mm_header), # Olympus FluoView + 34362: ('mm_stamp', read_mm_stamp), + 34386: ('mm_user_block', read_bytes), + 34412: ('cz_lsm_info', read_cz_lsm_info), # Carl Zeiss LSM + 43314: ('nih_image_header', read_nih_image_header), + # 40001: ('mc_ipwinscal', read_bytes), + 40100: ('mc_id_old', read_bytes), + 50288: ('mc_id', read_bytes), + 50296: ('mc_frame_properties', read_bytes), + 50839: ('imagej_metadata', read_bytes), + 51123: ('micromanager_metadata', read_json), +} + +# Max line length of printed output +PRINT_LINE_LEN = 79 + + +def imshow(data, title=None, vmin=0, vmax=None, cmap=None, + bitspersample=None, photometric='rgb', interpolation='nearest', + dpi=96, figure=None, subplot=111, maxdim=8192, **kwargs): + """Plot n-dimensional images using matplotlib.pyplot. + + Return figure, subplot and plot axis. + Requires pyplot already imported ``from matplotlib import pyplot``. + + Parameters + ---------- + bitspersample : int or None + Number of bits per channel in integer RGB images. + photometric : {'miniswhite', 'minisblack', 'rgb', or 'palette'} + The color space of the image data. + title : str + Window and subplot title. + figure : matplotlib.figure.Figure (optional). + Matplotlib to use for plotting. + subplot : int + A matplotlib.pyplot.subplot axis. + maxdim : int + maximum image size in any dimension. + kwargs : optional + Arguments for matplotlib.pyplot.imshow. + + """ + #if photometric not in ('miniswhite', 'minisblack', 'rgb', 'palette'): + # raise ValueError("Can't handle %s photometrics" % photometric) + # TODO: handle photometric == 'separated' (CMYK) + isrgb = photometric in ('rgb', 'palette') + data = numpy.atleast_2d(data.squeeze()) + data = data[(slice(0, maxdim), ) * len(data.shape)] + + dims = data.ndim + if dims < 2: + raise ValueError("not an image") + elif dims == 2: + dims = 0 + isrgb = False + else: + if isrgb and data.shape[-3] in (3, 4): + data = numpy.swapaxes(data, -3, -2) + data = numpy.swapaxes(data, -2, -1) + elif not isrgb and (data.shape[-1] < data.shape[-2] // 16 and + data.shape[-1] < data.shape[-3] // 16 and + data.shape[-1] < 5): + data = numpy.swapaxes(data, -3, -1) + data = numpy.swapaxes(data, -2, -1) + isrgb = isrgb and data.shape[-1] in (3, 4) + dims -= 3 if isrgb else 2 + + if photometric == 'palette' and isrgb: + datamax = data.max() + if datamax > 255: + data >>= 8 # possible precision loss + data = data.astype('B') + elif data.dtype.kind in 'ui': + if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None: + try: + bitspersample = int(math.ceil(math.log(data.max(), 2))) + except Exception: + bitspersample = data.dtype.itemsize * 8 + elif not isinstance(bitspersample, int): + # bitspersample can be tuple, e.g. (5, 6, 5) + bitspersample = data.dtype.itemsize * 8 + datamax = 2**bitspersample + if isrgb: + if bitspersample < 8: + data <<= 8 - bitspersample + elif bitspersample > 8: + data >>= bitspersample - 8 # precision loss + data = data.astype('B') + elif data.dtype.kind == 'f': + datamax = data.max() + if isrgb and datamax > 1.0: + if data.dtype.char == 'd': + data = data.astype('f') + data /= datamax + elif data.dtype.kind == 'b': + datamax = 1 + elif data.dtype.kind == 'c': + raise NotImplementedError("complex type") # TODO: handle complex types + + if not isrgb: + if vmax is None: + vmax = datamax + if vmin is None: + if data.dtype.kind == 'i': + dtmin = numpy.iinfo(data.dtype).min + vmin = numpy.min(data) + if vmin == dtmin: + vmin = numpy.min(data > dtmin) + if data.dtype.kind == 'f': + dtmin = numpy.finfo(data.dtype).min + vmin = numpy.min(data) + if vmin == dtmin: + vmin = numpy.min(data > dtmin) + else: + vmin = 0 + + pyplot = sys.modules['matplotlib.pyplot'] + + if figure is None: + pyplot.rc('font', family='sans-serif', weight='normal', size=8) + figure = pyplot.figure(dpi=dpi, figsize=(10.3, 6.3), frameon=True, + facecolor='1.0', edgecolor='w') + try: + figure.canvas.manager.window.title(title) + except Exception: + pass + pyplot.subplots_adjust(bottom=0.03*(dims+2), top=0.9, + left=0.1, right=0.95, hspace=0.05, wspace=0.0) + subplot = pyplot.subplot(subplot) + + if title: + try: + title = unicode(title, 'Windows-1252') + except TypeError: + pass + pyplot.title(title, size=11) + + if cmap is None: + if data.dtype.kind in 'ubf' or vmin == 0: + cmap = 'cubehelix' + else: + cmap = 'coolwarm' + if photometric == 'miniswhite': + cmap += '_r' + + image = pyplot.imshow(data[(0, ) * dims].squeeze(), vmin=vmin, vmax=vmax, + cmap=cmap, interpolation=interpolation, **kwargs) + + if not isrgb: + pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05 + + def format_coord(x, y): + # callback function to format coordinate display in toolbar + x = int(x + 0.5) + y = int(y + 0.5) + try: + if dims: + return "%s @ %s [%4i, %4i]" % (cur_ax_dat[1][y, x], + current, x, y) + else: + return "%s @ [%4i, %4i]" % (data[y, x], x, y) + except IndexError: + return "" + + pyplot.gca().format_coord = format_coord + + if dims: + current = list((0, ) * dims) + cur_ax_dat = [0, data[tuple(current)].squeeze()] + sliders = [pyplot.Slider( + pyplot.axes([0.125, 0.03*(axis+1), 0.725, 0.025]), + 'Dimension %i' % axis, 0, data.shape[axis]-1, 0, facecolor='0.5', + valfmt='%%.0f [%i]' % data.shape[axis]) for axis in range(dims)] + for slider in sliders: + slider.drawon = False + + def set_image(current, sliders=sliders, data=data): + # change image and redraw canvas + cur_ax_dat[1] = data[tuple(current)].squeeze() + image.set_data(cur_ax_dat[1]) + for ctrl, index in zip(sliders, current): + ctrl.eventson = False + ctrl.set_val(index) + ctrl.eventson = True + figure.canvas.draw() + + def on_changed(index, axis, data=data, current=current): + # callback function for slider change event + index = int(round(index)) + cur_ax_dat[0] = axis + if index == current[axis]: + return + if index >= data.shape[axis]: + index = 0 + elif index < 0: + index = data.shape[axis] - 1 + current[axis] = index + set_image(current) + + def on_keypressed(event, data=data, current=current): + # callback function for key press event + key = event.key + axis = cur_ax_dat[0] + if str(key) in '0123456789': + on_changed(key, axis) + elif key == 'right': + on_changed(current[axis] + 1, axis) + elif key == 'left': + on_changed(current[axis] - 1, axis) + elif key == 'up': + cur_ax_dat[0] = 0 if axis == len(data.shape)-1 else axis + 1 + elif key == 'down': + cur_ax_dat[0] = len(data.shape)-1 if axis == 0 else axis - 1 + elif key == 'end': + on_changed(data.shape[axis] - 1, axis) + elif key == 'home': + on_changed(0, axis) + + figure.canvas.mpl_connect('key_press_event', on_keypressed) + for axis, ctrl in enumerate(sliders): + ctrl.on_changed(lambda k, a=axis: on_changed(k, a)) + + return figure, subplot, image + + +def _app_show(): + """Block the GUI. For use as skimage plugin.""" + pyplot = sys.modules['matplotlib.pyplot'] + pyplot.show() + + +def main(argv=None): + """Command line usage main function.""" + if float(sys.version[0:3]) < 2.6: + print("This script requires Python version 2.6 or better.") + print("This is Python version %s" % sys.version) + return 0 + if argv is None: + argv = sys.argv + + import optparse + + parser = optparse.OptionParser( + usage="usage: %prog [options] path", + description="Display image data in TIFF files.", + version="%%prog %s" % __version__) + opt = parser.add_option + opt('-p', '--page', dest='page', type='int', default=-1, + help="display single page") + opt('-s', '--series', dest='series', type='int', default=-1, + help="display series of pages of same shape") + opt('--nomultifile', dest='nomultifile', action='store_true', + default=False, help="don't read OME series from multiple files") + opt('--noplot', dest='noplot', action='store_true', default=False, + help="don't display images") + opt('--interpol', dest='interpol', metavar='INTERPOL', default='bilinear', + help="image interpolation method") + opt('--dpi', dest='dpi', type='int', default=96, + help="set plot resolution") + opt('--debug', dest='debug', action='store_true', default=False, + help="raise exception on failures") + opt('--test', dest='test', action='store_true', default=False, + help="try read all images in path") + opt('--doctest', dest='doctest', action='store_true', default=False, + help="runs the docstring examples") + opt('-v', '--verbose', dest='verbose', action='store_true', default=True) + opt('-q', '--quiet', dest='verbose', action='store_false') + + settings, path = parser.parse_args() + path = ' '.join(path) + + if settings.doctest: + import doctest + doctest.testmod() + return 0 + if not path: + parser.error("No file specified") + if settings.test: + test_tifffile(path, settings.verbose) + return 0 + + if any(i in path for i in '?*'): + path = glob.glob(path) + if not path: + print('no files match the pattern') + return 0 + # TODO: handle image sequences + #if len(path) == 1: + path = path[0] + + print("Reading file structure...", end=' ') + start = time.time() + try: + tif = TiffFile(path, multifile=not settings.nomultifile) + except Exception as e: + if settings.debug: + raise + else: + print("\n", e) + sys.exit(0) + print("%.3f ms" % ((time.time()-start) * 1e3)) + + if tif.is_ome: + settings.norgb = True + + images = [(None, tif[0 if settings.page < 0 else settings.page])] + if not settings.noplot: + print("Reading image data... ", end=' ') + + def notnone(x): + return next(i for i in x if i is not None) + start = time.time() + try: + if settings.page >= 0: + images = [(tif.asarray(key=settings.page), + tif[settings.page])] + elif settings.series >= 0: + images = [(tif.asarray(series=settings.series), + notnone(tif.series[settings.series].pages))] + else: + images = [] + for i, s in enumerate(tif.series): + try: + images.append( + (tif.asarray(series=i), notnone(s.pages))) + except ValueError as e: + images.append((None, notnone(s.pages))) + if settings.debug: + raise + else: + print("\n* series %i failed: %s... " % (i, e), + end='') + print("%.3f ms" % ((time.time()-start) * 1e3)) + except Exception as e: + if settings.debug: + raise + else: + print(e) + + tif.close() + + print("\nTIFF file:", tif) + print() + for i, s in enumerate(tif.series): + print ("Series %i" % i) + print(s) + print() + for i, page in images: + print(page) + print(page.tags) + if page.is_palette: + print("\nColor Map:", page.color_map.shape, page.color_map.dtype) + for attr in ('cz_lsm_info', 'cz_lsm_scan_info', 'uic_tags', + 'mm_header', 'imagej_tags', 'micromanager_metadata', + 'nih_image_header'): + if hasattr(page, attr): + print("", attr.upper(), Record(getattr(page, attr)), sep="\n") + print() + if page.is_micromanager: + print('MICROMANAGER_FILE_METADATA') + print(Record(tif.micromanager_metadata)) + + if images and not settings.noplot: + try: + import matplotlib + matplotlib.use('TkAgg') + from matplotlib import pyplot + except ImportError as e: + warnings.warn("failed to import matplotlib.\n%s" % e) + else: + for img, page in images: + if img is None: + continue + vmin, vmax = None, None + if 'gdal_nodata' in page.tags: + try: + vmin = numpy.min(img[img > float(page.gdal_nodata)]) + except ValueError: + pass + if page.is_stk: + try: + vmin = page.uic_tags['min_scale'] + vmax = page.uic_tags['max_scale'] + except KeyError: + pass + else: + if vmax <= vmin: + vmin, vmax = None, None + title = "%s\n %s" % (str(tif), str(page)) + imshow(img, title=title, vmin=vmin, vmax=vmax, + bitspersample=page.bits_per_sample, + photometric=page.photometric, + interpolation=settings.interpol, + dpi=settings.dpi) + pyplot.show() + + +TIFFfile = TiffFile # backwards compatibility + +if sys.version_info[0] > 2: + basestring = str, bytes + unicode = str + +if __name__ == "__main__": + sys.exit(main()) + diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index c46fde01..567f561f 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -1,3 +1,4 @@ +from ._canny import canny from ._daisy import daisy from ._hog import hog from .texture import greycomatrix, greycoprops, local_binary_pattern @@ -9,6 +10,7 @@ from .corner import (corner_kitchen_rosenfeld, corner_harris, hessian_matrix_eigvals, hessian_matrix_det) from .corner_cy import corner_moravec, corner_orientations from .template import match_template +from .register_translation import register_translation from .brief import BRIEF from .censure import CENSURE from .orb import ORB @@ -17,7 +19,8 @@ from .util import plot_matches from .blob import blob_dog, blob_log, blob_doh -__all__ = ['daisy', +__all__ = ['canny', + 'daisy', 'hog', 'greycomatrix', 'greycoprops', @@ -38,6 +41,7 @@ __all__ = ['daisy', 'corner_fast', 'corner_orientations', 'match_template', + 'register_translation', 'BRIEF', 'CENSURE', 'ORB', diff --git a/skimage/filter/_canny.py b/skimage/feature/_canny.py similarity index 97% rename from skimage/filter/_canny.py rename to skimage/feature/_canny.py index 185bfd40..73566d00 100644 --- a/skimage/filter/_canny.py +++ b/skimage/feature/_canny.py @@ -16,7 +16,8 @@ import numpy as np import scipy.ndimage as ndi from scipy.ndimage import (gaussian_filter, generate_binary_structure, binary_erosion, label) -from skimage import dtype_limits +from .. import dtype_limits +from .._shared.utils import assert_nD def smooth_with_function_and_mask(image, function, mask): @@ -108,15 +109,15 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): Examples -------- - >>> from skimage import filter + >>> from skimage import feature >>> # Generate noisy image of a square >>> im = np.zeros((256, 256)) >>> im[64:-64, 64:-64] = 1 - >>> im += 0.2 * np.random.random(im.shape) + >>> im += 0.2 * np.random.rand(*im.shape) >>> # First trial with the Canny filter, with the default smoothing - >>> edges1 = filter.canny(im) + >>> edges1 = feature.canny(im) >>> # Increase the smoothing for better results - >>> edges2 = filter.canny(im, sigma=3) + >>> edges2 = feature.canny(im, sigma=3) """ # @@ -148,9 +149,7 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): # mask by one and then mask the output. We also mask out the border points # because who knows what lies beyond the edge of the image? # - - if image.ndim != 2: - raise TypeError("The input 'image' must be a two-dimensional array.") + assert_nD(image, 2) if low_threshold is None: low_threshold = 0.1 * dtype_limits(image)[1] diff --git a/skimage/feature/_daisy.py b/skimage/feature/_daisy.py index 3a55faf1..4ba42f25 100644 --- a/skimage/feature/_daisy.py +++ b/skimage/feature/_daisy.py @@ -1,8 +1,9 @@ import numpy as np from scipy import sqrt, pi, arctan2, cos, sin, exp from scipy.ndimage import gaussian_filter -import skimage.color -from skimage import img_as_float, draw +from .. import img_as_float, draw +from ..color import gray2rgb +from .._shared.utils import assert_nD def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8, @@ -93,9 +94,7 @@ def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8, .. [2] http://cvlab.epfl.ch/alumni/tola/daisy.html ''' - # Validate image format. - if img.ndim != 2: - raise ValueError('Only grey-level images are supported.') + assert_nD(img, 2, 'img') img = img_as_float(img) @@ -178,7 +177,7 @@ def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8, descs[:, :, i:i + orientations] /= norms[:, :, np.newaxis] if visualize: - descs_img = skimage.color.gray2rgb(img) + descs_img = gray2rgb(img) for i in range(descs.shape[0]): for j in range(descs.shape[1]): # Draw center histogram sigma diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 67922b92..11f7099d 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -1,6 +1,7 @@ import numpy as np from scipy import sqrt, pi, arctan2, cos, sin from scipy.ndimage import uniform_filter +from .._shared.utils import assert_nD def hog(image, orientations=9, pixels_per_cell=(8, 8), @@ -59,8 +60,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), shadowing and illumination variations. """ - if image.ndim > 2: - raise ValueError("Currently only supports grey-level images") + assert_nD(image, 2) if normalise: image = sqrt(image) @@ -80,10 +80,14 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # to avoid problems with subtracting unsigned numbers in np.diff() image = image.astype('float') - gx = np.zeros(image.shape) - gy = np.zeros(image.shape) - gx[:, :-1] = np.diff(image, n=1, axis=1) - gy[:-1, :] = np.diff(image, n=1, axis=0) + gx = np.empty(image.shape, dtype=np.double) + gx[:, 0] = 0 + gx[:, -1] = 0 + gx[:, 1:-1] = image[:, 2:] - image[:, :-2] + gy = np.empty(image.shape, dtype=np.double) + gy[0, :] = 0 + gy[-1, :] = 0 + gy[1:-1, :] = image[2:, :] - image[:-2, :] """ The third stage aims to produce an encoding that is sensitive to @@ -100,7 +104,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), cell are used to vote into the orientation histogram. """ - magnitude = sqrt(gx**2 + gy**2) + magnitude = sqrt(gx ** 2 + gy ** 2) orientation = arctan2(gy, gx) * (180 / pi) % 180 sy, sx = image.shape @@ -115,7 +119,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), 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 + # create new integral image for this orientation # isolate orientations in this range temp_ori = np.where(orientation < 180.0 / orientations * (i + 1), @@ -133,7 +137,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), hog_image = None if visualise: - from skimage import draw + from .. import draw radius = min(cx, cy) // 2 - 1 hog_image = np.zeros((sy, sx), dtype=float) @@ -144,9 +148,9 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), dx = radius * cos(float(o) / orientations * np.pi) dy = radius * sin(float(o) / orientations * np.pi) rr, cc = draw.line(int(centre[0] - dx), - int(centre[1] - dy), + int(centre[1] + dy), int(centre[0] + dx), - int(centre[1] + dy)) + int(centre[1] - dy)) hog_image[rr, cc] += orientation_histogram[y, x, o] """ @@ -173,7 +177,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), for y in range(n_blocksy): block = orientation_histogram[y:y + by, x:x + bx, :] eps = 1e-5 - normalised_blocks[y, x, :] = block / sqrt(block.sum()**2 + eps) + normalised_blocks[y, x, :] = block / sqrt(block.sum() ** 2 + eps) """ The final step collects the HOG descriptors from all blocks of a dense diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 8d49b377..5eb91486 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, round +from .._shared.interpolation cimport bilinear_interpolation, round def _glcm_loop(cnp.uint8_t[:, ::1] image, double[:] distances, @@ -115,7 +115,7 @@ def _local_binary_pattern(double[:, ::1] image, # pre-allocate arrays for computation cdef double[::1] texture = np.zeros(P, dtype=np.double) - cdef char[::1] signed_texture = np.zeros(P, dtype=np.int8) + cdef signed char[::1] signed_texture = np.zeros(P, dtype=np.int8) cdef int[::1] rotation_chain = np.zeros(P, dtype=np.int32) output_shape = (image.shape[0], image.shape[1]) diff --git a/skimage/feature/blob.py b/skimage/feature/blob.py index 134e7026..bc93d265 100644 --- a/skimage/feature/blob.py +++ b/skimage/feature/blob.py @@ -5,10 +5,11 @@ import itertools as itt import math from math import sqrt, hypot, log from numpy import arccos -from skimage.util import img_as_float +from ..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 +from ..transform import integral_image +from .._shared.utils import assert_nD # This basic blob detection algorithm is based on: @@ -52,8 +53,14 @@ def _blob_overlap(blob1, blob2): 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)) + ratio1 = (d ** 2 + r1 ** 2 - r2 ** 2) / (2 * d * r1) + ratio1 = np.clip(ratio1, -1, 1) + acos1 = arccos(ratio1) + + ratio2 = (d ** 2 + r2 ** 2 - r1 ** 2) / (2 * d * r2) + ratio2 = np.clip(ratio2, -1, 1) + acos2 = arccos(ratio2) + a = -d + r2 + r1 b = d - r2 + r1 c = d + r2 - r1 @@ -169,9 +176,7 @@ def blob_dog(image, min_sigma=1, max_sigma=50, sigma_ratio=1.6, threshold=2.0, ----- The radius of each blob is approximately :math:`\sqrt{2}sigma`. """ - - if image.ndim != 2: - raise ValueError("'image' must be a grayscale ") + assert_nD(image, 2) image = img_as_float(image) @@ -180,7 +185,7 @@ def blob_dog(image, min_sigma=1, max_sigma=50, sigma_ratio=1.6, threshold=2.0, # a geometric progression of standard deviations for gaussian kernels sigma_list = np.array([min_sigma * (sigma_ratio ** i) - for i in range(k + 1)]) + for i in range(k + 1)]) gaussian_images = [gaussian_filter(image, s) for s in sigma_list] @@ -275,8 +280,7 @@ def blob_log(image, min_sigma=1, max_sigma=50, num_sigma=10, threshold=.2, The radius of each blob is approximately :math:`\sqrt{2}sigma`. """ - if image.ndim != 2: - raise ValueError("'image' must be a grayscale ") + assert_nD(image, 2) image = img_as_float(image) @@ -385,8 +389,7 @@ def blob_doh(image, min_sigma=1, max_sigma=30, num_sigma=10, threshold=0.01, due to the box filters used in the approximation of Hessian Determinant. """ - if image.ndim != 2: - raise ValueError("'image' must be grayscale ") + assert_nD(image, 2) image = img_as_float(image) image = integral_image(image) diff --git a/skimage/feature/brief.py b/skimage/feature/brief.py index d1626f17..783cdbd0 100644 --- a/skimage/feature/brief.py +++ b/skimage/feature/brief.py @@ -5,6 +5,7 @@ from .util import (DescriptorExtractor, _mask_border_keypoints, _prepare_grayscale_input_2D) from .brief_cy import _brief_loop +from .._shared.utils import assert_nD class BRIEF(DescriptorExtractor): @@ -137,6 +138,7 @@ class BRIEF(DescriptorExtractor): Keypoint coordinates as ``(row, col)``. """ + assert_nD(image, 2) np.random.seed(self.sample_seed) @@ -169,13 +171,13 @@ class BRIEF(DescriptorExtractor): # Removing keypoints that are within (patch_size / 2) distance from the # image border self.mask = _mask_border_keypoints(image.shape, keypoints, - patch_size // 2) + 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') + dtype=bool, order='C') _brief_loop(image, self.descriptors.view(np.uint8), keypoints, pos1, pos2) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index eb69f115..d75c03ca 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -1,15 +1,13 @@ 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 import structure_tensor -from skimage.morphology import octagon, star -from skimage.feature.util import _mask_border_keypoints - -from skimage.feature.censure_cy import _censure_dob_loop - +from ..transform import integral_image +from ..feature import structure_tensor +from ..morphology import octagon, star +from ..feature.censure_cy import _censure_dob_loop +from ..feature.util import (FeatureDetector, _prepare_grayscale_input_2D, + _mask_border_keypoints) +from .._shared.utils import assert_nD # The paper(Reference [1]) mentions the sizes of the Octagon shaped filter # kernel for the first seven scales only. The sizes of the later scales @@ -31,7 +29,7 @@ STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), def _filter_image(image, min_scale, max_scale, mode): response = np.zeros((image.shape[0], image.shape[1], - max_scale - min_scale + 1), dtype=np.double) + max_scale - min_scale + 1), dtype=np.double) if mode == 'dob': @@ -48,8 +46,8 @@ def _filter_image(image, min_scale, max_scale, mode): # Constant multipliers for the outer region and the inner region # of the bi-level filters with the constraint of keeping the # DC bias 0. - inner_weight = (1.0 / (2 * n + 1)**2) - outer_weight = (1.0 / (12 * n**2 + 4 * n)) + inner_weight = (1.0 / (2 * n + 1) ** 2) + outer_weight = (1.0 / (12 * n ** 2 + 4 * n)) _censure_dob_loop(n, integral_img, response[:, :, i], inner_weight, outer_weight) @@ -79,8 +77,8 @@ def _filter_image(image, min_scale, max_scale, mode): 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 = (mo + 2 * no) ** 2 - 2 * no * (no + 1) + inner = (mi + 2 * ni) ** 2 - 2 * ni * (ni + 1) outer_weight = 1.0 / (outer - inner) inner_weight = 1.0 / inner c = ((mo + 2 * no) - (mi + 2 * ni)) // 2 @@ -110,7 +108,6 @@ def _suppress_lines(feature_mask, image, sigma, line_threshold): > line_threshold * (Axx * Ayy - Axy ** 2)] = False - class CENSURE(FeatureDetector): """CENSURE keypoint detector. @@ -160,30 +157,38 @@ class CENSURE(FeatureDetector): Examples -------- - >>> from skimage.data import lena + >>> from skimage.data import astronaut >>> from skimage.color import rgb2gray >>> from skimage.feature import CENSURE - >>> img = rgb2gray(lena()[100:300, 100:300]) + >>> img = rgb2gray(astronaut()[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]]) + array([[ 4, 148], + [ 12, 73], + [ 21, 176], + [ 91, 22], + [ 93, 56], + [ 94, 22], + [ 95, 54], + [100, 51], + [103, 51], + [106, 67], + [108, 15], + [117, 20], + [122, 60], + [125, 37], + [129, 37], + [133, 76], + [145, 44], + [146, 94], + [150, 114], + [153, 33], + [154, 156], + [155, 151], + [184, 63]]) >>> censure.scales - array([2, 4, 2, 3, 4, 2, 2, 3, 4, 6, 3, 2, 3, 4, 2]) + array([2, 6, 6, 2, 4, 3, 2, 3, 2, 6, 3, 2, 2, 3, 2, 2, 2, 3, 2, 2, 4, 2, 2]) """ @@ -231,6 +236,8 @@ class CENSURE(FeatureDetector): # (4) Finally, we remove the border keypoints and return the keypoints # along with its corresponding scale. + assert_nD(image, 2) + num_scales = self.max_scale - self.min_scale image = np.ascontiguousarray(_prepare_grayscale_input_2D(image)) diff --git a/skimage/feature/corner.py b/skimage/feature/corner.py index fefff40d..5e2251dc 100644 --- a/skimage/feature/corner.py +++ b/skimage/feature/corner.py @@ -2,10 +2,10 @@ import numpy as np from scipy import ndimage from scipy import stats -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 ..util import img_as_float, pad +from ..feature import peak_local_max +from ..feature.util import _prepare_grayscale_input_2D +from ..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 @@ -288,7 +288,7 @@ def hessian_matrix_eigvals(Hxx, Hxy, Hyy): """ - return _image_orthogonal_matrix22_eigvals(Hyy, Hxy, Hyy) + return _image_orthogonal_matrix22_eigvals(Hxx, Hxy, Hyy) def corner_kitchen_rosenfeld(image, mode='constant', cval=0): @@ -323,8 +323,8 @@ def corner_kitchen_rosenfeld(image, mode='constant', cval=0): 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) + numerator = (imxx * imy ** 2 + imyy * imx ** 2 - 2 * imxy * imx * imy) + denominator = (imx ** 2 + imy ** 2) response = np.zeros_like(image, dtype=np.double) @@ -403,12 +403,12 @@ def corner_harris(image, method='k', k=0.05, eps=1e-6, sigma=1): Axx, Axy, Ayy = structure_tensor(image, sigma) # determinant - detA = Axx * Ayy - Axy**2 + detA = Axx * Ayy - Axy ** 2 # trace traceA = Axx + Ayy if method == 'k': - response = detA - k * traceA**2 + response = detA - k * traceA ** 2 else: response = 2 * detA / (traceA + eps) @@ -473,7 +473,7 @@ def corner_shi_tomasi(image, sigma=1): Axx, Axy, Ayy = structure_tensor(image, sigma) # minimum eigenvalue of A - response = ((Axx + Ayy) - np.sqrt((Axx - Ayy)**2 + 4 * Axy**2)) / 2 + response = ((Axx + Ayy) - np.sqrt((Axx - Ayy) ** 2 + 4 * Axy ** 2)) / 2 return response @@ -543,7 +543,7 @@ def corner_foerstner(image, sigma=1): Axx, Axy, Ayy = structure_tensor(image, sigma) # determinant - detA = Axx * Ayy - Axy**2 + detA = Axx * Ayy - Axy ** 2 # trace traceA = Axx + Ayy @@ -553,7 +553,7 @@ def corner_foerstner(image, sigma=1): mask = traceA != 0 w[mask] = detA[mask] / traceA[mask] - q[mask] = 4 * detA[mask] / traceA[mask]**2 + q[mask] = 4 * detA[mask] / traceA[mask] ** 2 return w, q @@ -692,7 +692,7 @@ def corner_subpix(image, corners, window_size=11, alpha=0.99): b_edge = np.zeros((2, ), dtype=np.double) # critical statistical test values - redundancy = window_size**2 - 2 + redundancy = window_size ** 2 - 2 t_crit_dot = stats.f.isf(1 - alpha, redundancy, redundancy) t_crit_edge = stats.f.isf(alpha, redundancy, redundancy) diff --git a/skimage/feature/corner_cy.pyx b/skimage/feature/corner_cy.pyx index 86ad3c43..cef104e0 100644 --- a/skimage/feature/corner_cy.pyx +++ b/skimage/feature/corner_cy.pyx @@ -7,8 +7,8 @@ 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 ..util import img_as_float, pad +from ..color import rgb2grey from .util import _prepare_grayscale_input_2D @@ -87,7 +87,7 @@ def corner_moravec(image, Py_ssize_t window_size=1): cdef inline double _corner_fast_response(double curr_pixel, double* circle_intensities, - char* bins, char state, char n): + signed char* bins, signed char state, char n): cdef char consecutive_count = 0 cdef double curr_response cdef Py_ssize_t l, m @@ -104,22 +104,22 @@ cdef inline double _corner_fast_response(double curr_pixel, return 0 -def _corner_fast(double[:, ::1] image, char n, double threshold): +def _corner_fast(double[:, ::1] image, signed 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 signed 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 signed char *rp = [0, 1, 2, 3, 3, 3, 2, 1, 0, -1, -2, -3, -3, -3, -2, -1] + cdef signed char *cp = [3, 3, 2, 1, 0, -1, -2, -3, -3, -3, -2, -1, 0, 1, 2, 3] + cdef signed char bins[16] cdef double circle_intensities[16] cdef double curr_response diff --git a/skimage/feature/orb.py b/skimage/feature/orb.py index 2ddcf4f3..b8f2a89e 100644 --- a/skimage/feature/orb.py +++ b/skimage/feature/orb.py @@ -1,12 +1,13 @@ import numpy as np -from skimage.feature.util import (FeatureDetector, DescriptorExtractor, - _mask_border_keypoints, - _prepare_grayscale_input_2D) +from ..feature.util import (FeatureDetector, DescriptorExtractor, + _mask_border_keypoints, + _prepare_grayscale_input_2D) -from skimage.feature import (corner_fast, corner_orientations, corner_peaks, +from ..feature import (corner_fast, corner_orientations, corner_peaks, corner_harris) -from skimage.transform import pyramid_gaussian +from ..transform import pyramid_gaussian +from .._shared.utils import assert_nD from .orb_cy import _orb_loop @@ -15,7 +16,7 @@ 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 + OFAST_MASK[15 + j, 15 + i] = 1 class ORB(FeatureDetector, DescriptorExtractor): @@ -166,6 +167,7 @@ class ORB(FeatureDetector, DescriptorExtractor): Input image. """ + assert_nD(image, 2) pyramid = self._build_pyramid(image) @@ -183,7 +185,7 @@ class ORB(FeatureDetector, DescriptorExtractor): keypoints_list.append(keypoints * self.downscale ** octave) orientations_list.append(orientations) - scales_list.append(self.downscale ** octave + scales_list.append(self.downscale ** octave * np.ones(keypoints.shape[0], dtype=np.intp)) responses_list.append(responses) @@ -237,6 +239,7 @@ class ORB(FeatureDetector, DescriptorExtractor): Corresponding orientations in radians. """ + assert_nD(image, 2) pyramid = self._build_pyramid(image) @@ -282,6 +285,7 @@ class ORB(FeatureDetector, DescriptorExtractor): Input image. """ + assert_nD(image, 2) pyramid = self._build_pyramid(image) @@ -310,7 +314,7 @@ class ORB(FeatureDetector, DescriptorExtractor): keypoints_list.append(keypoints[mask] * self.downscale ** octave) responses_list.append(responses[mask]) orientations_list.append(orientations[mask]) - scales_list.append(self.downscale ** octave + scales_list.append(self.downscale ** octave * np.ones(keypoints.shape[0], dtype=np.intp)) descriptors_list.append(descriptors) diff --git a/skimage/feature/orb_cy.pyx b/skimage/feature/orb_cy.pyx index 6e0801f8..49c4e650 100644 --- a/skimage/feature/orb_cy.pyx +++ b/skimage/feature/orb_cy.pyx @@ -6,12 +6,12 @@ import os import numpy as np -from skimage import data_dir +from .. import data_dir cimport numpy as cnp from libc.math cimport sin, cos -from skimage._shared.interpolation cimport round +from .._shared.interpolation cimport round POS = np.loadtxt(os.path.join(data_dir, "orb_descriptor_positions.txt"), dtype=np.int8) @@ -25,10 +25,10 @@ def _orb_loop(double[:, ::1] image, Py_ssize_t[:, ::1] keypoints, 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 + cdef unsigned char[:, ::1] descriptors = \ + np.zeros((keypoints.shape[0], POS.shape[0]), dtype=np.uint8) + cdef signed char[:, ::1] cpos0 = POS0 + cdef signed char[:, ::1] cpos1 = POS1 for i in range(descriptors.shape[0]): diff --git a/skimage/feature/peak.py b/skimage/feature/peak.py index 378d22b4..599a6e58 100644 --- a/skimage/feature/peak.py +++ b/skimage/feature/peak.py @@ -1,6 +1,6 @@ import numpy as np import scipy.ndimage as ndi -from ..filter import rank_order +from ..filters import rank_order def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, @@ -146,10 +146,10 @@ def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, peak_threshold = max(np.max(image.ravel()) * threshold_rel, threshold_abs) # get coordinates of peaks - coordinates = np.transpose((image > peak_threshold).nonzero()) + coordinates = np.argwhere(image > peak_threshold) if coordinates.shape[0] > num_peaks: - intensities = image[coordinates[:, 0], coordinates[:, 1]] + intensities = image.flat[np.ravel_multi_index(coordinates.transpose(),image.shape)] idx_maxsort = np.argsort(intensities)[::-1] coordinates = coordinates[idx_maxsort][:num_peaks] diff --git a/skimage/feature/register_translation.py b/skimage/feature/register_translation.py new file mode 100644 index 00000000..6e94c867 --- /dev/null +++ b/skimage/feature/register_translation.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- """ +""" +Port of Manuel Guizar's code from: +http://www.mathworks.com/matlabcentral/fileexchange/18401-efficient-subpixel-image-registration-by-cross-correlation +""" + +import numpy as np + + +def _upsampled_dft(data, upsampled_region_size, + upsample_factor=1, axis_offsets=None): + """ + Upsampled DFT by matrix multiplication. + + This code is intended to provide the same result as if the following + operations were performed: + - Embed the array "data" in an array that is ``upsample_factor`` times + larger in each dimension. ifftshift to bring the center of the + image to (1,1). + - Take the FFT of the larger array. + - Extract an ``[upsampled_region_size]`` region of the result, starting + with the ``[axis_offsets+1]`` element. + + It achieves this result by computing the DFT in the output array without + the need to zeropad. Much faster and memory efficient than the zero-padded + FFT approach if ``upsampled_region_size`` is much smaller than + ``data.size * upsample_factor``. + + Parameters + ---------- + data : 2D ndarray + The input data array (DFT of original data) to upsample. + upsampled_region_size : integer or tuple of integers, optional + The size of the region to be sampled. If one integer is provided, it + is duplicated up to the dimensionality of ``data``. + upsample_factor : integer, optional + The upsampling factor. Defaults to 1. + axis_offsets : tuple of integers, optional + The offsets of the region to be sampled. Defaults to None (uses + image center) + + Returns + ------- + output : 2D ndarray + The upsampled DFT of the specified region. + """ + # if people pass in an integer, expand it to a list of equal-sized sections + if not hasattr(upsampled_region_size, "__iter__"): + upsampled_region_size = [upsampled_region_size, ] * data.ndim + else: + if len(upsampled_region_size) != data.ndim: + raise ValueError("shape of upsampled region sizes must be equal " + "to input data's number of dimensions.") + + if axis_offsets is None: + axis_offsets = [0, ] * data.ndim + else: + if len(axis_offsets) != data.ndim: + raise ValueError("number of axis offsets must be equal to input " + "data's number of dimensions.") + + col_kernel = np.exp( + (-1j * 2 * np.pi / (data.shape[1] * upsample_factor)) * + (np.fft.ifftshift(np.arange(data.shape[1]))[:, None] - + np.floor(data.shape[1] / 2)).dot( + np.arange(upsampled_region_size[1])[None, :] - axis_offsets[1]) + ) + row_kernel = np.exp( + (-1j * 2 * np.pi / (data.shape[0] * upsample_factor)) * + (np.arange(upsampled_region_size[0])[:, None] - axis_offsets[0]).dot( + np.fft.ifftshift(np.arange(data.shape[0]))[None, :] - + np.floor(data.shape[0] / 2)) + ) + + return row_kernel.dot(data).dot(col_kernel) + + +def _compute_phasediff(cross_correlation_max): + """ + Compute global phase difference between the two images (should be + zero if images are non-negative). + + Parameters + ---------- + cross_correlation_max : complex + The complex value of the cross correlation at its maximum point. + """ + return np.arctan2(cross_correlation_max.imag, cross_correlation_max.real) + + +def _compute_error(cross_correlation_max, src_amp, target_amp): + """ + Compute RMS error metric between ``src_image`` and ``target_image``. + + Parameters + ---------- + cross_correlation_max : complex + The complex value of the cross correlation at its maximum point. + src_amp : float + The normalized average image intensity of the source image + target_amp : float + The normalized average image intensity of the target image + """ + error = 1.0 - cross_correlation_max * cross_correlation_max.conj() /\ + (src_amp * target_amp) + return np.sqrt(np.abs(error)) + + +def register_translation(src_image, target_image, upsample_factor=1, + space="real"): + """ + Efficient subpixel image translation registration by cross-correlation. + + This code gives the same precision as the FFT upsampled cross-correlation + in a fraction of the computation time and with reduced memory requirements. + It obtains an initial estimate of the cross-correlation peak by an FFT and + then refines the shift estimation by upsampling the DFT only in a small + neighborhood of that estimate by means of a matrix-multiply DFT. + + Parameters + ---------- + src_image : ndarray + Reference image. + target_image : ndarray + Image to register. Must be same dimensionality as ``src_image``. + upsample_factor : int, optional + Upsampling factor. Images will be registered to within + ``1 / upsample_factor`` of a pixel. For example + ``upsample_factor == 20`` means the images will be registered + within 1/20th of a pixel. Default is 1 (no upsampling) + space : string, one of "real" or "fourier" + Defines how the algorithm interprets input data. "real" means data + will be FFT'd to compute the correlation, while "fourier" data will + bypass FFT of input data. Case insensitive. + + Returns + ------- + shifts : ndarray + Shift vector (in pixels) required to register ``target_image`` with + ``src_image``. Axis ordering is consistent with numpy (e.g. Z, Y, X) + error : float + Translation invariant normalized RMS error between ``src_image`` and + ``target_image``. + phasediff : float + Global phase difference between the two images (should be + zero if images are non-negative). + + References + ---------- + .. [1] Manuel Guizar-Sicairos, Samuel T. Thurman, and James R. Fienup, + "Efficient subpixel image registration algorithms," + Optics Letters 33, 156-158 (2008). + """ + # images must be the same shape + if src_image.shape != target_image.shape: + raise ValueError("Error: images must be same size for " + "register_translation") + + # only 2D data makes sense right now + if src_image.ndim != 2 and upsample_factor > 1: + raise NotImplementedError("Error: register_translation only supports " + "subpixel registration for 2D images") + + # assume complex data is already in Fourier space + if space.lower() == 'fourier': + src_freq = src_image + target_freq = target_image + # real data needs to be fft'd. + elif space.lower() == 'real': + src_image = np.array(src_image, dtype=np.complex128, copy=False) + target_image = np.array(target_image, dtype=np.complex128, copy=False) + src_freq = np.fft.fftn(src_image) + target_freq = np.fft.fftn(target_image) + else: + raise ValueError("Error: register_translation only knows the \"real\" " + "and \"fourier\" values for the ``space`` argument.") + + # Whole-pixel shift - Compute cross-correlation by an IFFT + shape = src_freq.shape + image_product = src_freq * target_freq.conj() + cross_correlation = np.fft.ifftn(image_product) + + # Locate maximum + maxima = np.unravel_index(np.argmax(np.abs(cross_correlation)), + cross_correlation.shape) + midpoints = np.array([np.fix(axis_size / 2) for axis_size in shape]) + + shifts = np.array(maxima, dtype=np.float64) + shifts[shifts > midpoints] -= np.array(shape)[shifts > midpoints] + + if upsample_factor == 1: + src_amp = np.sum(np.abs(src_freq) ** 2) / src_freq.size + target_amp = np.sum(np.abs(target_freq) ** 2) / target_freq.size + CCmax = cross_correlation.max() + # If upsampling > 1, then refine estimate with matrix multiply DFT + else: + # Initial shift estimate in upsampled grid + shifts = np.round(shifts * upsample_factor) / upsample_factor + upsampled_region_size = np.ceil(upsample_factor * 1.5) + # Center of output array at dftshift + 1 + dftshift = np.fix(upsampled_region_size / 2.0) + upsample_factor = np.array(upsample_factor, dtype=np.float64) + normalization = (src_freq.size * upsample_factor ** 2) + # Matrix multiply DFT around the current shift estimate + sample_region_offset = dftshift - shifts*upsample_factor + cross_correlation = _upsampled_dft(image_product.conj(), + upsampled_region_size, + upsample_factor, + sample_region_offset).conj() + cross_correlation /= normalization + # Locate maximum and map back to original pixel grid + maxima = np.array(np.unravel_index( + np.argmax(np.abs(cross_correlation)), + cross_correlation.shape), + dtype=np.float64) + maxima -= dftshift + shifts = shifts + maxima / upsample_factor + CCmax = cross_correlation.max() + src_amp = _upsampled_dft(src_freq * src_freq.conj(), + 1, upsample_factor)[0, 0] + src_amp /= normalization + target_amp = _upsampled_dft(target_freq * target_freq.conj(), + 1, upsample_factor)[0, 0] + target_amp /= normalization + + # If its only one row or column the shift along that dimension has no + # effect. We set to zero. + for dim in range(src_freq.ndim): + if shape[dim] == 1: + shifts[dim] = 0 + + return shifts, _compute_error(CCmax, src_amp, target_amp),\ + _compute_phasediff(CCmax) diff --git a/skimage/feature/template.py b/skimage/feature/template.py index fbf95866..7aad0dfb 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -1,18 +1,19 @@ import numpy as np from scipy.signal import fftconvolve -from skimage.util import pad +from ..util import pad +from .._shared.utils import assert_nD def _window_sum_2d(image, window_shape): window_sum = np.cumsum(image, axis=0) window_sum = (window_sum[window_shape[0]:-1] - - 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]) + - window_sum[:, :-window_shape[1] - 1]) return window_sum @@ -23,7 +24,7 @@ def _window_sum_3d(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]) + - window_sum[:, :, :-window_shape[2] - 1]) return window_sum @@ -102,9 +103,8 @@ def match_template(image, template, pad_input=False, mode='constant', [ 0. , 0. , 0. , 0.125, -1. , 0.125], [ 0. , 0. , 0. , 0.125, 0.125, 0.125]], dtype=float32) """ + assert_nD(image, (2, 3)) - 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.") @@ -126,13 +126,13 @@ def match_template(image, template, pad_input=False, mode='constant', # 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) + 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) + image_window_sum2 = _window_sum_3d(image ** 2, template.shape) template_volume = np.prod(template.shape) - template_ssd = np.sum((template - template.mean())**2) + template_ssd = np.sum((template - template.mean()) ** 2) if image.ndim == 2: xcorr = fftconvolve(image, template[::-1, ::-1], diff --git a/skimage/feature/tests/__init__.py b/skimage/feature/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/feature/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/filter/tests/test_canny.py b/skimage/feature/tests/test_canny.py similarity index 96% rename from skimage/filter/tests/test_canny.py rename to skimage/feature/tests/test_canny.py index 2c758edf..400a7f70 100644 --- a/skimage/filter/tests/test_canny.py +++ b/skimage/feature/tests/test_canny.py @@ -1,7 +1,7 @@ import unittest import numpy as np from scipy.ndimage import binary_dilation, binary_erosion -import skimage.filter as F +import skimage.feature as F class TestCanny(unittest.TestCase): @@ -60,7 +60,7 @@ class TestCanny(unittest.TestCase): self.assertTrue(point_count < 1600) def test_image_shape(self): - self.assertRaises(TypeError, F.canny, np.zeros((20, 20, 20)), 4, 0, 0) + self.assertRaises(ValueError, F.canny, np.zeros((20, 20, 20)), 4, 0, 0) def test_mask_none(self): result1 = F.canny(np.zeros((20, 20)), 4, 0, 0, np.ones((20, 20), bool)) diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 53c1c59b..95bfcda3 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -5,12 +5,13 @@ from skimage.feature import CENSURE img = moon() +np.random.seed(0) 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)) + rect_image = np.random.rand(300, 200) + square_image = np.random.rand(200, 200) CENSURE().detect((square_image)) CENSURE().detect((rect_image)) diff --git a/skimage/feature/tests/test_corner.py b/skimage/feature/tests/test_corner.py index 1c39dd2d..a9f5bb91 100644 --- a/skimage/feature/tests/test_corner.py +++ b/skimage/feature/tests/test_corner.py @@ -161,12 +161,12 @@ def test_squared_dot(): assert (results == np.array([[6, 6]])).all() -def test_rotated_lena(): +def test_rotated_img(): """ The harris filter should yield the same results with an image and it's rotation. """ - im = img_as_float(data.lena().mean(axis=2)) + im = img_as_float(data.astronaut().mean(axis=2)) im_rotated = im.T # Moravec @@ -235,13 +235,13 @@ def test_subpix_border(): 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""" + is run on the astronaut image in order to produce a sufficient number of corners""" - lena_corners = corner_harris(rgb2gray(data.lena())) + img_corners = corner_harris(rgb2gray(data.astronaut())) for i in range(20): n = np.random.random_integers(20) - results = peak_local_max(lena_corners, num_peaks=n) + results = peak_local_max(img_corners, num_peaks=n) assert (results.shape[0] == n) @@ -281,17 +281,44 @@ def test_corner_fast_image_unsupported_error(): 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]]) + img = rgb2gray(data.astronaut()) + expected = np.array([[101, 198], + [140, 205], + [141, 242], + [177, 156], + [188, 113], + [197, 148], + [213, 117], + [223, 375], + [232, 266], + [245, 137], + [249, 171], + [300, 244], + [305, 57], + [325, 245], + [339, 242], + [346, 279], + [353, 172], + [358, 307], + [362, 252], + [362, 328], + [363, 192], + [364, 147], + [369, 159], + [374, 171], + [379, 183], + [387, 195], + [390, 149], + [401, 197], + [403, 162], + [413, 181], + [444, 310], + [464, 251], + [476, 250], + [489, 155], + [492, 139], + [494, 169], + [496, 266]]) actual = corner_peaks(corner_fast(img, 12, 0.3)) assert_array_equal(actual, expected) diff --git a/skimage/feature/tests/test_daisy.py b/skimage/feature/tests/test_daisy.py index 32a7a5df..2e3b766c 100644 --- a/skimage/feature/tests/test_daisy.py +++ b/skimage/feature/tests/test_daisy.py @@ -13,7 +13,7 @@ def test_daisy_color_image_unsupported_error(): def test_daisy_desc_dims(): - img = img_as_float(data.lena()[:128, :128].mean(axis=2)) + img = img_as_float(data.astronaut()[:128, :128].mean(axis=2)) rings = 2 histograms = 4 orientations = 3 @@ -30,7 +30,7 @@ def test_daisy_desc_dims(): def test_descs_shape(): - img = img_as_float(data.lena()[:256, :256].mean(axis=2)) + img = img_as_float(data.astronaut()[:256, :256].mean(axis=2)) radius = 20 step = 8 descs = daisy(img, radius=radius, step=step) @@ -46,21 +46,21 @@ def test_descs_shape(): def test_daisy_sigmas_and_radii(): - img = img_as_float(data.lena()[:64, :64].mean(axis=2)) + img = img_as_float(data.astronaut()[: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()[:64, :64].mean(axis=2)) + img = img_as_float(data.astronaut()[:64, :64].mean(axis=2)) sigmas = [1, 2] radii = [1, 2] assert_raises(ValueError, daisy, img, sigmas=sigmas, ring_radii=radii) def test_daisy_normalization(): - img = img_as_float(data.lena()[:64, :64].mean(axis=2)) + img = img_as_float(data.astronaut()[:64, :64].mean(axis=2)) descs = daisy(img, normalization='l1') for i in range(descs.shape[0]): @@ -93,7 +93,7 @@ def test_daisy_normalization(): def test_daisy_visualization(): - img = img_as_float(data.lena()[:32, :32].mean(axis=2)) + img = img_as_float(data.astronaut()[:32, :32].mean(axis=2)) descs, descs_img = daisy(img, visualize=True) assert(descs_img.shape == (32, 32, 3)) diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index 8cab52f7..855bb557 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -10,7 +10,7 @@ from numpy.testing import (assert_raises, def test_histogram_of_oriented_gradients(): - img = img_as_float(data.lena()[:256, :].mean(axis=2)) + img = img_as_float(data.astronaut()[:256, :].mean(axis=2)) fd = feature.hog(img, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(1, 1)) diff --git a/skimage/feature/tests/test_match.py b/skimage/feature/tests/test_match.py index 1b0a622f..695116ed 100644 --- a/skimage/feature/tests/test_match.py +++ b/skimage/feature/tests/test_match.py @@ -32,7 +32,7 @@ def test_binary_descriptors_lena_rotation_crosscheck_false(): img = data.lena() img = rgb2gray(img) tform = tf.SimilarityTransform(scale=1, rotation=0.15, translation=(0, 0)) - rotated_img = tf.warp(img, tform) + rotated_img = tf.warp(img, tform, clip=False) extractor = BRIEF(descriptor_size=512) @@ -65,7 +65,7 @@ def test_binary_descriptors_lena_rotation_crosscheck_true(): img = data.lena() img = rgb2gray(img) tform = tf.SimilarityTransform(scale=1, rotation=0.15, translation=(0, 0)) - rotated_img = tf.warp(img, tform) + rotated_img = tf.warp(img, tform, clip=False) extractor = BRIEF(descriptor_size=512) diff --git a/skimage/feature/tests/test_orb.py b/skimage/feature/tests/test_orb.py index 30394d07..5bed2bf3 100644 --- a/skimage/feature/tests/test_orb.py +++ b/skimage/feature/tests/test_orb.py @@ -1,11 +1,11 @@ import numpy as np -from numpy.testing import assert_array_equal, assert_almost_equal +from numpy.testing import assert_equal, assert_almost_equal, run_module_suite from skimage.feature import ORB -from skimage.data import lena +from skimage import data from skimage.color import rgb2gray -img = rgb2gray(lena()) +img = rgb2gray(data.lena()) def test_keypoints_orb_desired_no_of_keypoints(): @@ -42,7 +42,6 @@ def test_keypoints_orb_desired_no_of_keypoints(): 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) @@ -102,14 +101,13 @@ def test_descriptor_orb(): 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]) + assert_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]) + assert_equal(exp_descriptors, + detector_extractor.descriptors[100:120, 10:20]) if __name__ == '__main__': - from numpy import testing - testing.run_module_suite() + run_module_suite() diff --git a/skimage/feature/tests/test_peak.py b/skimage/feature/tests/test_peak.py index 98242a4d..09e52495 100644 --- a/skimage/feature/tests/test_peak.py +++ b/skimage/feature/tests/test_peak.py @@ -5,6 +5,9 @@ import scipy.ndimage from skimage.feature import peak +np.random.seed(21) + + def test_trivial_case(): trivial = np.zeros((25, 25)) peak_indices = peak.peak_local_max(trivial, min_distance=1, indices=True) @@ -17,7 +20,7 @@ def test_noisy_peaks(): peak_locations = [(7, 7), (7, 13), (13, 7), (13, 13)] # image with noise of amplitude 0.8 and peaks of amplitude 1 - image = 0.8 * np.random.random((20, 20)) + image = 0.8 * np.random.rand(20, 20) for r, c in peak_locations: image[r, c] = 1 @@ -79,8 +82,16 @@ def test_num_peaks(): assert (3, 5) in peaks_limited +def test_num_peaks3D(): + # Issue 1354: the old code only hold for 2D arrays + # and this code would die with IndexError + image = np.zeros((10, 10, 100)) + image[5,5,::5] = np.arange(20) + peaks_limited = peak.peak_local_max(image, min_distance=1, num_peaks=2) + assert len(peaks_limited) == 2 + + def test_reorder_labels(): - np.random.seed(21) image = np.random.uniform(size=(40, 60)) i, j = np.mgrid[0:40, 0:60] labels = 1 + (i >= 20) + (j >= 30) * 2 @@ -100,7 +111,6 @@ def test_reorder_labels(): def test_indices_with_labels(): - np.random.seed(21) image = np.random.uniform(size=(40, 60)) i, j = np.mgrid[0:40, 0:60] labels = 1 + (i >= 20) + (j >= 30) * 2 @@ -233,7 +243,6 @@ def test_adjacent_different_objects(): def test_four_quadrants(): - np.random.seed(21) image = np.random.uniform(size=(40, 60)) i, j = np.mgrid[0:40, 0:60] labels = 1 + (i >= 20) + (j >= 30) * 2 @@ -255,7 +264,6 @@ def test_disk(): '''regression test of img-1194, footprint = [1] Test peak.peak_local_max when every point is a local maximum ''' - np.random.seed(31) image = np.random.uniform(size=(10, 20)) footprint = np.array([[1]]) result = peak.peak_local_max(image, labels=np.ones((10, 20)), diff --git a/skimage/feature/tests/test_register_translation.py b/skimage/feature/tests/test_register_translation.py new file mode 100644 index 00000000..6494630e --- /dev/null +++ b/skimage/feature/tests/test_register_translation.py @@ -0,0 +1,106 @@ +import numpy as np +from numpy.testing import assert_allclose, assert_raises + +from skimage.feature.register_translation import (register_translation, + _upsampled_dft) +from skimage.data import camera +from scipy.ndimage.fourier import fourier_shift + + +def test_correlation(): + reference_image = np.fft.fftn(camera()) + shift = (-7, 12) + shifted_image = fourier_shift(reference_image, shift) + + # pixel precision + result, error, diffphase = register_translation(reference_image, + shifted_image, + space="fourier") + assert_allclose(result[:2], -np.array(shift)) + + +def test_subpixel_precision(): + reference_image = np.fft.fftn(camera()) + subpixel_shift = (-2.4, 1.32) + shifted_image = fourier_shift(reference_image, subpixel_shift) + + # subpixel precision + result, error, diffphase = register_translation(reference_image, + shifted_image, 100, + space="fourier") + assert_allclose(result[:2], -np.array(subpixel_shift), atol=0.05) + + +def test_real_input(): + reference_image = camera() + subpixel_shift = (-2.4, 1.32) + shifted_image = fourier_shift(np.fft.fftn(reference_image), subpixel_shift) + shifted_image = np.fft.ifftn(shifted_image) + + # subpixel precision + result, error, diffphase = register_translation(reference_image, + shifted_image, 100) + assert_allclose(result[:2], -np.array(subpixel_shift), atol=0.05) + + +def test_size_one_dimension_input(): + # take a strip of the input image + reference_image = np.fft.fftn(camera()[:, 15]).reshape((-1, 1)) + subpixel_shift = (-2.4, 4) + shifted_image = fourier_shift(reference_image, subpixel_shift) + + # subpixel precision + result, error, diffphase = register_translation(reference_image, + shifted_image, 100, + space="fourier") + assert_allclose(result[:2], -np.array((-2.4, 0)), atol=0.05) + + +def test_3d_input(): + # TODO: this test case is waiting on a Phantom data set to be added to the + # data module. + # pixel precision + # result, error, diffphase = register_translation(ref_image, shifted_image) + + # assert_allclose(np.array(result[:2]), np.array(shift)) + pass + + +def test_unknown_space_input(): + image = np.ones((5, 5)) + assert_raises(ValueError, register_translation, image, image, + space="frank") + + +def test_wrong_input(): + # Dimensionality mismatch + image = np.ones((5, 5, 1)) + template = np.ones((5, 5)) + assert_raises(ValueError, register_translation, template, image) + + # Greater than 2 dimensions does not support subpixel precision + # (TODO: should support 3D at some point.) + image = np.ones((5, 5, 5)) + template = np.ones((5, 5, 5)) + assert_raises(NotImplementedError, register_translation, + template, image, 2) + + # Size mismatch + image = np.ones((5, 5)) + template = np.ones((4, 4)) + assert_raises(ValueError, register_translation, template, image) + + +def test_mismatch_upsampled_region_size(): + assert_raises(ValueError, _upsampled_dft, np.ones((4, 4)), + upsampled_region_size=[3, 2, 1, 4]) + + +def test_mismatch_offsets_size(): + assert_raises(ValueError, _upsampled_dft, np.ones((4, 4)), 3, + axis_offsets=[3, 2, 1, 4]) + + +if __name__ == "__main__": + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py index dac511d4..788ccbc6 100644 --- a/skimage/feature/tests/test_texture.py +++ b/skimage/feature/tests/test_texture.py @@ -199,7 +199,7 @@ class TestLBP(): np.random.seed(13141516) # Create random image with known variance. - image = np.random.random((500, 500)) + image = np.random.rand(500, 500) target_std = 0.3 image = image / image.std() * target_std diff --git a/skimage/feature/tests/test_util.py b/skimage/feature/tests/test_util.py index 2b601b94..f15bcb2a 100644 --- a/skimage/feature/tests/test_util.py +++ b/skimage/feature/tests/test_util.py @@ -1,5 +1,8 @@ import numpy as np -import matplotlib.pyplot as plt +try: + import matplotlib.pyplot as plt +except ImportError: + plt = None from numpy.testing import assert_equal, assert_raises from skimage.feature.util import (FeatureDetector, DescriptorExtractor, @@ -39,6 +42,7 @@ def test_mask_border_keypoints(): [0, 0, 0, 0, 1]) +@np.testing.decorators.skipif(plt is None) def test_plot_matches(): fig, ax = plt.subplots(nrows=1, ncols=1) diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index a4e69518..beb66ff9 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -3,7 +3,7 @@ Methods to characterize image textures. """ import numpy as np - +from .._shared.utils import assert_nD from ._texture import _glcm_loop, _local_binary_pattern @@ -11,7 +11,7 @@ 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 + A grey level co-occurrence matrix is a histogram of co-occurring greyscale values at a given offset over an image. Parameters @@ -89,17 +89,17 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, [0, 0, 0, 0]], dtype=uint32) """ + assert_nD(image, 2) + assert_nD(distances, 1, 'distances') + assert_nD(angles, 1, 'angles') 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') @@ -179,8 +179,8 @@ def greycoprops(P, prop='contrast'): [ 1.25 , 2.75 ]]) """ + assert_nD(P, 4, 'P') - assert P.ndim == 4 (num_level, num_level2, num_dist, num_angle) = P.shape assert num_level == num_level2 assert num_dist > 0 @@ -259,7 +259,7 @@ def local_binary_pattern(image, P, R, method='default'): finer quantization of the angular space which is gray scale and rotation invariant. * 'nri_uniform': non rotation-invariant uniform patterns variant - which is only gray scale invariant [2]. + which is only gray scale invariant [2]_. * 'var': rotation invariant variance measures of the contrast of local image texture which is rotation but not gray scale invariant. @@ -273,13 +273,13 @@ def local_binary_pattern(image, P, R, method='default'): .. [1] Multiresolution Gray-Scale and Rotation Invariant Texture Classification with Local Binary Patterns. Timo Ojala, Matti Pietikainen, Topi Maenpaa. - http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ - Articoliriferimento/LBP.pdf, 2002. + http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/Articoliriferimento/LBP.pdf, 2002. .. [2] Face recognition with local binary patterns. Timo Ahonen, Abdenour Hadid, Matti Pietikainen, http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.214.6851, 2004. """ + assert_nD(image, 2) methods = { 'default': ord('D'), diff --git a/skimage/feature/util.py b/skimage/feature/util.py index 5a3e5687..b0c66986 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -1,6 +1,7 @@ import numpy as np -from skimage.util import img_as_float +from ..util import img_as_float +from .._shared.utils import assert_nD class FeatureDetector(object): @@ -124,9 +125,7 @@ def plot_matches(ax, image1, image2, keypoints1, keypoints2, matches, def _prepare_grayscale_input_2D(image): image = np.squeeze(image) - if image.ndim != 2: - raise ValueError("Only 2-D gray-scale images supported.") - + assert_nD(image, 2) return img_as_float(image) diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 649eba6b..8b81c254 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,15 +1,30 @@ -from .lpi_filter import inverse, wiener, LPIFilter2D -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 ._rank_order import rank_order -from ._gabor import gabor_kernel, gabor_filter -from .thresholding import (threshold_adaptive, threshold_otsu, threshold_yen, - threshold_isodata) -from . import rank +from .._shared.utils import skimage_deprecation +from warnings import warn +global _import_warned + +warn(skimage_deprecation('The `skimage.filter` module has been renamed ' + 'to `skimage.filters`. This placeholder module ' + 'will be removed in v0.13.')) +_import_warned = True + +del warn +del skimage_deprecation + +from ..filters.lpi_filter import inverse, wiener, LPIFilter2D +from ..filters._gaussian import gaussian_filter +from ..filters.edges import (sobel, hsobel, vsobel, sobel_h, sobel_v, + scharr, hscharr, vscharr, scharr_h, scharr_v, + prewitt, hprewitt, vprewitt, prewitt_h, prewitt_v, + roberts, roberts_positive_diagonal, + roberts_negative_diagonal, roberts_pos_diag, + roberts_neg_diag) +from ..filters._rank_order import rank_order +from ..filters._gabor import gabor_kernel, gabor_filter +from ..filters.thresholding import (threshold_adaptive, threshold_otsu, threshold_yen, + threshold_isodata) +from ..filters import rank +from ..filters.rank import median from skimage._shared.utils import deprecated from skimage import restoration @@ -20,24 +35,40 @@ denoise_tv_bregman = deprecated('skimage.restoration.denoise_tv_bregman')\ denoise_tv_chambolle = deprecated('skimage.restoration.denoise_tv_chambolle')\ (restoration.denoise_tv_chambolle) +# Backward compatibility v<0.11 +@deprecated('skimage.feature.canny') +def canny(*args, **kwargs): + # Hack to avoid circular import + from skimage.feature._canny import canny as canny_ + return canny_(*args, **kwargs) + __all__ = ['inverse', 'wiener', 'LPIFilter2D', 'gaussian_filter', + 'median', 'canny', 'sobel', 'hsobel', 'vsobel', + 'sobel_h', + 'sobel_v', 'scharr', 'hscharr', 'vscharr', + 'scharr_h', + 'scharr_v', 'prewitt', 'hprewitt', 'vprewitt', + 'prewitt_h', + 'prewitt_v', 'roberts', 'roberts_positive_diagonal', 'roberts_negative_diagonal', + 'roberts_pos_diag', + 'roberts_neg_diag', 'denoise_tv_chambolle', 'denoise_bilateral', 'denoise_tv_bregman', diff --git a/skimage/filter/_gabor.py b/skimage/filter/_gabor.py deleted file mode 100644 index f766ac74..00000000 --- a/skimage/filter/_gabor.py +++ /dev/null @@ -1,121 +0,0 @@ -import numpy as np -from scipy import ndimage - - -__all__ = ['gabor_kernel', 'gabor_filter'] - - -def _sigma_prefactor(bandwidth): - b = bandwidth - # See http://www.cs.rug.nl/~imaging/simplecell.html - return 1.0 / np.pi * np.sqrt(np.log(2)/2.0) * (2.0**b + 1) / (2.0**b - 1) - - -def gabor_kernel(frequency, theta=0, bandwidth=1, sigma_x=None, sigma_y=None, - offset=0): - """Return complex 2D Gabor filter kernel. - - Frequency and orientation representations of the Gabor filter are similar - to those of the human visual system. It is especially suitable for texture - classification using Gabor filter banks. - - Parameters - ---------- - frequency : float - Frequency of the harmonic function. - theta : float - Orientation in radians. If 0, the harmonic is in the x-direction. - bandwidth : float - The bandwidth captured by the filter. For fixed bandwidth, `sigma_x` - and `sigma_y` will decrease with increasing frequency. This value is - ignored if `sigma_x` and `sigma_y` are set by the user. - sigma_x, sigma_y : float - Standard deviation in x- and y-directions. These directions apply to - the kernel *before* rotation. If `theta = pi/2`, then the kernel is - rotated 90 degrees so that `sigma_x` controls the *vertical* direction. - offset : float, optional - Phase offset of harmonic function in radians. - - Returns - ------- - g : complex array - Complex filter kernel. - - References - ---------- - .. [1] http://en.wikipedia.org/wiki/Gabor_filter - .. [2] http://mplab.ucsd.edu/tutorials/gabor.pdf - - """ - if sigma_x is None: - sigma_x = _sigma_prefactor(bandwidth) / frequency - if sigma_y is None: - sigma_y = _sigma_prefactor(bandwidth) / frequency - - n_stds = 3 - x0 = np.ceil(max(np.abs(n_stds * sigma_x * np.cos(theta)), - np.abs(n_stds * sigma_y * np.sin(theta)), 1)) - y0 = np.ceil(max(np.abs(n_stds * sigma_y * np.cos(theta)), - np.abs(n_stds * sigma_x * np.sin(theta)), 1)) - y, x = np.mgrid[-y0:y0+1, -x0:x0+1] - - rotx = x * np.cos(theta) + y * np.sin(theta) - roty = -x * np.sin(theta) + y * np.cos(theta) - - g = np.zeros(y.shape, dtype=np.complex) - g[:] = np.exp(-0.5 * (rotx**2 / sigma_x**2 + roty**2 / sigma_y**2)) - g /= 2 * np.pi * sigma_x * sigma_y - g *= np.exp(1j * (2 * np.pi * frequency * rotx + offset)) - - return g - - -def gabor_filter(image, frequency, theta=0, bandwidth=1, sigma_x=None, - sigma_y=None, offset=0, mode='reflect', cval=0): - """Return real and imaginary responses to Gabor filter. - - The real and imaginary parts of the Gabor filter kernel are applied to the - image and the response is returned as a pair of arrays. - - Frequency and orientation representations of the Gabor filter are similar - to those of the human visual system. It is especially suitable for texture - classification using Gabor filter banks. - - Parameters - ---------- - image : array - Input image. - frequency : float - Frequency of the harmonic function. - theta : float - Orientation in radians. If 0, the harmonic is in the x-direction. - bandwidth : float - The bandwidth captured by the filter. For fixed bandwidth, `sigma_x` - and `sigma_y` will decrease with increasing frequency. This value is - ignored if `sigma_x` and `sigma_y` are set by the user. - sigma_x, sigma_y : float - Standard deviation in x- and y-directions. These directions apply to - the kernel *before* rotation. If `theta = pi/2`, then the kernel is - rotated 90 degrees so that `sigma_x` controls the *vertical* direction. - offset : float, optional - Phase offset of harmonic function in radians. - - Returns - ------- - real, imag : arrays - Filtered images using the real and imaginary parts of the Gabor filter - kernel. - - References - ---------- - .. [1] http://en.wikipedia.org/wiki/Gabor_filter - .. [2] http://mplab.ucsd.edu/tutorials/gabor.pdf - - """ - - g = gabor_kernel(frequency, theta, bandwidth, sigma_x, sigma_y, offset) - - filtered_real = ndimage.convolve(image, np.real(g), mode=mode, cval=cval) - filtered_imag = ndimage.convolve(image, np.imag(g), mode=mode, cval=cval) - - return filtered_real, filtered_imag diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py index 8641b984..13b8ad84 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -1,11 +1,12 @@ -from .generic import (autolevel, bottomhat, equalize, gradient, maximum, mean, +from ...filters.rank.generic import (autolevel, bottomhat, equalize, gradient, maximum, mean, subtract_mean, median, minimum, modal, enhance_contrast, - pop, threshold, tophat, noise_filter, entropy, otsu, sum) -from ._percentile import (autolevel_percentile, gradient_percentile, + pop, threshold, tophat, noise_filter, entropy, otsu, + sum, windowed_histogram) +from ...filters.rank._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 +from ...filters.rank.bilateral import mean_bilateral, pop_bilateral, sum_bilateral __all__ = ['autolevel', @@ -37,4 +38,5 @@ __all__ = ['autolevel', 'noise_filter', 'entropy', 'otsu', - 'percentile'] + 'percentile', + 'windowed_histogram'] diff --git a/skimage/filter/rank/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx deleted file mode 100644 index 25a81766..00000000 --- a/skimage/filter/rank/bilateral_cy.pyx +++ /dev/null @@ -1,101 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from libc.math cimport log - -from .core_cy cimport dtype_t, dtype_t_out, _core - - -cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t bilat_pop = 0 - cdef Py_ssize_t mean = 0 - - if pop: - for i in range(max_bin): - if (g > (i - s0)) and (g < (i + s1)): - bilat_pop += histo[i] - mean += histo[i] * i - if bilat_pop: - return mean / bilat_pop - else: - return 0 - else: - return 0 - - -cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t bilat_pop = 0 - - if pop: - for i in range(max_bin): - if (g > (i - s0)) and (g < (i + s1)): - bilat_pop += histo[i] - return bilat_pop - else: - return 0 - -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, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, - Py_ssize_t max_bin): - - _core(_kernel_mean[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, s0, s1, max_bin) - - -def _pop(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, - Py_ssize_t max_bin): - - _core(_kernel_pop[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, s0, s1, max_bin) - -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_cy.pyx b/skimage/filter/rank/generic_cy.pyx deleted file mode 100644 index 1c26cf53..00000000 --- a/skimage/filter/rank/generic_cy.pyx +++ /dev/null @@ -1,531 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from libc.math cimport log - -from .core_cy cimport dtype_t, dtype_t_out, _core - - -cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax, delta - - if pop: - for i in range(max_bin - 1, -1, -1): - if histo[i]: - imax = i - break - for i in range(max_bin): - if histo[i]: - imin = i - break - delta = imax - imin - if delta > 0: - return (max_bin - 1) * (g - imin) / delta - else: - return 0 - else: - return 0 - - -cdef inline double _kernel_bottomhat(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(max_bin): - if histo[i]: - break - return g - i - else: - return 0 - - -cdef inline double _kernel_equalize(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t sum = 0 - - if pop: - for i in range(max_bin): - sum += histo[i] - if i >= g: - break - return ((max_bin - 1) * sum) / pop - else: - return 0 - - -cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax - - if pop: - for i in range(max_bin - 1, -1, -1): - if histo[i]: - imax = i - break - for i in range(max_bin): - if histo[i]: - imin = i - break - return imax - imin - else: - return 0 - - -cdef inline double _kernel_maximum(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(max_bin - 1, -1, -1): - if histo[i]: - return i - else: - return 0 - - -cdef inline double _kernel_mean(Py_ssize_t* histo, double pop,dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t mean = 0 - - if pop: - for i in range(max_bin): - mean += histo[i] * i - return mean / pop - else: - return 0 - - -cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, double p0, - double p1, Py_ssize_t s0, - Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t mean = 0 - - if pop: - for i in range(max_bin): - mean += histo[i] * i - return (g - mean / pop) / 2. + 127 - else: - return 0 - - -cdef inline double _kernel_median(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef double sum = pop / 2.0 - - if pop: - for i in range(max_bin): - if histo[i]: - sum -= histo[i] - if sum < 0: - return i - else: - return 0 - - -cdef inline double _kernel_minimum(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(max_bin): - if histo[i]: - return i - else: - return 0 - - -cdef inline double _kernel_modal(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t hmax = 0, imax = 0 - - if pop: - for i in range(max_bin): - if histo[i] > hmax: - hmax = histo[i] - imax = i - return imax - else: - return 0 - - -cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, double p0, - double p1, Py_ssize_t s0, - Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax - - if pop: - for i in range(max_bin - 1, -1, -1): - if histo[i]: - imax = i - break - for i in range(max_bin): - if histo[i]: - imin = i - break - if imax - g < g - imin: - return imax - else: - return imin - else: - return 0 - - -cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - return pop - - -cdef inline double _kernel_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, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t mean = 0 - - if pop: - for i in range(max_bin): - mean += histo[i] * i - return g > (mean / pop) - else: - return 0 - - -cdef inline double _kernel_tophat(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(max_bin - 1, -1, -1): - if histo[i]: - break - return i - g - else: - return 0 - - -cdef inline double _kernel_noise_filter(Py_ssize_t* histo, double pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, double p0, - double p1, Py_ssize_t s0, - Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t min_i - - # early stop if at least one pixel of the neighborhood has the same g - if histo[g] > 0: - return 0 - - for i in range(g, -1, -1): - if histo[i]: - break - min_i = g - i - for i in range(g, max_bin): - if histo[i]: - break - if i - g < min_i: - return i - g - else: - return min_i - - -cdef inline double _kernel_entropy(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef double e, p - - if pop: - e = 0. - for i in range(max_bin): - p = histo[i] / pop - if p > 0: - e -= p * log(p) / 0.6931471805599453 - return e - else: - return 0 - - -cdef inline double _kernel_otsu(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef Py_ssize_t max_i - cdef double P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b - cdef double mu = 0. - - # compute local mean - if pop: - for i in range(max_bin): - mu += histo[i] * i - mu = mu / pop - else: - return 0 - - # maximizing the between class variance - max_i = 0 - q1 = histo[0] / pop - mu1 = 0. - max_sigma_b = 0. - - for i in range(1, max_bin): - P = histo[i] / pop - new_q1 = q1 + P - if new_q1 > 0: - mu1 = (q1 * mu1 + i * P) / new_q1 - mu2 = (mu - new_q1 * mu1) / (1. - new_q1) - sigma_b = new_q1 * (1. - new_q1) * (mu1 - mu2) ** 2 - if sigma_b > max_sigma_b: - max_sigma_b = sigma_b - max_i = i - q1 = new_q1 - - return max_i - - -def _autolevel(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_autolevel[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _bottomhat(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_bottomhat[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _equalize(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_equalize[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _gradient(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_gradient[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _maximum(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_maximum[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _mean(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_mean[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _subtract_mean(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_subtract_mean[dtype_t], image, selem, mask, - out, shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _median(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_median[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _minimum(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_minimum[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _enhance_contrast(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, - out, shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _modal(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_modal[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _pop(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_pop[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - -def _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, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_threshold[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _tophat(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_tophat[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _noise_filter(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_noise_filter[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _entropy(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_entropy[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - - -def _otsu(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): - - _core(_kernel_otsu[dtype_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx deleted file mode 100644 index 38d04b33..00000000 --- a/skimage/filter/rank/percentile_cy.pyx +++ /dev/null @@ -1,333 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from .core_cy cimport dtype_t, dtype_t_out, _core, _min, _max - - -cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(max_bin): - sum += histo[i] - if sum > p0 * pop: - imin = i - break - sum = 0 - for i in range(max_bin - 1, -1, -1): - sum += histo[i] - if sum > p1 * pop: - imax = i - break - - delta = imax - imin - if delta > 0: - return (max_bin - 1) * (_min(_max(imin, g), imax) - - imin) / delta - else: - return imax - imin - else: - return 0 - - -cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(max_bin): - sum += histo[i] - if sum >= p0 * pop: - imin = i - break - sum = 0 - for i in range(max_bin - 1, -1, -1): - sum += histo[i] - if sum >= p1 * pop: - imax = i - break - - return imax - imin - else: - return 0 - - -cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, sum, mean, n - - if pop: - sum = 0 - mean = 0 - n = 0 - for i in range(max_bin): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - mean += histo[i] * i - - if n > 0: - return mean / n - else: - return 0 - else: - return 0 - -cdef inline double _kernel_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, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, double p0, - double p1, Py_ssize_t s0, - Py_ssize_t s1): - - cdef Py_ssize_t i, sum, mean, n - - if pop: - sum = 0 - mean = 0 - n = 0 - for i in range(max_bin): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - mean += histo[i] * i - if n > 0: - return (g - (mean / n)) * .5 + mid_bin - else: - return 0 - else: - return 0 - - -cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, double p0, - double p1, Py_ssize_t s0, - Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(max_bin): - sum += histo[i] - if sum > p0 * pop: - imin = i - break - sum = 0 - for i in range(max_bin - 1, -1, -1): - sum += histo[i] - if sum > p1 * pop: - imax = i - break - if g > imax: - return imax - if g < imin: - return imin - if imax - g < g - imin: - return imax - else: - return imin - else: - return 0 - - -cdef inline double _kernel_percentile(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t sum = 0 - - if pop: - if p0 == 1: # make sure p0 = 1 returns the maximum filter - for i in range(max_bin - 1, -1, -1): - if histo[i]: - break - else: - for i in range(max_bin): - sum += histo[i] - if sum > p0 * pop: - break - return i - else: - return 0 - - -cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, sum, n - - if pop: - sum = 0 - n = 0 - for i in range(max_bin): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - return n - else: - return 0 - - -cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i - cdef Py_ssize_t sum = 0 - - if pop: - for i in range(max_bin): - sum += histo[i] - if sum >= p0 * pop: - break - - return (max_bin - 1) * (g >= i) - else: - return 0 - - -def _autolevel(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, double p0, double p1, - Py_ssize_t max_bin): - - _core(_kernel_autolevel[dtype_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) - - -def _gradient(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, double p0, double p1, - Py_ssize_t max_bin): - - _core(_kernel_gradient[dtype_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) - - -def _mean(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, double p0, double p1, - Py_ssize_t max_bin): - - _core(_kernel_mean[dtype_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) - -def _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, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, double p0, double p1, - Py_ssize_t max_bin): - - _core(_kernel_subtract_mean[dtype_t], image, selem, mask, - out, shift_x, shift_y, p0, p1, 0, 0, max_bin) - - -def _enhance_contrast(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, double p0, double p1, - Py_ssize_t max_bin): - - _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, - out, shift_x, shift_y, p0, p1, 0, 0, max_bin) - - -def _percentile(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, double p0, double p1, - Py_ssize_t max_bin): - - _core(_kernel_percentile[dtype_t], image, selem, mask, out, - shift_x, shift_y, p0, 1, 0, 0, max_bin) - - -def _pop(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, double p0, double p1, - Py_ssize_t max_bin): - - _core(_kernel_pop[dtype_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) - - -def _threshold(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, double p0, double p1, - Py_ssize_t max_bin): - - _core(_kernel_threshold[dtype_t], image, selem, mask, out, - shift_x, shift_y, p0, 1, 0, 0, max_bin) diff --git a/skimage/filters/LICENSE.txt b/skimage/filters/LICENSE.txt new file mode 100644 index 00000000..3507299a --- /dev/null +++ b/skimage/filters/LICENSE.txt @@ -0,0 +1,28 @@ +threshold_li based on imagej code: + +Copyright (c) 2009 - 2015, Board of Regents of the University of +Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck +Institute of Molecular Cell Biology and Genetics. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/skimage/filters/__init__.py b/skimage/filters/__init__.py new file mode 100644 index 00000000..7cffb140 --- /dev/null +++ b/skimage/filters/__init__.py @@ -0,0 +1,70 @@ +from .lpi_filter import inverse, wiener, LPIFilter2D +from ._gaussian import gaussian_filter +from .edges import (sobel, hsobel, vsobel, sobel_h, sobel_v, + scharr, hscharr, vscharr, scharr_h, scharr_v, + prewitt, hprewitt, vprewitt, prewitt_h, prewitt_v, + roberts, roberts_positive_diagonal, + roberts_negative_diagonal, roberts_pos_diag, + roberts_neg_diag) +from ._rank_order import rank_order +from ._gabor import gabor_kernel, gabor_filter +from .thresholding import (threshold_adaptive, threshold_otsu, threshold_yen, + threshold_isodata, threshold_li) +from . import rank +from .rank import median + +from .._shared.utils import deprecated +from .. 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) + +# Backward compatibility v<0.11 +@deprecated('skimage.feature.canny') +def canny(*args, **kwargs): + # Hack to avoid circular import + from ..feature._canny import canny as canny_ + return canny_(*args, **kwargs) + + +__all__ = ['inverse', + 'wiener', + 'LPIFilter2D', + 'gaussian_filter', + 'median', + 'canny', + 'sobel', + 'hsobel', + 'vsobel', + 'sobel_h', + 'sobel_v', + 'scharr', + 'hscharr', + 'vscharr', + 'scharr_h', + 'scharr_v', + 'prewitt', + 'hprewitt', + 'vprewitt', + 'prewitt_h', + 'prewitt_v', + 'roberts', + 'roberts_positive_diagonal', + 'roberts_negative_diagonal', + 'roberts_pos_diag', + 'roberts_neg_diag', + 'denoise_tv_chambolle', + 'denoise_bilateral', + 'denoise_tv_bregman', + 'rank_order', + 'gabor_kernel', + 'gabor_filter', + 'threshold_adaptive', + 'threshold_otsu', + 'threshold_yen', + 'threshold_isodata', + 'threshold_li', + 'rank'] diff --git a/skimage/filter/_ctmf.pyx b/skimage/filters/_ctmf.pyx similarity index 100% rename from skimage/filter/_ctmf.pyx rename to skimage/filters/_ctmf.pyx diff --git a/skimage/filters/_gabor.py b/skimage/filters/_gabor.py new file mode 100644 index 00000000..862a593c --- /dev/null +++ b/skimage/filters/_gabor.py @@ -0,0 +1,175 @@ +import numpy as np +from scipy import ndimage +from .._shared.utils import assert_nD + + +__all__ = ['gabor_kernel', 'gabor_filter'] + + +def _sigma_prefactor(bandwidth): + b = bandwidth + # See http://www.cs.rug.nl/~imaging/simplecell.html + return 1.0 / np.pi * np.sqrt(np.log(2) / 2.0) * \ + (2.0 ** b + 1) / (2.0 ** b - 1) + + +def gabor_kernel(frequency, theta=0, bandwidth=1, sigma_x=None, sigma_y=None, + n_stds=3, offset=0): + """Return complex 2D Gabor filter kernel. + + Gabor kernel is a Gaussian kernel modulated by a complex harmonic function. + Harmonic function consists of an imaginary sine function and a real + cosine function. Spatial frequency is inversely proportional to the + wavelength of the harmonic and to the standard deviation of a Gaussian + kernel. The bandwidth is also inversely proportional to the standard + deviation. + + Parameters + ---------- + frequency : float + Spatial frequency of the harmonic function. Specified in pixels. + theta : float, optional + Orientation in radians. If 0, the harmonic is in the x-direction. + bandwidth : float, optional + The bandwidth captured by the filter. For fixed bandwidth, `sigma_x` + and `sigma_y` will decrease with increasing frequency. This value is + ignored if `sigma_x` and `sigma_y` are set by the user. + sigma_x, sigma_y : float, optional + Standard deviation in x- and y-directions. These directions apply to + the kernel *before* rotation. If `theta = pi/2`, then the kernel is + rotated 90 degrees so that `sigma_x` controls the *vertical* direction. + n_stds : scalar, optional + The linear size of the kernel is n_stds (3 by default) standard + deviations + offset : float, optional + Phase offset of harmonic function in radians. + + Returns + ------- + g : complex array + Complex filter kernel. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Gabor_filter + .. [2] http://mplab.ucsd.edu/tutorials/gabor.pdf + + Examples + -------- + >>> from skimage.filter import gabor_kernel + >>> from skimage import io + >>> from matplotlib import pyplot as plt # doctest: +SKIP + + >>> gk = gabor_kernel(frequency=0.2) + >>> plt.figure() # doctest: +SKIP + >>> io.imshow(gk.real) # doctest: +SKIP + >>> io.show() # doctest: +SKIP + + >>> # more ripples (equivalent to increasing the size of the + >>> # Gaussian spread) + >>> gk = gabor_kernel(frequency=0.2, bandwidth=0.1) + >>> plt.figure() # doctest: +SKIP + >>> io.imshow(gk.real) # doctest: +SKIP + >>> io.show() # doctest: +SKIP + """ + if sigma_x is None: + sigma_x = _sigma_prefactor(bandwidth) / frequency + if sigma_y is None: + sigma_y = _sigma_prefactor(bandwidth) / frequency + + x0 = np.ceil(max(np.abs(n_stds * sigma_x * np.cos(theta)), + np.abs(n_stds * sigma_y * np.sin(theta)), 1)) + y0 = np.ceil(max(np.abs(n_stds * sigma_y * np.cos(theta)), + np.abs(n_stds * sigma_x * np.sin(theta)), 1)) + y, x = np.mgrid[-y0:y0 + 1, -x0:x0 + 1] + + rotx = x * np.cos(theta) + y * np.sin(theta) + roty = -x * np.sin(theta) + y * np.cos(theta) + + g = np.zeros(y.shape, dtype=np.complex) + g[:] = np.exp(-0.5 * (rotx ** 2 / sigma_x ** 2 + roty ** 2 / sigma_y ** 2)) + g /= 2 * np.pi * sigma_x * sigma_y + g *= np.exp(1j * (2 * np.pi * frequency * rotx + offset)) + + return g + + +def gabor_filter(image, frequency, theta=0, bandwidth=1, sigma_x=None, + sigma_y=None, n_stds=3, offset=0, mode='reflect', cval=0): + """Return real and imaginary responses to Gabor filter. + + The real and imaginary parts of the Gabor filter kernel are applied to the + image and the response is returned as a pair of arrays. + + Gabor filter is a linear filter with a Gaussian kernel which is modulated + by a sinusoidal plane wave. Frequency and orientation representations of + the Gabor filter are similar to those of the human visual system. + Gabor filter banks are commonly used in computer vision and image + processing. They are especially suitable for edge detection and texture + classification. + + Parameters + ---------- + image : 2-D array + Input image. + frequency : float + Spatial frequency of the harmonic function. Specified in pixels. + theta : float, optional + Orientation in radians. If 0, the harmonic is in the x-direction. + bandwidth : float, optional + The bandwidth captured by the filter. For fixed bandwidth, `sigma_x` + and `sigma_y` will decrease with increasing frequency. This value is + ignored if `sigma_x` and `sigma_y` are set by the user. + sigma_x, sigma_y : float, optional + Standard deviation in x- and y-directions. These directions apply to + the kernel *before* rotation. If `theta = pi/2`, then the kernel is + rotated 90 degrees so that `sigma_x` controls the *vertical* direction. + n_stds : scalar, optional + The linear size of the kernel is n_stds (3 by default) standard + deviations. + offset : float, optional + Phase offset of harmonic function in radians. + mode : string, optional + Mode used to convolve image with a kernel, passed to `ndimage.convolve` + cval : scalar, optional + Value to fill past edges of input if `mode` of convolution is + 'constant'. The parameter is passed to `ndimage.convolve`. + + Returns + ------- + real, imag : arrays + Filtered images using the real and imaginary parts of the Gabor filter + kernel. Images are of the same dimensions as the input one. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Gabor_filter + .. [2] http://mplab.ucsd.edu/tutorials/gabor.pdf + + Examples + -------- + >>> from skimage.filter import gabor_filter + >>> from skimage import data, io + >>> from matplotlib import pyplot as plt # doctest: +SKIP + + >>> image = data.coins() + >>> # detecting edges in a coin image + >>> filt_real, filt_imag = gabor_filter(image, frequency=0.6) + >>> plt.figure() # doctest: +SKIP + >>> io.imshow(filt_real) # doctest: +SKIP + >>> io.show() # doctest: +SKIP + + >>> # less sensitivity to finer details with the lower frequency kernel + >>> filt_real, filt_imag = gabor_filter(image, frequency=0.1) + >>> plt.figure() # doctest: +SKIP + >>> io.imshow(filt_real) # doctest: +SKIP + >>> io.show() # doctest: +SKIP + """ + assert_nD(image, 2) + g = gabor_kernel(frequency, theta, bandwidth, sigma_x, sigma_y, n_stds, + offset) + + filtered_real = ndimage.convolve(image, np.real(g), mode=mode, cval=cval) + filtered_imag = ndimage.convolve(image, np.imag(g), mode=mode, cval=cval) + + return filtered_real, filtered_imag diff --git a/skimage/filter/_gaussian.py b/skimage/filters/_gaussian.py similarity index 91% rename from skimage/filter/_gaussian.py rename to skimage/filters/_gaussian.py index af63ba10..eedafbdb 100644 --- a/skimage/filter/_gaussian.py +++ b/skimage/filters/_gaussian.py @@ -11,12 +11,10 @@ __all__ = ['gaussian_filter'] def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, multichannel=None): - """ - Multi-dimensional Gaussian filter + """Multi-dimensional Gaussian filter Parameters ---------- - image : array-like input image (grayscale or color) to filter. sigma : scalar or sequence of scalars @@ -43,13 +41,11 @@ def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, Returns ------- - filtered_image : ndarray the filtered array Notes ----- - This function is a wrapper around :func:`scipy.ndimage.gaussian_filter`. Integer arrays are converted to float. @@ -84,15 +80,17 @@ def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, [ 0.12075024, 0.16630671, 0.12075024], [ 0.08767308, 0.12075024, 0.08767308]]) >>> # For RGB images, each is filtered separately - >>> from skimage.data import lena - >>> image = lena() - >>> filtered_lena = gaussian_filter(image, sigma=1, multichannel=True) + >>> from skimage.data import astronaut + >>> image = astronaut() + >>> filtered_img = gaussian_filter(image, sigma=1, multichannel=True) + """ + spatial_dims = guess_spatial_dimensions(image) if spatial_dims is None and multichannel is None: - msg = ("Images with dimensions (M, N, 3) are interpreted as 2D+RGB" + - " by default. Use `multichannel=False` to interpret as " + - " 3D image with last dimension of length 3.") + msg = ("Images with dimensions (M, N, 3) are interpreted as 2D+RGB " + "by default. Use `multichannel=False` to interpret as " + "3D image with last dimension of length 3.") warnings.warn(RuntimeWarning(msg)) multichannel = True if multichannel: diff --git a/skimage/filter/_rank_order.py b/skimage/filters/_rank_order.py similarity index 98% rename from skimage/filter/_rank_order.py rename to skimage/filters/_rank_order.py index c7a5ce60..4511104a 100644 --- a/skimage/filter/_rank_order.py +++ b/skimage/filters/_rank_order.py @@ -28,7 +28,7 @@ def rank_order(image): n - 1, where n is the number of distinct unique values in `image`. - original_values: 1-d ndarray + original_values: 1-D ndarray Unique original values of `image` Examples diff --git a/skimage/filter/edges.py b/skimage/filters/edges.py similarity index 52% rename from skimage/filter/edges.py rename to skimage/filters/edges.py index 764c7d34..1eeed4de 100644 --- a/skimage/filter/edges.py +++ b/skimage/filters/edges.py @@ -10,7 +10,8 @@ Original author: Lee Kamentsky """ import numpy as np -from skimage import img_as_float +from .. import img_as_float +from .._shared.utils import assert_nD, deprecated from scipy.ndimage import convolve, binary_erosion, generate_binary_structure @@ -71,19 +72,40 @@ def sobel(image, mask=None): output : 2-D array The Sobel edge map. + See also + -------- + scharr, prewitt, roberts, feature.canny + Notes ----- Take the square root of the sum of the squares of the horizontal and vertical Sobels to get a magnitude that's somewhat insensitive to direction. + The 3x3 convolution kernel used in the horizontal and vertical Sobels is + an approximation of the gradient of the image (with some slight blurring + since 9 pixels are used to compute the gradient at a given pixel). As an + approximation of the gradient, the Sobel operator is not completely + rotation-invariant. The Scharr operator should be used for a better + rotation invariance. + Note that ``scipy.ndimage.sobel`` returns a directional Sobel which has to be further processed to perform edge detection. + + Examples + -------- + >>> from skimage import data + >>> camera = data.camera() + >>> from skimage import filters + >>> edges = filters.sobel(camera) """ - return np.sqrt(hsobel(image, mask)**2 + vsobel(image, mask)**2) + assert_nD(image, 2) + out = np.sqrt(sobel_h(image, mask)**2 + sobel_v(image, mask)**2) + out /= np.sqrt(2) + return out -def hsobel(image, mask=None): +def sobel_h(image, mask=None): """Find the horizontal edges of an image using the Sobel transform. Parameters @@ -100,6 +122,71 @@ def hsobel(image, mask=None): output : 2-D array The Sobel edge map. + Notes + ----- + We use the following kernel:: + + 1 2 1 + 0 0 0 + -1 -2 -1 + + """ + assert_nD(image, 2) + image = img_as_float(image) + result = convolve(image, HSOBEL_WEIGHTS) + return _mask_filter_result(result, mask) + + +def sobel_v(image, mask=None): + """Find the vertical edges of an image using the Sobel transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Sobel edge map. + + Notes + ----- + We use the following kernel:: + + 1 0 -1 + 2 0 -2 + 1 0 -1 + + """ + assert_nD(image, 2) + image = img_as_float(image) + result = convolve(image, VSOBEL_WEIGHTS) + return _mask_filter_result(result, mask) + + +@deprecated("skimage.filters.sobel_h") +def hsobel(image, mask=None): + """Find the horizontal edges of an image using the Sobel transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The absolute Sobel edge map. + Notes ----- We use the following kernel and return the absolute value of the @@ -110,11 +197,10 @@ def hsobel(image, mask=None): -1 -2 -1 """ - image = img_as_float(image) - result = np.abs(convolve(image, HSOBEL_WEIGHTS)) - return _mask_filter_result(result, mask) + return np.abs(sobel_h(image, mask)) +@deprecated("skimage.filters.sobel_v") def vsobel(image, mask=None): """Find the vertical edges of an image using the Sobel transform. @@ -123,14 +209,14 @@ def vsobel(image, mask=None): image : 2-D array Image to process mask : 2-D array, optional - An optional mask to limit the application to a certain area + An optional mask to limit the application to a certain area. Note that pixels surrounding masked regions are also masked to prevent masked regions from affecting the result. Returns ------- output : 2-D array - The Sobel edge map. + The absolute Sobel edge map. Notes ----- @@ -142,9 +228,7 @@ def vsobel(image, mask=None): 1 0 -1 """ - image = img_as_float(image) - result = np.abs(convolve(image, VSOBEL_WEIGHTS)) - return _mask_filter_result(result, mask) + return np.abs(sobel_v(image, mask)) def scharr(image, mask=None): @@ -164,22 +248,37 @@ def scharr(image, mask=None): output : 2-D array The Scharr edge map. + See also + -------- + sobel, prewitt, canny + Notes ----- Take the square root of the sum of the squares of the horizontal and - vertical Scharrs to get a magnitude that's somewhat insensitive to - direction. + vertical Scharrs to get a magnitude that is somewhat insensitive to + direction. The Scharr operator has a better rotation invariance than + other edge filters such as the Sobel or the Prewitt operators. References ---------- - .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical Optimization - of Kernel Based Image Derivatives. + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical + Optimization of Kernel Based Image Derivatives. + .. [2] http://en.wikipedia.org/wiki/Sobel_operator#Alternative_operators + + Examples + -------- + >>> from skimage import data + >>> camera = data.camera() + >>> from skimage import filters + >>> edges = filters.scharr(camera) """ - return np.sqrt(hscharr(image, mask)**2 + vscharr(image, mask)**2) + out = np.sqrt(scharr_h(image, mask)**2 + scharr_v(image, mask)**2) + out /= np.sqrt(2) + return out -def hscharr(image, mask=None): +def scharr_h(image, mask=None): """Find the horizontal edges of an image using the Scharr transform. Parameters @@ -196,6 +295,81 @@ def hscharr(image, mask=None): output : 2-D array The Scharr edge map. + Notes + ----- + We use the following kernel:: + + 3 10 3 + 0 0 0 + -3 -10 -3 + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical + Optimization of Kernel Based Image Derivatives. + + """ + assert_nD(image, 2) + image = img_as_float(image) + result = convolve(image, HSCHARR_WEIGHTS) + return _mask_filter_result(result, mask) + + +def scharr_v(image, mask=None): + """Find the vertical edges of an image using the Scharr transform. + + Parameters + ---------- + image : 2-D array + Image to process + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Scharr edge map. + + Notes + ----- + We use the following kernel:: + + 3 0 -3 + 10 0 -10 + 3 0 -3 + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical + Optimization of Kernel Based Image Derivatives. + + """ + assert_nD(image, 2) + image = img_as_float(image) + result = convolve(image, VSCHARR_WEIGHTS) + return _mask_filter_result(result, mask) + + +@deprecated("skimage.filters.scharr_h") +def hscharr(image, mask=None): + """Find the horizontal edges of an image using the Scharr transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The absolute Scharr edge map. + Notes ----- We use the following kernel and return the absolute value of the @@ -207,15 +381,14 @@ def hscharr(image, mask=None): References ---------- - .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical Optimization - of Kernel Based Image Derivatives. + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical + Optimization of Kernel Based Image Derivatives. """ - image = img_as_float(image) - result = np.abs(convolve(image, HSCHARR_WEIGHTS)) - return _mask_filter_result(result, mask) + return np.abs(scharr_h(image, mask)) +@deprecated("skimage.filters.scharr_v") def vscharr(image, mask=None): """Find the vertical edges of an image using the Scharr transform. @@ -224,14 +397,14 @@ def vscharr(image, mask=None): image : 2-D array Image to process mask : 2-D array, optional - An optional mask to limit the application to a certain area + An optional mask to limit the application to a certain area. Note that pixels surrounding masked regions are also masked to prevent masked regions from affecting the result. Returns ------- output : 2-D array - The Scharr edge map. + The absolute Scharr edge map. Notes ----- @@ -244,13 +417,11 @@ def vscharr(image, mask=None): References ---------- - .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical Optimization - of Kernel Based Image Derivatives. + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical + Optimization of Kernel Based Image Derivatives. """ - image = img_as_float(image) - result = np.abs(convolve(image, VSCHARR_WEIGHTS)) - return _mask_filter_result(result, mask) + return np.abs(scharr_v(image, mask)) def prewitt(image, mask=None): @@ -270,15 +441,34 @@ def prewitt(image, mask=None): output : 2-D array The Prewitt edge map. + See also + -------- + sobel, scharr + Notes ----- Return the square root of the sum of squares of the horizontal - and vertical Prewitt transforms. + and vertical Prewitt transforms. The edge magnitude depends slightly + on edge directions, since the approximation of the gradient operator by + the Prewitt operator is not completely rotation invariant. For a better + rotation invariance, the Scharr operator should be used. The Sobel operator + has a better rotation invariance than the Prewitt operator, but a worse + rotation invariance than the Scharr operator. + + Examples + -------- + >>> from skimage import data + >>> camera = data.camera() + >>> from skimage import filters + >>> edges = filters.prewitt(camera) """ - return np.sqrt(hprewitt(image, mask)**2 + vprewitt(image, mask)**2) + assert_nD(image, 2) + out = np.sqrt(prewitt_h(image, mask)**2 + prewitt_v(image, mask)**2) + out /= np.sqrt(2) + return out -def hprewitt(image, mask=None): +def prewitt_h(image, mask=None): """Find the horizontal edges of an image using the Prewitt transform. Parameters @@ -297,20 +487,20 @@ def hprewitt(image, mask=None): Notes ----- - We use the following kernel and return the absolute value of the - result at each point:: + We use the following kernel:: 1 1 1 0 0 0 -1 -1 -1 """ + assert_nD(image, 2) image = img_as_float(image) - result = np.abs(convolve(image, HPREWITT_WEIGHTS)) + result = convolve(image, HPREWITT_WEIGHTS) return _mask_filter_result(result, mask) -def vprewitt(image, mask=None): +def prewitt_v(image, mask=None): """Find the vertical edges of an image using the Prewitt transform. Parameters @@ -327,6 +517,70 @@ def vprewitt(image, mask=None): output : 2-D array The Prewitt edge map. + Notes + ----- + We use the following kernel:: + + 1 0 -1 + 1 0 -1 + 1 0 -1 + + """ + assert_nD(image, 2) + image = img_as_float(image) + result = convolve(image, VPREWITT_WEIGHTS) + return _mask_filter_result(result, mask) + + +@deprecated("skimage.filters.prewitt_h") +def hprewitt(image, mask=None): + """Find the horizontal edges of an image using the Prewitt transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The absolute Prewitt edge map. + + Notes + ----- + We use the following kernel and return the absolute value of the + result at each point:: + + 1 1 1 + 0 0 0 + -1 -1 -1 + + """ + return np.abs(prewitt_h(image, mask)) + + +@deprecated("skimage.filters.prewitt_v") +def vprewitt(image, mask=None): + """Find the vertical edges of an image using the Prewitt transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The absolute Prewitt edge map. + Notes ----- We use the following kernel and return the absolute value of the @@ -337,9 +591,7 @@ def vprewitt(image, mask=None): 1 0 -1 """ - image = img_as_float(image) - result = np.abs(convolve(image, VPREWITT_WEIGHTS)) - return _mask_filter_result(result, mask) + return np.abs(prewitt_v(image, mask)) def roberts(image, mask=None): @@ -358,12 +610,27 @@ def roberts(image, mask=None): ------- output : 2-D array The Roberts' Cross edge map. + + See also + -------- + sobel, scharr, prewitt, feature.canny + + Examples + -------- + >>> from skimage import data + >>> camera = data.camera() + >>> from skimage import filters + >>> edges = filters.roberts(camera) + """ - return np.sqrt(roberts_positive_diagonal(image, mask)**2 + - roberts_negative_diagonal(image, mask)**2) + assert_nD(image, 2) + out = np.sqrt(roberts_pos_diag(image, mask)**2 + + roberts_neg_diag(image, mask)**2) + out /= np.sqrt(2) + return out -def roberts_positive_diagonal(image, mask=None): +def roberts_pos_diag(image, mask=None): """Find the cross edges of an image using Roberts' cross operator. The kernel is applied to the input image to produce separate measurements @@ -385,19 +652,19 @@ def roberts_positive_diagonal(image, mask=None): Notes ----- - We use the following kernel and return the absolute value of the - result at each point:: + We use the following kernel:: 1 0 0 -1 """ + assert_nD(image, 2) image = img_as_float(image) - result = np.abs(convolve(image, ROBERTS_PD_WEIGHTS)) + result = convolve(image, ROBERTS_PD_WEIGHTS) return _mask_filter_result(result, mask) -def roberts_negative_diagonal(image, mask=None): +def roberts_neg_diag(image, mask=None): """Find the cross edges of an image using the Roberts' Cross operator. The kernel is applied to the input image to produce separate measurements @@ -417,6 +684,74 @@ def roberts_negative_diagonal(image, mask=None): output : 2-D array The Robert's edge map. + Notes + ----- + We use the following kernel:: + + 0 1 + -1 0 + + """ + assert_nD(image, 2) + image = img_as_float(image) + result = convolve(image, ROBERTS_ND_WEIGHTS) + return _mask_filter_result(result, mask) + + +@deprecated("skimage.filters.roberts_pos_diag") +def roberts_positive_diagonal(image, mask=None): + """Find the cross edges of an image using Roberts' cross operator. + + The kernel is applied to the input image to produce separate measurements + of the gradient component one orientation. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The absolute Robert's edge map. + + Notes + ----- + We use the following kernel and return the absolute value of the + result at each point:: + + 1 0 + 0 -1 + + """ + return np.abs(roberts_pos_diag(image, mask)) + + +@deprecated("skimage.filters.roberts_neg_diag") +def roberts_negative_diagonal(image, mask=None): + """Find the cross edges of an image using the Roberts' Cross operator. + + The kernel is applied to the input image to produce separate measurements + of the gradient component one orientation. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The absolute Robert's edge map. + Notes ----- We use the following kernel and return the absolute value of the @@ -426,6 +761,4 @@ def roberts_negative_diagonal(image, mask=None): -1 0 """ - image = img_as_float(image) - result = np.abs(convolve(image, ROBERTS_ND_WEIGHTS)) - return _mask_filter_result(result, mask) + return np.abs(roberts_neg_diag(image, mask)) diff --git a/skimage/filter/lpi_filter.py b/skimage/filters/lpi_filter.py similarity index 96% rename from skimage/filter/lpi_filter.py rename to skimage/filters/lpi_filter.py index ef85c5cf..12943714 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filters/lpi_filter.py @@ -5,6 +5,7 @@ import numpy as np from scipy.fftpack import ifftshift +from .._shared.utils import assert_nD eps = np.finfo(float).eps @@ -42,6 +43,7 @@ class LPIFilter2D(object): """Linear Position-Invariant Filter (2-dimensional) """ + def __init__(self, impulse_response, **filter_params): """ Parameters @@ -65,9 +67,9 @@ class LPIFilter2D(object): Examples -------- - 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) @@ -118,6 +120,7 @@ class LPIFilter2D(object): data : (M,N) ndarray """ + assert_nD(data, 2, 'data') F, G = self._prepare(data) out = np.dual.ifftn(F * G) out = np.abs(_centre(out, data.shape)) @@ -155,6 +158,7 @@ def forward(data, impulse_response=None, filter_params={}, >>> filtered = forward(data.coins(), filt_func) """ + assert_nD(data, 2, 'data') if predefined_filter is None: predefined_filter = LPIFilter2D(impulse_response, **filter_params) return predefined_filter(data) @@ -184,6 +188,7 @@ def inverse(data, impulse_response=None, filter_params={}, max_gain=2, images, construct the LPIFilter2D and specify it here. """ + assert_nD(data, 2, 'data') if predefined_filter is None: filt = LPIFilter2D(impulse_response, **filter_params) else: @@ -222,6 +227,11 @@ def wiener(data, impulse_response=None, filter_params={}, K=0.25, images, construct the LPIFilter2D and specify it here. """ + assert_nD(data, 2, 'data') + + if not isinstance(K, float): + assert_nD(K, 2, 'K') + if predefined_filter is None: filt = LPIFilter2D(impulse_response, **filter_params) else: @@ -230,7 +240,7 @@ def wiener(data, impulse_response=None, filter_params={}, K=0.25, F, G = filt._prepare(data) _min_limit(F) - H_mag_sqr = np.abs(F)**2 + H_mag_sqr = np.abs(F) ** 2 F = 1 / F * H_mag_sqr / (H_mag_sqr + K) return _centre(np.abs(ifftshift(np.dual.ifftn(G * F))), data.shape) diff --git a/skimage/filter/rank/.gitignore b/skimage/filters/rank/.gitignore similarity index 100% rename from skimage/filter/rank/.gitignore rename to skimage/filters/rank/.gitignore diff --git a/skimage/filter/rank/README.rst b/skimage/filters/rank/README.rst similarity index 89% rename from skimage/filter/rank/README.rst rename to skimage/filters/rank/README.rst index e5c5a9ad..6e063ec2 100644 --- a/skimage/filter/rank/README.rst +++ b/skimage/filters/rank/README.rst @@ -16,7 +16,7 @@ followed by the moving window is given hereunder /--------------------------/ \-------------------------- ... -We compare cmorph.dilate to this histogram based method to show how +We compare grey.dilate to this histogram based method to show how computational costs increase with respect to image size or structuring element size. This implementation gives better results for large structuring elements. @@ -26,7 +26,7 @@ update the local histogram. The histogram size is 8-bit (256 bins) for 8-bit images and 2 to 16-bit for 16-bit images depending on the maximum value of the image. -The filter is applied up to the image border, the neighboorhood used is +The filter is applied up to the image border, the neighborhood used is adjusted accordingly. The user may provide a mask image (same size as input image) where non zero values are the part of the image participating in the histogram computation. By default the entire image is filtered. diff --git a/skimage/filters/rank/__init__.py b/skimage/filters/rank/__init__.py new file mode 100644 index 00000000..37b5f9cd --- /dev/null +++ b/skimage/filters/rank/__init__.py @@ -0,0 +1,42 @@ +from .generic import (autolevel, bottomhat, equalize, gradient, maximum, mean, + subtract_mean, median, minimum, modal, enhance_contrast, + pop, threshold, tophat, noise_filter, entropy, otsu, + sum, windowed_histogram) +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', + 'autolevel_percentile', + 'bottomhat', + 'equalize', + 'gradient', + 'gradient_percentile', + 'maximum', + 'mean', + 'mean_percentile', + 'mean_bilateral', + 'subtract_mean', + 'subtract_mean_percentile', + 'median', + 'minimum', + 'modal', + 'enhance_contrast', + 'enhance_contrast_percentile', + 'pop', + 'pop_percentile', + 'pop_bilateral', + 'sum', + 'sum_bilateral', + 'sum_percentile', + 'threshold', + 'threshold_percentile', + 'tophat', + 'noise_filter', + 'entropy', + 'otsu', + 'percentile', + 'windowed_histogram'] diff --git a/skimage/filter/rank/_percentile.py b/skimage/filters/rank/_percentile.py similarity index 99% rename from skimage/filter/rank/_percentile.py rename to skimage/filters/rank/_percentile.py index 01dd0b49..918ad348 100644 --- a/skimage/filter/rank/_percentile.py +++ b/skimage/filters/rank/_percentile.py @@ -23,6 +23,7 @@ References """ import numpy as np +from ..._shared.utils import assert_nD from . import percentile_cy from .generic import _handle_input @@ -37,13 +38,14 @@ __all__ = ['autolevel_percentile', 'gradient_percentile', def _apply(func, image, selem, out, mask, shift_x, shift_y, p0, p1, out_dtype=None): + assert_nD(image, 2) image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, out_dtype) func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, out=out, max_bin=max_bin, p0=p0, p1=p1) - return out + return out.reshape(out.shape[:2]) def autolevel_percentile(image, selem, out=None, mask=None, shift_x=False, diff --git a/skimage/filter/rank/bilateral.py b/skimage/filters/rank/bilateral.py similarity index 93% rename from skimage/filter/rank/bilateral.py rename to skimage/filters/rank/bilateral.py index d01680db..48e350b8 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filters/rank/bilateral.py @@ -24,7 +24,8 @@ References """ import numpy as np -from skimage import img_as_ubyte +from ... import img_as_ubyte +from ..._shared.utils import assert_nD from . import bilateral_cy from .generic import _handle_input @@ -36,13 +37,14 @@ __all__ = ['mean_bilateral', 'pop_bilateral', 'sum_bilateral'] def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1, out_dtype=None): + assert_nD(image, 2) image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, out_dtype) func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, out=out, max_bin=max_bin, s0=s0, s1=s1) - return out + return out.reshape(out.shape[:2]) def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, @@ -87,13 +89,13 @@ def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, See also -------- - skimage.filter.denoise_bilateral for a Gaussian bilateral filter. + skimage.filters.denoise_bilateral for a Gaussian bilateral filter. Examples -------- >>> from skimage import data >>> from skimage.morphology import disk - >>> from skimage.filter.rank import mean_bilateral + >>> from skimage.filters.rank import mean_bilateral >>> img = data.camera().astype(np.uint16) >>> bilat_img = mean_bilateral(img, disk(20), s0=10,s1=10) @@ -140,7 +142,7 @@ def pop_bilateral(image, selem, out=None, mask=None, shift_x=False, Examples -------- >>> from skimage.morphology import square - >>> import skimage.filter.rank as rank + >>> import skimage.filters.rank as rank >>> img = 255 * np.array([[0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], @@ -158,8 +160,9 @@ 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): + 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 @@ -203,13 +206,13 @@ def sum_bilateral(image, selem, out=None, mask=None, shift_x=False, See also -------- - skimage.filter.denoise_bilateral for a Gaussian bilateral filter. + skimage.filters.denoise_bilateral for a Gaussian bilateral filter. Examples -------- >>> from skimage import data >>> from skimage.morphology import disk - >>> from skimage.filter.rank import sum_bilateral + >>> from skimage.filters.rank import sum_bilateral >>> img = data.camera().astype(np.uint16) >>> bilat_img = sum_bilateral(img, disk(10), s0=10, s1=10) diff --git a/skimage/filters/rank/bilateral_cy.pyx b/skimage/filters/rank/bilateral_cy.pyx new file mode 100644 index 00000000..a008e682 --- /dev/null +++ b/skimage/filters/rank/bilateral_cy.pyx @@ -0,0 +1,109 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log + +from .core_cy cimport dtype_t, dtype_t_out, _core + + +cdef inline void _kernel_mean(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t bilat_pop = 0 + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + mean += histo[i] * i + if bilat_pop: + out[0] = (mean / bilat_pop) + else: + out[0] = 0 + else: + out[0] = 0 + + +cdef inline void _kernel_pop(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t bilat_pop = 0 + + if pop: + for i in range(max_bin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + out[0] = bilat_pop + else: + out[0] = 0 + + +cdef inline void _kernel_sum(dtype_t_out* out, Py_ssize_t odepth, + 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: + out[0] = sum + else: + out[0] = 0 + else: + out[0] = 0 + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): + + _core(_kernel_mean[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): + + _core(_kernel_pop[dtype_t_out, 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, + signed char shift_x, signed char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): + + _core(_kernel_sum[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) diff --git a/skimage/filter/rank/core_cy.pxd b/skimage/filters/rank/core_cy.pxd similarity index 61% rename from skimage/filter/rank/core_cy.pxd rename to skimage/filters/rank/core_cy.pxd index 2e97e50a..795f0819 100644 --- a/skimage/filter/rank/core_cy.pxd +++ b/skimage/filters/rank/core_cy.pxd @@ -15,14 +15,14 @@ cdef dtype_t _max(dtype_t a, dtype_t b) cdef dtype_t _min(dtype_t a, dtype_t b) -cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, - Py_ssize_t, Py_ssize_t, double, - double, Py_ssize_t, Py_ssize_t), +cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, + dtype_t, Py_ssize_t, Py_ssize_t, double, + double, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin) except * diff --git a/skimage/filter/rank/core_cy.pyx b/skimage/filters/rank/core_cy.pyx similarity index 85% rename from skimage/filter/rank/core_cy.pyx rename to skimage/filters/rank/core_cy.pyx index 02c2c8d0..822c7251 100644 --- a/skimage/filter/rank/core_cy.pyx +++ b/skimage/filters/rank/core_cy.pyx @@ -42,14 +42,14 @@ cdef inline char is_in_mask(Py_ssize_t rows, Py_ssize_t cols, return 0 -cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, - Py_ssize_t, Py_ssize_t, double, - double, Py_ssize_t, Py_ssize_t), +cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, + dtype_t, Py_ssize_t, Py_ssize_t, double, + double, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, - char shift_x, char shift_y, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin) except *: @@ -61,6 +61,7 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, cdef Py_ssize_t cols = image.shape[1] cdef Py_ssize_t srows = selem.shape[0] cdef Py_ssize_t scols = selem.shape[1] + cdef Py_ssize_t odepth = out.shape[2] cdef Py_ssize_t centre_r = (selem.shape[0] / 2) + shift_y cdef Py_ssize_t centre_c = (selem.shape[1] / 2) + shift_x @@ -111,16 +112,16 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, # build attack and release borders by using difference along axis t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) - cdef char[:, :] t_e = (np.diff(t, axis=1) < 0).view(np.uint8) + cdef unsigned char[:, :] t_e = (np.diff(t, axis=1) < 0).view(np.uint8) t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) - cdef char[:, :] t_w = (np.diff(t, axis=1) > 0).view(np.uint8) + cdef unsigned char[:, :] t_w = (np.diff(t, axis=1) > 0).view(np.uint8) t = np.vstack((selem, np.zeros((1, selem.shape[1])))) - cdef char[:, :] t_s = (np.diff(t, axis=0) < 0).view(np.uint8) + cdef unsigned char[:, :] t_s = (np.diff(t, axis=0) < 0).view(np.uint8) t = np.vstack((np.zeros((1, selem.shape[1])), selem)) - cdef char[:, :] t_n = (np.diff(t, axis=0) > 0).view(np.uint8) + cdef unsigned char[:, :] t_n = (np.diff(t, axis=0) > 0).view(np.uint8) for r in range(srows): for c in range(scols): @@ -151,8 +152,8 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, r = 0 c = 0 - out[r, c] = kernel(histo, pop, image[r, c], max_bin, mid_bin, - p0, p1, s0, s1) + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, mid_bin, + p0, p1, s0, s1) # main loop r = 0 @@ -172,8 +173,8 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], - max_bin, mid_bin, p0, p1, s0, s1) + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, + mid_bin, p0, p1, s0, s1) r += 1 # pass to the next row if r >= rows: @@ -192,8 +193,8 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], - max_bin, mid_bin, p0, p1, s0, s1) + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, + mid_bin, p0, p1, s0, s1) # ---> east to west for c in range(cols - 2, -1, -1): @@ -209,8 +210,8 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], - max_bin, mid_bin, p0, p1, s0, s1) + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, + mid_bin, p0, p1, s0, s1) r += 1 # pass to the next row if r >= rows: @@ -229,8 +230,8 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], - max_bin, mid_bin, p0, p1, s0, s1) + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) # release memory allocated by malloc free(se_e_r) diff --git a/skimage/filter/rank/generic.py b/skimage/filters/rank/generic.py similarity index 75% rename from skimage/filter/rank/generic.py rename to skimage/filters/rank/generic.py index ccc1166e..fd718b98 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filters/rank/generic.py @@ -18,7 +18,8 @@ References import warnings import numpy as np -from skimage import img_as_ubyte +from ... import img_as_ubyte +from ..._shared.utils import assert_nD from . import generic_cy @@ -28,8 +29,9 @@ __all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] -def _handle_input(image, selem, out, mask, out_dtype=None): +def _handle_input(image, selem, out, mask, out_dtype=None, pixel_size=1): + assert_nD(image, 2) if image.dtype not in (np.uint8, np.uint16): image = img_as_ubyte(image) @@ -42,13 +44,16 @@ def _handle_input(image, selem, out, mask, out_dtype=None): mask = img_as_ubyte(mask) mask = np.ascontiguousarray(mask) + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + if out is None: if out_dtype is None: out_dtype = image.dtype - out = np.empty_like(image, dtype=out_dtype) - - if image is out: - raise NotImplementedError("Cannot perform rank operation in place.") + out = np.empty(image.shape+(pixel_size,), dtype=out_dtype) + else: + if len(out.shape) == 2: + out = out.reshape(out.shape+(pixel_size,)) is_8bit = image.dtype in (np.uint8, np.int8) @@ -65,7 +70,8 @@ def _handle_input(image, selem, out, mask, out_dtype=None): return image, selem, out, mask, max_bin -def _apply(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): +def _apply_scalar_per_pixel(func, image, selem, out, mask, shift_x, shift_y, + out_dtype=None): image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, out_dtype) @@ -73,6 +79,19 @@ def _apply(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, out=out, max_bin=max_bin) + return out.reshape(out.shape[:2]) + + +def _apply_vector_per_pixel(func, image, selem, out, mask, shift_x, shift_y, + out_dtype=None, pixel_size=1): + + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype, + pixel_size=pixel_size) + + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin) + return out @@ -107,14 +126,15 @@ 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 + >>> from skimage.filters.rank import autolevel >>> img = data.camera() >>> auto = autolevel(img, disk(5)) """ - return _apply(generic_cy._autolevel, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._autolevel, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -148,14 +168,15 @@ def bottomhat(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 bottomhat + >>> from skimage.filters.rank import bottomhat >>> img = data.camera() >>> out = bottomhat(img, disk(5)) """ - return _apply(generic_cy._bottomhat, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._bottomhat, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -186,14 +207,15 @@ 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 + >>> from skimage.filters.rank import equalize >>> img = data.camera() >>> equ = equalize(img, disk(5)) """ - return _apply(generic_cy._equalize, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._equalize, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -224,14 +246,15 @@ def gradient(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 gradient + >>> from skimage.filters.rank import gradient >>> img = data.camera() >>> out = gradient(img, disk(5)) """ - return _apply(generic_cy._gradient, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._gradient, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -264,21 +287,22 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Notes ----- - The lower algorithm complexity makes the `skimage.filter.rank.maximum` + The lower algorithm complexity makes `skimage.filters.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 + >>> from skimage.filters.rank import maximum >>> img = data.camera() >>> out = maximum(img, disk(5)) """ - return _apply(generic_cy._maximum, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._maximum, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -309,14 +333,14 @@ 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 + >>> from skimage.filters.rank import mean >>> img = data.camera() >>> avg = mean(img, disk(5)) """ - return _apply(generic_cy._mean, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._mean, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) def subtract_mean(image, selem, out=None, mask=None, shift_x=False, @@ -348,14 +372,15 @@ def subtract_mean(image, selem, out=None, mask=None, shift_x=False, -------- >>> from skimage import data >>> from skimage.morphology import disk - >>> from skimage.filter.rank import subtract_mean + >>> from skimage.filters.rank import subtract_mean >>> img = data.camera() >>> out = subtract_mean(img, disk(5)) """ - return _apply(generic_cy._subtract_mean, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._subtract_mean, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -386,14 +411,15 @@ 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 + >>> from skimage.filters.rank import median >>> img = data.camera() >>> med = median(img, disk(5)) """ - return _apply(generic_cy._median, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._median, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -426,21 +452,22 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Notes ----- - The lower algorithm complexity makes the `skimage.filter.rank.minimum` more + The lower algorithm complexity makes `skimage.filters.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 + >>> from skimage.filters.rank import minimum >>> img = data.camera() >>> out = minimum(img, disk(5)) """ - return _apply(generic_cy._minimum, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._minimum, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -473,14 +500,15 @@ def modal(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 modal + >>> from skimage.filters.rank import modal >>> img = data.camera() >>> out = modal(img, disk(5)) """ - return _apply(generic_cy._modal, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._modal, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, @@ -516,14 +544,15 @@ 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 + >>> from skimage.filters.rank import enhance_contrast >>> img = data.camera() >>> out = enhance_contrast(img, disk(5)) """ - return _apply(generic_cy._enhance_contrast, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._enhance_contrast, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -556,7 +585,7 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Examples -------- >>> from skimage.morphology import square - >>> import skimage.filter.rank as rank + >>> import skimage.filters.rank as rank >>> img = 255 * np.array([[0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], @@ -571,8 +600,9 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._pop, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._pop, image, selem, out=out, + mask=mask, shift_x=shift_x, + shift_y=shift_y) def sum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -605,7 +635,7 @@ def sum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Examples -------- >>> from skimage.morphology import square - >>> import skimage.filter.rank as rank + >>> import skimage.filters.rank as rank >>> img = np.array([[0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], @@ -620,8 +650,9 @@ def sum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._sum, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(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): @@ -654,7 +685,7 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Examples -------- >>> from skimage.morphology import square - >>> from skimage.filter.rank import threshold + >>> from skimage.filters.rank import threshold >>> img = 255 * np.array([[0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], @@ -669,8 +700,9 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._threshold, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._threshold, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -704,19 +736,20 @@ def tophat(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 tophat + >>> from skimage.filters.rank import tophat >>> img = data.camera() >>> out = tophat(img, disk(5)) """ - return _apply(generic_cy._tophat, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._tophat, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def noise_filter(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Noise feature as described in [Hashimoto12]_. + """Noise feature. Parameters ---------- @@ -736,7 +769,7 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, References ---------- - .. [Hashimoto12] N. Hashimoto et al. Referenceless image quality evaluation + .. [1] N. Hashimoto et al. Referenceless image quality evaluation for whole slide imaging. J Pathol Inform 2012;3:9. Returns @@ -748,7 +781,7 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, -------- >>> from skimage import data >>> from skimage.morphology import disk - >>> from skimage.filter.rank import noise_filter + >>> from skimage.filters.rank import noise_filter >>> img = data.camera() >>> out = noise_filter(img, disk(5)) @@ -761,15 +794,17 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, selem_cpy = selem.copy() selem_cpy[centre_r, centre_c] = 0 - return _apply(generic_cy._noise_filter, image, selem_cpy, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._noise_filter, image, selem_cpy, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Local entropy [1]_. + """Local entropy. 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. + minimum number of bits needed to encode the local greylevel + distribution. Parameters ---------- @@ -799,16 +834,17 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Examples -------- >>> from skimage import data - >>> from skimage.filter.rank import entropy + >>> from skimage.filters.rank import entropy >>> from skimage.morphology import disk >>> img = data.camera() >>> ent = entropy(img, disk(5)) """ - return _apply(generic_cy._entropy, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, - out_dtype=np.double) + return _apply_scalar_per_pixel(generic_cy._entropy, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y, + out_dtype=np.double) def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -837,12 +873,12 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): References ---------- - .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method + .. [1] http://en.wikipedia.org/wiki/Otsu's_method Examples -------- >>> from skimage import data - >>> from skimage.filter.rank import otsu + >>> from skimage.filters.rank import otsu >>> from skimage.morphology import disk >>> img = data.camera() >>> local_otsu = otsu(img, disk(5)) @@ -850,5 +886,59 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._otsu, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._otsu, image, selem, out=out, + mask=mask, shift_x=shift_x, + shift_y=shift_y) + + +def windowed_histogram(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, n_bins=None): + """Normalized sliding window histogram + + Parameters + ---------- + image : ndarray + Image array (uint8 array). + 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. + 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). + n_bins : int or None + The number of histogram bins. Will default to ``image.max() + 1`` + if None is passed. + + Returns + ------- + out : 3-D array with float dtype of dimensions (H,W,N), where (H,W) are + the dimensions of the input image and N is n_bins or + ``image.max() + 1`` if no value is provided as a parameter. + Effectively, each pixel is a N-D feature vector that is the histogram. + The sum of the elements in the feature vector will be 1, unless no + pixels in the window were covered by both selem and mask, in which + case all elements will be 0. + + Examples + -------- + >>> from skimage import data + >>> from skimage.filters.rank import windowed_histogram + >>> from skimage.morphology import disk + >>> img = data.camera() + >>> hist_img = windowed_histogram(img, disk(5)) + + """ + + if n_bins is None: + n_bins = image.max() + 1 + + return _apply_vector_per_pixel(generic_cy._windowed_hist, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y, + out_dtype=np.double, + pixel_size=n_bins) diff --git a/skimage/filters/rank/generic_cy.pyx b/skimage/filters/rank/generic_cy.pyx new file mode 100644 index 00000000..3b28005a --- /dev/null +++ b/skimage/filters/rank/generic_cy.pyx @@ -0,0 +1,597 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log + +from .core_cy cimport dtype_t, dtype_t_out, _core + + +cdef inline void _kernel_autolevel(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, delta + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + delta = imax - imin + if delta > 0: + out[0] = ((max_bin - 1) * (g - imin) / delta) + else: + out[0] = 0 + else: + out[0] = 0 + + +cdef inline void _kernel_bottomhat(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin): + if histo[i]: + break + out[0] = (g - i) + else: + out[0] = 0 + + +cdef inline void _kernel_equalize(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + sum += histo[i] + if i >= g: + break + out[0] = (((max_bin - 1) * sum) / pop) + else: + out[0] = 0 + + +cdef inline void _kernel_gradient(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + out[0] = (imax - imin) + else: + out[0] = 0 + + +cdef inline void _kernel_maximum(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + out[0] = i + return + else: + out[0] = 0 + + +cdef inline void _kernel_mean(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + out[0] = (mean / pop) + else: + out[0] = 0 + + +cdef inline void _kernel_subtract_mean(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + out[0] = ((g - mean / pop) / 2. + 127) + else: + out[0] = 0 + + +cdef inline void _kernel_median(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef double sum = pop / 2.0 + + if pop: + for i in range(max_bin): + if histo[i]: + sum -= histo[i] + if sum < 0: + out[0] = i + return + else: + out[0] = 0 + + +cdef inline void _kernel_minimum(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin): + if histo[i]: + out[0] = i + return + else: + out[0] = 0 + + +cdef inline void _kernel_modal(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t hmax = 0, imax = 0 + + if pop: + for i in range(max_bin): + if histo[i] > hmax: + hmax = histo[i] + imax = i + out[0] = imax + else: + out[0] = 0 + + +cdef inline void _kernel_enhance_contrast(dtype_t_out* out, + Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + if imax - g < g - imin: + out[0] = imax + else: + out[0] = imin + else: + out[0] = 0 + + +cdef inline void _kernel_pop(dtype_t_out* out, Py_ssize_t odepth, + 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): + + out[0] = pop + + +cdef inline void _kernel_sum(dtype_t_out* out, Py_ssize_t odepth, + 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 + out[0] = sum + else: + out[0] = 0 + + +cdef inline void _kernel_threshold(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + out[0] = (g > (mean / pop)) + else: + out[0] = 0 + + +cdef inline void _kernel_tophat(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + break + out[0] = (i - g) + else: + out[0] = 0 + + +cdef inline void _kernel_noise_filter(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t min_i + + # early stop if at least one pixel of the neighborhood has the same g + if histo[g] > 0: + out[0] = 0 + + for i in range(g, -1, -1): + if histo[i]: + break + min_i = g - i + for i in range(g, max_bin): + if histo[i]: + break + if i - g < min_i: + out[0] = (i - g) + else: + out[0] = min_i + + +cdef inline void _kernel_entropy(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef double e, p + + if pop: + e = 0. + for i in range(max_bin): + p = histo[i] / pop + if p > 0: + e -= p * log(p) / 0.6931471805599453 + out[0] = e + else: + out[0] = 0 + + +cdef inline void _kernel_otsu(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef Py_ssize_t max_i + cdef double P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b + cdef double mu = 0. + + # compute local mean + if pop: + for i in range(max_bin): + mu += histo[i] * i + mu = mu / pop + else: + out[0] = 0 + + # maximizing the between class variance + max_i = 0 + q1 = histo[0] / pop + mu1 = 0. + max_sigma_b = 0. + + for i in range(1, max_bin): + P = histo[i] / pop + new_q1 = q1 + P + if new_q1 > 0: + mu1 = (q1 * mu1 + i * P) / new_q1 + mu2 = (mu - new_q1 * mu1) / (1. - new_q1) + sigma_b = new_q1 * (1. - new_q1) * (mu1 - mu2) ** 2 + if sigma_b > max_sigma_b: + max_sigma_b = sigma_b + max_i = i + q1 = new_q1 + + out[0] = max_i + + +cdef inline void _kernel_win_hist(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef Py_ssize_t max_i + cdef double scale + if pop: + scale = 1.0 / pop + for i in xrange(odepth): + out[i] = (histo[i] * scale) + else: + for i in xrange(odepth): + out[i] = 0 + + +def _autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_autolevel[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _bottomhat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_bottomhat[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _equalize(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_equalize[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_gradient[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _maximum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_maximum[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_mean[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _subtract_mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_subtract_mean[dtype_t_out, dtype_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _median(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_median[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _minimum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_minimum[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _enhance_contrast(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_enhance_contrast[dtype_t_out, dtype_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _modal(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_modal[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_pop[dtype_t_out, 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, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_sum[dtype_t_out, dtype_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_threshold[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _tophat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_tophat[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _noise_filter(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_noise_filter[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _entropy(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_entropy[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _otsu(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_otsu[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _windowed_hist(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): + + _core(_kernel_win_hist[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) diff --git a/skimage/filters/rank/percentile_cy.pyx b/skimage/filters/rank/percentile_cy.pyx new file mode 100644 index 00000000..4d90555f --- /dev/null +++ b/skimage/filters/rank/percentile_cy.pyx @@ -0,0 +1,352 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from .core_cy cimport dtype_t, dtype_t_out, _core, _min, _max + + +cdef inline void _kernel_autolevel(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range(max_bin - 1, -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + + delta = imax - imin + if delta > 0: + out[0] = ((max_bin - 1) * (_min(_max(imin, g), imax) + - imin) / delta) + else: + out[0] = (imax - imin) + else: + out[0] = 0 + + +cdef inline void _kernel_gradient(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum >= p0 * pop: + imin = i + break + sum = 0 + for i in range(max_bin - 1, -1, -1): + sum += histo[i] + if sum >= p1 * pop: + imax = i + break + + out[0] = (imax - imin) + else: + out[0] = 0 + + +cdef inline void _kernel_mean(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + + if n > 0: + out[0] = (mean / n) + else: + out[0] = 0 + else: + out[0] = 0 + +cdef inline void _kernel_sum(dtype_t_out* out, Py_ssize_t odepth, + 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: + out[0] = sum_g + else: + out[0] = 0 + else: + out[0] = 0 + +cdef inline void _kernel_subtract_mean(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + if n > 0: + out[0] = ((g - (mean / n)) * .5 + mid_bin) + else: + out[0] = 0 + else: + out[0] = 0 + + +cdef inline void _kernel_enhance_contrast(dtype_t_out* out, + Py_ssize_t odepth, + Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range(max_bin - 1, -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + if g > imax: + out[0] = imax + if g < imin: + out[0] = imin + if imax - g < g - imin: + out[0] = imax + else: + out[0] = imin + else: + out[0] = 0 + + +cdef inline void _kernel_percentile(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t sum = 0 + + if pop: + if p0 == 1: # make sure p0 = 1 returns the maximum filter + for i in range(max_bin - 1, -1, -1): + if histo[i]: + break + else: + for i in range(max_bin): + sum += histo[i] + if sum > p0 * pop: + break + out[0] = i + else: + out[0] = 0 + + +cdef inline void _kernel_pop(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, sum, n + + if pop: + sum = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + out[0] = n + else: + out[0] = 0 + + +cdef inline void _kernel_threshold(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + sum += histo[i] + if sum >= p0 * pop: + break + + out[0] = ((max_bin - 1) * (g >= i)) + else: + out[0] = 0 + + +def _autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_autolevel[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_gradient[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_mean[dtype_t_out, 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, + signed char shift_x, signed char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_sum[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _subtract_mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_subtract_mean[dtype_t_out, dtype_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _enhance_contrast(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_enhance_contrast[dtype_t_out, dtype_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _percentile(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_percentile[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_pop[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + signed char shift_x, signed char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_threshold[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) diff --git a/skimage/filters/rank/tests/__init__.py b/skimage/filters/rank/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/filters/rank/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filters/rank/tests/test_rank.py similarity index 62% rename from skimage/filter/rank/tests/test_rank.py rename to skimage/filters/rank/tests/test_rank.py index 72f9afe6..48628432 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filters/rank/tests/test_rank.py @@ -1,16 +1,92 @@ +import os import numpy as np -from numpy.testing import run_module_suite, assert_array_equal, assert_raises +from numpy.testing import run_module_suite, assert_equal, assert_raises -from skimage import img_as_ubyte, img_as_uint, img_as_float -from skimage import data, util -from skimage.morphology import cmorph, disk -from skimage.filter import rank +import skimage +from skimage import img_as_ubyte, img_as_float +from skimage import data, util, morphology +from skimage.morphology import grey, disk +from skimage.filters import rank +from skimage._shared._warnings import expected_warnings + + +def test_all(): + with expected_warnings(['precision loss', 'non-integer|\A\Z']): + check_all() + + +def check_all(): + image = np.random.rand(25, 25) + selem = morphology.disk(1) + refs = np.load(os.path.join(skimage.data_dir, "rank_filter_tests.npz")) + + assert_equal(refs["autolevel"], + rank.autolevel(image, selem)) + assert_equal(refs["autolevel_percentile"], + rank.autolevel_percentile(image, selem)) + assert_equal(refs["bottomhat"], + rank.bottomhat(image, selem)) + assert_equal(refs["equalize"], + rank.equalize(image, selem)) + assert_equal(refs["gradient"], + rank.gradient(image, selem)) + assert_equal(refs["gradient_percentile"], + rank.gradient_percentile(image, selem)) + assert_equal(refs["maximum"], + rank.maximum(image, selem)) + assert_equal(refs["mean"], + rank.mean(image, selem)) + assert_equal(refs["mean_percentile"], + rank.mean_percentile(image, selem)) + assert_equal(refs["mean_bilateral"], + rank.mean_bilateral(image, selem)) + assert_equal(refs["subtract_mean"], + rank.subtract_mean(image, selem)) + assert_equal(refs["subtract_mean_percentile"], + rank.subtract_mean_percentile(image, selem)) + assert_equal(refs["median"], + rank.median(image, selem)) + assert_equal(refs["minimum"], + rank.minimum(image, selem)) + assert_equal(refs["modal"], + rank.modal(image, selem)) + assert_equal(refs["enhance_contrast"], + rank.enhance_contrast(image, selem)) + assert_equal(refs["enhance_contrast_percentile"], + rank.enhance_contrast_percentile(image, selem)) + assert_equal(refs["pop"], + rank.pop(image, selem)) + assert_equal(refs["pop_percentile"], + rank.pop_percentile(image, selem)) + assert_equal(refs["pop_bilateral"], + rank.pop_bilateral(image, selem)) + assert_equal(refs["sum"], + rank.sum(image, selem)) + assert_equal(refs["sum_bilateral"], + rank.sum_bilateral(image, selem)) + assert_equal(refs["sum_percentile"], + rank.sum_percentile(image, selem)) + assert_equal(refs["threshold"], + rank.threshold(image, selem)) + assert_equal(refs["threshold_percentile"], + rank.threshold_percentile(image, selem)) + assert_equal(refs["tophat"], + rank.tophat(image, selem)) + assert_equal(refs["noise_filter"], + rank.noise_filter(image, selem)) + assert_equal(refs["entropy"], + rank.entropy(image, selem)) + assert_equal(refs["otsu"], + rank.otsu(image, selem)) + assert_equal(refs["percentile"], + rank.percentile(image, selem)) + assert_equal(refs["windowed_histogram"], + rank.windowed_histogram(image, selem)) def test_random_sizes(): # make sure the size is not a problem - niter = 10 elem = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8) for m, n in np.random.random_integers(1, 100, size=(10, 2)): mask = np.ones((m, n), dtype=np.uint8) @@ -19,54 +95,54 @@ def test_random_sizes(): out8 = np.empty_like(image8) rank.mean(image=image8, selem=elem, mask=mask, out=out8, shift_x=0, shift_y=0) - assert_array_equal(image8.shape, out8.shape) + assert_equal(image8.shape, out8.shape) rank.mean(image=image8, selem=elem, mask=mask, out=out8, shift_x=+1, shift_y=+1) - assert_array_equal(image8.shape, out8.shape) + assert_equal(image8.shape, out8.shape) image16 = np.ones((m, n), dtype=np.uint16) out16 = np.empty_like(image8, dtype=np.uint16) rank.mean(image=image16, selem=elem, mask=mask, out=out16, shift_x=0, shift_y=0) - assert_array_equal(image16.shape, out16.shape) + assert_equal(image16.shape, out16.shape) rank.mean(image=image16, selem=elem, mask=mask, out=out16, shift_x=+1, shift_y=+1) - assert_array_equal(image16.shape, out16.shape) + assert_equal(image16.shape, out16.shape) rank.mean_percentile(image=image16, mask=mask, out=out16, selem=elem, shift_x=0, shift_y=0, p0=.1, p1=.9) - assert_array_equal(image16.shape, out16.shape) + assert_equal(image16.shape, out16.shape) rank.mean_percentile(image=image16, mask=mask, out=out16, selem=elem, shift_x=+1, shift_y=+1, p0=.1, p1=.9) - assert_array_equal(image16.shape, out16.shape) + assert_equal(image16.shape, out16.shape) -def test_compare_with_cmorph_dilate(): +def test_compare_with_grey_dilation(): # compare the result of maximum filter with dilate - image = (np.random.random((100, 100)) * 256).astype(np.uint8) + image = (np.random.rand(100, 100) * 256).astype(np.uint8) out = np.empty_like(image) mask = np.ones(image.shape, dtype=np.uint8) - for r in range(1, 20, 1): + for r in range(3, 20, 2): elem = np.ones((r, r), dtype=np.uint8) rank.maximum(image=image, selem=elem, out=out, mask=mask) - cm = cmorph._dilate(image=image, selem=elem) - assert_array_equal(out, cm) + cm = grey.dilation(image=image, selem=elem) + assert_equal(out, cm) -def test_compare_with_cmorph_erode(): +def test_compare_with_grey_erosion(): # compare the result of maximum filter with erode - image = (np.random.random((100, 100)) * 256).astype(np.uint8) + image = (np.random.rand(100, 100) * 256).astype(np.uint8) out = np.empty_like(image) mask = np.ones(image.shape, dtype=np.uint8) - for r in range(1, 20, 1): + for r in range(3, 20, 2): elem = np.ones((r, r), dtype=np.uint8) rank.minimum(image=image, selem=elem, out=out, mask=mask) - cm = cmorph._erode(image=image, selem=elem) - assert_array_equal(out, cm) + cm = grey.erosion(image=image, selem=elem) + assert_equal(out, cm) def test_bitdepth(): @@ -78,8 +154,13 @@ def test_bitdepth(): for i in range(5): image = np.ones((100, 100), dtype=np.uint16) * 255 * 2 ** i - r = rank.mean_percentile(image=image, selem=elem, mask=mask, - out=out, shift_x=0, shift_y=0, p0=.1, p1=.9) + if i > 3: + expected = ["Bitdepth of"] + else: + expected = [] + with expected_warnings(expected): + rank.mean_percentile(image=image, selem=elem, mask=mask, + out=out, shift_x=0, shift_y=0, p0=.1, p1=.9) def test_population(): @@ -96,7 +177,7 @@ def test_population(): [6, 9, 9, 9, 6], [6, 9, 9, 9, 6], [4, 6, 6, 6, 4]]) - assert_array_equal(r, out) + assert_equal(r, out) def test_structuring_element8(): @@ -118,7 +199,7 @@ def test_structuring_element8(): rank.maximum(image=image, selem=elem, out=out, mask=mask, shift_x=1, shift_y=1) - assert_array_equal(r, out) + assert_equal(r, out) # 16-bit image = np.zeros((6, 6), dtype=np.uint16) @@ -127,7 +208,7 @@ def test_structuring_element8(): rank.maximum(image=image, selem=elem, out=out, mask=mask, shift_x=1, shift_y=1) - assert_array_equal(r, out) + assert_equal(r, out) def test_pass_on_bitdepth(): @@ -143,7 +224,7 @@ def test_inplace_output(): # rank filters are not supposed to filter inplace selem = disk(20) - image = (np.random.random((500, 500)) * 256).astype(np.uint8) + image = (np.random.rand(500, 500) * 256).astype(np.uint8) out = image assert_raises(NotImplementedError, rank.mean, image, selem, out=out) @@ -159,7 +240,7 @@ def test_compare_autolevels(): loc_perc_autolevel = rank.autolevel_percentile(image, selem=selem, p0=.0, p1=1.) - assert_array_equal(loc_autolevel, loc_perc_autolevel) + assert_equal(loc_autolevel, loc_perc_autolevel) def test_compare_autolevels_16bit(): @@ -173,7 +254,7 @@ def test_compare_autolevels_16bit(): loc_perc_autolevel = rank.autolevel_percentile(image, selem=selem, p0=.0, p1=1.) - assert_array_equal(loc_autolevel, loc_perc_autolevel) + assert_equal(loc_autolevel, loc_perc_autolevel) def test_compare_ubyte_vs_float(): @@ -188,8 +269,9 @@ def test_compare_ubyte_vs_float(): for method in methods: func = getattr(rank, method) out_u = func(image_uint, disk(3)) - out_f = func(image_float, disk(3)) - assert_array_equal(out_u, out_f) + with expected_warnings(['precision loss']): + out_f = func(image_float, disk(3)) + assert_equal(out_u, out_f) def test_compare_8bit_unsigned_vs_signed(): @@ -200,9 +282,9 @@ def test_compare_8bit_unsigned_vs_signed(): image = img_as_ubyte(data.camera()) image[image > 127] = 0 image_s = image.astype(np.int8) - image_u = img_as_ubyte(image_s) - - assert_array_equal(image_u, img_as_ubyte(image_s)) + with expected_warnings(['sign loss', 'precision loss']): + image_u = img_as_ubyte(image_s) + assert_equal(image_u, img_as_ubyte(image_s)) methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', 'subtract_mean', 'median', 'minimum', 'modal', @@ -210,9 +292,11 @@ def test_compare_8bit_unsigned_vs_signed(): for method in methods: func = getattr(rank, method) - out_u = func(image_u, disk(3)) - out_s = func(image_s, disk(3)) - assert_array_equal(out_u, out_s) + + with expected_warnings(['sign loss', 'precision loss']): + out_u = func(image_u, disk(3)) + out_s = func(image_s, disk(3)) + assert_equal(out_u, out_s) def test_compare_8bit_vs_16bit(): @@ -221,7 +305,7 @@ def test_compare_8bit_vs_16bit(): image8 = util.img_as_ubyte(data.camera()) image16 = image8.astype(np.uint16) - assert_array_equal(image8, image16) + assert_equal(image8, image16) methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', 'subtract_mean', 'median', 'minimum', 'modal', @@ -231,7 +315,7 @@ def test_compare_8bit_vs_16bit(): func = getattr(rank, method) f8 = func(image8, disk(3)) f16 = func(image16, disk(3)) - assert_array_equal(f8, f16) + assert_equal(f8, f16) def test_trivial_selem8(): @@ -248,13 +332,13 @@ def test_trivial_selem8(): elem = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.uint8) rank.mean(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) rank.minimum(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) rank.maximum(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) def test_trivial_selem16(): @@ -271,13 +355,13 @@ def test_trivial_selem16(): elem = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.uint8) rank.mean(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) rank.minimum(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) rank.maximum(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) def test_smallest_selem8(): @@ -294,13 +378,13 @@ def test_smallest_selem8(): elem = np.array([[1]], dtype=np.uint8) rank.mean(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) rank.minimum(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) rank.maximum(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) def test_smallest_selem16(): @@ -317,13 +401,13 @@ def test_smallest_selem16(): elem = np.array([[1]], dtype=np.uint8) rank.mean(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) rank.minimum(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) rank.maximum(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) def test_empty_selem(): @@ -342,13 +426,13 @@ def test_empty_selem(): rank.mean(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(res, out) + assert_equal(res, out) rank.minimum(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(res, out) + assert_equal(res, out) rank.maximum(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(res, out) + assert_equal(res, out) def test_otsu(): @@ -362,7 +446,7 @@ def test_otsu(): res = np.tile([1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1], (16, 1)) selem = np.ones((6, 6), dtype=np.uint8) th = 1 * (test >= rank.otsu(test, selem)) - assert_array_equal(th, res) + assert_equal(th, res) def test_entropy(): @@ -401,10 +485,12 @@ def test_entropy(): selem = np.ones((64, 64), dtype=np.uint8) data = np.tile( np.reshape(np.arange(4096), (64, 64)), (2, 2)).astype(np.uint16) - assert(np.max(rank.entropy(data, selem)) == 12) + with expected_warnings(['Bitdepth of 11']): + assert(np.max(rank.entropy(data, selem)) == 12) # make sure output is of dtype double - out = rank.entropy(data, np.ones((16, 16), dtype=np.uint8)) + with expected_warnings(['Bitdepth of 11']): + out = rank.entropy(data, np.ones((16, 16), dtype=np.uint8)) assert out.dtype == np.double @@ -422,10 +508,10 @@ def test_selem_dtypes(): elem = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=dtype) rank.mean(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) rank.mean_percentile(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - assert_array_equal(image, out) + assert_equal(image, out) def test_16bit(): @@ -435,10 +521,14 @@ def test_16bit(): for bitdepth in range(17): value = 2 ** bitdepth - 1 image[10, 10] = value - assert rank.minimum(image, selem)[10, 10] == 0 - assert rank.maximum(image, selem)[10, 10] == value - assert rank.mean(image, selem)[10, 10] == int(value / selem.size) - + if bitdepth > 11: + expected = ['Bitdepth of %s' % (bitdepth - 1)] + else: + expected = [] + with expected_warnings(expected): + assert rank.minimum(image, selem)[10, 10] == 0 + assert rank.maximum(image, selem)[10, 10] == value + assert rank.mean(image, selem)[10, 10] == int(value / selem.size) def test_bilateral(): image = np.zeros((21, 21), dtype=np.uint16) @@ -462,11 +552,11 @@ def test_percentile_min(): # check for 8bit img_p0 = rank.percentile(img, selem=selem, p0=0) img_min = rank.minimum(img, selem=selem) - assert_array_equal(img_p0, img_min) + assert_equal(img_p0, img_min) # check for 16bit img_p0 = rank.percentile(img16, selem=selem, p0=0) img_min = rank.minimum(img16, selem=selem) - assert_array_equal(img_p0, img_min) + assert_equal(img_p0, img_min) def test_percentile_max(): @@ -477,11 +567,11 @@ def test_percentile_max(): # check for 8bit img_p0 = rank.percentile(img, selem=selem, p0=1.) img_max = rank.maximum(img, selem=selem) - assert_array_equal(img_p0, img_max) + assert_equal(img_p0, img_max) # check for 16bit img_p0 = rank.percentile(img16, selem=selem, p0=1.) img_max = rank.maximum(img16, selem=selem) - assert_array_equal(img_p0, img_max) + assert_equal(img_p0, img_max) def test_percentile_median(): @@ -492,11 +582,12 @@ def test_percentile_median(): # check for 8bit img_p0 = rank.percentile(img, selem=selem, p0=.5) img_max = rank.median(img, selem=selem) - assert_array_equal(img_p0, img_max) + assert_equal(img_p0, img_max) # check for 16bit img_p0 = rank.percentile(img16, selem=selem, p0=.5) img_max = rank.median(img16, selem=selem) - assert_array_equal(img_p0, img_max) + assert_equal(img_p0, img_max) + def test_sum(): # check the number of valid pixels in the neighborhood @@ -506,39 +597,83 @@ def test_sum(): [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) + 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) + 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) + assert_equal(r, out8) + rank.sum_percentile( + image=image8, selem=elem, out=out8, mask=mask, p0=.0, p1=1.) + assert_equal(r, out8) + rank.sum_bilateral( + image=image8, selem=elem, out=out8, mask=mask, s0=255, s1=255) + assert_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) + 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) + assert_equal(r, out16) + rank.sum_percentile( + image=image16, selem=elem, out=out16, mask=mask, p0=.0, p1=1.) + assert_equal(r, out16) + rank.sum_bilateral( + image=image16, selem=elem, out=out16, mask=mask, s0=1000, s1=1000) + assert_equal(r, out16) + + +def test_windowed_histogram(): + # 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) + elem = np.ones((3, 3), dtype=np.uint8) + outf = np.empty(image8.shape + (2,), dtype=float) + mask = np.ones(image8.shape, dtype=np.uint8) + + # Population so we can normalize the expected output while maintaining + # code readability + pop = np.array([[4, 6, 6, 6, 4], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [4, 6, 6, 6, 4]], dtype=float) + + r0 = np.array([[3, 4, 3, 4, 3], + [4, 5, 3, 5, 4], + [3, 3, 0, 3, 3], + [4, 5, 3, 5, 4], + [3, 4, 3, 4, 3]], dtype=float) / pop + r1 = 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=float) / pop + rank.windowed_histogram(image=image8, selem=elem, out=outf, mask=mask) + assert_equal(r0, outf[:, :, 0]) + assert_equal(r1, outf[:, :, 1]) + + # Test n_bins parameter + larger_output = rank.windowed_histogram(image=image8, selem=elem, + mask=mask, n_bins=5) + assert larger_output.shape[2] == 5 if __name__ == "__main__": diff --git a/skimage/filter/setup.py b/skimage/filters/setup.py similarity index 96% rename from skimage/filter/setup.py rename to skimage/filters/setup.py index 2f5a72e6..8bfebcb7 100644 --- a/skimage/filter/setup.py +++ b/skimage/filters/setup.py @@ -9,7 +9,7 @@ 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('filter', parent_package, top_path) + config = Configuration('filters', parent_package, top_path) config.add_data_dir('tests') config.add_data_dir('rank/tests') diff --git a/skimage/filters/tests/__init__.py b/skimage/filters/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/filters/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/filters/tests/test_deprecated_imports.py b/skimage/filters/tests/test_deprecated_imports.py new file mode 100644 index 00000000..2ea21ca2 --- /dev/null +++ b/skimage/filters/tests/test_deprecated_imports.py @@ -0,0 +1,19 @@ +from warnings import catch_warnings, simplefilter +from ..._shared._warnings import expected_warnings +from ...data import moon + + +def test_filter_import(): + with catch_warnings(): + simplefilter('ignore') + from skimage import filter as F + + assert('sobel' in dir(F)) + assert F._import_warned + + +def test_canny_import(): + data = moon() + with expected_warnings(['skimage.feature.canny']): + from skimage.filters import canny + canny(data) diff --git a/skimage/filter/tests/test_edges.py b/skimage/filters/tests/test_edges.py similarity index 58% rename from skimage/filter/tests/test_edges.py rename to skimage/filters/tests/test_edges.py index 523d6dd8..d2c0f469 100644 --- a/skimage/filter/tests/test_edges.py +++ b/skimage/filters/tests/test_edges.py @@ -1,13 +1,14 @@ 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_, assert_allclose) -import skimage.filter as F -from skimage.filter.edges import _mask_filter_result +from skimage import filters +from skimage.filters.edges import _mask_filter_result def test_roberts_zeros(): """Roberts' filter on an array of all zeros.""" - result = F.roberts(np.zeros((10, 10)), np.ones((10, 10), bool)) + result = filters.roberts(np.zeros((10, 10)), np.ones((10, 10), bool)) assert (np.all(result == 0)) @@ -17,7 +18,7 @@ def test_roberts_diagonal1(): expected = ~(np.tri(10, 10, -1).astype(bool) | np.tri(10, 10, -2).astype(bool).transpose()) expected = _mask_filter_result(expected, None) - result = F.roberts(image).astype(bool) + result = filters.roberts(image).astype(bool) assert_close(result, expected) @@ -27,21 +28,21 @@ def test_roberts_diagonal2(): expected = ~np.rot90(np.tri(10, 10, -1).astype(bool) | np.tri(10, 10, -2).astype(bool).transpose()) expected = _mask_filter_result(expected, None) - result = F.roberts(image).astype(bool) + result = filters.roberts(image).astype(bool) assert_close(result, expected) def test_sobel_zeros(): """Sobel on an array of all zeros.""" - result = F.sobel(np.zeros((10, 10)), np.ones((10, 10), bool)) + result = filters.sobel(np.zeros((10, 10)), np.ones((10, 10), bool)) assert (np.all(result == 0)) def test_sobel_mask(): """Sobel on a masked array should be zero.""" np.random.seed(0) - result = F.sobel(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) + result = filters.sobel(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) assert (np.all(result == 0)) @@ -49,10 +50,10 @@ def test_sobel_horizontal(): """Sobel on a horizontal edge should be a horizontal line.""" i, j = np.mgrid[-5:6, -5:6] image = (i >= 0).astype(float) - result = F.sobel(image) + result = filters.sobel(image) * np.sqrt(2) # Fudge the eroded points i[np.abs(j) == 5] = 10000 - assert (np.all(result[i == 0] == 1)) + assert_allclose(result[i == 0], 1) assert (np.all(result[np.abs(i) > 1] == 0)) @@ -60,7 +61,7 @@ def test_sobel_vertical(): """Sobel on a vertical edge should be a vertical line.""" i, j = np.mgrid[-5:6, -5:6] image = (j >= 0).astype(float) - result = F.sobel(image) + result = filters.sobel(image) * np.sqrt(2) j[np.abs(i) == 5] = 10000 assert (np.all(result[j == 0] == 1)) assert (np.all(result[np.abs(j) > 1] == 0)) @@ -68,15 +69,15 @@ def test_sobel_vertical(): def test_hsobel_zeros(): """Horizontal sobel on an array of all zeros.""" - result = F.hsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) + result = filters.sobel_h(np.zeros((10, 10)), np.ones((10, 10), bool)) assert (np.all(result == 0)) def test_hsobel_mask(): """Horizontal Sobel on a masked array should be zero.""" np.random.seed(0) - result = F.hsobel(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) + result = filters.sobel_h(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) assert (np.all(result == 0)) @@ -84,7 +85,7 @@ def test_hsobel_horizontal(): """Horizontal Sobel on an edge should be a horizontal line.""" i, j = np.mgrid[-5:6, -5:6] image = (i >= 0).astype(float) - result = F.hsobel(image) + result = filters.sobel_h(image) # Fudge the eroded points i[np.abs(j) == 5] = 10000 assert (np.all(result[i == 0] == 1)) @@ -94,30 +95,30 @@ def test_hsobel_horizontal(): def test_hsobel_vertical(): """Horizontal Sobel on a vertical edge should be zero.""" i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.hsobel(image) - assert (np.all(result == 0)) + image = (j >= 0).astype(float) * np.sqrt(2) + result = filters.sobel_h(image) + assert_allclose(result, 0, atol=1e-10) def test_vsobel_zeros(): """Vertical sobel on an array of all zeros.""" - result = F.vsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.sobel_v(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert_allclose(result, 0) def test_vsobel_mask(): """Vertical Sobel on a masked array should be zero.""" np.random.seed(0) - result = F.vsobel(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.sobel_v(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert_allclose(result, 0) def test_vsobel_vertical(): """Vertical Sobel on an edge should be a vertical line.""" i, j = np.mgrid[-5:6, -5:6] image = (j >= 0).astype(float) - result = F.vsobel(image) + result = filters.sobel_v(image) # Fudge the eroded points j[np.abs(i) == 5] = 10000 assert (np.all(result[j == 0] == 1)) @@ -128,33 +129,32 @@ def test_vsobel_horizontal(): """vertical Sobel on a horizontal edge should be zero.""" i, j = np.mgrid[-5:6, -5:6] image = (i >= 0).astype(float) - result = F.vsobel(image) - eps = .000001 - assert (np.all(np.abs(result) < eps)) + result = filters.sobel_v(image) + assert_allclose(result, 0) def test_scharr_zeros(): """Scharr on an array of all zeros.""" - result = F.scharr(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.scharr(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result < 1e-16)) def test_scharr_mask(): """Scharr on a masked array should be zero.""" np.random.seed(0) - result = F.scharr(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.scharr(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert_allclose(result, 0) def test_scharr_horizontal(): """Scharr on an edge should be a horizontal line.""" i, j = np.mgrid[-5:6, -5:6] image = (i >= 0).astype(float) - result = F.scharr(image) + result = filters.scharr(image) * np.sqrt(2) # Fudge the eroded points i[np.abs(j) == 5] = 10000 - assert (np.all(result[i == 0] == 1)) + assert_allclose(result[i == 0], 1) assert (np.all(result[np.abs(i) > 1] == 0)) @@ -162,31 +162,31 @@ def test_scharr_vertical(): """Scharr on a vertical edge should be a vertical line.""" i, j = np.mgrid[-5:6, -5:6] image = (j >= 0).astype(float) - result = F.scharr(image) + result = filters.scharr(image) * np.sqrt(2) j[np.abs(i) == 5] = 10000 - assert (np.all(result[j == 0] == 1)) + assert_allclose(result[j == 0], 1) assert (np.all(result[np.abs(j) > 1] == 0)) def test_hscharr_zeros(): """Horizontal Scharr on an array of all zeros.""" - result = F.hscharr(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.scharr_h(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert_allclose(result, 0) def test_hscharr_mask(): """Horizontal Scharr on a masked array should be zero.""" np.random.seed(0) - result = F.hscharr(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.scharr_h(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert_allclose(result, 0) def test_hscharr_horizontal(): """Horizontal Scharr on an edge should be a horizontal line.""" i, j = np.mgrid[-5:6, -5:6] image = (i >= 0).astype(float) - result = F.hscharr(image) + result = filters.scharr_h(image) # Fudge the eroded points i[np.abs(j) == 5] = 10000 assert (np.all(result[i == 0] == 1)) @@ -197,29 +197,29 @@ def test_hscharr_vertical(): """Horizontal Scharr on a vertical edge should be zero.""" i, j = np.mgrid[-5:6, -5:6] image = (j >= 0).astype(float) - result = F.hscharr(image) - assert (np.all(result == 0)) + result = filters.scharr_h(image) + assert_allclose(result, 0) def test_vscharr_zeros(): """Vertical Scharr on an array of all zeros.""" - result = F.vscharr(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.scharr_v(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert_allclose(result, 0) def test_vscharr_mask(): """Vertical Scharr on a masked array should be zero.""" np.random.seed(0) - result = F.vscharr(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.scharr_v(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert_allclose(result, 0) def test_vscharr_vertical(): """Vertical Scharr on an edge should be a vertical line.""" i, j = np.mgrid[-5:6, -5:6] image = (j >= 0).astype(float) - result = F.vscharr(image) + result = filters.scharr_v(image) # Fudge the eroded points j[np.abs(i) == 5] = 10000 assert (np.all(result[j == 0] == 1)) @@ -230,154 +230,161 @@ def test_vscharr_horizontal(): """vertical Scharr on a horizontal edge should be zero.""" i, j = np.mgrid[-5:6, -5:6] image = (i >= 0).astype(float) - result = F.vscharr(image) - eps = .000001 - assert (np.all(np.abs(result) < eps)) + result = filters.scharr_v(image) + assert_allclose(result, 0) def test_prewitt_zeros(): """Prewitt on an array of all zeros.""" - result = F.prewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.prewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert_allclose(result, 0) def test_prewitt_mask(): """Prewitt on a masked array should be zero.""" np.random.seed(0) - result = F.prewitt(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - eps = .000001 - assert (np.all(np.abs(result) < eps)) + result = filters.prewitt(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert_allclose(np.abs(result), 0) def test_prewitt_horizontal(): """Prewitt on an edge should be a horizontal line.""" i, j = np.mgrid[-5:6, -5:6] image = (i >= 0).astype(float) - result = F.prewitt(image) + result = filters.prewitt(image) * np.sqrt(2) # Fudge the eroded points i[np.abs(j) == 5] = 10000 - eps = .000001 assert (np.all(result[i == 0] == 1)) - assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) + assert_allclose(result[np.abs(i) > 1], 0, atol=1e-10) def test_prewitt_vertical(): """Prewitt on a vertical edge should be a vertical line.""" i, j = np.mgrid[-5:6, -5:6] image = (j >= 0).astype(float) - result = F.prewitt(image) - eps = .000001 + result = filters.prewitt(image) * np.sqrt(2) j[np.abs(i) == 5] = 10000 - assert (np.all(result[j == 0] == 1)) - assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) + assert_allclose(result[j == 0], 1) + assert_allclose(result[np.abs(j) > 1], 0, atol=1e-10) def test_hprewitt_zeros(): """Horizontal prewitt on an array of all zeros.""" - result = F.hprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.prewitt_h(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert_allclose(result, 0) def test_hprewitt_mask(): """Horizontal prewitt on a masked array should be zero.""" np.random.seed(0) - result = F.hprewitt(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - eps = .000001 - assert (np.all(np.abs(result) < eps)) + result = filters.prewitt_h(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert_allclose(result, 0) def test_hprewitt_horizontal(): """Horizontal prewitt on an edge should be a horizontal line.""" i, j = np.mgrid[-5:6, -5:6] image = (i >= 0).astype(float) - result = F.hprewitt(image) + result = filters.prewitt_h(image) # Fudge the eroded points i[np.abs(j) == 5] = 10000 - eps = .000001 assert (np.all(result[i == 0] == 1)) - assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) + assert_allclose(result[np.abs(i) > 1], 0, atol=1e-10) def test_hprewitt_vertical(): """Horizontal prewitt on a vertical edge should be zero.""" i, j = np.mgrid[-5:6, -5:6] image = (j >= 0).astype(float) - result = F.hprewitt(image) - eps = .000001 - assert (np.all(np.abs(result) < eps)) + result = filters.prewitt_h(image) + assert_allclose(result, 0, atol=1e-10) def test_vprewitt_zeros(): """Vertical prewitt on an array of all zeros.""" - result = F.vprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.prewitt_v(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert_allclose(result, 0) def test_vprewitt_mask(): """Vertical prewitt on a masked array should be zero.""" np.random.seed(0) - result = F.vprewitt(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) + result = filters.prewitt_v(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert_allclose(result, 0) def test_vprewitt_vertical(): """Vertical prewitt on an edge should be a vertical line.""" i, j = np.mgrid[-5:6, -5:6] image = (j >= 0).astype(float) - result = F.vprewitt(image) + result = filters.prewitt_v(image) # Fudge the eroded points j[np.abs(i) == 5] = 10000 assert (np.all(result[j == 0] == 1)) - eps = .000001 - assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) + assert_allclose(result[np.abs(j) > 1], 0, atol=1e-10) def test_vprewitt_horizontal(): """Vertical prewitt on a horizontal edge should be zero.""" i, j = np.mgrid[-5:6, -5:6] image = (i >= 0).astype(float) - result = F.vprewitt(image) - eps = .000001 - assert (np.all(np.abs(result) < eps)) + result = filters.prewitt_v(image) + assert_allclose(result, 0) def test_horizontal_mask_line(): """Horizontal edge filters mask pixels surrounding input mask.""" - vgrad, _ = np.mgrid[:1:11j, :1:11j] # vertical gradient with spacing 0.1 - vgrad[5, :] = 1 # bad horizontal line + vgrad, _ = np.mgrid[:1:11j, :1:11j] # vertical gradient with spacing 0.1 + vgrad[5, :] = 1 # bad horizontal line mask = np.ones_like(vgrad) - mask[5, :] = 0 # mask bad line + mask[5, :] = 0 # mask bad line expected = np.zeros_like(vgrad) - expected[1:-1, 1:-1] = 0.2 # constant gradient for most of image, - expected[4:7, 1:-1] = 0 # but line and neighbors masked + expected[1:-1, 1:-1] = 0.2 # constant gradient for most of image, + expected[4:7, 1:-1] = 0 # but line and neighbors masked - for grad_func in (F.hprewitt, F.hsobel, F.hscharr): + for grad_func in (filters.prewitt_h, filters.sobel_h, filters.scharr_h): result = grad_func(vgrad, mask) yield assert_close, result, expected def test_vertical_mask_line(): """Vertical edge filters mask pixels surrounding input mask.""" - _, hgrad = np.mgrid[:1:11j, :1:11j] # horizontal gradient with spacing 0.1 - hgrad[:, 5] = 1 # bad vertical line + _, hgrad = np.mgrid[:1:11j, :1:11j] # horizontal gradient with spacing 0.1 + hgrad[:, 5] = 1 # bad vertical line mask = np.ones_like(hgrad) - mask[:, 5] = 0 # mask bad line + mask[:, 5] = 0 # mask bad line expected = np.zeros_like(hgrad) - expected[1:-1, 1:-1] = 0.2 # constant gradient for most of image, - expected[1:-1, 4:7] = 0 # but line and neighbors masked + expected[1:-1, 1:-1] = 0.2 # constant gradient for most of image, + expected[1:-1, 4:7] = 0 # but line and neighbors masked - for grad_func in (F.vprewitt, F.vsobel, F.vscharr): + for grad_func in (filters.prewitt_v, filters.sobel_v, filters.scharr_v): result = grad_func(hgrad, mask) yield assert_close, result, expected +def test_range(): + """Output of edge detection should be in [0, 1]""" + image = np.random.random((100, 100)) + for detector in (filters.sobel, filters.scharr, + filters.prewitt, filters.roberts): + out = detector(image) + assert_(out.min() >= 0, + "Minimum of `{0}` is smaller than zero".format( + detector.__name__) + ) + assert_(out.max() <= 1, + "Maximum of `{0}` is larger than 1".format( + detector.__name__) + ) + + if __name__ == "__main__": from numpy import testing testing.run_module_suite() diff --git a/skimage/filter/tests/test_gabor.py b/skimage/filters/tests/test_gabor.py similarity index 97% rename from skimage/filter/tests/test_gabor.py rename to skimage/filters/tests/test_gabor.py index 7035d82a..34b26e1f 100644 --- a/skimage/filter/tests/test_gabor.py +++ b/skimage/filters/tests/test_gabor.py @@ -2,7 +2,7 @@ import numpy as np from numpy.testing import (assert_equal, assert_almost_equal, assert_array_almost_equal) -from skimage.filter._gabor import gabor_kernel, gabor_filter, _sigma_prefactor +from skimage.filters._gabor import gabor_kernel, gabor_filter, _sigma_prefactor def test_gabor_kernel_size(): diff --git a/skimage/filter/tests/test_gaussian.py b/skimage/filters/tests/test_gaussian.py similarity index 86% rename from skimage/filter/tests/test_gaussian.py rename to skimage/filters/tests/test_gaussian.py index 9118bfae..c88ee1f8 100644 --- a/skimage/filter/tests/test_gaussian.py +++ b/skimage/filters/tests/test_gaussian.py @@ -1,5 +1,6 @@ import numpy as np -from skimage.filter._gaussian import gaussian_filter +from skimage.filters._gaussian import gaussian_filter +from skimage._shared._warnings import expected_warnings def test_null_sigma(): @@ -25,7 +26,8 @@ def test_multichannel(): assert np.allclose([a[..., i].mean() for i in range(3)], [gaussian_rgb_a[..., i].mean() for i in range(3)]) # Test multichannel = None - gaussian_rgb_a = gaussian_filter(a, sigma=1, mode='reflect') + with expected_warnings(['multichannel']): + gaussian_rgb_a = gaussian_filter(a, sigma=1, mode='reflect') # Check that the mean value is conserved in each channel # (color channels are not mixed together) assert np.allclose([a[..., i].mean() for i in range(3)], diff --git a/skimage/filter/tests/test_lpi_filter.py b/skimage/filters/tests/test_lpi_filter.py similarity index 73% rename from skimage/filter/tests/test_lpi_filter.py rename to skimage/filters/tests/test_lpi_filter.py index f6176691..ce952304 100644 --- a/skimage/filter/tests/test_lpi_filter.py +++ b/skimage/filters/tests/test_lpi_filter.py @@ -1,17 +1,13 @@ -import os.path - import numpy as np -from numpy.testing import * +from numpy.testing import (assert_raises, assert_, assert_equal, + run_module_suite) -from skimage import data_dir -from skimage.io import * -from skimage.filter import * +from skimage import data +from skimage.filters import LPIFilter2D, inverse, wiener class TestLPIFilter2D(object): - - img = imread(os.path.join(data_dir, 'camera.png'), - flatten=True)[:50, :50] + img = data.camera()[:50, :50] def filt_func(self, r, c): return np.exp(-np.hypot(r, c) / 1) @@ -36,14 +32,14 @@ class TestLPIFilter2D(object): assert_equal(g.shape, self.img.shape) g1 = inverse(F[::-1, ::-1], predefined_filter=self.f) - assert ((g - g1[::-1, ::-1]).sum() < 55) + assert_((g - g1[::-1, ::-1]).sum() < 55) # test cache g1 = inverse(F[::-1, ::-1], predefined_filter=self.f) - assert ((g - g1[::-1, ::-1]).sum() < 55) + assert_((g - g1[::-1, ::-1]).sum() < 55) g1 = inverse(F[::-1, ::-1], self.filt_func) - assert ((g - g1[::-1, ::-1]).sum() < 55) + assert_((g - g1[::-1, ::-1]).sum() < 55) def test_wiener(self): F = self.f(self.img) @@ -51,10 +47,10 @@ class TestLPIFilter2D(object): assert_equal(g.shape, self.img.shape) g1 = wiener(F[::-1, ::-1], predefined_filter=self.f) - assert ((g - g1[::-1, ::-1]).sum() < 1) + assert_((g - g1[::-1, ::-1]).sum() < 1) g1 = wiener(F[::-1, ::-1], self.filt_func) - assert ((g - g1[::-1, ::-1]).sum() < 1) + assert_((g - g1[::-1, ::-1]).sum() < 1) def test_non_callable(self): assert_raises(ValueError, LPIFilter2D, None) diff --git a/skimage/filters/tests/test_median.py b/skimage/filters/tests/test_median.py new file mode 100644 index 00000000..213c06ff --- /dev/null +++ b/skimage/filters/tests/test_median.py @@ -0,0 +1,2 @@ +def test_median_in_filters(): + from skimage.filters import median diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filters/tests/test_thresholding.py similarity index 53% rename from skimage/filter/tests/test_thresholding.py rename to skimage/filters/tests/test_thresholding.py index f0a68f7d..a47b6f20 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filters/tests/test_thresholding.py @@ -1,12 +1,13 @@ import numpy as np -from numpy.testing import assert_array_equal +from numpy.testing import assert_equal, assert_almost_equal import skimage from skimage import data -from skimage.filter.thresholding import (threshold_adaptive, - threshold_otsu, - threshold_yen, - threshold_isodata) +from skimage.filters.thresholding import (threshold_adaptive, + threshold_otsu, + threshold_li, + threshold_yen, + threshold_isodata) class TestSimpleImage(): @@ -28,6 +29,17 @@ class TestSimpleImage(): image = np.float64(self.image) assert 2 <= threshold_otsu(image) < 3 + def test_li(self): + assert int(threshold_li(self.image)) == 2 + + def test_li_negative_int(self): + image = self.image - 2 + assert int(threshold_li(image)) == 0 + + def test_li_float_image(self): + image = np.float64(self.image) + assert 2 <= threshold_li(image) < 3 + def test_yen(self): assert threshold_yen(self.image) == 2 @@ -59,13 +71,25 @@ class TestSimpleImage(): def test_isodata(self): assert threshold_isodata(self.image) == 2 + assert threshold_isodata(self.image, return_all=True) == [2] def test_isodata_blank_zero(self): image = np.zeros((5, 5), dtype=np.uint8) assert threshold_isodata(image) == 0 + assert threshold_isodata(image, return_all=True) == [0] def test_isodata_linspace(self): - assert -63.8 < threshold_isodata(np.linspace(-127, 0, 256)) < -63.6 + image = np.linspace(-127, 0, 256) + assert -63.8 < threshold_isodata(image) < -63.6 + assert_almost_equal(threshold_isodata(image, return_all=True), + [-63.74804688, -63.25195312]) + + def test_isodata_16bit(self): + np.random.seed(0) + imfloat = np.random.rand(256, 256) + assert 0.49 < threshold_isodata(imfloat, nbins=1024) < 0.51 + assert all(0.49 < threshold_isodata(imfloat, nbins=1024, + return_all=True)) def test_threshold_adaptive_generic(self): def func(arr): @@ -78,7 +102,7 @@ class TestSimpleImage(): [ True, True, False, False, False]] ) out = threshold_adaptive(self.image, 3, method='generic', param=func) - assert_array_equal(ref, out) + assert_equal(ref, out) def test_threshold_adaptive_gaussian(self): ref = np.array( @@ -89,7 +113,10 @@ class TestSimpleImage(): [ True, True, False, False, False]] ) out = threshold_adaptive(self.image, 3, method='gaussian') - assert_array_equal(ref, out) + assert_equal(ref, out) + + out = threshold_adaptive(self.image, 3, method='gaussian', param=1.0 / 3.0) + assert_equal(ref, out) def test_threshold_adaptive_mean(self): ref = np.array( @@ -100,7 +127,7 @@ class TestSimpleImage(): [ True, True, False, False, False]] ) out = threshold_adaptive(self.image, 3, method='mean') - assert_array_equal(ref, out) + assert_equal(ref, out) def test_threshold_adaptive_median(self): ref = np.array( @@ -111,7 +138,7 @@ class TestSimpleImage(): [False, True, False, False, False]] ) out = threshold_adaptive(self.image, 3, method='median') - assert_array_equal(ref, out) + assert_equal(ref, out) def test_otsu_camera_image(): @@ -130,9 +157,31 @@ def test_otsu_coins_image_as_float(): def test_otsu_lena_image(): - lena = skimage.img_as_ubyte(data.lena()) - assert 140 < threshold_otsu(lena) < 142 + img = skimage.img_as_ubyte(data.lena()) + assert 140 < threshold_otsu(img) < 142 +def test_otsu_astro_image(): + img = skimage.img_as_ubyte(data.astronaut()) + assert 109 < threshold_otsu(img) < 111 + +def test_li_camera_image(): + camera = skimage.img_as_ubyte(data.camera()) + assert 63 < threshold_li(camera) < 65 + + +def test_li_coins_image(): + coins = skimage.img_as_ubyte(data.coins()) + assert 95 < threshold_li(coins) < 97 + + +def test_li_coins_image_as_float(): + coins = skimage.img_as_float(data.coins()) + assert 0.37 < threshold_li(coins) < 0.38 + + +def test_li_astro_image(): + img = skimage.img_as_ubyte(data.astronaut()) + assert 66 < threshold_li(img) < 68 def test_yen_camera_image(): camera = skimage.img_as_ubyte(data.camera()) @@ -151,29 +200,67 @@ def test_yen_coins_image_as_float(): def test_isodata_camera_image(): camera = skimage.img_as_ubyte(data.camera()) - assert threshold_isodata(camera) == 88 + + threshold = threshold_isodata(camera) + assert np.floor((camera[camera <= threshold].mean() + + camera[camera > threshold].mean()) / 2.0) == threshold + assert threshold == 87 + + assert threshold_isodata(camera, return_all=True) == [87] def test_isodata_coins_image(): coins = skimage.img_as_ubyte(data.coins()) - assert threshold_isodata(coins) == 107 + + threshold = threshold_isodata(coins) + assert np.floor((coins[coins <= threshold].mean() + + coins[coins > threshold].mean()) / 2.0) == threshold + assert threshold == 107 + + assert threshold_isodata(coins, return_all=True) == [107] def test_isodata_moon_image(): moon = skimage.img_as_ubyte(data.moon()) - assert threshold_isodata(moon) == 87 + + threshold = threshold_isodata(moon) + assert np.floor((moon[moon <= threshold].mean() + + moon[moon > threshold].mean()) / 2.0) == threshold + assert threshold == 86 + + thresholds = threshold_isodata(moon, return_all=True) + for threshold in thresholds: + assert np.floor((moon[moon <= threshold].mean() + + moon[moon > threshold].mean()) / 2.0) == threshold + assert_equal(thresholds, [86, 87, 88, 122, 123, 124, 139, 140]) def test_isodata_moon_image_negative_int(): moon = skimage.img_as_ubyte(data.moon()).astype(np.int32) moon -= 100 - assert threshold_isodata(moon) == -13 + + threshold = threshold_isodata(moon) + assert np.floor((moon[moon <= threshold].mean() + + moon[moon > threshold].mean()) / 2.0) == threshold + assert threshold == -14 + + thresholds = threshold_isodata(moon, return_all=True) + for threshold in thresholds: + assert np.floor((moon[moon <= threshold].mean() + + moon[moon > threshold].mean()) / 2.0) == threshold + assert_equal(thresholds, [-14, -13, -12, 22, 23, 24, 39, 40]) 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 + + assert -14 < threshold_isodata(moon) < -13 + + thresholds = threshold_isodata(moon, return_all=True) + assert_almost_equal(thresholds, + [-13.83789062, -12.84179688, -11.84570312, 22.02148438, + 23.01757812, 24.01367188, 38.95507812, 39.95117188]) if __name__ == '__main__': diff --git a/skimage/filter/thresholding.py b/skimage/filters/thresholding.py similarity index 58% rename from skimage/filter/thresholding.py rename to skimage/filters/thresholding.py index 6ccf8f6b..473db40a 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filters/thresholding.py @@ -1,11 +1,13 @@ __all__ = ['threshold_adaptive', 'threshold_otsu', 'threshold_yen', - 'threshold_isodata'] + 'threshold_isodata', + 'threshold_li', ] import numpy as np import scipy.ndimage -from skimage.exposure import histogram +from ..exposure import histogram +from .._shared.utils import assert_nD def threshold_adaptive(image, block_size, method='gaussian', offset=0, @@ -65,6 +67,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, >>> func = lambda arr: arr.mean() >>> binary_image2 = threshold_adaptive(image, 15, 'generic', param=func) """ + assert_nD(image, 2) thresh_image = np.zeros(image.shape, 'double') if method == 'generic': scipy.ndimage.generic_filter(image, param, block_size, @@ -119,7 +122,7 @@ def threshold_otsu(image, nbins=256): >>> thresh = threshold_otsu(image) >>> binary = image <= thresh """ - hist, bin_centers = histogram(image, nbins) + hist, bin_centers = histogram(image.ravel(), nbins) hist = hist.astype(float) # class probabilities for all possible thresholds @@ -174,7 +177,7 @@ def threshold_yen(image, nbins=256): >>> thresh = threshold_yen(image) >>> binary = image <= thresh """ - hist, bin_centers = histogram(image, nbins) + hist, bin_centers = histogram(image.ravel(), 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: @@ -193,10 +196,21 @@ def threshold_yen(image, nbins=256): return bin_centers[crit.argmax()] -def threshold_isodata(image, nbins=256): - """Return threshold value based on ISODATA method. +def threshold_isodata(image, nbins=256, return_all=False): + """Return threshold value(s) based on ISODATA method. - Histogram-based threshold, known as Ridler-Calvard method or intermeans. + Histogram-based threshold, known as Ridler-Calvard method or inter-means. + Threshold values returned satisfy the following equality: + + `threshold = (image[image <= threshold].mean() +` + `image[image > threshold].mean()) / 2.0` + + That is, returned thresholds are intensities that separate the image into + two groups of pixels, where the threshold intensity is midway between the + mean intensities of these groups. + + For integer images, the above equality holds to within one; for floating- + point images, the equality holds to within the histogram bin-width. Parameters ---------- @@ -205,12 +219,14 @@ def threshold_isodata(image, nbins=256): nbins : int, optional Number of bins used to calculate histogram. This value is ignored for integer arrays. + return_all: bool, optional + If False (default), return only the lowest threshold that satisfies + the above equality. If True, return all valid thresholds. 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. + threshold : float or int or array + Threshold value(s). References ---------- @@ -232,27 +248,124 @@ def threshold_isodata(image, nbins=256): >>> 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) + hist, bin_centers = histogram(image.ravel(), nbins) - 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 + # image only contains one unique value + if len(bin_centers) == 1: + if return_all: + return bin_centers + else: + return bin_centers[0] + + hist = hist.astype(np.float32) + + # csuml and csumh contain the count of pixels in that bin or lower, and + # in all bins strictly higher than that bin, respectively + csuml = np.cumsum(hist) + csumh = np.cumsum(hist[::-1])[::-1] - hist + + # intensity_sum contains the total pixel intensity from each bin + intensity_sum = hist * bin_centers + + # l and h contain average value of all pixels in that bin or lower, and + # in all bins strictly higher than that bin, respectively. + # Note that since exp.histogram does not include empty bins at the low or + # high end of the range, csuml and csumh are strictly > 0, except in the + # last bin of csumh, which is zero by construction. + # So no worries about division by zero in the following lines, except + # for the last bin, but we can ignore that because no valid threshold + # can be in the top bin. So we just patch up csumh[-1] to not cause 0/0 + # errors. + csumh[-1] = 1 + l = np.cumsum(intensity_sum) / csuml + h = (np.cumsum(intensity_sum[::-1])[::-1] - intensity_sum) / csumh + + # isodata finds threshold values that meet the criterion t = (l + m)/2 + # where l is the mean of all pixels <= t and h is the mean of all pixels + # > t, as calculated above. So we are looking for places where + # (l + m) / 2 equals the intensity value for which those l and m figures + # were calculated -- which is, of course, the histogram bin centers. + # We only require this equality to be within the precision of the bin + # width, of course. + all_mean = (l + h) / 2.0 + bin_width = bin_centers[1] - bin_centers[0] + + # Look only at thresholds that are below the actual all_mean value, + # for consistency with the threshold being included in the lower pixel + # group. Otherwise can get thresholds that are not actually fixed-points + # of the isodata algorithm. For float images, this matters less, since + # there really can't be any guarantees anymore anyway. + distances = all_mean - bin_centers + thresholds = bin_centers[(distances >= 0) & (distances < bin_width)] + + if return_all: + return thresholds + else: + return thresholds[0] + + +def threshold_li(image): + """Return threshold value based on adaptation of Li's Minimum Cross Entropy method. + + Parameters + ---------- + image : array + Input image. + + Returns + ------- + threshold : float + Upper threshold value. All pixels intensities more than + this value are assumed to be foreground. + + References + ---------- + .. [1] Li C.H. and Lee C.K. (1993) "Minimum Cross Entropy Thresholding" + Pattern Recognition, 26(4): 617-625 + .. [2] Li C.H. and Tam P.K.S. (1998) "An Iterative Algorithm for Minimum + Cross Entropy Thresholding" Pattern Recognition Letters, 18(8): 771-776 + .. [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://citeseer.ist.psu.edu/sezgin04survey.html + .. [4] ImageJ AutoThresholder code, http://fiji.sc/wiki/index.php/Auto_Threshold + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera() + >>> thresh = threshold_li(image) + >>> binary = image > thresh + """ + # Requires positive image (because of log(mean)) + offset = image.min() + # Can not use fixed tolerance for float image + imrange = image.max() - offset + image -= offset + + tolerance = 0.5 * imrange / 256.0 + # Calculate the mean gray-level + mean = image.mean() + + # Initial estimate + new_thresh = mean + old_thresh = new_thresh + 2 * tolerance + + # Stop the iterations when the difference between the + # new and old threshold values is less than the tolerance + while abs(new_thresh - old_thresh) > tolerance: + old_thresh = new_thresh + threshold = old_thresh + tolerance # range + # Calculate the means of background and object pixels + mean_back = image[image <= threshold].mean() + mean_obj = image[image > threshold].mean() + + temp = (mean_back - mean_obj) / (np.log(mean_back) - np.log(mean_obj)) + + if temp < 0: + new_thresh = temp - tolerance + else: + new_thresh = temp + tolerance + + return threshold + offset diff --git a/skimage/future/__init__.py b/skimage/future/__init__.py new file mode 100644 index 00000000..0292a6ef --- /dev/null +++ b/skimage/future/__init__.py @@ -0,0 +1,10 @@ +"""Functionality with an experimental API. Although you can count on the +functions in this package being around in the future, the API may change with +any version update **and will not follow the skimage two-version deprecation +path**. Therefore, use the functions herein with care, and do not use them in +production code that will depend on updated skimage versions. +""" + +from . import graph + +__all__ = ['graph'] diff --git a/skimage/future/graph/__init__.py b/skimage/future/graph/__init__.py new file mode 100644 index 00000000..6b7e0aaa --- /dev/null +++ b/skimage/future/graph/__init__.py @@ -0,0 +1,12 @@ +from .graph_cut import cut_threshold, cut_normalized +from .rag import rag_mean_color, RAG, draw_rag +from .graph_merge import merge_hierarchical +ncut = cut_normalized + +__all__ = ['rag_mean_color', + 'cut_threshold', + 'cut_normalized', + 'ncut', + 'draw_rag', + 'merge_hierarchical', + 'RAG'] diff --git a/skimage/future/graph/_ncut.py b/skimage/future/graph/_ncut.py new file mode 100644 index 00000000..05be8cd1 --- /dev/null +++ b/skimage/future/graph/_ncut.py @@ -0,0 +1,85 @@ +try: + import networkx as nx +except ImportError: + import warnings + warnings.warn('RAGs require networkx') +import numpy as np +from scipy import sparse +from . import _ncut_cy + + +def DW_matrices(graph): + """Returns the diagonal and weight matrices of a graph. + + Parameters + ---------- + graph : RAG + A Region Adjacency Graph. + + Returns + ------- + D : csc_matrix + The diagonal matrix of the graph. ``D[i, i]`` is the sum of weights of + all edges incident on `i`. All other entries are `0`. + W : csc_matrix + The weight matrix of the graph. ``W[i, j]`` is the weight of the edge + joining `i` to `j`. + """ + # sparse.eighsh is most efficient with CSC-formatted input + W = nx.to_scipy_sparse_matrix(graph, format='csc') + entries = W.sum(axis=0) + D = sparse.dia_matrix((entries, 0), shape=W.shape).tocsc() + return D, W + + +def ncut_cost(cut, D, W): + """Returns the N-cut cost of a bi-partition of a graph. + + Parameters + ---------- + cut : ndarray + The mask for the nodes in the graph. Nodes corresponding to a `True` + value are in one set. + D : csc_matrix + The diagonal matrix of the graph. + W : csc_matrix + The weight matrix of the graph. + + Returns + ------- + cost : float + The cost of performing the N-cut. + + References + ---------- + .. [1] Normalized Cuts and Image Segmentation, Jianbo Shi and + Jitendra Malik, IEEE Transactions on Pattern Analysis and Machine + Intelligence, Page 889, Equation 2. + """ + cut = np.array(cut) + cut_cost = _ncut_cy.cut_cost(cut, W) + + # D has elements only along the diagonal, one per node, so we can directly + # index the data attribute with cut. + assoc_a = D.data[cut].sum() + assoc_b = D.data[~cut].sum() + + return (cut_cost / assoc_a) + (cut_cost / assoc_b) + + +def normalize(a): + """Normalize values in an array between `0` and `1`. + + Parameters + ---------- + a : ndarray + The array to be normalized. + + Returns + ------- + out : ndarray + The normalized array. + """ + mi = a.min() + mx = a.max() + return (a - mi) / (mx - mi) diff --git a/skimage/future/graph/_ncut_cy.pyx b/skimage/future/graph/_ncut_cy.pyx new file mode 100644 index 00000000..14f3a94a --- /dev/null +++ b/skimage/future/graph/_ncut_cy.pyx @@ -0,0 +1,78 @@ +# cython: cdivision=True +# cython: boundscheck=False +# cython: nonecheck=False +# cython: wraparound=False +cimport numpy as cnp +import numpy as np + + +def argmin2(cnp.double_t[:] array): + """Return the index of the 2nd smallest value in an array. + + Parameters + ---------- + array : array + The array to process. + + Returns + ------- + min_idx2 : int + The index of the second smallest value. + """ + cdef cnp.float64_t min1 = np.inf + cdef cnp.float64_t min2 = np.inf + cdef Py_ssize_t min_idx1 = 0 + cdef Py_ssize_t min_idx2 = 0 + cdef Py_ssize_t i = 0 + cdef Py_ssize_t n = array.shape[0] + + for i in range(n): + x = array[i] + if x < min1: + min2 = min1 + min_idx2 = min_idx1 + min1 = x + min_idx1 = i + elif x > min1 and x < min2: + min2 = x + min_idx2 = i + i += 1 + + return min_idx2 + + +def cut_cost(cut, W): + """Return the total weight of crossing edges in a bi-partition. + + Parameters + ---------- + cut : array + A array of booleans. Elements set to `True` belong to one + set. + W : array + The weight matrix of the graph. + + Returns + ------- + cost : float + The total weight of crossing edges. + """ + cdef cnp.ndarray[cnp.uint8_t, cast = True] cut_mask = np.array(cut) + cdef Py_ssize_t num_rows, num_cols + cdef cnp.int32_t row, col + cdef cnp.int32_t[:] indices = W.indices + cdef cnp.int32_t[:] indptr = W.indptr + cdef cnp.double_t[:] data = W.data.astype(np.double) + cdef cnp.int32_t row_index + cdef cnp.double_t cost = 0 + + num_rows = W.shape[0] + num_cols = W.shape[1] + + for col in range(num_cols): + for row_index in range(indptr[col], indptr[col + 1]): + row = indices[row_index] + if cut_mask[row] != cut_mask[col]: + cost += data[row_index] + + return cost * 0.5 diff --git a/skimage/future/graph/graph_cut.py b/skimage/future/graph/graph_cut.py new file mode 100644 index 00000000..d727eeac --- /dev/null +++ b/skimage/future/graph/graph_cut.py @@ -0,0 +1,285 @@ +try: + import networkx as nx +except ImportError: + import warnings + warnings.warn('RAGs require networkx') +import numpy as np +from . import _ncut +from . import _ncut_cy +from scipy.sparse import linalg + + +def cut_threshold(labels, rag, thresh, in_place=True): + """Combine regions separated by weight less than threshold. + + Given an image's labels and its RAG, output new labels by + combining regions whose nodes are separated by a weight less + than the given threshold. + + Parameters + ---------- + labels : ndarray + The array of labels. + rag : RAG + The region adjacency graph. + thresh : float + The threshold. Regions connected by edges with smaller weights are + combined. + in_place : bool + If set, modifies `rag` in place. The function will remove the edges + with weights less that `thresh`. If set to `False` the function + makes a copy of `rag` before proceeding. + + Returns + ------- + out : ndarray + The new labelled array. + + Examples + -------- + >>> from skimage import data, segmentation + >>> from skimage.future import graph + >>> img = data.astronaut() + >>> labels = segmentation.slic(img) + >>> rag = graph.rag_mean_color(img, labels) + >>> new_labels = graph.cut_threshold(labels, rag, 10) + + References + ---------- + .. [1] Alain Tremeau and Philippe Colantoni + "Regions Adjacency Graph Applied To Color Image Segmentation" + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.11.5274 + + """ + if not in_place: + rag = rag.copy() + + # Because deleting edges while iterating through them produces an error. + to_remove = [(x, y) for x, y, d in rag.edges_iter(data=True) + if d['weight'] >= thresh] + rag.remove_edges_from(to_remove) + + comps = nx.connected_components(rag) + + # We construct an array which can map old labels to the new ones. + # All the labels within a connected component are assigned to a single + # label in the output. + map_array = np.arange(labels.max() + 1, dtype=labels.dtype) + for i, nodes in enumerate(comps): + for node in nodes: + for label in rag.node[node]['labels']: + map_array[label] = i + + return map_array[labels] + + +def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True, + max_edge=1.0): + """Perform Normalized Graph cut on the Region Adjacency Graph. + + Given an image's labels and its similarity RAG, recursively perform + a 2-way normalized cut on it. All nodes belonging to a subgraph + that cannot be cut further are assigned a unique label in the + output. + + Parameters + ---------- + labels : ndarray + The array of labels. + rag : RAG + The region adjacency graph. + thresh : float + The threshold. A subgraph won't be further subdivided if the + value of the N-cut exceeds `thresh`. + num_cuts : int + The number or N-cuts to perform before determining the optimal one. + in_place : bool + If set, modifies `rag` in place. For each node `n` the function will + set a new attribute ``rag.node[n]['ncut label']``. + max_edge : float, optional + The maximum possible value of an edge in the RAG. This corresponds to + an edge between identical regions. This is used to put self + edges in the RAG. + + Returns + ------- + out : ndarray + The new labeled array. + + Examples + -------- + >>> from skimage import data, segmentation + >>> from skimage.future import graph + >>> img = data.astronaut() + >>> labels = segmentation.slic(img, compactness=30, n_segments=400) + >>> rag = graph.rag_mean_color(img, labels, mode='similarity') + >>> new_labels = graph.cut_normalized(labels, rag) + + References + ---------- + .. [1] Shi, J.; Malik, J., "Normalized cuts and image segmentation", + Pattern Analysis and Machine Intelligence, + IEEE Transactions on, vol. 22, no. 8, pp. 888-905, August 2000. + + """ + if not in_place: + rag = rag.copy() + + for node in rag.nodes_iter(): + rag.add_edge(node, node, weight=max_edge) + + _ncut_relabel(rag, thresh, num_cuts) + + map_array = np.zeros(labels.max() + 1, dtype=labels.dtype) + # Mapping from old labels to new + for n, d in rag.nodes_iter(data=True): + map_array[d['labels']] = d['ncut label'] + + return map_array[labels] + + +def partition_by_cut(cut, rag): + """Compute resulting subgraphs from given bi-parition. + + Parameters + ---------- + cut : array + A array of booleans. Elements set to `True` belong to one + set. + rag : RAG + The Region Adjacency Graph. + + Returns + ------- + sub1, sub2 : RAG + The two resulting subgraphs from the bi-partition. + """ + # `cut` is derived from `D` and `W` matrices, which also follow the + # ordering returned by `rag.nodes()` because we use + # nx.to_scipy_sparse_matrix. + + # Example + # rag.nodes() = [3, 7, 9, 13] + # cut = [True, False, True, False] + # nodes1 = [3, 9] + # nodes2 = [7, 10] + + nodes1 = [n for i, n in enumerate(rag.nodes()) if cut[i]] + nodes2 = [n for i, n in enumerate(rag.nodes()) if not cut[i]] + + sub1 = rag.subgraph(nodes1) + sub2 = rag.subgraph(nodes2) + + return sub1, sub2 + + +def get_min_ncut(ev, d, w, num_cuts): + """Threshold an eigenvector evenly, to determine minimum ncut. + + Parameters + ---------- + ev : array + The eigenvector to threshold. + d : ndarray + The diagonal matrix of the graph. + w : ndarray + The weight matrix of the graph. + num_cuts : int + The number of evenly spaced thresholds to check for. + + Returns + ------- + mask : array + The array of booleans which denotes the bi-partition. + mcut : float + The value of the minimum ncut. + """ + mcut = np.inf + + # Refer Shi & Malik 2001, Section 3.1.3, Page 892 + # Perform evenly spaced n-cuts and determine the optimal one. + for t in np.linspace(0, 1, num_cuts, endpoint=False): + mask = ev > t + cost = _ncut.ncut_cost(mask, d, w) + if cost < mcut: + min_mask = mask + mcut = cost + + return min_mask, mcut + + +def _label_all(rag, attr_name): + """Assign a unique integer to the given attribute in the RAG. + + This function assumes that all labels in `rag` are unique. It + picks up a random label from them and assigns it to the `attr_name` + attribute of all the nodes. + + rag : RAG + The Region Adjacency Graph. + attr_name : string + The attribute to which a unique integer is assigned. + """ + node = rag.nodes()[0] + new_label = rag.node[node]['labels'][0] + for n, d in rag.nodes_iter(data=True): + d[attr_name] = new_label + + +def _ncut_relabel(rag, thresh, num_cuts): + """Perform Normalized Graph cut on the Region Adjacency Graph. + + Recursively partition the graph into 2, until further subdivision + yields a cut greater than `thresh` or such a cut cannot be computed. + For such a subgraph, indices to labels of all its nodes map to a single + unique value. + + Parameters + ---------- + labels : ndarray + The array of labels. + rag : RAG + The region adjacency graph. + thresh : float + The threshold. A subgraph won't be further subdivided if the + value of the N-cut exceeds `thresh`. + num_cuts : int + The number or N-cuts to perform before determining the optimal one. + map_array : array + The array which maps old labels to new ones. This is modified inside + the function. + """ + d, w = _ncut.DW_matrices(rag) + m = w.shape[0] + + if m > 2: + d2 = d.copy() + # Since d is diagonal, we can directly operate on its data + # the inverse of the square root + d2.data = np.reciprocal(np.sqrt(d2.data, out=d2.data), out=d2.data) + + # Refer Shi & Malik 2001, Equation 7, Page 891 + vals, vectors = linalg.eigsh(d2 * (d - w) * d2, which='SM', + k=min(100, m - 2)) + + # Pick second smallest eigenvector. + # Refer Shi & Malik 2001, Section 3.2.3, Page 893 + vals, vectors = np.real(vals), np.real(vectors) + index2 = _ncut_cy.argmin2(vals) + ev = _ncut.normalize(vectors[:, index2]) + + cut_mask, mcut = get_min_ncut(ev, d, w, num_cuts) + if (mcut < thresh): + # Sub divide and perform N-cut again + # Refer Shi & Malik 2001, Section 3.2.5, Page 893 + sub1, sub2 = partition_by_cut(cut_mask, rag) + + _ncut_relabel(sub1, thresh, num_cuts) + _ncut_relabel(sub2, thresh, num_cuts) + return + + # The N-cut wasn't small enough, or could not be computed. + # The remaining graph is a region. + # Assign `ncut label` by picking any label from the existing nodes, since + # `labels` are unique, `new_label` is also unique. + _label_all(rag, 'ncut label') diff --git a/skimage/future/graph/graph_merge.py b/skimage/future/graph/graph_merge.py new file mode 100644 index 00000000..308f7f37 --- /dev/null +++ b/skimage/future/graph/graph_merge.py @@ -0,0 +1,137 @@ +import numpy as np +import heapq + + +def _revalidate_node_edges(rag, node, heap_list): + """Handles validation and invalidation of edges incident to a node. + + This function invalidates all existing edges incident on `node` and inserts + new items in `heap_list` updated with the valid weights. + + rag : RAG + The Region Adjacency Graph. + node : int + The id of the node whose incident edges are to be validated/invalidated + . + heap_list : list + The list containing the existing heap of edges. + """ + # networkx updates data dictionary if edge exists + # this would mean we have to reposition these edges in + # heap if their weight is updated. + # instead we invalidate them + + for nbr in rag.neighbors(node): + data = rag[node][nbr] + try: + # invalidate edges incident on `dst`, they have new weights + data['heap item'][3] = False + _invalidate_edge(rag, node, nbr) + except KeyError: + # will handle the case where the edge did not exist in the existing + # graph + pass + + wt = data['weight'] + heap_item = [wt, node, nbr, True] + data['heap item'] = heap_item + heapq.heappush(heap_list, heap_item) + + +def _rename_node(graph, node_id, copy_id): + """ Rename `node_id` in `graph` to `copy_id`. """ + + graph._add_node_silent(copy_id) + graph.node[copy_id] = graph.node[node_id] + + for nbr in graph.neighbors(node_id): + wt = graph[node_id][nbr]['weight'] + graph.add_edge(nbr, copy_id, {'weight': wt}) + + graph.remove_node(node_id) + + +def _invalidate_edge(graph, n1, n2): + """ Invalidates the edge (n1, n2) in the heap. """ + graph[n1][n2]['heap item'][3] = False + + +def merge_hierarchical(labels, rag, thresh, rag_copy, in_place_merge, + merge_func, weight_func): + """Perform hierarchical merging of a RAG. + + Greedily merges the most similar pair of nodes until no edges lower than + `thresh` remain. + + Parameters + ---------- + labels : ndarray + The array of labels. + rag : RAG + The Region Adjacency Graph. + thresh : float + Regions connected by an edge with weight smaller than `thresh` are + merged. + rag_copy : bool + If set, the RAG copied before modifying. + in_place_merge : bool + If set, the nodes are merged in place. Otherwise, a new node is + created for each merge.. + merge_func : callable + This function is called before merging two nodes. For the RAG `graph` + while merging `src` and `dst`, it is called as follows + ``merge_func(graph, src, dst)``. + weight_func : callable + The function to compute the new weights of the nodes adjacent to the + merged node. This is directly supplied as the argument `weight_func` + to `merge_nodes`. + + Returns + ------- + out : ndarray + The new labeled array. + + """ + if rag_copy: + rag = rag.copy() + + edge_heap = [] + for n1, n2, data in rag.edges_iter(data=True): + # Push a valid edge in the heap + wt = data['weight'] + heap_item = [wt, n1, n2, True] + heapq.heappush(edge_heap, heap_item) + + # Reference to the heap item in the graph + data['heap item'] = heap_item + + while len(edge_heap) > 0 and edge_heap[0][0] < thresh: + _, n1, n2, valid = heapq.heappop(edge_heap) + + # Ensure popped edge is valid, if not, the edge is discarded + if valid: + # Invalidate all neigbors of `src` before its deleted + + for nbr in rag.neighbors(n1): + _invalidate_edge(rag, n1, nbr) + + for nbr in rag.neighbors(n2): + _invalidate_edge(rag, n2, nbr) + + if not in_place_merge: + next_id = rag.next_id() + _rename_node(rag, n2, next_id) + src, dst = n1, next_id + else: + src, dst = n1, n2 + + merge_func(rag, src, dst) + new_id = rag.merge_nodes(src, dst, weight_func) + _revalidate_node_edges(rag, new_id, edge_heap) + + label_map = np.arange(labels.max() + 1) + for ix, (n, d) in enumerate(rag.nodes_iter(data=True)): + for label in d['labels']: + label_map[label] = ix + + return label_map[labels] diff --git a/skimage/future/graph/rag.py b/skimage/future/graph/rag.py new file mode 100644 index 00000000..11441800 --- /dev/null +++ b/skimage/future/graph/rag.py @@ -0,0 +1,430 @@ +try: + import networkx as nx +except ImportError: + msg = "Graph functions require networkx, which is not installed" + + class nx: + class Graph: + def __init__(self, *args, **kwargs): + raise ImportError(msg) + import warnings + warnings.warn(msg) + +import numpy as np +from scipy.ndimage import filters +from scipy import ndimage as nd +import math +from ... import draw, measure, segmentation, util, color +try: + from matplotlib import colors + from matplotlib import cm +except ImportError: + pass + + +def min_weight(graph, src, dst, n): + """Callback to handle merging nodes by choosing minimum weight. + + Returns either the weight between (`src`, `n`) or (`dst`, `n`) + in `graph` or the minimum of the two when both exist. + + Parameters + ---------- + graph : RAG + The graph under consideration. + src, dst : int + The verices in `graph` to be merged. + n : int + A neighbor of `src` or `dst` or both. + + Returns + ------- + weight : float + The weight between (`src`, `n`) or (`dst`, `n`) in `graph` or the + minimum of the two when both exist. + + """ + + # cover the cases where n only has edge to either `src` or `dst` + default = {'weight': np.inf} + w1 = graph[n].get(src, default)['weight'] + w2 = graph[n].get(dst, default)['weight'] + return min(w1, w2) + + +class RAG(nx.Graph): + + """ + The Region Adjacency Graph (RAG) of an image, subclasses + `networx.Graph `_ + """ + + def __init__(self, data=None, **attr): + + super(RAG, self).__init__(data, **attr) + try: + self.max_id = max(self.nodes_iter()) + except ValueError: + # Empty sequence + self.max_id = 0 + + def merge_nodes(self, src, dst, weight_func=min_weight, in_place=True, + extra_arguments=[], extra_keywords={}): + """Merge node `src` and `dst`. + + The new combined node is adjacent to all the neighbors of `src` + and `dst`. `weight_func` is called to decide the weight of edges + incident on the new node. + + Parameters + ---------- + src, dst : int + Nodes to be merged. + weight_func : callable, optional + Function to decide edge weight of edges incident on the new node. + For each neighbor `n` for `src and `dst`, `weight_func` will be + called as follows: `weight_func(src, dst, n, *extra_arguments, + **extra_keywords)`. `src`, `dst` and `n` are IDs of vertices in the + RAG object which is in turn a subclass of + `networkx.Graph`. + in_place : bool, optional + If set to `True`, the merged node has the id `dst`, else merged + node has a new id which is returned. + extra_arguments : sequence, optional + The sequence of extra positional arguments passed to + `weight_func`. + extra_keywords : dictionary, optional + The dict of keyword arguments passed to the `weight_func`. + + Returns + ------- + id : int + The id of the new node. + + Notes + ----- + If `in_place` is `False` the resulting node has a new id, rather than + `dst`. + """ + src_nbrs = set(self.neighbors(src)) + dst_nbrs = set(self.neighbors(dst)) + neighbors = (src_nbrs | dst_nbrs) - set([src, dst]) + + if in_place: + new = dst + else: + new = self.next_id() + self.add_node(new) + + for neighbor in neighbors: + w = weight_func(self, src, new, neighbor, *extra_arguments, + **extra_keywords) + self.add_edge(neighbor, new, weight=w) + + self.node[new]['labels'] = (self.node[src]['labels'] + + self.node[dst]['labels']) + self.remove_node(src) + + if not in_place: + self.remove_node(dst) + + return new + + def add_node(self, n, attr_dict=None, **attr): + """Add node `n` while updating the maximum node id. + + .. seealso:: :func:`networkx.Graph.add_node`.""" + super(RAG, self).add_node(n, attr_dict, **attr) + self.max_id = max(n, self.max_id) + + def add_edge(self, u, v, attr_dict=None, **attr): + """Add an edge between `u` and `v` while updating max node id. + + .. seealso:: :func:`networkx.Graph.add_edge`.""" + super(RAG, self).add_edge(u, v, attr_dict, **attr) + self.max_id = max(u, v, self.max_id) + + def copy(self): + """Copy the graph with its max node id. + + .. seealso:: :func:`networkx.Graph.copy`.""" + g = super(RAG, self).copy() + g.max_id = self.max_id + return g + + def next_id(self): + """Returns the `id` for the new node to be inserted. + + The current implementation returns one more than the maximum `id`. + + Returns + ------- + id : int + The `id` of the new node to be inserted. + """ + return self.max_id + 1 + + def _add_node_silent(self, n): + """Add node `n` without updating the maximum node id. + + This is a convenience method used internally. + + .. seealso:: :func:`networkx.Graph.add_node`.""" + super(RAG, self).add_node(n) + + +def _add_edge_filter(values, graph): + """Create edge in `g` between the first element of `values` and the rest. + + Add an edge between the first element in `values` and + all other elements of `values` in the graph `g`. `values[0]` + is expected to be the central value of the footprint used. + + Parameters + ---------- + values : array + The array to process. + graph : RAG + The graph to add edges in. + + Returns + ------- + 0 : int + Always returns 0. The return value is required so that `generic_filter` + can put it in the output array. + + """ + values = values.astype(int) + current = values[0] + for value in values[1:]: + if value != current: + graph.add_edge(current, value) + + return 0 + + +def rag_mean_color(image, labels, connectivity=2, mode='distance', + sigma=255.0): + """Compute the Region Adjacency Graph using mean colors. + + Given an image and its initial segmentation, this method constructs the + corresponding Region Adjacency Graph (RAG). Each node in the RAG + represents a set of pixels within `image` with the same label in `labels`. + The weight between two adjacent regions represents how similar or + dissimilar two regions are depending on the `mode` parameter. + + Parameters + ---------- + image : ndarray, shape(M, N, [..., P,] 3) + Input image. + labels : ndarray, shape(M, N, [..., P,]) + The labelled image. This should have one dimension less than + `image`. If `image` has dimensions `(M, N, 3)` `labels` should have + dimensions `(M, N)`. + connectivity : int, optional + Pixels with a squared distance less than `connectivity` from each other + are considered adjacent. It can range from 1 to `labels.ndim`. Its + behavior is the same as `connectivity` parameter in + `scipy.ndimage.filters.generate_binary_structure`. + mode : {'distance', 'similarity'}, optional + The strategy to assign edge weights. + + 'distance' : The weight between two adjacent regions is the + :math:`|c_1 - c_2|`, where :math:`c_1` and :math:`c_2` are the mean + colors of the two regions. It represents the Euclidean distance in + their average color. + + 'similarity' : The weight between two adjacent is + :math:`e^{-d^2/sigma}` where :math:`d=|c_1 - c_2|`, where + :math:`c_1` and :math:`c_2` are the mean colors of the two regions. + It represents how similar two regions are. + sigma : float, optional + Used for computation when `mode` is "similarity". It governs how + close to each other two colors should be, for their corresponding edge + weight to be significant. A very large value of `sigma` could make + any two colors behave as though they were similar. + + Returns + ------- + out : RAG + The region adjacency graph. + + Examples + -------- + >>> from skimage import data, segmentation + >>> from skimage.future import graph + >>> img = data.astronaut() + >>> labels = segmentation.slic(img) + >>> rag = graph.rag_mean_color(img, labels) + + References + ---------- + .. [1] Alain Tremeau and Philippe Colantoni + "Regions Adjacency Graph Applied To Color Image Segmentation" + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.11.5274 + + """ + graph = RAG() + + # The footprint is constructed in such a way that the first + # element in the array being passed to _add_edge_filter is + # the central value. + fp = nd.generate_binary_structure(labels.ndim, connectivity) + for d in range(fp.ndim): + fp = fp.swapaxes(0, d) + fp[0, ...] = 0 + fp = fp.swapaxes(0, d) + + # For example + # if labels.ndim = 2 and connectivity = 1 + # fp = [[0,0,0], + # [0,1,1], + # [0,1,0]] + # + # if labels.ndim = 2 and connectivity = 2 + # fp = [[0,0,0], + # [0,1,1], + # [0,1,1]] + + filters.generic_filter( + labels, + function=_add_edge_filter, + footprint=fp, + mode='nearest', + output=np.zeros(labels.shape, dtype=np.uint8), + extra_arguments=(graph,)) + + for n in graph: + graph.node[n].update({'labels': [n], + 'pixel count': 0, + 'total color': np.array([0, 0, 0], + dtype=np.double)}) + + for index in np.ndindex(labels.shape): + current = labels[index] + graph.node[current]['pixel count'] += 1 + graph.node[current]['total color'] += image[index] + + for n in graph: + graph.node[n]['mean color'] = (graph.node[n]['total color'] / + graph.node[n]['pixel count']) + + for x, y, d in graph.edges_iter(data=True): + diff = graph.node[x]['mean color'] - graph.node[y]['mean color'] + diff = np.linalg.norm(diff) + if mode == 'similarity': + d['weight'] = math.e ** (-(diff ** 2) / sigma) + elif mode == 'distance': + d['weight'] = diff + else: + raise ValueError("The mode '%s' is not recognised" % mode) + + return graph + + +def draw_rag(labels, rag, img, border_color=None, node_color='#ffff00', + edge_color='#00ff00', colormap=None, thresh=np.inf, + desaturate=False, in_place=True): + """Draw a Region Adjacency Graph on an image. + + Given a labelled image and its corresponding RAG, draw the nodes and edges + of the RAG on the image with the specified colors. Nodes are marked by + the centroids of the corresponding regions. + + Parameters + ---------- + labels : ndarray, shape (M, N) + The labelled image. + rag : RAG + The Region Adjacency Graph. + img : ndarray, shape (M, N, 3) + Input image. + border_color : colorspec, optional + Any matplotlib colorspec. + node_color : colorspec, optional + Any matplotlib colorspec. Yellow by default. + edge_color : colorspec, optional + Any matplotlib colorspec. Green by default. + colormap : colormap, optional + Any matplotlib colormap. If specified the edges are colormapped with + the specified color map. + thresh : float, optional + Edges with weight below `thresh` are not drawn, or considered for color + mapping. + desaturate : bool, optional + Convert the image to grayscale before displaying. Particularly helps + visualization when using the `colormap` option. + in_place : bool, optional + If set, the RAG is modified in place. For each node `n` the function + will set a new attribute ``rag.node[n]['centroid']``. + + Returns + ------- + out : ndarray, shape (M, N, 3) + The image with the RAG drawn. + + Examples + -------- + >>> from skimage import data, segmentation + >>> from skimage.future import graph + >>> img = data.coffee() + >>> labels = segmentation.slic(img) + >>> g = graph.rag_mean_color(img, labels) + >>> out = graph.draw_rag(labels, g, img) + """ + if not in_place: + rag = rag.copy() + + if desaturate: + img = color.rgb2gray(img) + img = color.gray2rgb(img) + + out = util.img_as_float(img, force_copy=True) + cc = colors.ColorConverter() + + edge_color = cc.to_rgb(edge_color) + node_color = cc.to_rgb(node_color) + + # Handling the case where one node has multiple labels + # offset is 1 so that regionprops does not ignore 0 + offset = 1 + map_array = np.arange(labels.max() + 1) + for n, d in rag.nodes_iter(data=True): + for label in d['labels']: + map_array[label] = offset + offset += 1 + + rag_labels = map_array[labels] + regions = measure.regionprops(rag_labels) + + for (n, data), region in zip(rag.nodes_iter(data=True), regions): + data['centroid'] = region['centroid'] + + if border_color is not None: + border_color = cc.to_rgb(border_color) + out = segmentation.mark_boundaries(out, rag_labels, color=border_color) + + if colormap is not None: + edge_weight_list = [d['weight'] for x, y, d in + rag.edges_iter(data=True) if d['weight'] < thresh] + norm = colors.Normalize() + norm.autoscale(edge_weight_list) + smap = cm.ScalarMappable(norm, colormap) + + for n1, n2, data in rag.edges_iter(data=True): + + if data['weight'] >= thresh: + continue + r1, c1 = map(int, rag.node[n1]['centroid']) + r2, c2 = map(int, rag.node[n2]['centroid']) + line = draw.line(r1, c1, r2, c2) + + if colormap is not None: + out[line] = smap.to_rgba([data['weight']])[0][:-1] + else: + out[line] = edge_color + + circle = draw.circle(r1, c1, 2) + out[circle] = node_color + + return out diff --git a/skimage/future/graph/setup.py b/skimage/future/graph/setup.py new file mode 100644 index 00000000..059a0779 --- /dev/null +++ b/skimage/future/graph/setup.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +from skimage._build import cython +import os.path + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs + + config = Configuration('graph', parent_package, top_path) + config.add_data_dir('tests') + + # This function tries to create C files from the given .pyx files. If + # it fails, try to build with pre-generated .c files. + cython(['_ncut_cy.pyx'], working_path=base_path) + config.add_extension('_ncut_cy', sources=['_ncut_cy.c'], + include_dirs=[get_numpy_include_dirs()]) + return config + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Graph-based Image-processing Algorithms', + url='https://github.com/scikit-image/scikit-image', + license='Modified BSD', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/future/graph/tests/test_rag.py b/skimage/future/graph/tests/test_rag.py new file mode 100644 index 00000000..1cac2b2d --- /dev/null +++ b/skimage/future/graph/tests/test_rag.py @@ -0,0 +1,161 @@ +import numpy as np +from skimage.future import graph +from skimage._shared.version_requirements import is_installed +from numpy.testing.decorators import skipif +from skimage import segmentation +from numpy import testing + + +def max_edge(g, src, dst, n): + default = {'weight': -np.inf} + w1 = g[n].get(src, default)['weight'] + w2 = g[n].get(dst, default)['weight'] + return max(w1, w2) + + +@skipif(not is_installed('networkx')) +def test_rag_merge(): + g = graph.rag.RAG() + + for i in range(5): + g.add_node(i, {'labels': [i]}) + + g.add_edge(0, 1, {'weight': 10}) + g.add_edge(1, 2, {'weight': 20}) + g.add_edge(2, 3, {'weight': 30}) + g.add_edge(3, 0, {'weight': 40}) + g.add_edge(0, 2, {'weight': 50}) + g.add_edge(3, 4, {'weight': 60}) + + gc = g.copy() + + # We merge nodes and ensure that the minimum weight is chosen + # when there is a conflict. + g.merge_nodes(0, 2) + assert g.edge[1][2]['weight'] == 10 + assert g.edge[2][3]['weight'] == 30 + + # We specify `max_edge` as `weight_func` as ensure that maximum + # weight is chosen in case on conflict + gc.merge_nodes(0, 2, weight_func=max_edge) + assert gc.edge[1][2]['weight'] == 20 + assert gc.edge[2][3]['weight'] == 40 + + g.merge_nodes(1, 4) + g.merge_nodes(2, 3) + n = g.merge_nodes(3, 4, in_place=False) + assert sorted(g.node[n]['labels']) == list(range(5)) + assert g.edges() == [] + + +@skipif(not is_installed('networkx')) +def test_threshold_cut(): + + img = np.zeros((100, 100, 3), dtype='uint8') + img[:50, :50] = 255, 255, 255 + img[:50, 50:] = 254, 254, 254 + img[50:, :50] = 2, 2, 2 + img[50:, 50:] = 1, 1, 1 + + labels = np.zeros((100, 100), dtype='uint8') + labels[:50, :50] = 0 + labels[:50, 50:] = 1 + labels[50:, :50] = 2 + labels[50:, 50:] = 3 + + rag = graph.rag_mean_color(img, labels) + new_labels = graph.cut_threshold(labels, rag, 10, in_place=False) + # Two labels + assert new_labels.max() == 1 + + new_labels = graph.cut_threshold(labels, rag, 10) + # Two labels + assert new_labels.max() == 1 + + +@skipif(not is_installed('networkx')) +def test_cut_normalized(): + + img = np.zeros((100, 100, 3), dtype='uint8') + img[:50, :50] = 255, 255, 255 + img[:50, 50:] = 254, 254, 254 + img[50:, :50] = 2, 2, 2 + img[50:, 50:] = 1, 1, 1 + + labels = np.zeros((100, 100), dtype='uint8') + labels[:50, :50] = 0 + labels[:50, 50:] = 1 + labels[50:, :50] = 2 + labels[50:, 50:] = 3 + + rag = graph.rag_mean_color(img, labels, mode='similarity') + + new_labels = graph.cut_normalized(labels, rag, in_place=False) + new_labels, _, _ = segmentation.relabel_sequential(new_labels) + # Two labels + assert new_labels.max() == 1 + + new_labels = graph.cut_normalized(labels, rag) + new_labels, _, _ = segmentation.relabel_sequential(new_labels) + assert new_labels.max() == 1 + + +@skipif(not is_installed('networkx')) +def test_rag_error(): + img = np.zeros((10, 10, 3), dtype='uint8') + labels = np.zeros((10, 10), dtype='uint8') + labels[:5, :] = 0 + labels[5:, :] = 1 + testing.assert_raises(ValueError, graph.rag_mean_color, img, labels, + 2, 'non existant mode') + + +def _weight_mean_color(graph, src, dst, n): + diff = graph.node[dst]['mean color'] - graph.node[n]['mean color'] + diff = np.linalg.norm(diff) + return diff + + +def _pre_merge_mean_color(graph, src, dst): + graph.node[dst]['total color'] += graph.node[src]['total color'] + graph.node[dst]['pixel count'] += graph.node[src]['pixel count'] + graph.node[dst]['mean color'] = (graph.node[dst]['total color'] / + graph.node[dst]['pixel count']) + + +def merge_hierarchical_mean_color(labels, rag, thresh, rag_copy=True, + in_place_merge=False): + return graph.merge_hierarchical(labels, rag, thresh, rag_copy, + in_place_merge, _pre_merge_mean_color, + _weight_mean_color) + + +@skipif(not is_installed('networkx')) +def test_rag_hierarchical(): + img = np.zeros((8, 8, 3), dtype='uint8') + labels = np.zeros((8, 8), dtype='uint8') + + img[:, :, :] = 31 + labels[:, :] = 1 + + img[0:4, 0:4, :] = 10, 10, 10 + labels[0:4, 0:4] = 2 + + img[4:, 0:4, :] = 20, 20, 20 + labels[4:, 0:4] = 3 + + g = graph.rag_mean_color(img, labels) + g2 = g.copy() + thresh = 20 # more than 11*sqrt(3) but less than + + result = merge_hierarchical_mean_color(labels, g, thresh) + assert(np.all(result[:, :4] == result[0, 0])) + assert(np.all(result[:, 4:] == result[-1, -1])) + + result = merge_hierarchical_mean_color(labels, g2, thresh, + in_place_merge=True) + assert(np.all(result[:, :4] == result[0, 0])) + assert(np.all(result[:, 4:] == result[-1, -1])) + + result = graph.cut_threshold(labels, g, thresh) + assert np.all(result == result[0, 0]) diff --git a/skimage/future/setup.py b/skimage/future/setup.py new file mode 100644 index 00000000..aaded0c7 --- /dev/null +++ b/skimage/future/setup.py @@ -0,0 +1,12 @@ + +def configuration(parent_package='skimage', top_path=None): + from numpy.distutils.misc_util import Configuration + config = Configuration('future', parent_package, top_path) + config.add_subpackage('graph') + return config + +if __name__ == "__main__": + from numpy.distutils.core import setup + + config = configuration(top_path='').todict() + setup(**config) diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index a335971d..89260f1f 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -1,9 +1,17 @@ from .spath import shortest_path 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 + 'route_through_array', + 'rag_mean_color', + 'cut_threshold', + 'cut_normalized', + 'ncut', + 'draw_rag', + 'merge_hierarchical', + 'RAG'] diff --git a/skimage/graph/heap.pyx b/skimage/graph/heap.pyx index dde8bd60..a4368595 100644 --- a/skimage/graph/heap.pyx +++ b/skimage/graph/heap.pyx @@ -711,7 +711,7 @@ cdef class FastUpdateBinaryHeap(BinaryHeap): Returns ------- pushed : bool - True if an append/update occured, False if otherwise. + True if an append/update occurred, False if otherwise. Raises ------ diff --git a/skimage/graph/setup.py b/skimage/graph/setup.py index 463d2739..4c6aba06 100644 --- a/skimage/graph/setup.py +++ b/skimage/graph/setup.py @@ -24,7 +24,6 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('heap', sources=['heap.c'], include_dirs=[get_numpy_include_dirs()]) - return config if __name__ == '__main__': diff --git a/skimage/graph/spath.py b/skimage/graph/spath.py index d8ec3526..8ce77ec8 100644 --- a/skimage/graph/spath.py +++ b/skimage/graph/spath.py @@ -73,8 +73,8 @@ def shortest_path(arr, reach=1, axis=-1, output_indexlist=False): if not output_indexlist: traceback = np.array(traceback) - traceback = np.concatenate([traceback[:, :axis], traceback[:, axis + 1:]], - axis=1) + traceback = np.concatenate([traceback[:, :axis], + traceback[:, axis + 1:]], axis=1) traceback = np.squeeze(traceback) return traceback, cost diff --git a/skimage/graph/tests/__init__.py b/skimage/graph/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/graph/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index 5c44abad..4dc69858 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -5,6 +5,7 @@ from numpy.testing import (assert_array_equal, import skimage.graph.mcp as mcp +np.random.seed(0) a = np.ones((8, 8), dtype=np.float32) a[1:-1, 1] = 0 a[1, 1:-1] = 0 @@ -133,15 +134,14 @@ def test_crashing(): def _test_random(shape): # Just tests for crashing -- not for correctness. - np.random.seed(0) - a = np.random.random(shape).astype(np.float32) + a = np.random.rand(*shape).astype(np.float32) starts = [[0] * len(shape), [-1] * len(shape), - (np.random.random(len(shape)) * shape).astype(int)] - ends = [(np.random.random(len(shape)) * shape).astype(int) + (np.random.rand(len(shape)) * shape).astype(int)] + ends = [(np.random.rand(len(shape)) * shape).astype(int) for i in range(4)] m = mcp.MCP(a, fully_connected=True) costs, offsets = m.find_costs(starts) - for point in [(np.random.random(len(shape)) * shape).astype(int) + for point in [(np.random.rand(len(shape)) * shape).astype(int) for i in range(4)]: m.traceback(point) m._reset() diff --git a/skimage/io/__init__.py b/skimage/io/__init__.py index 144fef57..d0ec1f43 100644 --- a/skimage/io/__init__.py +++ b/skimage/io/__init__.py @@ -26,7 +26,7 @@ def _format_plugin_info_table(info_table, column_lengths): 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)) + info_table.append(_separator('=', column_lengths)) def _update_doc(doc): @@ -36,11 +36,14 @@ def _update_doc(doc): """ from textwrap import wrap - info_table = [(p, plugin_info(p).get('description', 'no description')) for p in available_plugins if not p == 'test'] - name_length = max([len(n) for (n, _) in info_table]) + if len(info_table) > 0: + name_length = max([len(n) for (n, _) in info_table]) + else: + name_length = 0 + description_length = WRAP_LEN - 1 - name_length column_lengths = [name_length, description_length] _format_plugin_info_table(info_table, column_lengths) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 97f8718e..f44bfd8b 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -3,8 +3,8 @@ from io import BytesIO import numpy as np import six -from skimage.io.manage_plugins import call_plugin -from skimage.color import rgb2grey +from ..io.manage_plugins import call_plugin +from ..color import rgb2grey from .util import file_or_url_context @@ -193,7 +193,7 @@ def show(): >>> import skimage.io as io >>> for i in range(4): - ... io.imshow(np.random.random((50, 50))) + ... ax_im = io.imshow(np.random.rand(50, 50)) >>> io.show() # doctest: +SKIP ''' diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index a26125fa..79c97641 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -34,15 +34,29 @@ def _generate_candidate_libs(): return lib_dirs, lib_paths +if sys.platform == 'win32': + LOADER = ctypes.windll + FUNCTYPE = ctypes.WINFUNCTYPE +else: + LOADER = ctypes.cdll + FUNCTYPE = ctypes.CFUNCTYPE + +def handle_errors(): + global FT_ERROR_STR + if FT_ERROR_STR: + tmp = FT_ERROR_STR + FT_ERROR_STR = None + raise RuntimeError(tmp) + +FT_ERROR_STR = None +# This MUST happen in module scope, or the function pointer is garbage +# collected, leading to a segfault when error_handler is called. +@FUNCTYPE(None, ctypes.c_int, ctypes.c_char_p) +def c_error_handler(fif, message): + global FT_ERROR_STR + FT_ERROR_STR = 'FreeImage error: %s' % message def load_freeimage(): - if sys.platform == 'win32': - loader = ctypes.windll - functype = ctypes.WINFUNCTYPE - else: - loader = ctypes.cdll - functype = ctypes.CFUNCTYPE - freeimage = None errors = [] # First try a few bare library names that ctypes might be able to find @@ -54,7 +68,7 @@ def load_freeimage(): lib_paths = bare_libs + lib_paths for lib in lib_paths: try: - freeimage = loader.LoadLibrary(lib) + freeimage = LOADER.LoadLibrary(lib) break except Exception: if lib not in bare_libs: @@ -71,7 +85,7 @@ def load_freeimage(): if errors: # No freeimage library loaded, and load-errors reported for some # candidate libs - err_txt = ['%s:\n%s' % (l, str(e.message)) for l, e in errors] + err_txt = ['%s:\n%s' % (l, str(e)) for l, e in errors] raise RuntimeError('One or more FreeImage libraries were found, but ' 'could not be loaded due to the following errors:\n' '\n\n'.join(err_txt)) @@ -81,11 +95,7 @@ def load_freeimage(): '\n'.join(lib_dirs)) # FreeImage found - @functype(None, ctypes.c_int, ctypes.c_char_p) - def error_handler(fif, message): - raise RuntimeError('FreeImage error: %s' % message) - - freeimage.FreeImage_SetOutputMessage(error_handler) + freeimage.FreeImage_SetOutputMessage(c_error_handler) return freeimage _FI = load_freeimage() @@ -189,13 +199,17 @@ class FI_TYPES(object): @classmethod def get_type_and_shape(cls, bitmap): w = _FI.FreeImage_GetWidth(bitmap) + handle_errors() h = _FI.FreeImage_GetHeight(bitmap) + handle_errors() fi_type = _FI.FreeImage_GetImageType(bitmap) + handle_errors() if not fi_type: raise ValueError('Unknown image pixel type') dtype = cls.dtypes[fi_type] if fi_type == cls.FIT_BITMAP: bpp = _FI.FreeImage_GetBPP(bitmap) + handle_errors() if bpp == 8: extra_dims = [] elif bpp == 24: @@ -377,9 +391,11 @@ class METADATA_DATATYPE(object): def _process_bitmap(filename, flags, process_func): filename = asbytes(filename) ftype = _FI.FreeImage_GetFileType(filename, 0) + handle_errors() if ftype == -1: raise ValueError('Cannot determine type of file %s' % filename) bitmap = _FI.FreeImage_Load(ftype, filename, flags) + handle_errors() bitmap = ctypes.c_void_p(bitmap) if not bitmap: raise ValueError('Could not load file %s' % filename) @@ -387,6 +403,7 @@ def _process_bitmap(filename, flags, process_func): return process_func(bitmap) finally: _FI.FreeImage_Unload(bitmap) + handle_errors() def read(filename, flags=0): @@ -414,6 +431,7 @@ def read_metadata(filename): def _process_multipage(filename, flags, process_func): filename = asbytes(filename) ftype = _FI.FreeImage_GetFileType(filename, 0) + handle_errors() if ftype == -1: raise ValueError('Cannot determine type of file %s' % filename) create_new = False @@ -422,14 +440,17 @@ def _process_multipage(filename, flags, process_func): multibitmap = _FI.FreeImage_OpenMultiBitmap(ftype, filename, create_new, read_only, keep_cache_in_memory, flags) + handle_errors() multibitmap = ctypes.c_void_p(multibitmap) if not multibitmap: raise ValueError('Could not open %s as multi-page image.' % filename) try: pages = _FI.FreeImage_GetPageCount(multibitmap) + handle_errors() out = [] for i in range(pages): bitmap = _FI.FreeImage_LockPage(multibitmap, i) + handle_errors() bitmap = ctypes.c_void_p(bitmap) if not bitmap: raise ValueError('Could not open %s as a multi-page image.' @@ -438,9 +459,11 @@ def _process_multipage(filename, flags, process_func): out.append(process_func(bitmap)) finally: _FI.FreeImage_UnlockPage(multibitmap, bitmap, False) + handle_errors() return out finally: _FI.FreeImage_CloseMultiBitmap(multibitmap, 0) + handle_errors() def read_multipage(filename, flags=0): @@ -469,6 +492,7 @@ def _wrap_bitmap_bits_in_array(bitmap, shape, dtype): """ pitch = _FI.FreeImage_GetPitch(bitmap) + handle_errors() height = shape[-1] byte_size = height * pitch itemsize = dtype.itemsize @@ -478,6 +502,7 @@ def _wrap_bitmap_bits_in_array(bitmap, shape, dtype): else: strides = (itemsize, pitch) bits = _FI.FreeImage_GetBits(bitmap) + handle_errors() array = numpy.ndarray(shape, dtype=dtype, buffer=(ctypes.c_char * byte_size).from_address(bits), strides=strides) @@ -493,7 +518,6 @@ def _array_from_bitmap(bitmap): # swizzle the color components and flip the scanlines to go from # FreeImage's BGR[A] and upside-down internal memory format to something # more normal - def n(arr): return arr[..., ::-1].T if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ @@ -502,6 +526,7 @@ def _array_from_bitmap(bitmap): g = n(array[1]) r = n(array[2]) if shape[0] == 3: + handle_errors() return numpy.dstack((r, g, b)) elif shape[0] == 4: a = n(array[3]) @@ -523,6 +548,7 @@ def _read_metadata(bitmap): for model_name, number in models: mdhandle = _FI.FreeImage_FindFirstMetadata(number, bitmap, ctypes.byref(tag)) + handle_errors() mdhandle = ctypes.c_void_p(mdhandle) if mdhandle: more = True @@ -530,8 +556,10 @@ def _read_metadata(bitmap): tag_name = asstr(_FI.FreeImage_GetTagKey(tag)) tag_type = _FI.FreeImage_GetTagType(tag) byte_size = _FI.FreeImage_GetTagLength(tag) + handle_errors() char_ptr = ctypes.c_char * byte_size tag_str = char_ptr.from_address(_FI.FreeImage_GetTagValue(tag)) + handle_errors() if tag_type == METADATA_DATATYPE.FIDT_ASCII: tag_val = asstr(tag_str.value) else: @@ -541,7 +569,9 @@ def _read_metadata(bitmap): tag_val = tag_val[0] metadata[(model_name, tag_name)] = tag_val more = _FI.FreeImage_FindNextMetadata(mdhandle, ctypes.byref(tag)) + handle_errors() _FI.FreeImage_FindCloseMetadata(mdhandle) + handle_errors() return metadata @@ -556,6 +586,7 @@ def write(array, filename, flags=0): array = numpy.asarray(array) filename = asbytes(filename) ftype = _FI.FreeImage_GetFIFFromFilename(filename) + handle_errors() if ftype == -1: raise ValueError('Cannot determine type for %s' % filename) bitmap, fi_type = _array_to_bitmap(array) @@ -563,16 +594,20 @@ def write(array, filename, flags=0): if fi_type == FI_TYPES.FIT_BITMAP: can_write = _FI.FreeImage_FIFSupportsExportBPP(ftype, _FI.FreeImage_GetBPP(bitmap)) + handle_errors() else: can_write = _FI.FreeImage_FIFSupportsExportType(ftype, fi_type) + handle_errors() if not can_write: raise TypeError('Cannot save image of this format ' 'to this file type') res = _FI.FreeImage_Save(ftype, bitmap, filename, flags) + handle_errors() if not res: raise RuntimeError('Could not save image properly.') finally: _FI.FreeImage_Unload(bitmap) + handle_errors() def write_multipage(arrays, filename, flags=0): @@ -637,17 +672,28 @@ def _array_to_bitmap(array): raise RuntimeError('Could not allocate image for storage') try: def n(arr): # normalise to freeimage's in-memory format - return arr.T[:, ::-1] + return arr.T[..., ::-1] + wrapped_array = _wrap_bitmap_bits_in_array(bitmap, w_shape, dtype) # swizzle the color components and flip the scanlines to go to # FreeImage's BGR[A] and upside-down internal memory format - if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ - dtype.type == numpy.uint8: - wrapped_array[0] = n(array[:, :, 2]) - wrapped_array[1] = n(array[:, :, 1]) - wrapped_array[2] = n(array[:, :, 0]) + if len(shape) == 3 and _FI.FreeImage_IsLittleEndian(): + R = array[:, :, 0] + G = array[:, :, 1] + B = array[:, :, 2] + + if dtype.type == numpy.uint8: + wrapped_array[0] = n(B) + wrapped_array[1] = n(G) + wrapped_array[2] = n(R) + elif dtype.type == numpy.uint16: + wrapped_array[0] = n(R) + wrapped_array[1] = n(G) + wrapped_array[2] = n(B) + if shape[2] == 4: - wrapped_array[3] = n(array[:, :, 3]) + A = array[:, :, 3] + wrapped_array[3] = n(A) else: wrapped_array[:] = n(array) if len(shape) == 2 and dtype.type == numpy.uint8: diff --git a/skimage/io/_plugins/imread_plugin.py b/skimage/io/_plugins/imread_plugin.py index 46382cdf..9311f18f 100644 --- a/skimage/io/_plugins/imread_plugin.py +++ b/skimage/io/_plugins/imread_plugin.py @@ -1,6 +1,6 @@ __all__ = ['imread', 'imsave'] -from skimage.utils.dtype import convert +from ...util.dtype import convert try: import imread as _imread @@ -33,7 +33,7 @@ def imsave(fname, arr, format_str=None): arr : ndarray of uint8 or uint16 Array (image) to save. format_str: str,optional - Format to save as. + Format to save as. Notes ----- diff --git a/skimage/io/_plugins/matplotlib_plugin.py b/skimage/io/_plugins/matplotlib_plugin.py index ece44d93..2de61ecf 100644 --- a/skimage/io/_plugins/matplotlib_plugin.py +++ b/skimage/io/_plugins/matplotlib_plugin.py @@ -1,10 +1,153 @@ +from collections import namedtuple +import numpy as np +import warnings import matplotlib.pyplot as plt +from skimage.util import dtype as dtypes -def imshow(*args, **kwargs): +_default_colormap = 'gray' +_nonstandard_colormap = 'cubehelix' +_diverging_colormap = 'RdBu' + + +ImageProperties = namedtuple('ImageProperties', + ['signed', 'out_of_range_float', + 'low_dynamic_range', 'unsupported_dtype']) + + +def _get_image_properties(image): + """Determine nonstandard properties of an input image. + + Parameters + ---------- + image : array + The input image. + + Returns + ------- + ip : ImageProperties named tuple + The properties of the image: + + - signed: whether the image has negative values. + - out_of_range_float: if the image has floating point data + outside of [-1, 1]. + - low_dynamic_range: if the image is in the standard image + range (e.g. [0, 1] for a floating point image) but its + dynamic range would be too small to display with standard + image ranges. + - unsupported_dtype: if the image data type is not a + standard skimage type, e.g. ``numpy.uint64``. + """ + immin, immax = np.min(image), np.max(image) + imtype = image.dtype.type + try: + lo, hi = dtypes.dtype_range[imtype] + except KeyError: + lo, hi = immin, immax + + signed = immin < 0 + out_of_range_float = (np.issubdtype(image.dtype, np.float) and + (immin < lo or immax > hi)) + low_dynamic_range = (immin != immax and + (float(immax - immin) / (hi - lo)) < (1. / 255)) + unsupported_dtype = image.dtype not in dtypes._supported_types + + return ImageProperties(signed, out_of_range_float, + low_dynamic_range, unsupported_dtype) + + +def _raise_warnings(image_properties): + """Raise the appropriate warning for each nonstandard image type. + + Parameters + ---------- + image_properties : ImageProperties named tuple + The properties of the considered image. + """ + ip = image_properties + if ip.unsupported_dtype: + warnings.warn("Non-standard image type; displaying image with " + "stretched contrast.") + if ip.low_dynamic_range: + warnings.warn("Low image dynamic range; displaying image with " + "stretched contrast.") + if ip.out_of_range_float: + warnings.warn("Float image out of standard range; displaying image " + "with stretched contrast.") + + +def _get_display_range(image): + """Return the display range for a given set of image properties. + + Parameters + ---------- + image : array + The input image. + + Returns + ------- + lo, hi : same type as immin, immax + The display range to be used for the input image. + cmap : string + The name of the colormap to use. + """ + ip = _get_image_properties(image) + immin, immax = np.min(image), np.max(image) + if ip.signed: + magnitude = max(abs(immin), abs(immax)) + lo, hi = -magnitude, magnitude + cmap = _diverging_colormap + elif any(ip): + _raise_warnings(ip) + lo, hi = immin, immax + cmap = _nonstandard_colormap + else: + lo = 0 + imtype = image.dtype.type + hi = dtypes.dtype_range[imtype][1] + cmap = _default_colormap + return lo, hi, cmap + + +def imshow(im, *args, **kwargs): + """Show the input image and return the current axes. + + By default, the image is displayed in greyscale, rather than + the matplotlib default colormap. + + Images are assumed to have standard range for their type. For + example, if a floating point image has values in [0, 0.5], the + most intense color will be gray50, not white. + + If the image exceeds the standard range, or if the range is too + small to display, we fall back on displaying exactly the range of + the input image, along with a colorbar to clearly indicate that + this range transformation has occurred. + + For signed images, we use a diverging colormap centered at 0. + + Parameters + ---------- + im : array, shape (M, N[, 3]) + The image to display. + + *args, **kwargs : positional and keyword arguments + These are passed directly to `matplotlib.pyplot.imshow`. + + Returns + ------- + ax_im : `matplotlib.pyplot.AxesImage` + The `AxesImage` object returned by `plt.imshow`. + """ + lo, hi, cmap = _get_display_range(im) kwargs.setdefault('interpolation', 'nearest') - kwargs.setdefault('cmap', 'gray') - plt.imshow(*args, **kwargs) + kwargs.setdefault('cmap', cmap) + kwargs.setdefault('vmin', lo) + kwargs.setdefault('vmax', hi) + ax_im = plt.imshow(im, *args, **kwargs) + if cmap != _default_colormap: + plt.colorbar() + return ax_im imread = plt.imread show = plt.show diff --git a/skimage/io/_plugins/null_plugin.ini b/skimage/io/_plugins/null_plugin.ini deleted file mode 100644 index 9703aab0..00000000 --- a/skimage/io/_plugins/null_plugin.ini +++ /dev/null @@ -1,3 +0,0 @@ -[null] -description = Default plugin that does nothing -provides = imshow, imread, imsave, _app_show diff --git a/skimage/io/_plugins/null_plugin.py b/skimage/io/_plugins/null_plugin.py deleted file mode 100644 index 4eb7adf3..00000000 --- a/skimage/io/_plugins/null_plugin.py +++ /dev/null @@ -1,25 +0,0 @@ -__all__ = ['imshow', 'imread', 'imsave', '_app_show'] - -import warnings - -message = '''\ -No plugin has been loaded. Please refer to - -skimage.io.plugins() - -for a list of available plugins.''' - - -def imshow(*args, **kwargs): - warnings.warn(RuntimeWarning(message)) - - -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.ini b/skimage/io/_plugins/pil_plugin.ini index e6382e5e..14034410 100644 --- a/skimage/io/_plugins/pil_plugin.ini +++ b/skimage/io/_plugins/pil_plugin.ini @@ -1,3 +1,3 @@ [pil] description = Image reading via the Python Imaging Library -provides = imread, imsave, imshow, _app_show +provides = imread, imsave diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 88e7f04b..ae74ff7b 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -1,42 +1,124 @@ -__all__ = ['imread'] +__all__ = ['imread', 'imsave'] import numpy as np - -try: - from PIL import Image -except ImportError: - raise ImportError("The Python Image Library could not be found. " - "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 six import string_types +from PIL import Image + +from ...util import img_as_ubyte, img_as_uint +from ...external.tifffile import imread as tif_imread, imsave as tif_imsave -def imread(fname, dtype=None): +def imread(fname, dtype=None, img_num=None, **kwargs): """Load an image from file. - """ - im = Image.open(fname) - if im.mode == 'P': - if _palette_is_grayscale(im): - im = im.convert('L') - else: - im = im.convert('RGB') - elif im.mode == '1': - im = im.convert('L') - elif im.mode.startswith('I;16'): - shape = im.size - dtype = '>u2' if im.mode.endswith('B') else ' 1: + return np.array(frames) + elif frames: + return frames[0] + elif img_num: + raise IndexError('Could not find image #%s' % img_num) def _palette_is_grayscale(pil_image): @@ -63,6 +145,57 @@ def _palette_is_grayscale(pil_image): return np.allclose(np.diff(valid_palette), 0) +def ndarray_to_pil(arr, format_str=None): + """Export an ndarray to a PIL object. + + Parameters + ---------- + Refer to ``imsave``. + + """ + if arr.ndim == 3: + arr = img_as_ubyte(arr) + mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] + + elif format_str in ['png', 'PNG']: + mode = 'I;16' + mode_base = 'I' + + if arr.dtype.kind == 'f': + arr = img_as_uint(arr) + + elif arr.max() < 256 and arr.min() >= 0: + arr = arr.astype(np.uint8) + mode = mode_base = 'L' + + else: + arr = img_as_uint(arr) + + else: + arr = img_as_ubyte(arr) + mode = 'L' + mode_base = 'L' + + try: + array_buffer = arr.tobytes() + except AttributeError: + array_buffer = arr.tostring() # Numpy < 1.9 + + if arr.ndim == 2: + im = Image.new(mode_base, arr.T.shape) + try: + im.frombytes(array_buffer, 'raw', mode) + except AttributeError: + im.fromstring(array_buffer, 'raw', mode) # PIL 1.1.7 + else: + image_shape = (arr.shape[1], arr.shape[0]) + try: + im = Image.frombytes(mode, image_shape, array_buffer) + except AttributeError: + im = Image.fromstring(mode, image_shape, array_buffer) # PIL 1.1.7 + return im + + def imsave(fname, arr, format_str=None): """Save an image to disk. @@ -80,10 +213,46 @@ def imsave(fname, arr, format_str=None): Notes ----- - Currently, only 8-bit precision is supported. + Tiff files are handled by Christophe Golhke's tifffile.py [1]_, + and support many advanced image types including multi-page and + floating point. + All other image formats use the Python Imaging Libary. + See PIL docs [2]_ for a list of other supported formats. + All images besides single channel PNGs are converted using `img_as_uint8`. + Single Channel PNGs have the following behavior: + - Integer values in [0, 255] and Boolean types -> img_as_uint8 + - Floating point and other integers -> img_as_uint16 + + References + ---------- + .. [1] http://www.lfd.uci.edu/~gohlke/code/tifffile.py.html + .. [2] http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html """ - arr = np.asarray(arr).squeeze() + # default to PNG if file-like object + if not isinstance(fname, string_types) and format_str is None: + format_str = "PNG" + # Check for png in filename + if (isinstance(fname, string_types) + and fname.lower().endswith(".png")): + format_str = "PNG" + + arr = np.asanyarray(arr).squeeze() + + if arr.dtype.kind == 'b': + arr = arr.astype(np.uint8) + + use_tif = False + if hasattr(fname, 'lower'): + if fname.lower().endswith(('.tiff', '.tif')): + use_tif = True + if not format_str is None: + if format_str.lower() in ['tiff', 'tif']: + use_tif = True + + if use_tif: + tif_imsave(fname, arr) + return if arr.ndim not in (2, 3): raise ValueError("Invalid shape for image array: %s" % arr.shape) @@ -92,47 +261,5 @@ def imsave(fname, arr, format_str=None): if arr.shape[2] not in (3, 4): raise ValueError("Invalid number of channels in image array.") - # Image is floating point, assume in [0, 1] - if np.issubdtype(arr.dtype, float): - arr = arr * 255 - - arr = arr.astype(np.uint8) - - if arr.ndim == 2: - mode = 'L' - - elif arr.shape[2] in (3, 4): - mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] - - # Force all integers to bytes - arr = arr.astype(np.uint8) - - # default to PNG if file-like object - if not isinstance(fname, string_types) and format_str is None: - format_str = "PNG" - - 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 = ndarray_to_pil(arr, format_str=format_str) img.save(fname, format=format_str) - - -def imshow(arr): - """Display an image, using PIL's default display command. - - Parameters - ---------- - arr : ndarray - Image to display. Images of dtype float are assumed to be in - [0, 1]. Images of dtype uint8 are in [0, 255]. - - """ - Image.fromarray(img_as_ubyte(arr)).show() - - -def _app_show(): - pass diff --git a/skimage/io/_plugins/qt_plugin.py b/skimage/io/_plugins/qt_plugin.py index db4cb4cc..f3a3b8e0 100644 --- a/skimage/io/_plugins/qt_plugin.py +++ b/skimage/io/_plugins/qt_plugin.py @@ -158,7 +158,7 @@ def imsave(filename, img, format_str=None): qbuffer.open(QtCore.QIODevice.ReadWrite) saved = qimg.save(qbuffer, format_str.upper()) qbuffer.seek(0) - filename.write(qbuffer.readAll()) + filename.write(qbuffer.readAll().data()) qbuffer.close() else: saved = qimg.save(filename) diff --git a/skimage/io/_plugins/skivi.py b/skimage/io/_plugins/skivi.py index fcfe8bc6..ee7b421d 100644 --- a/skimage/io/_plugins/skivi.py +++ b/skimage/io/_plugins/skivi.py @@ -209,7 +209,7 @@ class SkiviImageWindow(QMainWindow): self.rgb_hsv_disp.update_vals((x, y, r, g, b, h, s, v)) def save_to_stack(self): - from skimage import io + from ... import io img = self.arr.copy() io.push(img) msg = dedent(''' @@ -227,7 +227,7 @@ class SkiviImageWindow(QMainWindow): dialog.exec_() def save_to_file(self): - from skimage import io + from ... import io filename = str(QtGui.QFileDialog.getSaveFileName()) if len(filename) == 0: return diff --git a/skimage/io/_plugins/tifffile_plugin.py b/skimage/io/_plugins/tifffile_plugin.py index 58038db4..a3a766f1 100644 --- a/skimage/io/_plugins/tifffile_plugin.py +++ b/skimage/io/_plugins/tifffile_plugin.py @@ -1,6 +1 @@ -try: - from tifffile import imread, imsave -except ImportError: - raise ImportError("The tifffile module could not be found.\n" - "It can be obtained at " - "\n") +from ...external.tifffile import imread, imsave diff --git a/skimage/io/_plugins/util.py b/skimage/io/_plugins/util.py index 98dd4f87..609680d8 100644 --- a/skimage/io/_plugins/util.py +++ b/skimage/io/_plugins/util.py @@ -2,7 +2,7 @@ import numpy as np from . import _colormixer from . import _histograms import threading -from skimage.util import img_as_ubyte +from ...util import img_as_ubyte # utilities to make life easier for plugin writers. diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 83811fc0..49d6aa25 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -9,6 +9,9 @@ from copy import copy import numpy as np import six +from PIL import Image + +from ..external.tifffile import TiffFile __all__ = ['MultiImage', 'ImageCollection', 'concatenate_images', @@ -71,156 +74,8 @@ def alphanumeric_key(s): return k -class MultiImage(object): - """A class containing a single multi-frame image. - - Parameters - ---------- - filename : str - The complete path to the image file. - conserve_memory : bool, optional - Whether to conserve memory by only caching a single frame. Default is - True. - - Notes - ----- - If ``conserve_memory=True`` the memory footprint can be reduced, however - the performance can be affected because frames have to be read from file - more often. - - The last accessed frame is cached, all other frames will have to be read - from file. - - The current implementation makes use of PIL. - - Examples - -------- - >>> from skimage import data_dir - - >>> img = MultiImage(data_dir + '/multipage.tif') # doctest: +SKIP - >>> len(img) # doctest: +SKIP - 2 - >>> for frame in img: # doctest: +SKIP - ... print(frame.shape) # doctest: +SKIP - (15, 10) - (15, 10) - - """ - def __init__(self, filename, conserve_memory=True, dtype=None): - """Load a multi-img.""" - self._filename = filename - self._conserve_memory = conserve_memory - self._dtype = dtype - self._cached = None - - from PIL import Image - img = Image.open(self._filename) - if self._conserve_memory: - self._numframes = self._find_numframes(img) - else: - self._frames = self._getallframes(img) - self._numframes = len(self._frames) - - @property - def filename(self): - return self._filename - - @property - def conserve_memory(self): - return self._conserve_memory - - def _find_numframes(self, img): - """Find the number of frames in the multi-img.""" - i = 0 - while True: - i += 1 - try: - img.seek(i) - except EOFError: - break - return i - - def _getframe(self, framenum): - """Open the image and extract the frame.""" - from PIL import Image - img = Image.open(self.filename) - img.seek(framenum) - return np.asarray(img, dtype=self._dtype) - - def _getallframes(self, img): - """Extract all frames from the multi-img.""" - frames = [] - try: - i = 0 - while True: - frames.append(np.asarray(img, dtype=self._dtype)) - i += 1 - img.seek(i) - except EOFError: - return frames - - def __getitem__(self, n): - """Return the n-th frame as an array. - - Parameters - ---------- - n : int - Number of the required frame. - - Returns - ------- - frame : ndarray - The n-th frame. - """ - numframes = self._numframes - if -numframes <= n < numframes: - n = n % numframes - else: - raise IndexError("There are only %s frames in the image" - % numframes) - - if self.conserve_memory: - if not self._cached == n: - frame = self._getframe(n) - self._cached = n - self._cachedframe = frame - return self._cachedframe - else: - return self._frames[n] - - def __iter__(self): - """Iterate over the frames.""" - for i in range(len(self)): - yield self[i] - - def __len__(self): - """Number of images in collection.""" - return self._numframes - - def __str__(self): - return str(self.filename) + ' [%s frames]' % self._numframes - - def concatenate(self): - """Concatenate all images in the multi-image into an array. - - Returns - ------- - ar : np.ndarray - An array having one more dimension than the images in `self`. - - See Also - -------- - concatenate_images - - Raises - ------ - ValueError - If images in the `MultiImage` don't have identical shapes. - """ - return concatenate_images(self) - - class ImageCollection(object): + """Load and manage a collection of image files. Note that files are always stored in alphabetical order. Also note that @@ -279,6 +134,9 @@ class ImageCollection(object): ic = ImageCollection('/tmp/*.png', load_func=imread_convert) + For files with multiple images, the images will be flattened into a list + and added to the list of available images. + Examples -------- >>> import skimage.io as io @@ -293,21 +151,27 @@ class ImageCollection(object): >>> ic = io.ImageCollection('/tmp/work/*.png:/tmp/other/*.jpg') """ - def __init__(self, load_pattern, conserve_memory=True, load_func=None): + + def __init__(self, load_pattern, conserve_memory=True, load_func=None, + **load_func_kwargs): """Load and manage a collection of images.""" if isinstance(load_pattern, six.string_types): - load_pattern = load_pattern.split(os.pathsep) + load_pattern = load_pattern.replace(os.pathsep, ':') + load_pattern = load_pattern.split(':') self._files = [] for pattern in load_pattern: self._files.extend(glob(pattern)) self._files = sorted(self._files, key=alphanumeric_key) + self._numframes = self._find_images() else: self._files = load_pattern + self._numframes = len(load_pattern) + self._frame_index = None if conserve_memory: memory_slots = 1 else: - memory_slots = len(self._files) + memory_slots = self._numframes self._conserve_memory = conserve_memory self._cached = None @@ -318,6 +182,8 @@ class ImageCollection(object): else: self.load_func = load_func + self.load_func_kwargs = load_func_kwargs + self.data = np.empty(memory_slots, dtype=object) @property @@ -328,6 +194,36 @@ class ImageCollection(object): def conserve_memory(self): return self._conserve_memory + def _find_images(self): + index = [] + for fname in self._files: + if fname.lower().endswith(('.tiff', '.tif')): + img = TiffFile(fname) + index += [(fname, i) for i in range(len(img.pages))] + else: + im = Image.open(fname) + try: + # this will raise an IOError if the file is not readable + im.getdata()[0] + except IOError: + site = "http://pillow.readthedocs.org/en/latest/installation.html#external-libraries" + raise ValueError( + 'Could not load "%s"\nPlease see documentation at: %s' % (fname, site)) + else: + i = 0 + while True: + try: + im.seek(i) + except EOFError: + break + index.append((fname, i)) + i += 1 + if hasattr(im, 'fp') and im.fp: + im.fp.close() + + self._frame_index = index + return len(index) + def __getitem__(self, n): """Return selected image(s) in the collection. @@ -356,9 +252,15 @@ class ImageCollection(object): n = self._check_imgnum(n) idx = n % len(self.data) - if (self.conserve_memory and n != self._cached) or \ - (self.data[idx] is None): - self.data[idx] = self.load_func(self.files[n]) + if ((self.conserve_memory and n != self._cached) or + (self.data[idx] is None)): + if self._frame_index: + fname, img_num = self._frame_index[n] + self.data[idx] = self.load_func(fname, img_num=img_num, + **self.load_func_kwargs) + else: + self.data[idx] = self.load_func(self.files[n], + **self.load_func_kwargs) self._cached = n return self.data[idx] @@ -367,9 +269,17 @@ class ImageCollection(object): # object. Any loaded image data in the original ImageCollection # will be copied by reference to the new object. Image data # loaded after this creation is not linked. - fidx = range(len(self.files))[n] + fidx = range(self._numframes)[n] new_ic = copy(self) - new_ic._files = [self.files[i] for i in fidx] + + if self._frame_index: + new_ic._files = [self._frame_index[i][0] for i in fidx] + new_ic._frame_index = [self._frame_index[i] for i in fidx] + else: + new_ic._files = [self._files[i] for i in fidx] + + new_ic._numframes = len(fidx) + if self.conserve_memory: if self._cached in fidx: new_ic._cached = fidx.index(self._cached) @@ -382,7 +292,7 @@ class ImageCollection(object): def _check_imgnum(self, n): """Check that the given image number is valid.""" - num = len(self.files) + num = self._numframes if -num <= n < num: n = n % num else: @@ -397,7 +307,7 @@ class ImageCollection(object): def __len__(self): """Number of images in collection.""" - return len(self.files) + return self._numframes def __str__(self): return str(self.files) @@ -458,3 +368,59 @@ def imread_collection_wrapper(imread): return ImageCollection(load_pattern, conserve_memory=conserve_memory, load_func=imread) return imread_collection + + +class MultiImage(ImageCollection): + + """A class containing a single multi-frame image. + + Parameters + ---------- + filename : str + The complete path to the image file. + conserve_memory : bool, optional + Whether to conserve memory by only caching a single frame. Default is + True. + + Notes + ----- + If ``conserve_memory=True`` the memory footprint can be reduced, however + the performance can be affected because frames have to be read from file + more often. + + The last accessed frame is cached, all other frames will have to be read + from file. + + The current implementation makes use of ``tifffile`` for Tiff files and + PIL otherwise. + + Examples + -------- + >>> from skimage import data_dir + + >>> img = MultiImage(data_dir + '/multipage.tif') # doctest: +SKIP + >>> len(img) # doctest: +SKIP + 2 + >>> for frame in img: # doctest: +SKIP + ... print(frame.shape) # doctest: +SKIP + (15, 10) + (15, 10) + + """ + + def __init__(self, filename, conserve_memory=True, dtype=None, + **imread_kwargs): + """Load a multi-img.""" + from ._io import imread + + def load_func(fname, **kwargs): + kwargs.setdefault('dtype', dtype) + return imread(fname, **kwargs) + + self._filename = filename + super(MultiImage, self).__init__(filename, conserve_memory, + load_func=load_func, **imread_kwargs) + + @property + def filename(self): + return self._filename diff --git a/skimage/io/manage_plugins.py b/skimage/io/manage_plugins.py index d31e6dbe..62b3fc1c 100644 --- a/skimage/io/manage_plugins.py +++ b/skimage/io/manage_plugins.py @@ -44,10 +44,8 @@ plugin_meta_data = {} # 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'], + 'all': ['pil', 'matplotlib', 'qt', 'freeimage'], + 'imshow': ['matplotlib'] } @@ -121,7 +119,7 @@ def _scan_plugins(): for p in provides: if not p in plugin_store: - print("Plugin `%s` wants to provide non-existent `%s`." \ + print("Plugin `%s` wants to provide non-existent `%s`." " Ignoring." % (name, p)) # Add plugins that provide 'imread' as provider of 'imread_collection'. @@ -203,7 +201,7 @@ def call_plugin(kind, *args, **kwargs): try: func = [f for (p, f) in plugin_funcs if p == plugin][0] except IndexError: - raise RuntimeError('Could not find the plugin "%s" for %s.' % \ + raise RuntimeError('Could not find the plugin "%s" for %s.' % (plugin, kind)) return func(*args, **kwargs) @@ -242,7 +240,7 @@ def use_plugin(name, kind=None): kind = plugin_store.keys() else: if not kind in plugin_provides[name]: - raise RuntimeError("Plugin %s does not support `%s`." % \ + raise RuntimeError("Plugin %s does not support `%s`." % (name, kind)) if kind == 'imshow': @@ -301,7 +299,7 @@ def _load(plugin): 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." % \ + print("Plugin %s does not provide %s as advertised. Ignoring." % (plugin, p)) continue diff --git a/skimage/io/sift.py b/skimage/io/sift.py index d80ba427..05e6b75e 100644 --- a/skimage/io/sift.py +++ b/skimage/io/sift.py @@ -42,15 +42,15 @@ def _sift_read(f, mode='SIFT'): if mode == 'SIFT': nr_features, feature_len = map(int, f.readline().split()) datatype = np.dtype([('row', float), ('column', float), - ('scale', float), ('orientation', float), - ('data', (float, feature_len))]) + ('scale', float), ('orientation', float), + ('data', (float, feature_len))]) else: mode = 'SURF' feature_len = int(f.readline()) - 1 nr_features = int(f.readline()) datatype = np.dtype([('column', float), ('row', float), - ('second_moment', (float, 3)), - ('sign', float), ('data', (float, feature_len))]) + ('second_moment', (float, 3)), + ('sign', float), ('data', (float, feature_len))]) data = np.fromfile(f, sep=' ') if data.size != nr_features * datatype.itemsize / np.dtype(float).itemsize: raise IOError("Invalid %s feature file." % mode) diff --git a/skimage/io/tests/__init__.py b/skimage/io/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/io/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 1e9fea73..c97e63ac 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -5,6 +5,7 @@ from numpy.testing import assert_raises, assert_equal, assert_allclose from skimage import data_dir from skimage.io.collection import ImageCollection, alphanumeric_key +from skimage.io import reset_plugins def test_string_split(): @@ -31,6 +32,7 @@ class TestImageCollection(): for pic in ['camera.png', 'moon.png']] def setUp(self): + reset_plugins() # Generic image collection with images of different shapes. self.images = ImageCollection(self.pattern) # Image collection with images having shapes that match. diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index 32e01bd7..81f8058a 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -14,6 +14,8 @@ try: except RuntimeError: FI_available = False +np.random.seed(0) + def setup_module(self): """The effect of the `plugin.use` call may be overridden by later imports. @@ -37,6 +39,11 @@ def test_imread(): assert img.shape == (370, 371, 3) assert all(img[274, 135] == [0, 130, 253]) +@skipif(not FI_available) +def test_imread_truncated_jpg(): + assert_raises((RuntimeError, ValueError), + sio.imread, + os.path.join(si.data_dir, 'truncated.jpg')) @skipif(not FI_available) def test_imread_uint16(): @@ -57,7 +64,7 @@ def test_imread_uint16_big_endian(): @skipif(not FI_available) def test_write_multipage(): shape = (64, 64, 64) - x = np.ones(shape, dtype=np.uint8) * np.random.random(shape) * 255 + x = np.ones(shape, dtype=np.uint8) * np.random.rand(*shape) * 255 x = x.astype(np.uint8) f = NamedTemporaryFile(suffix='.tif') fname = f.name @@ -74,19 +81,19 @@ class TestSave: f.close() sio.imsave(fname, x) y = sio.imread(fname) - assert_array_equal(x, y) + assert_array_equal(y, x) @skipif(not FI_available) def test_imsave_roundtrip(self): for shape, dtype, format in [ [(10, 10), (np.uint8, np.uint16), ('tif', 'png')], [(10, 10), (np.float32,), ('tif',)], - [(10, 10, 3), (np.uint8,), ('png',)], - [(10, 10, 4), (np.uint8,), ('png',)] + [(10, 10, 3), (np.uint8, np.uint16), ('png',)], + [(10, 10, 4), (np.uint8, np.uint16), ('png',)] ]: tests = [(d, f) for d in dtype for f in format] for d, f in tests: - x = np.ones(shape, dtype=d) * np.random.random(shape) + x = np.ones(shape, dtype=d) * np.random.rand(*shape) if not np.issubdtype(d, float): x = (x * 255).astype(d) yield self.roundtrip, d, x, f diff --git a/skimage/io/tests/test_imread.py b/skimage/io/tests/test_imread.py index afb9eac9..759e8c67 100644 --- a/skimage/io/tests/test_imread.py +++ b/skimage/io/tests/test_imread.py @@ -7,16 +7,22 @@ from tempfile import NamedTemporaryFile from skimage import data_dir from skimage.io import imread, imsave, use_plugin, reset_plugins +import skimage.io as sio try: import imread as _imread - use_plugin('imread') except ImportError: imread_available = False else: imread_available = True +def setup(): + if imread_available: + np.random.seed(0) + use_plugin('imread') + + def teardown(): reset_plugins() @@ -38,13 +44,20 @@ def test_imread_palette(): assert img.ndim == 3 +@skipif(not imread_available) +def test_imread_truncated_jpg(): + assert_raises((RuntimeError, ValueError), + sio.imread, + os.path.join(data_dir, 'truncated.jpg')) + + @skipif(not imread_available) def test_bilevel(): expected = np.zeros((10, 10), bool) expected[::2] = 1 img = imread(os.path.join(data_dir, 'checker_bilevel.png')) - assert_array_equal(img, expected) + assert_array_equal(img.astype(bool), expected) class TestSave: @@ -61,7 +74,7 @@ class TestSave: def test_imsave_roundtrip(self): dtype = np.uint8 for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: - x = np.ones(shape, dtype=dtype) * np.random.random(shape) + x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) if np.issubdtype(dtype, float): yield self.roundtrip, x, 255 diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index 14879e64..81e7830f 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -29,17 +29,5 @@ 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_mpl_imshow.py b/skimage/io/tests/test_mpl_imshow.py new file mode 100644 index 00000000..aad51048 --- /dev/null +++ b/skimage/io/tests/test_mpl_imshow.py @@ -0,0 +1,107 @@ +from __future__ import division + +import numpy as np +from skimage import io +from skimage._shared._warnings import expected_warnings +import matplotlib.pyplot as plt + + +def setup(): + io.reset_plugins() + +# test images. Note that they don't have their full range for their dtype, +# but we still expect the display range to equal the full dtype range. +im8 = np.array([[0, 64], [128, 240]], np.uint8) +im16 = im8.astype(np.uint16) * 256 +im64 = im8.astype(np.uint64) +imf = im8 / 255 +im_lo = imf / 1000 +im_hi = imf + 10 + + + +def n_subplots(ax_im): + """Return the number of subplots in the figure containing an ``AxesImage``. + + Parameters + ---------- + ax_im : matplotlib.pyplot.AxesImage object + The input ``AxesImage``. + + Returns + ------- + n : int + The number of subplots in the corresponding figure. + + Notes + ----- + This function is intended to check whether a colorbar was drawn, in + which case two subplots are expected. For standard imshows, one + subplot is expected. + """ + return len(ax_im.get_figure().get_axes()) + + +def test_uint8(): + ax_im = io.imshow(im8) + assert ax_im.cmap.name == 'gray' + assert ax_im.get_clim() == (0, 255) + # check that no colorbar was created + assert n_subplots(ax_im) == 1 + assert ax_im.colorbar is None + + +def test_uint16(): + ax_im = io.imshow(im16) + assert ax_im.cmap.name == 'gray' + assert ax_im.get_clim() == (0, 65535) + assert n_subplots(ax_im) == 1 + assert ax_im.colorbar is None + + +def test_float(): + ax_im = io.imshow(imf) + assert ax_im.cmap.name == 'gray' + assert ax_im.get_clim() == (0, 1) + assert n_subplots(ax_im) == 1 + assert ax_im.colorbar is None + + +def test_low_dynamic_range(): + with expected_warnings(["Low image dynamic range"]): + ax_im = io.imshow(im_lo) + assert ax_im.get_clim() == (im_lo.min(), im_lo.max()) + # check that a colorbar was created + assert n_subplots(ax_im) == 2 + assert ax_im.colorbar is not None + + +def test_outside_standard_range(): + plt.figure() + with expected_warnings(["out of standard range"]): + ax_im = io.imshow(im_hi) + assert ax_im.get_clim() == (im_hi.min(), im_hi.max()) + assert n_subplots(ax_im) == 2 + assert ax_im.colorbar is not None + + +def test_nonstandard_type(): + plt.figure() + with expected_warnings(["Non-standard image type"]): + ax_im = io.imshow(im64) + assert ax_im.get_clim() == (im64.min(), im64.max()) + assert n_subplots(ax_im) == 2 + assert ax_im.colorbar is not None + + +def test_signed_image(): + plt.figure() + im_signed = np.array([[-0.5, -0.2], [0.1, 0.4]]) + ax_im = io.imshow(im_signed) + assert ax_im.get_clim() == (-0.5, 0.5) + assert n_subplots(ax_im) == 2 + assert ax_im.colorbar is not None + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/io/tests/test_multi_image.py b/skimage/io/tests/test_multi_image.py index ebaa71dc..04a51ec6 100644 --- a/skimage/io/tests/test_multi_image.py +++ b/skimage/io/tests/test_multi_image.py @@ -1,18 +1,10 @@ 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 +from skimage.io.collection import MultiImage, ImageCollection import six @@ -22,46 +14,78 @@ 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')) + paths = [os.path.join(data_dir, 'multipage.tif'), + os.path.join(data_dir, 'no_time_for_that.gif')] + self.imgs = [MultiImage(paths[0]), + MultiImage(paths[0], conserve_memory=False), + MultiImage(paths[1]), + MultiImage(paths[1], conserve_memory=False), + ImageCollection(paths[0]), + ImageCollection(paths[1], conserve_memory=False), + ImageCollection('%s:%s' % (paths[0], paths[1]))] - @skipif(not PIL_available) def test_len(self): - assert len(self.img) == 2 + assert len(self.imgs[0]) == len(self.imgs[1]) == 2 + assert len(self.imgs[2]) == len(self.imgs[3]) == 24 + assert len(self.imgs[4]) == 2 + assert len(self.imgs[5]) == 24 + assert len(self.imgs[6]) == 26, len(self.imgs[6]) + + def test_slicing(self): + img = self.imgs[-1] + assert type(img[:]) is ImageCollection + assert len(img[:]) == 26, len(img[:]) + assert len(img[:1]) == 1 + assert len(img[1:]) == 25 + assert_allclose(img[0], img[:1][0]) + assert_allclose(img[1], img[1:][0]) + assert_allclose(img[-1], img[::-1][0]) + assert_allclose(img[0], img[::-1][-1]) - @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]) + for img in self.imgs: + num = len(img) - # 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) + for i in range(-num, num): + assert type(img[i]) is np.ndarray + assert_allclose(img[0], img[-num]) + + assert_raises(AssertionError, + assert_allclose, + img[0], img[1]) + + # assert_raises expects a callable, hence this thin wrapper function. + def return_img(n): + return 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) + for img in self.imgs: + if isinstance(img, ImageCollection): + continue - def set_filename(f): - self.img.filename = f - assert_raises(AttributeError, set_filename, 'newfile') + assert isinstance(img.filename, six.string_types) + + def set_filename(f): + 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) + for img in self.imgs: + assert isinstance(img.conserve_memory, bool) - def set_mem(val): - self.img.conserve_memory = val - assert_raises(AttributeError, set_mem, True) + def set_mem(val): + 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) + for img in self.imgs: + if img[0].shape != img[-1].shape: + assert_raises(ValueError, img.concatenate) + continue + array = img.concatenate() + assert_equal(array.shape, (len(img),) + img[0].shape) if __name__ == "__main__": diff --git a/skimage/io/tests/test_null.py b/skimage/io/tests/test_null.py deleted file mode 100644 index 56f5df89..00000000 --- a/skimage/io/tests/test_null.py +++ /dev/null @@ -1,50 +0,0 @@ -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 243d2fd1..fc28ce89 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -1,25 +1,28 @@ import os.path import numpy as np -from numpy.testing import * -from numpy.testing.decorators import skipif +from numpy.testing import ( + assert_array_equal, assert_array_almost_equal, assert_raises, + assert_allclose, run_module_suite) from tempfile import NamedTemporaryFile -from skimage import data_dir -from skimage.io import (imread, imsave, use_plugin, reset_plugins, +from ... import data_dir +from .. import (imread, imsave, use_plugin, reset_plugins, Image as ioImage) +from ..._shared.testing import mono_check, color_check +from ..._shared._warnings import expected_warnings from six import BytesIO +from PIL import Image +from .._plugins.pil_plugin import ( + pil_to_ndarray, ndarray_to_pil, _palette_is_grayscale) +from ...measure import structural_similarity as ssim +from ...color import rgb2lab -try: - from PIL import Image - from skimage.io._plugins.pil_plugin import _palette_is_grayscale + +def setup(): use_plugin('pil') -except ImportError: - PIL_available = False -else: - PIL_available = True def teardown(): @@ -37,8 +40,6 @@ def setup_module(self): pass - -@skipif(not PIL_available) def test_imread_flatten(): # a color image is flattened img = imread(os.path.join(data_dir, 'color.png'), flatten=True) @@ -49,7 +50,6 @@ def test_imread_flatten(): assert np.sctype2char(img.dtype) in np.typecodes['AllInteger'] -@skipif(not PIL_available) def test_imread_palette(): img = imread(os.path.join(data_dir, 'palette_gray.png')) assert img.ndim == 2 @@ -57,7 +57,6 @@ def test_imread_palette(): assert img.ndim == 3 -@skipif(not PIL_available) def test_palette_is_gray(): gray = Image.open(os.path.join(data_dir, 'palette_gray.png')) assert _palette_is_grayscale(gray) @@ -65,7 +64,6 @@ def test_palette_is_gray(): assert not _palette_is_grayscale(color) -@skipif(not PIL_available) def test_bilevel(): expected = np.zeros((10, 10)) expected[::2] = 255 @@ -74,7 +72,6 @@ def test_bilevel(): assert_array_equal(img, expected) -@skipif(not PIL_available) def test_imread_uint16(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16.tif')) @@ -82,7 +79,6 @@ def test_imread_uint16(): assert_array_almost_equal(img, expected) -@skipif(not PIL_available) def test_repr_png(): img_path = os.path.join(data_dir, 'camera.png') original_img = ioImage(imread(img_path)) @@ -96,47 +92,63 @@ def test_repr_png(): assert np.all(original_img == round_trip) -# Big endian images not correctly loaded for PIL < 1.1.7 -# Renable test when PIL 1.1.7 is more common. -@skipif(True) +def test_imread_truncated_jpg(): + assert_raises((IOError, ValueError), imread, + os.path.join(data_dir, 'truncated.jpg')) + + def test_imread_uint16_big_endian(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16B.tif')) - assert img.dtype == np.dtype('>u2') + assert img.dtype == np.uint16 assert_array_almost_equal(img, expected) class TestSave: - def roundtrip(self, dtype, x, scaling=1): + def roundtrip_file(self, x): f = NamedTemporaryFile(suffix='.png') fname = f.name f.close() imsave(fname, x) y = imread(fname) + return y + def roundtrip_pil_image(self, x): + pil_image = ndarray_to_pil(x) + y = pil_to_ndarray(pil_image) + return y + + def verify_roundtrip(self, dtype, x, y, scaling=1): assert_array_almost_equal((x * scaling).astype(np.int32), y) - @skipif(not PIL_available) - def test_imsave_roundtrip(self): + def verify_imsave_roundtrip(self, roundtrip_function): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: for dtype in (np.uint8, np.uint16, np.float32, np.float64): - x = np.ones(shape, dtype=dtype) * np.random.random(shape) + x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) if np.issubdtype(dtype, float): - yield self.roundtrip, dtype, x, 255 + yield (self.verify_roundtrip, dtype, x, + roundtrip_function(x), 255) else: x = (x * 255).astype(dtype) - yield self.roundtrip, dtype, x + yield (self.verify_roundtrip, dtype, x, + roundtrip_function(x)) + + def test_imsave_roundtrip_file(self): + self.verify_imsave_roundtrip(self.roundtrip_file) + + def test_imsave_roundtrip_pil_image(self): + self.verify_imsave_roundtrip(self.roundtrip_pil_image) -@skipif(not PIL_available) def test_imsave_filelike(): shape = (2, 2) image = np.zeros(shape) s = BytesIO() # save to file-like object - imsave(s, image) + with expected_warnings(['precision loss|unclosed file']): + imsave(s, image) # read from file-like object s.seek(0) @@ -145,5 +157,81 @@ def test_imsave_filelike(): assert_allclose(out, image) +def test_imexport_imimport(): + shape = (2, 2) + image = np.zeros(shape) + with expected_warnings(['precision loss']): + pil_image = ndarray_to_pil(image) + out = pil_to_ndarray(pil_image) + assert out.shape == shape + + +def test_all_color(): + color_check('pil') + color_check('pil', 'bmp') + + +def test_all_mono(): + mono_check('pil') + mono_check('pil', 'tiff') + + +def test_multi_page_gif(): + img = imread(os.path.join(data_dir, 'no_time_for_that.gif')) + assert img.shape == (24, 280, 500, 3), img.shape + img2 = imread(os.path.join(data_dir, 'no_time_for_that.gif'), + img_num=5) + assert img2.shape == (280, 500, 3) + assert_allclose(img[5], img2) + + +def test_cmyk(): + ref = imread(os.path.join(data_dir, 'color.png')) + + img = Image.open(os.path.join(data_dir, 'color.png')) + img = img.convert('CMYK') + + f = NamedTemporaryFile(suffix='.jpg') + fname = f.name + f.close() + img.save(fname) + try: + img.close() + except AttributeError: # `close` not available on PIL + pass + + new = imread(fname) + + ref_lab = rgb2lab(ref) + new_lab = rgb2lab(new) + + for i in range(3): + newi = np.ascontiguousarray(new_lab[:, :, i]) + refi = np.ascontiguousarray(ref_lab[:, :, i]) + sim = ssim(refi, newi, dynamic_range=refi.max() - refi.min()) + assert sim > 0.99 + + +class TestSaveTIF: + def roundtrip(self, dtype, x): + f = NamedTemporaryFile(suffix='.tif') + fname = f.name + f.close() + imsave(fname, x) + y = imread(fname) + assert_array_equal(x, y) + + def test_imsave_roundtrip(self): + for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: + for dtype in (np.uint8, np.uint16, np.int16, np.float32, + np.float64, np.bool): + x = np.random.rand(*shape) + + if not np.issubdtype(dtype, float) and not dtype == np.bool: + x = (x * np.iinfo(dtype).max).astype(dtype) + else: + x = x.astype(dtype) + yield self.roundtrip, dtype, x + if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/test_plugin.py b/skimage/io/tests/test_plugin.py index 06f3a600..00dd5b15 100644 --- a/skimage/io/tests/test_plugin.py +++ b/skimage/io/tests/test_plugin.py @@ -4,21 +4,10 @@ from numpy.testing import assert_equal, raises from skimage import io from skimage.io import manage_plugins -from numpy.testing.decorators import skipif -try: - io.use_plugin('pil') - PIL_available = True - priority_plugin = 'pil' -except ImportError: - PIL_available = False -try: - io.use_plugin('freeimage') - FI_available = True - priority_plugin = 'freeimage' -except RuntimeError: - FI_available = False +io.use_plugin('pil') +priority_plugin = 'pil' def setup_module(): @@ -65,7 +54,6 @@ 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] @@ -76,7 +64,6 @@ def test_use_priority(): 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] @@ -106,28 +93,31 @@ def test_available(): def test_load_preferred_plugins_all(): - from skimage.io._plugins import null_plugin + from skimage.io._plugins import pil_plugin, matplotlib_plugin with protect_preferred_plugins(): - manage_plugins.preferred_plugins = {'all': ['null']} + manage_plugins.preferred_plugins = {'all': ['pil'], + 'imshow': ['matplotlib']} manage_plugins.reset_plugins() - for plugin_type in ('imread', 'imsave', 'imshow'): + for plugin_type in ('imread', 'imsave'): plug, func = manage_plugins.plugin_store[plugin_type][0] - assert func == getattr(null_plugin, plugin_type) + assert func == getattr(pil_plugin, plugin_type) + plug, func = manage_plugins.plugin_store['imshow'][0] + assert func == getattr(matplotlib_plugin, 'imshow') def test_load_preferred_plugins_imread(): - from skimage.io._plugins import null_plugin + from skimage.io._plugins import pil_plugin, matplotlib_plugin with protect_preferred_plugins(): - manage_plugins.preferred_plugins['imread'] = ['null'] + manage_plugins.preferred_plugins['imread'] = ['pil'] manage_plugins.reset_plugins() plug, func = manage_plugins.plugin_store['imread'][0] - assert func == null_plugin.imread + assert func == pil_plugin.imread plug, func = manage_plugins.plugin_store['imshow'][0] - assert func != null_plugin.imshow + assert func == matplotlib_plugin.imshow, func.__module__ if __name__ == "__main__": diff --git a/skimage/io/tests/test_plugin_util.py b/skimage/io/tests/test_plugin_util.py index 41f28339..069622fb 100644 --- a/skimage/io/tests/test_plugin_util.py +++ b/skimage/io/tests/test_plugin_util.py @@ -1,36 +1,47 @@ from skimage.io._plugins.util import prepare_for_display, WindowManager +from skimage._shared._warnings import expected_warnings from numpy.testing import * import numpy as np +np.random.seed(0) + class TestPrepareForDisplay: def test_basic(self): - prepare_for_display(np.random.random((10, 10))) + with expected_warnings(['precision loss']): + prepare_for_display(np.random.rand(10, 10)) def test_dtype(self): - x = prepare_for_display(np.random.random((10, 15))) + with expected_warnings(['precision loss']): + x = prepare_for_display(np.random.rand(10, 15)) assert x.dtype == np.dtype(np.uint8) def test_grey(self): - x = prepare_for_display(np.arange(12, dtype=float).reshape((4, 3)) / 11) + with expected_warnings(['precision loss']): + tmp = np.arange(12, dtype=float).reshape((4, 3)) / 11 + x = prepare_for_display(tmp) assert_array_equal(x[..., 0], x[..., 2]) assert x[0, 0, 0] == 0 assert x[3, 2, 0] == 255 def test_colour(self): - prepare_for_display(np.random.random((10, 10, 3))) + with expected_warnings(['precision loss']): + prepare_for_display(np.random.rand(10, 10, 3)) def test_alpha(self): - prepare_for_display(np.random.random((10, 10, 4))) + with expected_warnings(['precision loss']): + prepare_for_display(np.random.rand(10, 10, 4)) @raises(ValueError) def test_wrong_dimensionality(self): - prepare_for_display(np.random.random((10, 10, 1, 1))) + with expected_warnings(['precision loss']): + prepare_for_display(np.random.rand(10, 10, 1, 1)) @raises(ValueError) def test_wrong_depth(self): - prepare_for_display(np.random.random((10, 10, 5))) + with expected_warnings(['precision loss']): + prepare_for_display(np.random.rand(10, 10, 5)) class TestWindowManager: diff --git a/skimage/io/tests/test_simpleitk.py b/skimage/io/tests/test_simpleitk.py index bf3d3614..e7fb1e83 100644 --- a/skimage/io/tests/test_simpleitk.py +++ b/skimage/io/tests/test_simpleitk.py @@ -1,6 +1,7 @@ import os.path import numpy as np from numpy.testing.decorators import skipif +from numpy.testing import assert_raises from tempfile import NamedTemporaryFile @@ -15,6 +16,8 @@ except ImportError: else: sitk_available = True +np.random.seed(0) + def teardown(): reset_plugins() @@ -50,6 +53,15 @@ def test_bilevel(): img = imread(os.path.join(data_dir, 'checker_bilevel.png')) np.testing.assert_array_equal(img, expected) +""" +#TODO: This test causes a Segmentation fault +@skipif(not sitk_available) +def test_imread_truncated_jpg(): + assert_raises((RuntimeError, ValueError), + imread, + os.path.join(data_dir, 'truncated.jpg')) +""" + @skipif(not sitk_available) def test_imread_uint16(): @@ -80,7 +92,7 @@ class TestSave: def test_imsave_roundtrip(self): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: for dtype in (np.uint8, np.uint16, np.float32, np.float64): - x = np.ones(shape, dtype=dtype) * np.random.random(shape) + x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) if np.issubdtype(dtype, float): yield self.roundtrip, dtype, x diff --git a/skimage/io/tests/test_tifffile.py b/skimage/io/tests/test_tifffile.py index 0f128bf1..fc958145 100644 --- a/skimage/io/tests/test_tifffile.py +++ b/skimage/io/tests/test_tifffile.py @@ -3,24 +3,22 @@ import skimage as si import skimage.io as sio import numpy as np -from numpy.testing import * -from numpy.testing.decorators import skipif +from numpy.testing import ( + assert_array_equal, assert_array_almost_equal, run_module_suite) + from tempfile import NamedTemporaryFile -try: - import skimage.io._plugins.tifffile_plugin as tf - _plugins = sio.plugin_order() - TF_available = True - sio.use_plugin('tifffile') -except ImportError: - TF_available = False +_plugins = sio.plugin_order() +sio.use_plugin('tifffile') + + +np.random.seed(0) def teardown(): sio.reset_plugins() -@skipif(not TF_available) def test_imread_uint16(): expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16.tif')) @@ -28,7 +26,6 @@ def test_imread_uint16(): assert_array_almost_equal(img, expected) -@skipif(not TF_available) def test_imread_uint16_big_endian(): expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16B.tif')) @@ -45,14 +42,16 @@ class TestSave: y = sio.imread(fname) assert_array_equal(x, y) - @skipif(not TF_available) def test_imsave_roundtrip(self): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: - for dtype in (np.uint8, np.uint16, np.float32, np.float64): - x = np.ones(shape, dtype=dtype) * np.random.random(shape) + for dtype in (np.uint8, np.uint16, np.float32, np.int16, + np.float64): + x = np.random.rand(*shape) if not np.issubdtype(dtype, float): - x = (x * 255).astype(dtype) + x = (x * np.iinfo(dtype).max).astype(dtype) + else: + x = x.astype(dtype) yield self.roundtrip, dtype, x diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index e07b9789..e2a4a51a 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -4,11 +4,12 @@ from ._marching_cubes import (marching_cubes, mesh_surface_area, from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon +from ._pnpoly import points_in_poly, grid_points_in_poly 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 +from ._ccomp import label __all__ = ['find_contours', @@ -30,4 +31,6 @@ __all__ = ['find_contours', 'mesh_surface_area', 'correct_mesh_orientation', 'profile_line', - 'label'] + 'label', + 'points_in_poly', + 'grid_points_in_poly'] diff --git a/skimage/measure/_ccomp.pxd b/skimage/measure/_ccomp.pxd index 62d21fec..98ecf15a 100644 --- a/skimage/measure/_ccomp.pxd +++ b/skimage/measure/_ccomp.pxd @@ -7,4 +7,3 @@ ctypedef cnp.intp_t DTYPE_t cdef DTYPE_t find_root(DTYPE_t *forest, DTYPE_t n) cdef void set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root) cdef void join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m) -cdef void link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node) diff --git a/skimage/measure/_ccomp.pyx b/skimage/measure/_ccomp.pyx index 26b9645a..a3b27593 100644 --- a/skimage/measure/_ccomp.pyx +++ b/skimage/measure/_ccomp.pyx @@ -24,15 +24,219 @@ See also: """ +DTYPE = np.intp + +# Short int - could be more graceful to the CPU cache +ctypedef cnp.int32_t INTS_t + +cdef struct s_shpinfo + +ctypedef s_shpinfo shape_info +ctypedef int (* fun_ravel)(int, int, int, shape_info *) + + +# For having stuff concerning background in one place +ctypedef struct bginfo: + ## The value in the image (i.e. not the label!) that identifies + ## the background. + DTYPE_t background_val + DTYPE_t background_node + ## Identification of the background in the labelled image + DTYPE_t background_label + + +cdef void get_bginfo(background_val, bginfo *ret) except *: + if background_val is None: + warnings.warn(DeprecationWarning( + 'The default value for `background` will change to 0 in v0.12' + )) + ret.background_val = -1 + else: + ret.background_val = background_val + + # The node -999 doesn't exist, it will get subsituted by a meaningful value + # upon the first background pixel occurence + ret.background_node = -999 + ret.background_label = -1 + + +# A pixel has neighbors that have already been scanned. +# In the paper, the pixel is denoted by E and its neighbors: +# +# z=1 z=0 x +# ----------------------> +# | A B C F G H +# | D E . I J K +# | . . . L M N +# | +# y V +# +# D_ea represents offset of A from E etc. - see the definition of +# get_shape_info +cdef enum: + # the 0D neighbor + # D_ee, # We don't need D_ee + # the 1D neighbor + D_ed, + # 2D neighbors + D_ea, D_eb, D_ec, + # 3D neighbors + D_ef, D_eg, D_eh, D_ei, D_ej, D_ek, D_el, D_em, D_en, + D_COUNT + + +# Structure for centralised access to shape data +# Contains information related to the shape of the input array +cdef struct s_shpinfo: + INTS_t x + INTS_t y + INTS_t z + + # Number of elements + DTYPE_t numels + # Number of of the input array + INTS_t ndim + + # Offsets between elements recalculated to linear index increments + # DEX[D_ea] is offset between E and A (i.e. to the point to upper left) + # The name DEX is supposed to evoke DE., where . = A, B, C, D, F etc. + INTS_t DEX[D_COUNT] + + # Function pointer to a function that recalculates multiindex to linear + # index. Heavily depends on dimensions of the input array. + fun_ravel ravel_index + + +cdef void get_shape_info(inarr_shape, shape_info *res) except *: + """ + Precalculates all the needed data from the input array shape + and stores them in the shape_info struct. + """ + res.y = 1 + res.z = 1 + res.ravel_index = ravel_index2D + # A shape (3, 1, 4) would make the algorithm crash, but the corresponding + # good_shape (i.e. the array with axis swapped) (1, 3, 4) is OK. + # Having an axis length of 1 when an axis on the left is longer than 1 + # (in this case, it has length of 3) is NOT ALLOWED. + good_shape = tuple(sorted(inarr_shape)) + + res.ndim = len(inarr_shape) + if res.ndim == 1: + res.x = inarr_shape[0] + res.ravel_index = ravel_index1D + elif res.ndim == 2: + res.x = inarr_shape[1] + res.y = inarr_shape[0] + res.ravel_index = ravel_index2D + if res.x == 1 and res.y > 1: + # Should not occur, but better be safe than sorry + raise ValueError( + "Swap axis of your %s array so it has a %s shape" + % (inarr_shape, good_shape)) + elif res.ndim == 3: + res.x = inarr_shape[2] + res.y = inarr_shape[1] + res.z = inarr_shape[0] + res.ravel_index = ravel_index3D + if ((res.x == 1 and res.y > 1) + or res.y == 1 and res.z > 1): + # Should not occur, but better be safe than sorry + raise ValueError( + "Swap axes of your %s array so it has a %s shape" + % (inarr_shape, good_shape)) + else: + raise NotImplementedError( + "Only for images of dimension 1-3 are supported, got a %sD one" + % res.ndim) + + res.numels = res.x * res.y * res.z + + # When reading this for the first time, look at the diagram by the enum + # definition above (keyword D_ee) + # Difference between E and G is (x=0, y=-1, z=-1), E and A (-1, -1, 0) etc. + # Here, it is recalculated to linear (raveled) indices of flattened arrays + # with their last (=contiguous) dimension is x. + + # So now the 1st (needed for 1D, 2D and 3D) part, y = 1, z = 1 + res.DEX[D_ed] = -1 + + # Not needed, just for illustration + # res.DEX[D_ee] = 0 + + # So now the 2nd (needed for 2D and 3D) part, y = 0, z = 1 + res.DEX[D_ea] = res.ravel_index(-1, -1, 0, res) + res.DEX[D_eb] = res.DEX[D_ea] + 1 + res.DEX[D_ec] = res.DEX[D_eb] + 1 + + # And now the 3rd (needed only for 3D) part, z = 0 + res.DEX[D_ef] = res.ravel_index(-1, -1, -1, res) + res.DEX[D_eg] = res.DEX[D_ef] + 1 + res.DEX[D_eh] = res.DEX[D_ef] + 2 + res.DEX[D_ei] = res.DEX[D_ef] - res.DEX[D_eb] # DEX[D_eb] = one row up, remember? + res.DEX[D_ej] = res.DEX[D_ei] + 1 + res.DEX[D_ek] = res.DEX[D_ei] + 2 + res.DEX[D_el] = res.DEX[D_ei] - res.DEX[D_eb] + res.DEX[D_em] = res.DEX[D_el] + 1 + res.DEX[D_en] = res.DEX[D_el] + 2 + + +cdef inline void join_trees_wrapper(DTYPE_t *data_p, DTYPE_t *forest_p, + DTYPE_t rindex, INTS_t idxdiff): + if data_p[rindex] == data_p[rindex + idxdiff]: + join_trees(forest_p, rindex, rindex + idxdiff) + + +cdef int ravel_index1D(int x, int y, int z, shape_info *shapeinfo): + """ + Ravel index of a 1D array - trivial. y and z are ignored. + """ + return x + + +cdef int ravel_index2D(int x, int y, int z, shape_info *shapeinfo): + """ + Ravel index of a 2D array. z is ignored + """ + cdef int ret = x + y * shapeinfo.x + return ret + + +cdef int ravel_index3D(int x, int y, int z, shape_info *shapeinfo): + """ + Ravel index of a 3D array + """ + cdef int ret = x + y * shapeinfo.x + z * shapeinfo.y * shapeinfo.x + return ret + + # Tree operations implemented by an array as described in Wu et al. # The term "forest" is used to indicate an array that stores one or more trees -DTYPE = np.intp - +# Consider a following tree: +# +# 5 ----> 3 ----> 2 ----> 1 <---- 6 <---- 7 +# | | +# 4 >----/ \----< 8 <---- 9 +# +# The vertices are a unique number, so the tree can be represented by an +# array where a the tuple (index, array[index]) represents an edge, +# so for our example, array[2] == 1, array[7] == 6 and array[1] == 1, because +# 1 is the root. +# Last but not least, one array can hold more than one tree as long as their +# indices are different. It is the case in this algorithm, so for that reason +# the array is referred to as the "forest" = multiple trees next to each +# other. +# +# In this algorithm, there are as many indices as there are elements in the +# array to label and array[x] == x for all x. As the labelling progresses, +# equivalence between so-called provisional (i.e. not final) labels is +# discovered and trees begin to surface. +# When we found out that label 5 and 3 are the same, we assign array[5] = 3. cdef DTYPE_t find_root(DTYPE_t *forest, DTYPE_t n): """Find the root of node n. - + Given the example above, for any integer from 1 to 9, 1 is always returned """ cdef DTYPE_t root = n while (forest[root] < root): @@ -43,7 +247,10 @@ cdef DTYPE_t find_root(DTYPE_t *forest, DTYPE_t n): cdef inline void set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root): """ Set all nodes on a path to point to new_root. - + Given the example above, given n=9, root=6, it would "reconnect" the tree. + so forest[9] = 6 and forest[8] = 6 + The ultimate goal is that all tree nodes point to the real root, + which is element 1 in this case. """ cdef DTYPE_t j while (forest[n] < n): @@ -56,12 +263,17 @@ cdef inline void set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root): cdef inline void join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m): """Join two trees containing nodes n and m. - + If we imagine that in the example tree, the root 1 is not known, we + rather have two disjoint trees with roots 2 and 6. + Joining them would mean that all elements of both trees become connected + to the element 2, so forest[9] == 2, forest[6] == 2 etc. + However, when the relationship between 1 and 2 can still be discovered later. """ - cdef DTYPE_t root = find_root(forest, n) + cdef DTYPE_t root cdef DTYPE_t root_m if (n != m): + root = find_root(forest, n) root_m = find_root(forest, m) if (root > root_m): @@ -71,30 +283,99 @@ cdef inline void join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m): set_root(forest, m, root) -cdef inline void link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node): +def _get_swaps(shp): """ - Link a node to the background node. + What axes to swap if we want to convert an illegal array shape + to a legal one. + Args: + shp (tuple-like): The array shape + + Returns: + list: List of tuples to be passed to np.swapaxes """ - if background_node[0] == -999: - background_node[0] = n + shp = np.array(shp) + swaps = [] - join_trees(forest, n, background_node[0]) + # Dimensions where the array is "flat" + ones = np.where(shp == 1)[0][::-1] + # The other dimensions + bigs = np.where(shp > 1)[0] + # We now go from opposite directions and swap axes if an index of a flat + # axis is higher than the one of a thick axis + for one, big in zip(ones, bigs): + if one < big: + # We are ordered already + break + else: + swaps.append((one, big)) + # TODO: Add more swaps so the array is longer along x and shorter along y + return swaps + + +def _apply_swaps(arr, swaps): + arr2 = arr + for one, two in swaps: + arr2 = arr.swapaxes(one, two) + return arr2 + + +def reshape_array(arr): + """ + "Rotates" the array so it gains a shape that the algorithm can work with. + An illegal shape is (3, 1, 4), and correct one is (1, 3, 4) or (1, 4, 3). + The point is to have all 1s of the shape at the beginning, not in the + middle of the shape tuple. + + Note: The greater-than-one shape component should go from greatest to + lowest numbers since it is more friendly to the CPU cache (so (1, 3, 4) is + less optimal than (1, 4, 3). Keyword to this is "memory spatial locality" + + Args: + arr (np.ndarray): The array we want to fix + + Returns: + tuple (result, swaps): The result is the "fixed" array and a record + of what has been done with it. + """ + swaps = _get_swaps(arr.shape) + reshaped = _apply_swaps(arr, swaps) + return reshaped, swaps + + +def undo_reshape_array(arr, swaps): + """ + Does the opposite of what :func:`reshape_array` does + + Args: + arr (np.ndarray): The array to "revert" + swaps (list): The second result of :func:`reshape_array` + + Returns: + np.ndarray: The array of the original shape + """ + # It is safer to undo axes swaps in the opposite order + # than the application order + reshaped = _apply_swaps(arr, swaps[::-1]) + return reshaped # Connected components search as described in Fiorio et al. -def label(input, DTYPE_t neighbors=8, background=None, return_num=False): - """Label connected regions of an integer array. +def label(input, neighbors=None, background=None, return_num=False, + connectivity=None): + r"""Label connected regions of an integer array. Two pixels are connected when they are neighbors and have the same value. - They can be neighbors either in a 4- or 8-connected sense:: + In 2D, they can be neighbors either in a 1- or 2-connected sense. + The value refers to the maximum number of orthogonal hops to consider a + pixel/voxel a neighbor:: - 4-connectivity 8-connectivity + 1-connectivity 2-connectivity diagonal connection close-up - [ ] [ ] [ ] [ ] - | \ | / - [ ]--[ ]--[ ] [ ]--[ ]--[ ] - | / | \\ + [ ] [ ] [ ] [ ] [ ] + | \ | / | <- hop 2 + [ ]--[x]--[ ] [ ]--[x]--[ ] [x]--[ ] + | / | \ hop 1 [ ] [ ] [ ] [ ] Parameters @@ -102,13 +383,20 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): input : ndarray of dtype int Image to label. neighbors : {4, 8}, int, optional - Whether to use 4- or 8-connectivity. + Whether to use 4- or 8-"connectivity". + In 3D, 4-"connectivity" means connected pixels have to share face, + whereas with 8-"connectivity", they have to share only edge or vertex. + **Deprecated, use ``connectivity`` instead.** background : int, optional Consider all pixels with this value as background pixels, and label 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. + connectivity : int, optional + Maximum number of orthogonal hops to consider a pixel/voxel + as a neighbor. + Accepted values are ranging from 1 to input.ndim. Returns ------- @@ -121,18 +409,19 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): Examples -------- + >>> import numpy as np >>> x = np.eye(3).astype(int) >>> print(x) [[1 0 0] [0 1 0] [0 0 1]] - - >>> print(m.label(x, neighbors=4)) + >>> from skimage.measure import label + >>> print(label(x, connectivity=1)) [[0 1 1] [2 3 1] [2 2 4]] - >>> print(m.label(x, neighbors=8)) + >>> print(label(x, connectivity=2)) [[0 1 1] [1 0 1] [1 1 0]] @@ -141,100 +430,395 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): ... [1, 1, 5], ... [0, 0, 0]]) - >>> print(m.label(x, background=0)) + >>> print(label(x, background=0)) [[ 0 -1 -1] [ 0 0 1] [-1 -1 -1]] """ - cdef DTYPE_t rows = input.shape[0] - cdef DTYPE_t cols = input.shape[1] + # We have to ensure that the shape of the input can be handled by the + # algorithm the input if it is the case + input_corrected, swaps = reshape_array(input) - cdef cnp.ndarray[DTYPE_t, ndim=2] data = np.array(input, copy=True, - dtype=DTYPE) - cdef cnp.ndarray[DTYPE_t, ndim=2] forest + # Do the labelling + res, ctr = _label(input_corrected, neighbors, background, connectivity) - forest = np.arange(data.size, dtype=DTYPE).reshape((rows, cols)) + res_orig = undo_reshape_array(res, swaps) + + if return_num: + return res_orig, ctr + else: + return res_orig + + +# Connected components search as described in Fiorio et al. +def _label(input, neighbors=None, background=None, connectivity=None): + cdef cnp.ndarray[DTYPE_t, ndim=1] data + cdef cnp.ndarray[DTYPE_t, ndim=1] forest + + # Having data a 2D array slows down access considerably using linear + # indices even when using the data_p pointer :-( + data = np.copy(input.flatten().astype(DTYPE)) + forest = np.arange(data.size, dtype=DTYPE) cdef DTYPE_t *forest_p = forest.data cdef DTYPE_t *data_p = data.data - cdef DTYPE_t i, j + cdef shape_info shapeinfo + cdef bginfo bg - cdef DTYPE_t background_val + get_shape_info(input.shape, &shapeinfo) + get_bginfo(background, &bg) - 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 + if neighbors is None and connectivity is None: + # use the full connectivity by default + connectivity = input.ndim + elif neighbors is not None: + DeprecationWarning("The argument 'neighbors' is deprecated, use " + "'connectivity' instead") + # backwards-compatible neighbors recalc to connectivity, + if neighbors == 4: + connectivity = 1 + elif neighbors == 8: + connectivity = input.ndim + else: + raise ValueError("Neighbors must be either 4 or 8, got '%d'.\n" + % neighbors) - cdef DTYPE_t background_node = -999 + if not 1 <= connectivity <= input.ndim: + raise ValueError( + "Connectivity below 1 or above %d is illegal." + % input.ndim) - if neighbors != 4 and neighbors != 8: - raise ValueError('Neighbors must be either 4 or 8.') - - # Initialize the first row - if data[0, 0] == background_val: - link_bg(forest_p, 0, &background_node) - - for j in range(1, cols): - if data[0, j] == background_val: - link_bg(forest_p, j, &background_node) - - if data[0, j] == data[0, j-1]: - join_trees(forest_p, j, j-1) - - for i in range(1, rows): - # Handle the first column - if data[i, 0] == background_val: - link_bg(forest_p, i * cols, &background_node) - - if data[i, 0] == data[i-1, 0]: - join_trees(forest_p, i*cols, (i-1)*cols) - - if neighbors == 8: - if data[i, 0] == data[i-1, 1]: - join_trees(forest_p, i*cols, (i-1)*cols + 1) - - for j in range(1, cols): - if data[i, j] == background_val: - link_bg(forest_p, i * cols + j, &background_node) - - if neighbors == 8: - if data[i, j] == data[i-1, j-1]: - join_trees(forest_p, i*cols + j, (i-1)*cols + j - 1) - - if data[i, j] == data[i-1, j]: - join_trees(forest_p, i*cols + j, (i-1)*cols + j) - - if neighbors == 8: - if j < cols - 1: - if data[i, j] == data[i - 1, j + 1]: - join_trees(forest_p, i*cols + j, (i-1)*cols + j + 1) - - if data[i, j] == data[i, j-1]: - join_trees(forest_p, i*cols + j, i*cols + j - 1) + scanBG(data_p, forest_p, &shapeinfo, &bg) + # the data are treated as degenerated 3D arrays if needed + # witout any performance sacrifice + scan3D(data_p, forest_p, &shapeinfo, &bg, connectivity) # Label output - cdef DTYPE_t ctr = 0 - for i in range(rows): - for j in range(cols): - if (i*cols + j) == background_node: - data[i, j] = -1 - elif (i*cols + j) == forest[i, j]: - data[i, j] = ctr - ctr = ctr + 1 - else: - data[i, j] = data_p[forest[i, j]] + cdef DTYPE_t ctr + ctr = resolve_labels(data_p, forest_p, &shapeinfo, &bg) # Work around a bug in ndimage's type checking on 32-bit platforms if data.dtype == np.int32: data = data.view(np.int32) - if return_num: - return data, ctr - else: - return data + res = data.reshape(input.shape) + + return res, ctr + + +cdef DTYPE_t resolve_labels(DTYPE_t *data_p, DTYPE_t *forest_p, + shape_info *shapeinfo, bginfo *bg): + """ + We iterate through the provisional labels and assign final labels based on + our knowledge of prov. labels relationship. + We also track how many distinct final labels we have. + """ + cdef DTYPE_t counter = bg.background_label + 1, i + + for i in range(shapeinfo.numels): + if i == bg.background_node: + data_p[i] = bg.background_label + elif i == forest_p[i]: + # We have stumbled across a root which is something new to us (root + # is the LOWEST of all prov. labels that are equivalent to it) + data_p[i] = counter + counter += 1 + else: + data_p[i] = data_p[forest_p[i]] + return counter + + +cdef void scanBG(DTYPE_t *data_p, DTYPE_t *forest_p, shape_info *shapeinfo, + bginfo *bg): + """ + Settle all background pixels now and don't bother with them later. + Since this only requires one linar sweep through the array, it is fast + and it makes sense to do it separately. + + The result of this function is update of forest_p and bg parameter. + """ + cdef DTYPE_t i, bgval = bg.background_val, firstbg + # We find the provisional label of the background, which is the index of + # the first background pixel + for i in range(shapeinfo.numels): + if data_p[i] == bgval: + firstbg = i + bg.background_node = firstbg + break + + # And then we apply this provisional label to the whole background + for i in range(firstbg, shapeinfo.numels): + if data_p[i] == bgval: + forest_p[i] = firstbg + + +# Here, we work with flat arrays regardless whether the data is 1, 2 or 3D. +# The lookup to the neighbor in a 2D array is achieved by precalculating an +# offset and adding it to the index. +# The forward scan mask looks like this (the center point is actually E): +# (take a look at shape_info docs for more exhaustive info) +# A B C +# D E +# +# So if I am in the point E and want to take a look to A, I take the index of +# E and add shapeinfo.DEX[D_ea] to it and get the index of A. +# The 1D indices are "raveled" or "linear", that's where "rindex" comes from. + + +cdef void scan1D(DTYPE_t *data_p, DTYPE_t *forest_p, shape_info *shapeinfo, + bginfo *bg, DTYPE_t connectivity, DTYPE_t y, DTYPE_t z): + """ + Perform forward scan on a 1D object, usually the first row of an image + """ + # Initialize the first row + cdef DTYPE_t x, rindex, bgval = bg.background_val + cdef INTS_t *DEX = shapeinfo.DEX + rindex = shapeinfo.ravel_index(0, y, z, shapeinfo) + + for x in range(1, shapeinfo.x): + rindex += 1 + # Handle the first row + if data_p[rindex] == bgval: + # Nothing to do if we are background + continue + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) + + +cdef void scan2D(DTYPE_t *data_p, DTYPE_t *forest_p, shape_info *shapeinfo, + bginfo *bg, DTYPE_t connectivity, DTYPE_t z): + """ + Perform forward scan on a 2D array. + """ + cdef DTYPE_t x, y, rindex, bgval = bg.background_val + cdef INTS_t *DEX = shapeinfo.DEX + scan1D(data_p, forest_p, shapeinfo, bg, connectivity, 0, z) + for y in range(1, shapeinfo.y): + # BEGINNING of x = 0 + rindex = shapeinfo.ravel_index(0, y, 0, shapeinfo) + # Handle the first column + if data_p[rindex] != bgval: + # Nothing to do if we are background + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ec]) + # END of x = 0 + + for x in range(1, shapeinfo.x - 1): + # We have just moved to another column (of the same row) + # so we increment the raveled index. It will be reset when we get + # to another row, so we don't have to worry about altering it here. + rindex += 1 + if data_p[rindex] == bgval: + # Nothing to do if we are background + continue + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ea]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ec]) + + # Finally, the last column + # BEGINNING of x = max + rindex += 1 + if data_p[rindex] != bgval: + # Nothing to do if we are background + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ea]) + # END of x = max + + +cdef void scan3D(DTYPE_t *data_p, DTYPE_t *forest_p, shape_info *shapeinfo, + bginfo *bg, DTYPE_t connectivity): + """ + Perform forward scan on a 2D array. + + """ + cdef DTYPE_t x, y, z, rindex, bgval = bg.background_val + cdef INTS_t *DEX = shapeinfo.DEX + # Handle first plane + scan2D(data_p, forest_p, shapeinfo, bg, connectivity, 0) + for z in range(1, shapeinfo.z): + # Handle first row in 3D manner + # BEGINNING of y = 0 + # BEGINNING of x = 0 + rindex = shapeinfo.ravel_index(0, 0, z, shapeinfo) + if data_p[rindex] != bgval: + # Nothing to do if we are background + + # Now we have pixels below + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ek]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_em]) + if connectivity >= 3: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_en]) + # END of x = 0 + + for x in range(1, shapeinfo.x - 1): + rindex += 1 + # Handle the first row + if data_p[rindex] == bgval: + # Nothing to do if we are background + continue + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ei]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ek]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_em]) + if connectivity >= 3: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_el]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_en]) + + # BEGINNING of x = max + rindex += 1 + # Handle the last element of the first row + if data_p[rindex] != bgval: + # Nothing to do if we are background + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ei]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_em]) + if connectivity >= 3: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_el]) + # END of x = max + # END of y = 0 + + # BEGINNING of y = ... + for y in range(1, shapeinfo.y - 1): + # BEGINNING of x = 0 + rindex = shapeinfo.ravel_index(0, y, z, shapeinfo) + # Handle the first column in 3D manner + if data_p[rindex] != bgval: + # Nothing to do if we are background + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ec]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eg]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ek]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_em]) + if connectivity >= 3: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eh]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_en]) + # END of x = 0 + + # Handle the rest of columns + for x in range(1, shapeinfo.x - 1): + rindex += 1 + if data_p[rindex] == bgval: + # Nothing to do if we are background + continue + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ea]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ec]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eg]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ei]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ek]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_em]) + if connectivity >= 3: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ef]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eh]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_el]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_en]) + + # BEGINNING of x = max + rindex += 1 + if data_p[rindex] != bgval: + # Nothing to do if we are background + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ea]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eg]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ei]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_em]) + if connectivity >= 3: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ef]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_el]) + # END of x = max + # END of y = ... + + # BEGINNING of y = max + # BEGINNING of x = 0 + rindex = shapeinfo.ravel_index(0, shapeinfo.y - 1, z, shapeinfo) + # Handle the first column in 3D manner + if data_p[rindex] != bgval: + # Nothing to do if we are background + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ec]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eg]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ek]) + if connectivity >= 3: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eh]) + # END of x = 0 + + # Handle the rest of columns + for x in range(1, shapeinfo.x - 1): + rindex += 1 + if data_p[rindex] == bgval: + # Nothing to do if we are background + continue + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ea]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ec]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eg]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ei]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ek]) + if connectivity >= 3: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ef]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eh]) + + # BEGINNING of x = max + rindex += 1 + if data_p[rindex] != bgval: + # Nothing to do if we are background + + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) + + if connectivity >= 2: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ea]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eg]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ei]) + if connectivity >= 3: + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ef]) + # END of x = max + # END of y = max diff --git a/skimage/measure/_find_contours.py b/skimage/measure/_find_contours.py index 0eea9126..3e45f2cd 100755 --- a/skimage/measure/_find_contours.py +++ b/skimage/measure/_find_contours.py @@ -35,7 +35,7 @@ def find_contours(array, level, ------- contours : list of (n,2)-ndarrays Each contour is an ndarray of shape ``(n, 2)``, - consisting of n ``(x, y)`` coordinates along the contour. + consisting of n ``(row, column)`` coordinates along the contour. Notes ----- @@ -167,7 +167,10 @@ def _assemble_contours(points_iterator): head.extend(tail) # remove all traces of tail: del starts[to_point] - del ends[tail[-1]] + try: + del ends[tail[-1]] + except KeyError: + pass del contours[tail_num] # remove the old end of head and add the new end. del ends[from_point] diff --git a/skimage/measure/_label.py b/skimage/measure/_label.py index 47e8af74..e3ac6034 100644 --- a/skimage/measure/_label.py +++ b/skimage/measure/_label.py @@ -1,6 +1,7 @@ from ._ccomp import label as _label -def label(input, neighbors=8, background=None, return_num=False): - return _label(input, neighbors, background, return_num) +def label(input, neighbors=None, background=None, return_num=False, + connectivity=None): + return _label(input, neighbors, background, return_num, connectivity) label.__doc__ = _label.__doc__ diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py index 8f9dfcbc..a334fbbd 100644 --- a/skimage/measure/_marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -105,6 +105,9 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): raise ValueError("Input volume must have 3 dimensions.") if level < volume.min() or level > volume.max(): raise ValueError("Contour level must be within volume data range.") + if len(spacing) != 3: + raise ValueError("`spacing` must consist of three floats.") + volume = np.array(volume, dtype=np.float64, order="C") # Extract raw triangles using marching cubes in Cython @@ -113,14 +116,14 @@ 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_faces = _marching_cubes_cy.iterate_and_store_3d(volume, float(level), - spacing) + raw_faces = _marching_cubes_cy.iterate_and_store_3d(volume, float(level)) # 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_faces) - return np.asarray(verts), np.asarray(faces) + # Adjust for non-isotropic spacing in `verts` at time of return + return np.asarray(verts) * np.r_[spacing], np.asarray(faces) def mesh_surface_area(verts, faces): diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index 085108ab..096d502d 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -55,33 +55,21 @@ def unpack_unique_verts(list trilist): return vert_list, face_list -def iterate_and_store_3d(double[:, :, ::1] arr, double level, - tuple spacing=(1., 1., 1.)): +def iterate_and_store_3d(double[:, :, ::1] arr, double level): """Iterate across the given array in a marching-cubes fashion, looking for volumes with edges that cross 'level'. If such a volume is found, appropriate triangulations are added to a growing list of faces to be returned by this function. - If `spacing` is not provided, vertices are returned in the indexing - coordinate system (assuming all 3 spatial dimensions sampled equally). - If `spacing` is provided, vertices will be returned in volume coordinates - relative to the origin, regularly spaced as specified in each dimension. - """ if arr.shape[0] < 2 or arr.shape[1] < 2 or arr.shape[2] < 2: raise ValueError("Input array must be at least 2x2x2.") - if len(spacing) != 3: - raise ValueError("`spacing` must be (double, double, double)") cdef list face_list = [] cdef list norm_list = [] cdef Py_ssize_t n - cdef bint odd_spacing, plus_z + cdef bint plus_z plus_z = False - if [float(i) for i in spacing] == [1.0, 1.0, 1.0]: - odd_spacing = False - else: - odd_spacing = True # The plan is to iterate a 2x2x2 cube across the input array. This means # the upper-left corner of the cube needs to iterate across a sub-array @@ -107,12 +95,6 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, coords[1] = 0 coords[2] = 0 - # Extract doubles from `spacing` for speed - cdef double[3] spacing2 - spacing2[0] = spacing[0] - spacing2[1] = spacing[1] - spacing2[2] = spacing[2] - # Calculate the number of iterations we'll need cdef Py_ssize_t num_cube_steps = ((arr.shape[0] - 1) * (arr.shape[1] - 1) * @@ -120,7 +102,7 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, cdef unsigned char cube_case = 0 cdef tuple e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12 - cdef double v1, v2, v3, v4, v5, v6, v7, v8, r0, r1, c0, c1, d0, d1 + cdef double v1, v2, v3, v4, v5, v6, v7, v8 cdef Py_ssize_t x0, y0, z0, x1, y1, z1 e5, e6, e7, e8 = (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0) @@ -138,18 +120,6 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, x0, y0, z0 = coords[0], coords[1], coords[2] x1, y1, z1 = x0 + 1, y0 + 1, z0 + 1 - if odd_spacing: - # These doubles are the modified world coordinates; they are only - # calculated if non-default `spacing` provided. - r0 = coords[0] * spacing2[0] - c0 = coords[1] * spacing2[1] - d0 = coords[2] * spacing2[2] - r1 = r0 + spacing2[0] - c1 = c0 + spacing2[1] - d1 = d0 + spacing2[2] - else: - r0, c0, d0, r1, c1, d1 = x0, y0, z0, x1, y1, z1 - # We use a right-handed coordinate system, UNlike the paper, but want # to index in agreement - the coordinate adjustment takes place here. v1 = arr[x0, y0, z0] @@ -192,40 +162,24 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, e3 = e7 e4 = e8 else: - # Calculate edges normally - if odd_spacing: - e1 = r0 + _get_fraction(v1, v2, level) * spacing2[0], c0, d0 - e2 = r1, c0 + _get_fraction(v2, v3, level) * spacing2[1], d0 - e3 = r0 + _get_fraction(v4, v3, level) * spacing2[0], c1, d0 - e4 = r0, c0 + _get_fraction(v1, v4, level) * spacing2[1], d0 - else: - e1 = r0 + _get_fraction(v1, v2, level), c0, d0 - e2 = r1, c0 + _get_fraction(v2, v3, level), d0 - e3 = r0 + _get_fraction(v4, v3, level), c1, d0 - e4 = r0, c0 + _get_fraction(v1, v4, level), d0 + # Calculate these edges normally + e1 = x0 + _get_fraction(v1, v2, level), y0, z0 + e2 = x1, y0 + _get_fraction(v2, v3, level), z0 + e3 = x0 + _get_fraction(v4, v3, level), y1, z0 + e4 = x0, y0 + _get_fraction(v1, v4, level), z0 # These must be calculated at each point unless we implemented a # large, growing lookup table for all adjacent values; could save # ~30% in terms of runtime at the expense of memory usage and # much greater complexity. - if odd_spacing: - e5 = r0 + _get_fraction(v5, v6, level) * spacing2[0], c0, d1 - e6 = r1, c0 + _get_fraction(v6, v7, level) * spacing2[1], d1 - e7 = r0 + _get_fraction(v8, v7, level) * spacing2[0], c1, d1 - e8 = r0, c0 + _get_fraction(v5, v8, level) * spacing2[1], d1 - e9 = r0, c0, d0 + _get_fraction(v1, v5, level) * spacing2[2] - e10 = r1, c0, d0 + _get_fraction(v2, v6, level) * spacing2[2] - e11 = r0, c1, d0 + _get_fraction(v4, v8, level) * spacing2[2] - e12 = r1, c1, d0 + _get_fraction(v3, v7, level) * spacing2[2] - else: - e5 = r0 + _get_fraction(v5, v6, level), c0, d1 - e6 = r1, c0 + _get_fraction(v6, v7, level), d1 - e7 = r0 + _get_fraction(v8, v7, level), c1, d1 - e8 = r0, c0 + _get_fraction(v5, v8, level), d1 - e9 = r0, c0, d0 + _get_fraction(v1, v5, level) - e10 = r1, c0, d0 + _get_fraction(v2, v6, level) - e11 = r0, c1, d0 + _get_fraction(v4, v8, level) - e12 = r1, c1, d0 + _get_fraction(v3, v7, level) + e5 = x0 + _get_fraction(v5, v6, level), y0, z1 + e6 = x1, y0 + _get_fraction(v6, v7, level), z1 + e7 = x0 + _get_fraction(v8, v7, level), y1, z1 + e8 = x0, y0 + _get_fraction(v5, v8, level), z1 + e9 = x0, y0, z0 + _get_fraction(v1, v5, level) + e10 = x1, y0, z0 + _get_fraction(v2, v6, level) + e11 = x0, y1, z0 + _get_fraction(v4, v8, level) + e12 = x1, y1, z0 + _get_fraction(v3, v7, level) # Append appropriate triangles to the growing output `face_list` diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.py similarity index 69% rename from skimage/measure/_moments.pyx rename to skimage/measure/_moments.py index e6ccdb75..65b8f571 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.py @@ -1,11 +1,9 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False +# coding: utf-8 import numpy as np +from . import _moments_cy -def moments(double[:, :] image, Py_ssize_t order=3): +def moments(image, order=3): """Calculate all raw image moments up to a certain order. The following properties can be calculated from raw image moments: @@ -17,7 +15,7 @@ def moments(double[:, :] image, Py_ssize_t order=3): Parameters ---------- - image : 2D double array + image : 2D double or uint8 array Rasterized shape as image. order : int, optional Maximum order of moments. Default is 3. @@ -39,11 +37,10 @@ def moments(double[:, :] image, Py_ssize_t order=3): .. [4] http://en.wikipedia.org/wiki/Image_moment """ - return moments_central(image, 0, 0, order) + return _moments_cy.moments_central(image, 0, 0, order) -def moments_central(double[:, :] image, double cr, double cc, - Py_ssize_t order=3): +def moments_central(image, cr, cc, order=3): """Calculate all central image moments up to a certain order. Note that central moments are translation invariant but not scale and @@ -51,7 +48,7 @@ def moments_central(double[:, :] image, double cr, double cc, Parameters ---------- - image : 2D double array + image : 2D double or uint8 array Rasterized shape as image. cr : double Center row coordinate. @@ -77,17 +74,11 @@ def moments_central(double[:, :] image, double cr, double cc, .. [4] http://en.wikipedia.org/wiki/Image_moment """ - cdef Py_ssize_t p, q, r, c - cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) - for p in range(order + 1): - for q in range(order + 1): - for r in range(image.shape[0]): - for c in range(image.shape[1]): - mu[p, q] += image[r, c] * (r - cr) ** q * (c - cc) ** p - return np.asarray(mu) + + return _moments_cy.moments_central(image, cr, cc, order) -def moments_normalized(double[:, :] mu, Py_ssize_t order=3): +def moments_normalized(mu, order=3): """Calculate all normalized central image moments up to a certain order. Note that normalized central moments are translation and scale invariant @@ -117,18 +108,10 @@ def moments_normalized(double[:, :] mu, Py_ssize_t order=3): .. [4] http://en.wikipedia.org/wiki/Image_moment """ - cdef Py_ssize_t p, q - cdef double[:, ::1] nu = np.zeros((order + 1, order + 1), dtype=np.double) - for p in range(order + 1): - for q in range(order + 1): - if p + q >= 2: - nu[p, q] = mu[p, q] / mu[0, 0] ** ((p + q) / 2 + 1) - else: - nu[p, q] = np.nan - return np.asarray(nu) + return _moments_cy.moments_normalized(mu.astype(np.double), order) -def moments_hu(double[:, :] nu): +def moments_hu(nu): """Calculate Hu's set of image moments. Note that this set of moments is proofed to be translation, scale and @@ -159,23 +142,4 @@ def moments_hu(double[:, :] nu): """ - cdef double[::1] hu = np.zeros((7, ), dtype=np.double) - cdef double t0 = nu[3, 0] + nu[1, 2] - cdef double t1 = nu[2, 1] + nu[0, 3] - cdef double q0 = t0 * t0 - cdef double q1 = t1 * t1 - cdef double n4 = 4 * nu[1, 1] - cdef double s = nu[2, 0] + nu[0, 2] - cdef double d = nu[2, 0] - nu[0, 2] - hu[0] = s - hu[1] = d * d + n4 * nu[1, 1] - hu[3] = q0 + q1 - hu[5] = d * (q0 - q1) + n4 * t0 * t1 - t0 *= q0 - 3 * q1 - t1 *= 3 * q0 - q1 - q0 = nu[3, 0]- 3 * nu[1, 2] - q1 = 3 * nu[2, 1] - nu[0, 3] - hu[2] = q0 * q0 + q1 * q1 - hu[4] = q0 * t0 + q1 * t1 - hu[6] = q1 * t0 - q0 * t1 - return np.asarray(hu) + return _moments_cy.moments_hu(nu.astype(np.double)) diff --git a/skimage/measure/_moments_cy.pyx b/skimage/measure/_moments_cy.pyx new file mode 100644 index 00000000..a1d0ebde --- /dev/null +++ b/skimage/measure/_moments_cy.pyx @@ -0,0 +1,65 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +import cython + + +ctypedef fused image_t: + cython.uchar[:, :] + cython.double[:, :] + + +cpdef moments_central(image_t image, double cr, double cc, Py_ssize_t order): + cdef Py_ssize_t p, q, r, c + cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) + cdef double val, dr, dc, dcp, drq + for r in range(image.shape[0]): + dr = r - cr + for c in range(image.shape[1]): + dc = c - cc + val = image[r, c] + dcp = 1 + for p in range(order + 1): + drq = 1 + for q in range(order + 1): + mu[p, q] += val * drq * dcp + drq *= dr + dcp *= dc + return np.asarray(mu) + + +def moments_normalized(double[:, :] mu, Py_ssize_t order=3): + cdef Py_ssize_t p, q + cdef double[:, ::1] nu = np.zeros((order + 1, order + 1), dtype=np.double) + for p in range(order + 1): + for q in range(order + 1): + if p + q >= 2: + nu[p, q] = mu[p, q] / mu[0, 0] ** ((p + q) / 2 + 1) + else: + nu[p, q] = np.nan + return np.asarray(nu) + + +def moments_hu(double[:, :] nu): + cdef double[::1] hu = np.zeros((7, ), dtype=np.double) + cdef double t0 = nu[3, 0] + nu[1, 2] + cdef double t1 = nu[2, 1] + nu[0, 3] + cdef double q0 = t0 * t0 + cdef double q1 = t1 * t1 + cdef double n4 = 4 * nu[1, 1] + cdef double s = nu[2, 0] + nu[0, 2] + cdef double d = nu[2, 0] - nu[0, 2] + hu[0] = s + hu[1] = d * d + n4 * nu[1, 1] + hu[3] = q0 + q1 + hu[5] = d * (q0 - q1) + n4 * t0 * t1 + t0 *= q0 - 3 * q1 + t1 *= 3 * q0 - q1 + q0 = nu[3, 0]- 3 * nu[1, 2] + q1 = 3 * nu[2, 1] - nu[0, 3] + hu[2] = q0 * q0 + q1 * q1 + hu[4] = q0 * t0 + q1 * t1 + hu[6] = q1 * t0 - q0 * t1 + return np.asarray(hu) diff --git a/skimage/morphology/_pnpoly.pyx b/skimage/measure/_pnpoly.pyx similarity index 90% rename from skimage/morphology/_pnpoly.pyx rename to skimage/measure/_pnpoly.pyx index 12b48e5d..d3c52451 100644 --- a/skimage/morphology/_pnpoly.pyx +++ b/skimage/measure/_pnpoly.pyx @@ -5,10 +5,10 @@ import numpy as np cimport numpy as cnp -from skimage._shared.geometry cimport point_in_polygon, points_in_polygon +from .._shared.geometry cimport point_in_polygon, points_in_polygon -def grid_points_inside_poly(shape, verts): +def grid_points_in_poly(shape, verts): """Test whether points on a specified grid are inside a polygon. For each ``(r, c)`` coordinate on a grid, i.e. ``(0, 0)``, ``(0, 1)`` etc., @@ -23,6 +23,10 @@ def grid_points_inside_poly(shape, verts): or anti-clockwise. The first point may (but does not need to be) duplicated. + See Also + -------- + points_in_poly + Returns ------- mask : (M, N) ndarray of bool @@ -50,7 +54,7 @@ def grid_points_inside_poly(shape, verts): return out.view(bool) -def points_inside_poly(points, verts): +def points_in_poly(points, verts): """Test whether points lie inside a polygon. Parameters @@ -61,6 +65,10 @@ def points_inside_poly(points, verts): Vertices of the polygon, sorted either clockwise or anti-clockwise. The first point may (but does not need to be) duplicated. + See Also + -------- + grid_points_in_poly + Returns ------- mask : (N,) array of bool diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index c6e1fa03..20e7e590 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -115,21 +115,21 @@ class _RegionProperties(object): self._intensity_image = intensity_image self._cache_active = cache_active - @_cached_property + @property def area(self): return self.moments[0, 0] - @_cached_property + @property def bbox(self): return (self._slice[0].start, self._slice[1].start, self._slice[0].stop, self._slice[1].stop) - @_cached_property + @property def centroid(self): row, col = self.local_centroid return row + self._slice[0].start, col + self._slice[1].start - @_cached_property + @property def convex_area(self): return np.sum(self.convex_image) @@ -138,35 +138,35 @@ class _RegionProperties(object): from ..morphology.convex_hull import convex_hull_image return convex_hull_image(self.image) - @_cached_property + @property def coords(self): rr, cc = np.nonzero(self.image) return np.vstack((rr + self._slice[0].start, cc + self._slice[1].start)).T - @_cached_property + @property def eccentricity(self): l1, l2 = self.inertia_tensor_eigvals if l1 == 0: return 0 return sqrt(1 - l2 / l1) - @_cached_property + @property def equivalent_diameter(self): return sqrt(4 * self.moments[0, 0] / PI) - @_cached_property + @property def euler_number(self): euler_array = self.filled_image != self.image _, num = label(euler_array, neighbors=8, return_num=True) return -num + 1 - @_cached_property + @property def extent(self): rows, cols = self.image.shape return self.moments[0, 0] / (rows * cols) - @_cached_property + @property def filled_area(self): return np.sum(self.filled_image) @@ -178,10 +178,6 @@ class _RegionProperties(object): def image(self): return self._label_image[self._slice] == self.label - @_cached_property - def _image_double(self): - return self.image.astype(np.double) - @_cached_property def inertia_tensor(self): mu = self.moments_central @@ -204,49 +200,50 @@ class _RegionProperties(object): raise AttributeError('No intensity image specified.') return self._intensity_image[self._slice] * self.image - @_cached_property + @property def _intensity_image_double(self): return self.intensity_image.astype(np.double) - @_cached_property + @property def local_centroid(self): m = self.moments row = m[0, 1] / m[0, 0] col = m[1, 0] / m[0, 0] return row, col - @_cached_property + @property def max_intensity(self): return np.max(self.intensity_image[self.image]) - @_cached_property + @property def mean_intensity(self): return np.mean(self.intensity_image[self.image]) - @_cached_property + @property def min_intensity(self): return np.min(self.intensity_image[self.image]) - @_cached_property + @property def major_axis_length(self): l1, _ = self.inertia_tensor_eigvals return 4 * sqrt(l1) - @_cached_property + @property def minor_axis_length(self): _, l2 = self.inertia_tensor_eigvals return 4 * sqrt(l2) @_cached_property def moments(self): - return _moments.moments(self._image_double, 3) + return _moments.moments(self.image.astype(np.uint8), 3) @_cached_property def moments_central(self): row, col = self.local_centroid - return _moments.moments_central(self._image_double, row, col, 3) + return _moments.moments_central(self.image.astype(np.uint8), + row, col, 3) - @_cached_property + @property def moments_hu(self): return _moments.moments_hu(self.moments_normalized) @@ -254,7 +251,7 @@ class _RegionProperties(object): def moments_normalized(self): return _moments.moments_normalized(self.moments_central, 3) - @_cached_property + @property def orientation(self): a, b, b, c = self.inertia_tensor.flat b = -b @@ -266,20 +263,20 @@ class _RegionProperties(object): else: return - 0.5 * atan2(2 * b, (a - c)) - @_cached_property + @property def perimeter(self): return perimeter(self.image, 4) - @_cached_property + @property def solidity(self): return self.moments[0, 0] / np.sum(self.convex_image) - @_cached_property + @property def weighted_centroid(self): row, col = self.weighted_local_centroid return row + self._slice[0].start, col + self._slice[1].start - @_cached_property + @property def weighted_local_centroid(self): m = self.weighted_moments row = m[0, 1] / m[0, 0] @@ -296,7 +293,7 @@ class _RegionProperties(object): return _moments.moments_central(self._intensity_image_double, row, col, 3) - @_cached_property + @property def weighted_moments_hu(self): return _moments.moments_hu(self.weighted_moments_normalized) @@ -335,7 +332,7 @@ def regionprops(label_image, intensity_image=None, cache=True): Parameters ---------- label_image : (N, M) ndarray - Labeled input image. + Labeled input image. Labels with value 0 are ignored. intensity_image : (N, M) ndarray, optional Intensity image with same size as labeled image. Default is None. cache : bool, optional @@ -476,11 +473,13 @@ def regionprops(label_image, intensity_image=None, cache=True): >>> from skimage import data, util >>> from skimage.morphology import label >>> img = util.img_as_ubyte(data.coins()) > 110 - >>> label_img = label(img) + >>> label_img = label(img, connectivity=img.ndim) >>> props = regionprops(label_img) - >>> props[0].centroid # centroid of first labeled object + >>> # centroid of first labeled object + >>> props[0].centroid (22.729879860483141, 81.912285234465827) - >>> props[0]['centroid'] # centroid of first labeled object + >>> # centroid of first labeled object + >>> props[0]['centroid'] (22.729879860483141, 81.912285234465827) """ diff --git a/skimage/measure/_structural_similarity.py b/skimage/measure/_structural_similarity.py index e08c10b5..d3150667 100644 --- a/skimage/measure/_structural_similarity.py +++ b/skimage/measure/_structural_similarity.py @@ -7,6 +7,7 @@ import numpy as np from ..util.dtype import dtype_range from ..util.shape import view_as_windows + def structural_similarity(X, Y, win_size=7, gradient=False, dynamic_range=None): """Compute the mean structural similarity index between two images. @@ -28,7 +29,7 @@ def structural_similarity(X, Y, win_size=7, Returns ------- s : float - Strucutural similarity. + Structural similarity. grad : (N * N,) ndarray Gradient of the structural similarity index between X and Y. This is only returned if `gradient` is set to True. @@ -64,34 +65,32 @@ def structural_similarity(X, Y, win_size=7, uy = np.mean(np.mean(YW, axis=2), axis=2) # Compute variances var(X), var(Y) and var(X, Y) - cov_norm = 1 / (win_size**2 - 1) + cov_norm = 1 / (win_size ** 2 - 1) XWM = XW - ux[..., None, None] YWM = YW - uy[..., None, None] - vx = cov_norm * np.sum(np.sum(XWM**2, axis=2), axis=2) - vy = cov_norm * np.sum(np.sum(YWM**2, axis=2), axis=2) + vx = cov_norm * np.sum(np.sum(XWM ** 2, axis=2), axis=2) + vy = cov_norm * np.sum(np.sum(YWM ** 2, axis=2), axis=2) vxy = cov_norm * np.sum(np.sum(XWM * YWM, axis=2), axis=2) R = dynamic_range K1 = 0.01 K2 = 0.03 - C1 = (K1 * R)**2 - C2 = (K2 * R)**2 + C1 = (K1 * R) ** 2 + C2 = (K2 * R) ** 2 A1, A2, B1, B2 = (v[..., None, None] for v in (2 * ux * uy + C1, 2 * vxy + C2, - ux**2 + uy**2 + C1, + ux ** 2 + uy ** 2 + C1, vx + vy + C2)) S = np.mean((A1 * A2) / (B1 * B2)) if gradient: - local_grad = 2 / (NP * B1**2 * B2**2) * \ - ( - A1 * B1 * (B2 * XW - A2 * YW) - \ - B1 * B2 * (A2 - A1) * ux[..., None, None] + \ - A1 * A2 * (B1 - B2) * uy[..., None, None] - ) + local_grad = 2 / (NP * B1 ** 2 * B2 ** 2) * \ + (A1 * B1 * (B2 * XW - A2 * YW) - + B1 * B2 * (A2 - A1) * ux[..., None, None] + + A1 * A2 * (B1 - B2) * uy[..., None, None]) grad = np.zeros_like(X, dtype=float) OW = view_as_windows(grad, (win_size, win_size)) diff --git a/skimage/measure/block.py b/skimage/measure/block.py index aabd1a39..1691bef3 100644 --- a/skimage/measure/block.py +++ b/skimage/measure/block.py @@ -1,5 +1,5 @@ import numpy as np -from skimage.util import view_as_blocks, pad +from ..util import view_as_blocks, pad def block_reduce(image, block_size, func=np.sum, cval=0): @@ -13,8 +13,8 @@ def block_reduce(image, block_size, func=np.sum, cval=0): Array containing down-sampling integer factor along each axis. func : callable Function object which is used to calculate the return value for each - local block. This function must implement an ``axis`` parameter such as - ``numpy.sum`` or ``numpy.min``. + local block. This function must implement an ``axis`` parameter such + as ``numpy.sum`` or ``numpy.min``. cval : float Constant padding value if image is not perfectly divisible by the block size. @@ -58,6 +58,10 @@ def block_reduce(image, block_size, func=np.sum, cval=0): pad_width = [] for i in range(len(block_size)): + if block_size[i] < 1: + raise ValueError("Down-sampling factors must be >= 1. Use " + "`skimage.transform.resize` to up-sample an " + "image.") if image.shape[i] % block_size[i] != 0: after_width = block_size[i] - (image.shape[i] % block_size[i]) else: diff --git a/skimage/measure/fit.py b/skimage/measure/fit.py index 8f9ceb9c..7de90291 100644 --- a/skimage/measure/fit.py +++ b/skimage/measure/fit.py @@ -4,6 +4,9 @@ import numpy as np from scipy import optimize +_EPSILON = np.spacing(1) + + def _check_data_dim(data, dim): if data.ndim != 2 or data.shape[1] != dim: raise ValueError('Input data must have shape (N, %d).' % dim) @@ -465,9 +468,41 @@ class EllipseModel(BaseModel): return np.concatenate((x[..., None], y[..., None]), axis=t.ndim) +def _dynamic_max_trials(n_inliers, n_samples, min_samples, probability): + """Determine number trials such that at least one outlier-free subset is + sampled for the given inlier/outlier ratio. + + Parameters + ---------- + n_inliers : int + Number of inliers in the data. + n_samples : int + Total number of samples in the data. + min_samples : int + Minimum number of samples chosen randomly from original data. + probability : float + Probability (confidence) that one outlier-free sample is generated. + + Returns + ------- + trials : int + Number of trials. + + """ + inlier_ratio = n_inliers / float(n_samples) + nom = max(_EPSILON, 1 - probability) + denom = max(_EPSILON, 1 - inlier_ratio ** min_samples) + if nom == 1: + return 0 + if denom == 1: + return float('inf') + return abs(float(np.ceil(np.log(nom) / np.log(denom)))) + + def ransac(data, model_class, min_samples, residual_threshold, is_data_valid=None, is_model_valid=None, - max_trials=100, stop_sample_num=np.inf, stop_residuals_sum=0): + max_trials=100, stop_sample_num=np.inf, stop_residuals_sum=0, + stop_probability=1): """Fit a model to data with the RANSAC (random sample consensus) algorithm. RANSAC is an iterative algorithm for the robust estimation of parameters @@ -525,7 +560,19 @@ def ransac(data, model_class, min_samples, residual_threshold, stop_sample_num : int, optional Stop iteration if at least this number of inliers are found. stop_residuals_sum : float, optional - Stop iteration if sum of residuals is less equal than this threshold. + Stop iteration if sum of residuals is less than or equal to this + threshold. + stop_probability : float in range [0, 1], optional + RANSAC iteration stops if at least one outlier-free set of the + training data is sampled with ``probability >= stop_probability``, + depending on the current best model's inlier ratio and the number + of trials. This requires to generate at least N samples (trials): + + N >= log(1 - probability) / log(1 - e**m) + + where the probability (confidence) is typically set to a high value + such as 0.99, and e is the current fraction of inliers w.r.t. the + total number of samples. Returns ------- @@ -586,7 +633,8 @@ def ransac(data, model_class, min_samples, residual_threshold, Robustly estimate geometric transformation: >>> from skimage.transform import SimilarityTransform - >>> src = 100 * np.random.random((50, 2)) + >>> np.random.seed(0) + >>> src = 100 * np.random.rand(50, 2) >>> model0 = SimilarityTransform(scale=0.5, rotation=1, ... translation=(10, 20)) >>> dst = model0(src) @@ -609,19 +657,28 @@ def ransac(data, model_class, min_samples, residual_threshold, best_inlier_residuals_sum = np.inf best_inliers = None + if min_samples < 0: + raise ValueError("`min_samples` must be greater than zero") + + if max_trials < 0: + raise ValueError("`max_trials` must be greater than zero") + + if stop_probability < 0 or stop_probability > 1: + raise ValueError("`stop_probability` must be in range [0, 1]") + if not isinstance(data, list) and not isinstance(data, tuple): data = [data] # make sure data is list and not tuple, so it can be modified below data = list(data) # number of samples - N = data[0].shape[0] + num_samples = data[0].shape[0] - for _ in range(max_trials): + for num_trials in range(max_trials): # choose random sample set samples = [] - random_idxs = np.random.randint(0, N, min_samples) + random_idxs = np.random.randint(0, num_samples, min_samples) for d in data: samples.append(d[random_idxs]) @@ -659,6 +716,9 @@ def ransac(data, model_class, min_samples, residual_threshold, if ( best_inlier_num >= stop_sample_num or best_inlier_residuals_sum <= stop_residuals_sum + or num_trials + >= _dynamic_max_trials(best_inlier_num, num_samples, + min_samples, stop_probability) ): break diff --git a/skimage/measure/profile.py b/skimage/measure/profile.py index fef3fac8..15353e51 100644 --- a/skimage/measure/profile.py +++ b/skimage/measure/profile.py @@ -9,7 +9,7 @@ def profile_line(img, src, dst, linewidth=1, Parameters ---------- img : numeric array, shape (M, N[, C]) - The image, either grayscale (2D array) or multichannel + 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) @@ -109,4 +109,3 @@ def _line_profile_coordinates(src, dst, linewidth=1): 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 0fab0787..ef92450a 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -14,18 +14,21 @@ def configuration(parent_package='', top_path=None): cython(['_ccomp.pyx'], working_path=base_path) cython(['_find_contours_cy.pyx'], working_path=base_path) - cython(['_moments.pyx'], working_path=base_path) + cython(['_moments_cy.pyx'], working_path=base_path) cython(['_marching_cubes_cy.pyx'], working_path=base_path) + cython(['_pnpoly.pyx'], working_path=base_path) 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'], + config.add_extension('_moments_cy', sources=['_moments_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_marching_cubes_cy', sources=['_marching_cubes_cy.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_pnpoly', sources=['_pnpoly.c'], + include_dirs=[get_numpy_include_dirs(), '../_shared']) return config diff --git a/skimage/measure/tests/__init__.py b/skimage/measure/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/measure/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/measure/tests/test_block.py b/skimage/measure/tests/test_block.py index a8bc62a9..d8d93a96 100644 --- a/skimage/measure/tests/test_block.py +++ b/skimage/measure/tests/test_block.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import assert_array_equal +from numpy.testing import assert_equal, assert_raises from skimage.measure import block_reduce @@ -8,13 +8,13 @@ def test_block_reduce_sum(): out1 = block_reduce(image1, (2, 3)) expected1 = np.array([[ 24, 42], [ 96, 114]]) - assert_array_equal(expected1, out1) + assert_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) out2 = block_reduce(image2, (3, 3)) expected2 = np.array([[ 81, 108, 87], [174, 192, 138]]) - assert_array_equal(expected2, out2) + assert_equal(expected2, out2) def test_block_reduce_mean(): @@ -22,13 +22,13 @@ def test_block_reduce_mean(): out1 = block_reduce(image1, (2, 3), func=np.mean) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) - assert_array_equal(expected1, out1) + assert_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) out2 = block_reduce(image2, (4, 5), func=np.mean) expected2 = np.array([[14. , 10.8], [ 8.5, 5.7]]) - assert_array_equal(expected2, out2) + assert_equal(expected2, out2) def test_block_reduce_median(): @@ -36,17 +36,17 @@ def test_block_reduce_median(): out1 = block_reduce(image1, (2, 3), func=np.median) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) - assert_array_equal(expected1, out1) + assert_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) out2 = block_reduce(image2, (4, 5), func=np.median) expected2 = np.array([[ 14., 17.], [ 0., 0.]]) - assert_array_equal(expected2, out2) + assert_equal(expected2, out2) image3 = np.array([[1, 5, 5, 5], [5, 5, 5, 1000]]) out3 = block_reduce(image3, (2, 4), func=np.median) - assert_array_equal(5, out3) + assert_equal(5, out3) def test_block_reduce_min(): @@ -54,13 +54,13 @@ def test_block_reduce_min(): out1 = block_reduce(image1, (2, 3), func=np.min) expected1 = np.array([[ 0, 3], [12, 15]]) - assert_array_equal(expected1, out1) + assert_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) out2 = block_reduce(image2, (4, 5), func=np.min) expected2 = np.array([[0, 0], [0, 0]]) - assert_array_equal(expected2, out2) + assert_equal(expected2, out2) def test_block_reduce_max(): @@ -68,13 +68,20 @@ def test_block_reduce_max(): out1 = block_reduce(image1, (2, 3), func=np.max) expected1 = np.array([[ 8, 11], [20, 23]]) - assert_array_equal(expected1, out1) + assert_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) out2 = block_reduce(image2, (4, 5), func=np.max) expected2 = np.array([[28, 31], [36, 39]]) - assert_array_equal(expected2, out2) + assert_equal(expected2, out2) + + +def test_invalid_block_size(): + image = np.arange(4 * 6).reshape(4, 6) + + assert_raises(ValueError, block_reduce, image, [1, 2, 3]) + assert_raises(ValueError, block_reduce, image, [1, 0.5]) if __name__ == "__main__": diff --git a/skimage/measure/tests/test_fit.py b/skimage/measure/tests/test_fit.py index 3ef7a0f0..1b960d4b 100644 --- a/skimage/measure/tests/test_fit.py +++ b/skimage/measure/tests/test_fit.py @@ -2,6 +2,8 @@ import numpy as np from numpy.testing import assert_equal, assert_raises, assert_almost_equal from skimage.measure import LineModel, CircleModel, EllipseModel, ransac from skimage.transform import AffineTransform +from skimage.measure.fit import _dynamic_max_trials +from skimage._shared._warnings import expected_warnings def test_line_model_invalid_input(): @@ -179,7 +181,7 @@ def test_ransac_geometric(): model_est, inliers = ransac((src, dst), AffineTransform, 2, 20) # test whether estimated parameters equal original parameters - assert_almost_equal(model0._matrix, model_est._matrix) + assert_almost_equal(model0.params, model_est.params) assert np.all(np.nonzero(inliers == False)[0] == outliers) @@ -204,12 +206,54 @@ def test_ransac_is_model_valid(): assert_equal(inliers, None) +def test_ransac_dynamic_max_trials(): + # Numbers hand-calculated and confirmed on page 119 (Table 4.3) in + # Hartley, R.~I. and Zisserman, A., 2004, + # Multiple View Geometry in Computer Vision, Second Edition, + # Cambridge University Press, ISBN: 0521540518 + + # e = 0%, min_samples = X + assert_equal(_dynamic_max_trials(100, 100, 2, 0.99), 1) + + # e = 5%, min_samples = 2 + assert_equal(_dynamic_max_trials(95, 100, 2, 0.99), 2) + # e = 10%, min_samples = 2 + assert_equal(_dynamic_max_trials(90, 100, 2, 0.99), 3) + # e = 30%, min_samples = 2 + assert_equal(_dynamic_max_trials(70, 100, 2, 0.99), 7) + # e = 50%, min_samples = 2 + assert_equal(_dynamic_max_trials(50, 100, 2, 0.99), 17) + + # e = 5%, min_samples = 8 + assert_equal(_dynamic_max_trials(95, 100, 8, 0.99), 5) + # e = 10%, min_samples = 8 + assert_equal(_dynamic_max_trials(90, 100, 8, 0.99), 9) + # e = 30%, min_samples = 8 + assert_equal(_dynamic_max_trials(70, 100, 8, 0.99), 78) + # e = 50%, min_samples = 8 + assert_equal(_dynamic_max_trials(50, 100, 8, 0.99), 1177) + + # e = 0%, min_samples = 10 + assert_equal(_dynamic_max_trials(1, 100, 10, 0), 0) + assert_equal(_dynamic_max_trials(1, 100, 10, 1), float('inf')) + + +def test_ransac_invalid_input(): + assert_raises(ValueError, ransac, np.zeros((10, 2)), None, min_samples=2, + residual_threshold=0, max_trials=-1) + assert_raises(ValueError, ransac, np.zeros((10, 2)), None, min_samples=2, + residual_threshold=0, stop_probability=-1) + assert_raises(ValueError, ransac, np.zeros((10, 2)), None, min_samples=2, + residual_threshold=0, stop_probability=1.01) + + 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) + with expected_warnings(['`_params`']): + assert_equal(model.params, model._params) if __name__ == "__main__": diff --git a/skimage/morphology/tests/test_pnpoly.py b/skimage/measure/tests/test_pnpoly.py similarity index 51% rename from skimage/morphology/tests/test_pnpoly.py rename to skimage/measure/tests/test_pnpoly.py index da468b08..e98b355f 100644 --- a/skimage/morphology/tests/test_pnpoly.py +++ b/skimage/measure/tests/test_pnpoly.py @@ -1,8 +1,7 @@ import numpy as np from numpy.testing import assert_array_equal -from skimage.morphology._pnpoly import points_inside_poly, \ - grid_points_inside_poly +from skimage.measure import points_in_poly, grid_points_in_poly class test_npnpoly(): @@ -11,29 +10,29 @@ class test_npnpoly(): [0, 1], [1, 1], [1, 0]]) - assert(points_inside_poly([[0.5, 0.5]], v)[0]) - assert(not points_inside_poly([[-0.1, 0.1]], v)[0]) + assert(points_in_poly([[0.5, 0.5]], v)[0]) + assert(not points_in_poly([[-0.1, 0.1]], v)[0]) def test_triangle(self): v = np.array([[0, 0], [1, 0], [0.5, 0.75]]) - assert(points_inside_poly([[0.5, 0.7]], v)[0]) - assert(not points_inside_poly([[0.5, 0.76]], v)[0]) - assert(not points_inside_poly([[0.7, 0.5]], v)[0]) + assert(points_in_poly([[0.5, 0.7]], v)[0]) + assert(not points_in_poly([[0.5, 0.76]], v)[0]) + assert(not points_in_poly([[0.7, 0.5]], v)[0]) def test_type(self): - assert(points_inside_poly([[0, 0]], [[0, 0]]).dtype == np.bool) + assert(points_in_poly([[0, 0]], [[0, 0]]).dtype == np.bool) -def test_grid_points_inside_poly(): +def test_grid_points_in_poly(): v = np.array([[0, 0], [5, 0], [5, 5]]) expected = np.tril(np.ones((5, 5), dtype=bool)) - assert_array_equal(grid_points_inside_poly((5, 5), v), + assert_array_equal(grid_points_in_poly((5, 5), v), expected) if __name__ == "__main__": diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index 934e209e..3ac735c7 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -4,6 +4,7 @@ import numpy as np import math from skimage.measure._regionprops import regionprops, PROPS, perimeter +from skimage._shared._warnings import expected_warnings SAMPLE = np.array( @@ -125,12 +126,14 @@ def test_equiv_diameter(): def test_euler_number(): - en = regionprops(SAMPLE)[0].euler_number + with expected_warnings(['`background`|CObject type']): + en = regionprops(SAMPLE)[0].euler_number assert en == 0 SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[7, -3] = 0 - en = regionprops(SAMPLE_mod)[0].euler_number + with expected_warnings(['`background`|CObject type']): + en = regionprops(SAMPLE_mod)[0].euler_number assert en == -1 @@ -369,8 +372,9 @@ def test_equals(): 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") + with expected_warnings(['`background`|CObject type']): + 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/measure/tests/test_structural_similarity.py b/skimage/measure/tests/test_structural_similarity.py index e08f2c31..35bbeeb8 100644 --- a/skimage/measure/tests/test_structural_similarity.py +++ b/skimage/measure/tests/test_structural_similarity.py @@ -9,8 +9,8 @@ np.random.seed(1234) def test_ssim_patch_range(): N = 51 - X = (np.random.random((N, N)) * 255).astype(np.uint8) - Y = (np.random.random((N, N)) * 255).astype(np.uint8) + X = (np.random.rand(N, N) * 255).astype(np.uint8) + Y = (np.random.rand(N, N) * 255).astype(np.uint8) assert(ssim(X, Y, win_size=N) < 0.1) assert_equal(ssim(X, X, win_size=N), 1) @@ -18,8 +18,8 @@ def test_ssim_patch_range(): def test_ssim_image(): N = 100 - X = (np.random.random((N, N)) * 255).astype(np.uint8) - Y = (np.random.random((N, N)) * 255).astype(np.uint8) + X = (np.random.rand(N, N) * 255).astype(np.uint8) + Y = (np.random.rand(N, N) * 255).astype(np.uint8) S0 = ssim(X, X, win_size=3) assert_equal(S0, 1) @@ -31,8 +31,8 @@ def test_ssim_image(): # NOTE: This test is known to randomly fail on some systems (Mac OS X 10.6) def test_ssim_grad(): N = 30 - X = np.random.random((N, N)) * 255 - Y = np.random.random((N, N)) * 255 + X = np.random.rand(N, N) * 255 + Y = np.random.rand(N, N) * 255 f = ssim(X, Y, dynamic_range=255) g = ssim(X, Y, dynamic_range=255, gradient=True) @@ -44,8 +44,8 @@ def test_ssim_grad(): def test_ssim_dtype(): N = 30 - X = np.random.random((N, N)) - Y = np.random.random((N, N)) + X = np.random.rand(N, N) + Y = np.random.rand(N, N) S1 = ssim(X, Y) diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index 5b659093..9bf311c4 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -11,7 +11,7 @@ from .greyreconstruct import reconstruction from .misc import remove_small_objects from ..measure._label import label -from skimage._shared.utils import deprecated as _deprecated +from .._shared.utils import deprecated as _deprecated label = _deprecated('skimage.measure.label')(label) diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py index ddc62008..d310bad3 100644 --- a/skimage/morphology/_skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -163,23 +163,19 @@ def medial_axis(image, mask=None, return_distance=False): Parameters ---------- - - image : binary ndarray - - mask : binary ndarray, optional - If a mask is given, only those elements with a true value in `mask` - are used for computing the medial axis. - + image : binary ndarray, shape (M, N) + The image of the shape to be skeletonized. + mask : binary ndarray, shape (M, N), optional + If a mask is given, only those elements in `image` with a true + value in `mask` are used for computing the medial axis. return_distance : bool, optional If true, the distance transform is returned as well as the skeleton. Returns ------- - out : ndarray of bools Medial axis transform of the image - - dist : ndarray of ints + dist : ndarray of ints, optional Distance transform of the image (only returned if `return_distance` is True) diff --git a/skimage/morphology/_skeletonize_cy.pyx b/skimage/morphology/_skeletonize_cy.pyx index c4471a11..8dc8eb03 100644 --- a/skimage/morphology/_skeletonize_cy.pyx +++ b/skimage/morphology/_skeletonize_cy.pyx @@ -15,7 +15,6 @@ Original author: Lee Kamentsky ''' import numpy as np - cimport numpy as cnp diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 24c5c0ea..0ff5c8f7 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -1,9 +1,15 @@ -import warnings +""" +Binary morphological operations +""" import numpy as np -from scipy import ndimage +from scipy import ndimage as nd +from .misc import default_selem -def binary_erosion(image, selem, out=None): +# The default_selem decorator provides a diamond structuring element as default +# with the same dimension as the input image and size 3 along each axis. +@default_selem +def binary_erosion(image, selem=None, out=None): """Return fast binary morphological erosion of an image. This function returns the same result as greyscale erosion but performs @@ -17,35 +23,28 @@ def binary_erosion(image, selem, out=None): ---------- image : ndarray Binary input image. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray of bool + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray of bool, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. Returns ------- eroded : ndarray of bool or uint - The result of the morphological erosion with values in ``[0, 1]``. + The result of the morphological erosion taking values in + ``[False, True]``. """ - selem = (selem != 0) - selem_sum = np.sum(selem) - - if selem_sum <= 255: - conv = np.empty_like(image, dtype=np.uint8) - else: - conv = np.empty_like(image, dtype=np.uint) - - binary = (image > 0).view(np.uint8) - ndimage.convolve(binary, selem, mode='constant', cval=1, output=conv) - if out is None: - out = conv - return np.equal(conv, selem_sum, out=out) + out = np.empty(image.shape, dtype=np.bool) + nd.binary_erosion(image, structure=selem, output=out) + return out -def binary_dilation(image, selem, out=None): +@default_selem +def binary_dilation(image, selem=None, out=None): """Return fast binary morphological dilation of an image. This function returns the same result as greyscale dilation but performs @@ -60,34 +59,27 @@ def binary_dilation(image, selem, out=None): image : ndarray Binary input image. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray of bool + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray of bool, optional The array to store the result of the morphology. If None, is passed, a new array will be allocated. Returns ------- dilated : ndarray of bool or uint - The result of the morphological dilation with values in ``[0, 1]``. - + The result of the morphological dilation with values in + ``[False, True]``. """ - selem = (selem != 0) - - if np.sum(selem) <= 255: - conv = np.empty_like(image, dtype=np.uint8) - else: - conv = np.empty_like(image, dtype=np.uint) - - binary = (image > 0).view(np.uint8) - ndimage.convolve(binary, selem, mode='constant', cval=0, output=conv) - if out is None: - out = conv - return np.not_equal(conv, 0, out=out) + out = np.empty(image.shape, dtype=np.bool) + nd.binary_dilation(image, structure=selem, output=out) + return out -def binary_opening(image, selem, out=None): +@default_selem +def binary_opening(image, selem=None, out=None): """Return fast binary morphological opening of an image. This function returns the same result as greyscale opening but performs @@ -102,9 +94,10 @@ def binary_opening(image, selem, out=None): ---------- image : ndarray Binary input image. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray of bool + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray of bool, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. @@ -119,7 +112,8 @@ def binary_opening(image, selem, out=None): return out -def binary_closing(image, selem, out=None): +@default_selem +def binary_closing(image, selem=None, out=None): """Return fast binary morphological closing of an image. This function returns the same result as greyscale closing but performs @@ -134,9 +128,10 @@ def binary_closing(image, selem, out=None): ---------- image : ndarray Binary input image. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray of bool + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray of bool, optional The array to store the result of the morphology. If None, is passed, a new array will be allocated. @@ -146,7 +141,6 @@ def binary_closing(image, selem, out=None): The result of the morphological closing. """ - dilated = binary_dilation(image, selem) out = binary_erosion(dilated, selem, out=out) return out diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 4d491c3b..d248ea9c 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -11,7 +11,7 @@ from libc.stdlib cimport malloc, free def _dilate(np.uint8_t[:, :] image, np.uint8_t[:, :] selem, np.uint8_t[:, :] out=None, - char shift_x=0, char shift_y=0): + signed char shift_x=0, signed char shift_y=0): """Return greyscale morphological dilation of an image. Morphological dilation sets a pixel at (i,j) to the maximum over all pixels @@ -49,8 +49,6 @@ def _dilate(np.uint8_t[:, :] image, image = np.ascontiguousarray(image) if out is None: out = np.zeros((rows, cols), dtype=np.uint8) - else: - out = np.ascontiguousarray(out) cdef Py_ssize_t r, c, rr, cc, s, value, local_max @@ -88,7 +86,7 @@ def _dilate(np.uint8_t[:, :] image, def _erode(np.uint8_t[:, :] image, np.uint8_t[:, :] selem, np.uint8_t[:, :] out=None, - char shift_x=0, char shift_y=0): + signed char shift_x=0, signed char shift_y=0): """Return greyscale morphological erosion of an image. Morphological erosion sets a pixel at (i,j) to the minimum over all pixels @@ -125,8 +123,6 @@ def _erode(np.uint8_t[:, :] image, image = np.ascontiguousarray(image) if out is None: out = np.zeros((rows, cols), dtype=np.uint8) - else: - out = np.ascontiguousarray(out) cdef int r, c, rr, cc, s, value, local_min diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index c5b9eb3f..5926e1e0 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -1,10 +1,10 @@ __all__ = ['convex_hull_image', 'convex_hull_object'] import numpy as np -from ._pnpoly import grid_points_inside_poly +from ..measure import grid_points_in_poly from ._convex_hull import possible_hull from ..measure._label import label -from skimage.util import unique_rows +from ..util import unique_rows def convex_hull_image(image): @@ -72,7 +72,7 @@ def convex_hull_image(image): # For each pixel coordinate, check whether that pixel # lies inside the convex hull - mask = grid_points_inside_poly(image.shape[:2], v) + mask = grid_points_in_poly(image.shape[:2], v) return mask diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 13e9dfbb..fdc8381c 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -1,14 +1,134 @@ -import warnings -from skimage import img_as_ubyte - -from . import cmorph - +""" +Grayscale morphological operations +""" +import functools +import numpy as np +from scipy import ndimage as nd +from .misc import default_selem +from ..util import pad, crop __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', 'black_tophat'] -def erosion(image, selem, out=None, shift_x=False, shift_y=False): +def _shift_selem(selem, shift_x, shift_y): + """Shift the binary image `selem` in the left and/or up. + + This only affects 2D structuring elements with even number of rows + or columns. + + Parameters + ---------- + selem : 2D array, shape (M, N) + The input structuring element. + shift_x, shift_y : bool + Whether to move `selem` along each axis. + + Returns + ------- + out : 2D array, shape (M + int(shift_x), N + int(shift_y)) + The shifted structuring element. + """ + if selem.ndim > 2: + # do nothing for 3D or higher structuring elements + return selem + m, n = selem.shape + if m % 2 == 0: + extra_row = np.zeros((1, n), selem.dtype) + if shift_x: + selem = np.vstack((selem, extra_row)) + else: + selem = np.vstack((extra_row, selem)) + m += 1 + if n % 2 == 0: + extra_col = np.zeros((m, 1), selem.dtype) + if shift_y: + selem = np.hstack((selem, extra_col)) + else: + selem = np.hstack((extra_col, selem)) + return selem + + +def _invert_selem(selem): + """Change the order of the values in `selem`. + + This is a patch for the *weird* footprint inversion in + `nd.grey_morphology` [1]_. + + Parameters + ---------- + selem : array + The input structuring element. + + Returns + ------- + inverted : array, same shape and type as `selem` + The structuring element, in opposite order. + + Examples + -------- + >>> selem = np.array([[0, 0, 0], [0, 1, 1], [0, 1, 1]], np.uint8) + >>> _invert_selem(selem) + array([[1, 1, 0], + [1, 1, 0], + [0, 0, 0]], dtype=uint8) + + References + ---------- + [1] https://github.com/scipy/scipy/blob/ec20ababa400e39ac3ffc9148c01ef86d5349332/scipy/ndimage/morphology.py#L1285 + """ + inverted = selem[(slice(None, None, -1),) * selem.ndim] + return inverted + + +def pad_for_eccentric_selems(func): + """Pad input images for certain morphological operations. + + Parameters + ---------- + func : callable + A morphological function, either opening or closing, that + supports eccentric structuring elements. Its parameters must + include at least `image`, `selem`, and `out`. + + Returns + ------- + func_out : callable + The same function, but correctly padding the input image before + applying the input function. + + See Also + -------- + opening, closing. + """ + @functools.wraps(func) + def func_out(image, selem, out=None, *args, **kwargs): + pad_widths = [] + padding = False + if out is None: + out = np.empty_like(image) + for axis_len in selem.shape: + if axis_len % 2 == 0: + axis_pad_width = axis_len - 1 + padding = True + else: + axis_pad_width = 0 + pad_widths.append((axis_pad_width,) * 2) + if padding: + image = pad(image, pad_widths, mode='edge') + out_temp = np.empty_like(image) + else: + out_temp = out + out_temp = func(image, selem, out=out_temp, *args, **kwargs) + if padding: + out[:] = crop(out_temp, pad_widths) + else: + out = out_temp + return out + return func_out + +@default_selem +def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological erosion of an image. Morphological erosion sets a pixel at (i,j) to the minimum over all pixels @@ -19,20 +139,27 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): ---------- image : ndarray Image array. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + selem : ndarray, optional + The neighborhood expressed as an array of 1's and 0's. + If None, use cross-shaped structuring element (connectivity=1). + out : ndarrays, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. - shift_x, shift_y : bool + shift_x, shift_y : bool, optional shift structuring element about center point. This only affects eccentric structuring elements (i.e. selem with even numbered sides). Returns ------- - eroded : uint8 array + eroded : array, same shape as `image` The result of the morphological erosion. + Notes + ----- + For ``uint8`` (and ``uint16`` up to a certain bit-depth) data, the + lower algorithm complexity makes the `skimage.filter.rank.minimum` + function more efficient for larger images and structuring elements. + Examples -------- >>> # Erosion shrinks bright regions @@ -51,16 +178,16 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): [0, 0, 0, 0, 0]], dtype=uint8) """ - - if image is out: - raise NotImplementedError("In-place erosion not supported!") - image = img_as_ubyte(image) - selem = img_as_ubyte(selem) - return cmorph._erode(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) + selem = np.array(selem) + selem = _shift_selem(selem, shift_x, shift_y) + if out is None: + out = np.empty_like(image) + nd.grey_erosion(image, footprint=selem, output=out) + return out -def dilation(image, selem, out=None, shift_x=False, shift_y=False): +@default_selem +def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological dilation of an image. Morphological dilation sets a pixel at (i,j) to the maximum over all pixels @@ -72,20 +199,27 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): image : ndarray Image array. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray, optional The array to store the result of the morphology. If None, is passed, a new array will be allocated. - shift_x, shift_y : bool + shift_x, shift_y : bool, optional shift structuring element about center point. This only affects eccentric structuring elements (i.e. selem with even numbered sides). Returns ------- - dilated : uint8 array + dilated : uint8 array, same shape and type as `image` The result of the morphological dilation. + Notes + ----- + For `uint8` (and `uint16` up to a certain bit-depth) data, the lower + algorithm complexity makes the `skimage.filter.rank.maximum` function more + efficient for larger images and structuring elements. + Examples -------- >>> # Dilation enlarges bright regions @@ -104,16 +238,23 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): [0, 0, 0, 0, 0]], dtype=uint8) """ - - if image is out: - raise NotImplementedError("In-place dilation not supported!") - image = img_as_ubyte(image) - selem = img_as_ubyte(selem) - return cmorph._dilate(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) + selem = np.array(selem) + selem = _shift_selem(selem, shift_x, shift_y) + # Inside ndimage.grey_dilation, the structuring element is inverted, + # eg. `selem = selem[::-1, ::-1]` for 2D [1]_, for reasons unknown to + # this author (@jni). To "patch" this behaviour, we invert our own + # selem before passing it to `nd.grey_dilation`. + # [1] https://github.com/scipy/scipy/blob/ec20ababa400e39ac3ffc9148c01ef86d5349332/scipy/ndimage/morphology.py#L1285 + selem = _invert_selem(selem) + if out is None: + out = np.empty_like(image) + nd.grey_dilation(image, footprint=selem, output=out) + return out -def opening(image, selem, out=None): +@default_selem +@pad_for_eccentric_selems +def opening(image, selem=None, out=None): """Return greyscale morphological opening of an image. The morphological opening on an image is defined as an erosion followed by @@ -125,15 +266,16 @@ def opening(image, selem, out=None): ---------- image : ndarray Image array. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + selem : ndarray, optional + The neighborhood expressed as an array of 1's and 0's. + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. Returns ------- - opening : uint8 array + opening : array, same shape and type as `image` The result of the morphological opening. Examples @@ -154,17 +296,15 @@ def opening(image, selem, out=None): [0, 0, 0, 0, 0]], dtype=uint8) """ - - h, w = selem.shape - shift_x = True if (w % 2) == 0 else False - shift_y = True if (h % 2) == 0 else False - eroded = erosion(image, selem) - out = dilation(eroded, selem, out=out, shift_x=shift_x, shift_y=shift_y) + # note: shift_x, shift_y do nothing if selem side length is odd + out = dilation(eroded, selem, out=out, shift_x=True, shift_y=True) return out -def closing(image, selem, out=None): +@default_selem +@pad_for_eccentric_selems +def closing(image, selem=None, out=None): """Return greyscale morphological closing of an image. The morphological closing on an image is defined as a dilation followed by @@ -176,15 +316,16 @@ def closing(image, selem, out=None): ---------- image : ndarray Image array. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + selem : ndarray, optional + The neighborhood expressed as an array of 1's and 0's. + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray, optional The array to store the result of the morphology. If None, is passed, a new array will be allocated. Returns ------- - closing : uint8 array + closing : array, same shape and type as `image` The result of the morphological closing. Examples @@ -205,17 +346,14 @@ def closing(image, selem, out=None): [0, 0, 0, 0, 0]], dtype=uint8) """ - - h, w = selem.shape - shift_x = True if (w % 2) == 0 else False - shift_y = True if (h % 2) == 0 else False - dilated = dilation(image, selem) - out = erosion(dilated, selem, out=out, shift_x=shift_x, shift_y=shift_y) + # note: shift_x, shift_y do nothing if selem side length is odd + out = erosion(dilated, selem, out=out, shift_x=True, shift_y=True) return out -def white_tophat(image, selem, out=None): +@default_selem +def white_tophat(image, selem=None, out=None): """Return white top hat of an image. The white top hat of an image is defined as the image minus its @@ -226,15 +364,16 @@ def white_tophat(image, selem, out=None): ---------- image : ndarray Image array. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + selem : ndarray, optional + The neighborhood expressed as an array of 1's and 0's. + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. Returns ------- - opening : uint8 array + out : array, same shape and type as `image` The result of the morphological white top hat. Examples @@ -254,16 +393,20 @@ def white_tophat(image, selem, out=None): [0, 0, 1, 0, 0], [0, 0, 0, 0, 0]], dtype=uint8) - """ - if image is out: - raise NotImplementedError("Cannot perform white top hat in place.") - - out = opening(image, selem, out=out) - out = image - out + """ + selem = np.array(selem) + if out is image: + opened = opening(image, selem) + out -= opened + return out + elif out is None: + out = np.empty_like(image) + out = nd.white_tophat(image, footprint=selem, output=out) return out -def black_tophat(image, selem, out=None): +@default_selem +def black_tophat(image, selem=None, out=None): """Return black top hat of an image. The black top hat of an image is defined as its morphological closing minus @@ -275,15 +418,16 @@ def black_tophat(image, selem, out=None): ---------- image : ndarray Image array. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. Returns ------- - opening : uint8 array + opening : array, same shape and type as `image` The result of the black top filter. Examples @@ -304,10 +448,10 @@ def black_tophat(image, selem, out=None): [0, 0, 0, 0, 0]], dtype=uint8) """ - - if image is out: - raise NotImplementedError("Cannot perform white top hat in place.") - + if out is image: + original = image.copy() + else: + original = image out = closing(image, selem, out=out) - out = out - image + out -= original return out diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index cea200a4..47f1f42d 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -11,7 +11,7 @@ Original author: Lee Kamentsky """ import numpy as np -from skimage.filter._rank_order import rank_order +from ..filters._rank_order import rank_order def reconstruction(seed, mask, method='dilation', selem=None, offset=None): @@ -131,7 +131,7 @@ 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.astype(bool, copy=True) + selem = selem.astype(bool) if offset is None: if not all([d % 2 == 1 for d in selem.shape]): @@ -152,6 +152,9 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): pad_value = np.min(seed) elif method == 'erosion': pad_value = np.max(seed) + else: + raise ValueError("Reconstruction method can be one of 'erosion' " + "or 'dilation'. Got '%s'." % method) images = np.ones(dims) * pad_value images[[0] + inside_slices] = seed images[[1] + inside_slices] = mask diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index dc8290d2..663e1e02 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -1,5 +1,42 @@ import numpy as np +import functools +import warnings import scipy.ndimage as nd +from .selem import _default_selem + +# Our function names don't exactly correspond to ndimages. +# This dictionary translates from our names to scipy's. +funcs = ('erosion', 'dilation', 'opening', 'closing') +skimage2ndimage = dict((x, 'grey_' + x) for x in funcs) + +# These function names are the same in ndimage. +funcs = ('binary_erosion', 'binary_dilation', 'binary_opening', + 'binary_closing', 'black_tophat', 'white_tophat') +skimage2ndimage.update(dict((x, x) for x in funcs)) + + +def default_selem(func): + """Decorator to add a default structuring element to morphology functions. + + Parameters + ---------- + func : function + A morphology function such as erosion, dilation, opening, closing, + white_tophat, or black_tophat. + + Returns + ------- + func_out : function + The function, using a default structuring element of same dimension + as the input image with connectivity 1. + """ + @functools.wraps(func) + def func_out(image, selem=None, *args, **kwargs): + if selem is None: + selem = _default_selem(image.ndim) + return func(image, selem=selem, *args, **kwargs) + + return func_out def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): @@ -78,6 +115,10 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): "relabeling the input with `scipy.ndimage.label` or " "`skimage.morphology.label`.") + if len(component_sizes) == 2: + warnings.warn("Only one label was provided to `remove_small_objects`. " + "Did you mean to use a boolean array?") + too_small = component_sizes < min_size too_small_mask = too_small[ccs] out[too_small_mask] = 0 diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 7e566773..937ff45d 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -4,18 +4,19 @@ """ import numpy as np - +from scipy import ndimage +from .. import draw def square(width, dtype=np.uint8): - """ - Generates a flat, square-shaped structuring element. Every pixel - along the perimeter has a chessboard distance no greater than radius - (radius=floor(width/2)) pixels. + """Generates a flat, square-shaped structuring element. + + Every pixel along the perimeter has a chessboard distance + no greater than radius (radius=floor(width/2)) pixels. Parameters ---------- width : int - The width and height of the square + The width and height of the square. Other Parameters ---------------- @@ -33,17 +34,17 @@ def square(width, dtype=np.uint8): def rectangle(width, height, dtype=np.uint8): - """ - Generates a flat, rectangular-shaped structuring element of a - given width and height. Every pixel in the rectangle belongs - to the neighboorhood. + """Generates a flat, rectangular-shaped structuring element. + + Every pixel in the rectangle generated for a given width and given height + belongs to the neighboorhood. Parameters ---------- width : int - The width of the rectangle + The width of the rectangle. height : int - The height of the rectangle + The height of the rectangle. Other Parameters ---------------- @@ -61,11 +62,11 @@ def rectangle(width, height, dtype=np.uint8): def diamond(radius, dtype=np.uint8): - """ - Generates a flat, diamond-shaped structuring element of a given - radius. A pixel is part of the neighborhood (i.e. labeled 1) if - the city block/manhattan distance between it and the center of the - neighborhood is no greater than radius. + """Generates a flat, diamond-shaped structuring element. + + A pixel is part of the neighborhood (i.e. labeled 1) if + the city block/manhattan distance between it and the center of + the neighborhood is no greater than radius. Parameters ---------- @@ -84,15 +85,15 @@ def diamond(radius, dtype=np.uint8): The structuring element where elements of the neighborhood are 1 and 0 otherwise. """ - half = radius - (I, J) = np.meshgrid(range(0, radius * 2 + 1), range(0, radius * 2 + 1)) - s = np.abs(I - half) + np.abs(J - half) - return np.array(s <= radius, dtype=dtype) + L = np.arange(0, radius * 2 + 1) + I, J = np.meshgrid(L, L) + return np.array(np.abs(I - radius) + np.abs(J - radius) <= radius, + dtype=dtype) def disk(radius, dtype=np.uint8): - """ - Generates a flat, disk-shaped structuring element of a given radius. + """Generates a flat, disk-shaped structuring element. + A pixel is within the neighborhood if the euclidean distance between it and the origin is no greater than radius. @@ -112,23 +113,65 @@ def disk(radius, dtype=np.uint8): The structuring element where elements of the neighborhood are 1 and 0 otherwise. """ - L = np.linspace(-radius, radius, 2 * radius + 1) - (X, Y) = np.meshgrid(L, L) - s = X**2 - s += Y**2 - return np.array(s <= radius * radius, dtype=dtype) + L = np.arange(-radius, radius + 1) + X, Y = np.meshgrid(L, L) + return np.array((X ** 2 + Y ** 2) <= radius ** 2, dtype=dtype) + + +def ellipse(width, height, dtype=np.uint8): + """Generates a flat, ellipse-shaped structuring element. + + Every pixel along the perimeter of ellipse satisfies + the equation ``(x/width+1)**2 + (y/height+1)**2 = 1``. + + Parameters + ---------- + width : int + The width of the ellipse-shaped structuring element. + height : int + The height of the ellipse-shaped structuring element. + + Other Parameters + ---------------- + dtype : data-type + The data type of the structuring element. + + Returns + ------- + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + + Examples + -------- + >>> from skimage.morphology import selem + >>> selem.ellipse(5, 3) + array([[0, 0, 1, 1, 1, 1, 1, 1, 1, 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], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0]], dtype=uint8) + + """ + selem = np.zeros((2 * height + 1, 2 * width + 1), dtype=dtype) + rows, cols = draw.ellipse(height, width, height + 1, width + 1) + selem[rows, cols] = 1 + return selem def cube(width, dtype=np.uint8): - """ - Generates a cube-shaped structuring element (the 3D equivalent of - a square). Every pixel along the perimeter has a chessboard distance + """ Generates a cube-shaped structuring element. + + This is the 3D equivalent of a square. + Every pixel along the perimeter has a chessboard distance no greater than radius (radius=floor(width/2)) pixels. Parameters ---------- width : int - The width, height and depth of the cube + The width, height and depth of the cube. Other Parameters ---------------- @@ -146,12 +189,12 @@ def cube(width, dtype=np.uint8): def octahedron(radius, dtype=np.uint8): - """ - Generates a octahedron-shaped structuring element of a given radius - (the 3D equivalent of a diamond). A pixel is part of the - neighborhood (i.e. labeled 1) if the city block/manhattan distance - between it and the center of the neighborhood is no greater than - radius. + """Generates a octahedron-shaped structuring element. + + This is the 3D equivalent of a diamond. + A pixel is part of the neighborhood (i.e. labeled 1) if + the city block/manhattan distance between it and the center of + the neighborhood is no greater than radius. Parameters ---------- @@ -172,19 +215,19 @@ def octahedron(radius, dtype=np.uint8): """ # note that in contrast to diamond(), this method allows non-integer radii n = 2 * radius + 1 - Z, Y, X = np.mgrid[-radius:radius:n*1j, - -radius:radius:n*1j, - -radius:radius:n*1j] + Z, Y, X = np.mgrid[-radius:radius:n * 1j, + -radius:radius:n * 1j, + -radius:radius:n * 1j] s = np.abs(X) + np.abs(Y) + np.abs(Z) return np.array(s <= radius, dtype=dtype) def ball(radius, dtype=np.uint8): - """ - Generates a ball-shaped structuring element of a given radius (the - 3D equivalent of a disk). A pixel is within the neighborhood if the - euclidean distance between it and the origin is no greater than - radius. + """Generates a ball-shaped structuring element. + + This is the 3D equivalent of a disk. + A pixel is within the neighborhood if the euclidean distance between + it and the origin is no greater than radius. Parameters ---------- @@ -203,18 +246,19 @@ def ball(radius, dtype=np.uint8): are 1 and 0 otherwise. """ n = 2 * radius + 1 - Z, Y, X = np.mgrid[-radius:radius:n*1j, - -radius:radius:n*1j, - -radius:radius:n*1j] - s = X**2 + Y**2 + Z**2 + Z, Y, X = np.mgrid[-radius:radius:n * 1j, + -radius:radius:n * 1j, + -radius:radius:n * 1j] + s = X ** 2 + Y ** 2 + Z ** 2 return np.array(s <= radius * radius, dtype=dtype) def octagon(m, n, dtype=np.uint8): - """ - Generates an octagon shaped structuring element with a given size of - horizontal and vertical sides and a given height or width of slanted - sides. The slanted sides are 45 or 135 degrees to the horizontal axis + """Generates an octagon shaped structuring element. + + For a given size of (m) horizontal and vertical sides + and a given (n) height or width of slanted sides octagon is generated. + The slanted sides are 45 or 135 degrees to the horizontal axis and hence the widths and heights are equal. Parameters @@ -237,7 +281,7 @@ def octagon(m, n, dtype=np.uint8): """ from . import convex_hull_image - selem = np.zeros((m + 2*n, m + 2*n)) + selem = np.zeros((m + 2 * n, m + 2 * n)) selem[0, n] = 1 selem[n, 0] = 1 selem[0, m + n - 1] = 1 @@ -251,9 +295,10 @@ def octagon(m, n, dtype=np.uint8): def star(a, dtype=np.uint8): - """ - Generates a star shaped structuring element that has 8 vertices and is an - overlap of square of size `2*a + 1` with its 45 degree rotated version. + """Generates a star shaped structuring element. + + Start has 8 vertices and is an overlap of square of size `2*a + 1` + with its 45 degree rotated version. The slanted sides are 45 or 135 degrees to the horizontal axis. Parameters @@ -275,18 +320,44 @@ def star(a, dtype=np.uint8): """ from . import convex_hull_image + if a == 1: bfilter = np.zeros((3, 3), dtype) bfilter[:] = 1 return bfilter + m = 2 * a + 1 n = a // 2 selem_square = np.zeros((m + 2 * n, m + 2 * n)) selem_square[n: m + n, n: m + n] = 1 + c = (m + 2 * n - 1) // 2 selem_rotated = np.zeros((m + 2 * n, m + 2 * n)) - selem_rotated[0, c] = selem_rotated[-1, c] = selem_rotated[c, 0] = selem_rotated[c, -1] = 1 + selem_rotated[0, c] = selem_rotated[-1, c] = 1 + selem_rotated[c, 0] = selem_rotated[c, -1] = 1 selem_rotated = convex_hull_image(selem_rotated).astype(int) + selem = selem_square + selem_rotated selem[selem > 0] = 1 + return selem.astype(dtype) + + +def _default_selem(ndim): + """Generates a cross-shaped structuring element (connectivity=1). + + This is the default structuring element (selem) if no selem was specified. + + Parameters + ---------- + ndim : int + Number of dimensions of the image. + + Returns + ------- + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + + """ + return ndimage.morphology.generate_binary_structure(ndim, 1) diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index f0f69764..dbbcad8b 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -12,21 +12,15 @@ def configuration(parent_package='', top_path=None): config = Configuration('morphology', parent_package, top_path) config.add_data_dir('tests') - cython(['cmorph.pyx'], working_path=base_path) cython(['_watershed.pyx'], working_path=base_path) cython(['_skeletonize_cy.pyx'], working_path=base_path) - cython(['_pnpoly.pyx'], working_path=base_path) cython(['_convex_hull.pyx'], working_path=base_path) cython(['_greyreconstruct.pyx'], working_path=base_path) - config.add_extension('cmorph', sources=['cmorph.c'], - include_dirs=[get_numpy_include_dirs()]) config.add_extension('_watershed', sources=['_watershed.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_skeletonize_cy', sources=['_skeletonize_cy.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_pnpoly', sources=['_pnpoly.c'], - include_dirs=[get_numpy_include_dirs(), '../_shared']) config.add_extension('_convex_hull', sources=['_convex_hull.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_greyreconstruct', sources=['_greyreconstruct.c'], diff --git a/skimage/morphology/tests/__init__.py b/skimage/morphology/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/morphology/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index deab3d82..6d8097b4 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -4,51 +4,52 @@ from numpy import testing from skimage import data, color from skimage.util import img_as_bool from skimage.morphology import binary, grey, selem +from scipy import ndimage -lena = color.rgb2gray(data.lena()) -bw_lena = lena > 100 +img = color.rgb2gray(data.astronaut()) +bw_img = img > 100 def test_non_square_image(): strel = selem.square(3) - binary_res = binary.binary_erosion(bw_lena[:100, :200], strel) - grey_res = img_as_bool(grey.erosion(bw_lena[:100, :200], strel)) + binary_res = binary.binary_erosion(bw_img[:100, :200], strel) + grey_res = img_as_bool(grey.erosion(bw_img[:100, :200], strel)) testing.assert_array_equal(binary_res, grey_res) def test_binary_erosion(): strel = selem.square(3) - binary_res = binary.binary_erosion(bw_lena, strel) - grey_res = img_as_bool(grey.erosion(bw_lena, strel)) + binary_res = binary.binary_erosion(bw_img, strel) + grey_res = img_as_bool(grey.erosion(bw_img, strel)) testing.assert_array_equal(binary_res, grey_res) def test_binary_dilation(): strel = selem.square(3) - binary_res = binary.binary_dilation(bw_lena, strel) - grey_res = img_as_bool(grey.dilation(bw_lena, strel)) + binary_res = binary.binary_dilation(bw_img, strel) + grey_res = img_as_bool(grey.dilation(bw_img, strel)) testing.assert_array_equal(binary_res, grey_res) def test_binary_closing(): strel = selem.square(3) - binary_res = binary.binary_closing(bw_lena, strel) - grey_res = img_as_bool(grey.closing(bw_lena, strel)) + binary_res = binary.binary_closing(bw_img, strel) + grey_res = img_as_bool(grey.closing(bw_img, strel)) testing.assert_array_equal(binary_res, grey_res) def test_binary_opening(): strel = selem.square(3) - binary_res = binary.binary_opening(bw_lena, strel) - grey_res = img_as_bool(grey.opening(bw_lena, strel)) + binary_res = binary.binary_opening(bw_img, strel) + grey_res = img_as_bool(grey.opening(bw_img, strel)) testing.assert_array_equal(binary_res, grey_res) def test_selem_overflow(): strel = np.ones((17, 17), dtype=np.uint8) - img = np.zeros((20, 20)) - img[2:19, 2:19] = 1 + img = np.zeros((20, 20), dtype=bool) + img[2:19, 2:19] = True binary_res = binary.binary_erosion(img, strel) grey_res = img_as_bool(grey.erosion(img, strel)) testing.assert_array_equal(binary_res, grey_res) @@ -64,5 +65,106 @@ def test_out_argument(): testing.assert_(np.any(out != out_saved)) testing.assert_array_equal(out, func(img, strel)) +def test_default_selem(): + functions = [binary.binary_erosion, binary.binary_dilation, + binary.binary_opening, binary.binary_closing] + strel = selem.diamond(radius=1) + image = 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, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 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]], np.uint8) + for function in functions: + im_expected = function(image, strel) + im_test = function(image) + yield testing.assert_array_equal, im_expected, im_test + +def test_3d_fallback_default_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 + + opened = binary.binary_opening(image) + + # expect a "hyper-cross" centered in the 5x5x5: + image_expected = np.zeros((7, 7, 7), dtype=bool) + image_expected[2:5, 2:5, 2:5] = ndimage.generate_binary_structure(3, 1) + testing.assert_array_equal(opened, image_expected) + +def test_3d_fallback_cube_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 + + cube = np.ones((3, 3, 3), dtype=np.uint8) + + for function in [binary.binary_closing, binary.binary_opening]: + new_image = function(image, cube) + yield testing.assert_array_equal, new_image, image + +def test_2d_ndimage_equivalence(): + image = np.zeros((9, 9), np.uint16) + image[2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3] = 2**15 + image[4, 4] = 2**16-1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + selem = ndimage.generate_binary_structure(2, 1) + ndimage_opened = ndimage.binary_opening(image, structure=selem) + ndimage_closed = ndimage.binary_closing(image, structure=selem) + + testing.assert_array_equal(bin_opened, ndimage_opened) + testing.assert_array_equal(bin_closed, ndimage_closed) + +def test_binary_output_2d(): + image = np.zeros((9, 9), np.uint16) + image[2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3] = 2**15 + image[4, 4] = 2**16-1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + int_opened = np.empty_like(image, dtype=np.uint8) + int_closed = np.empty_like(image, dtype=np.uint8) + binary.binary_opening(image, out=int_opened) + binary.binary_closing(image, out=int_closed) + + testing.assert_equal(bin_opened.dtype, np.bool) + testing.assert_equal(bin_closed.dtype, np.bool) + + testing.assert_equal(int_opened.dtype, np.uint8) + testing.assert_equal(int_closed.dtype, np.uint8) + +def test_binary_output_3d(): + image = np.zeros((9, 9, 9), np.uint16) + image[2:-2, 2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3, 3:-3] = 2**15 + image[4, 4, 4] = 2**16-1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + int_opened = np.empty_like(image, dtype=np.uint8) + int_closed = np.empty_like(image, dtype=np.uint8) + binary.binary_opening(image, out=int_opened) + binary.binary_closing(image, out=int_closed) + + testing.assert_equal(bin_opened.dtype, np.bool) + testing.assert_equal(bin_closed.dtype, np.bool) + + testing.assert_equal(int_opened.dtype, np.uint8) + testing.assert_equal(int_closed.dtype, np.uint8) + if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index e934a5b7..901a18b9 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -1,9 +1,15 @@ 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 +from skimage.measure import label +import skimage.measure._ccomp as ccomp +from skimage._shared._warnings import expected_warnings + + +# The background label value +# is supposed to be changed to 0 soon +BG = -1 + class TestConnectedComponents: def setup(self): @@ -18,15 +24,16 @@ class TestConnectedComponents: [6, 5, 5, 7, 8, 9]]) def test_basic(self): - assert_array_equal(label(self.x), self.labels) + with expected_warnings(['`background`']): + assert_array_equal(label(self.x), self.labels) # Make sure data wasn't modified assert self.x[0, 2] == 3 def test_random(self): - x = (np.random.random((20, 30)) * 5).astype(np.int) + x = (np.random.rand(20, 30) * 5).astype(np.int) - with catch_warnings(): + with expected_warnings(['`background`']): labels = label(x) n = labels.max() @@ -38,13 +45,13 @@ class TestConnectedComponents: x = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]) - with catch_warnings(): + with expected_warnings(['`background`']): assert_array_equal(label(x), x) def test_4_vs_8(self): x = np.array([[0, 1], [1, 0]], dtype=int) - with catch_warnings(): + with expected_warnings(['`background`']): assert_array_equal(label(x, 4), [[0, 1], [2, 3]]) @@ -57,7 +64,7 @@ class TestConnectedComponents: [1, 1, 5], [0, 0, 0]]) - with catch_warnings(): + with expected_warnings(['`background`']): assert_array_equal(label(x), [[0, 1, 1], [0, 0, 2], [3, 3, 3]]) @@ -72,10 +79,11 @@ class TestConnectedComponents: [0, 0, 6], [5, 5, 5]]) - assert_array_equal(label(x, background=0), + res = label(x, background=0) + assert_array_equal(res, [[-1, -1, 0], [-1, -1, 0], - [ 1, 1, 1]]) + [+1, 1, 1]]) def test_background_one_region_center(self): x = np.array([[0, 0, 0], @@ -92,11 +100,187 @@ class TestConnectedComponents: [0, 0, 6], [5, 5, 5]]) - with catch_warnings(): + with expected_warnings(['`background`']): assert_array_equal(label(x, return_num=True)[1], 4) assert_array_equal(label(x, background=0, return_num=True)[1], 3) +class TestConnectedComponents3d: + def setup(self): + self.x = np.zeros((3, 4, 5), int) + self.x[0] = np.array([[0, 3, 2, 1, 9], + [0, 1, 9, 2, 9], + [0, 1, 9, 9, 9], + [3, 1, 5, 3, 0]]) + + self.x[1] = np.array([[3, 3, 2, 1, 9], + [0, 3, 9, 2, 1], + [0, 3, 3, 1, 1], + [3, 1, 3, 3, 0]]) + + self.x[2] = np.array([[3, 3, 8, 8, 0], + [2, 3, 9, 8, 8], + [2, 3, 0, 8, 0], + [2, 1, 0, 0, 0]]) + + self.labels = np.zeros((3, 4, 5), int) + + self.labels[0] = np.array([[0, 1, 2, 3, 4], + [0, 5, 4, 2, 4], + [0, 5, 4, 4, 4], + [1, 5, 6, 1, 7]]) + + self.labels[1] = np.array([[1, 1, 2, 3, 4], + [0, 1, 4, 2, 3], + [0, 1, 1, 3, 3], + [1, 5, 1, 1, 7]]) + + self.labels[2] = np.array([[1, 1, 8, 8, 9], + [10, 1, 4, 8, 8], + [10, 1, 7, 8, 7], + [10, 5, 7, 7, 7]]) + + def test_basic(self): + with expected_warnings(['`background`']): + labels = label(self.x) + assert_array_equal(labels, self.labels) + + assert self.x[0, 0, 2] == 2, \ + "Data was modified!" + + def test_random(self): + x = (np.random.rand(20, 30) * 5).astype(np.int) + + with expected_warnings(['`background`']): + labels = label(x) + + n = labels.max() + for i in range(n): + values = x[labels == i] + assert np.all(values == values[0]) + + def test_diag(self): + x = np.zeros((3, 3, 3), int) + x[0, 2, 2] = 1 + x[1, 1, 1] = 1 + x[2, 0, 0] = 1 + with expected_warnings(['`background`']): + assert_array_equal(label(x), x) + + def test_4_vs_8(self): + x = np.zeros((2, 2, 2), int) + x[0, 1, 1] = 1 + x[1, 0, 0] = 1 + label4 = x.copy() + label4[1, 0, 0] = 2 + with expected_warnings(['`background`']): + assert_array_equal(label(x, 4), label4) + assert_array_equal(label(x, 8), x) + + def test_background(self): + x = np.zeros((2, 3, 3), int) + x[0] = np.array([[1, 0, 0], + [1, 0, 0], + [0, 0, 0]]) + x[1] = np.array([[0, 0, 0], + [0, 1, 5], + [0, 0, 0]]) + + lnb = x.copy() + lnb[0] = np.array([[0, 1, 1], + [0, 1, 1], + [1, 1, 1]]) + lnb[1] = np.array([[1, 1, 1], + [1, 0, 2], + [1, 1, 1]]) + lb = x.copy() + lb[0] = np.array([[0, BG, BG], + [0, BG, BG], + [BG, BG, BG]]) + lb[1] = np.array([[BG, BG, BG], + [BG, 0, 1], + [BG, BG, BG]]) + + with expected_warnings(['`background`']): + assert_array_equal(label(x), lnb) + + assert_array_equal(label(x, background=0), lb) + + def test_background_two_regions(self): + x = np.zeros((2, 3, 3), int) + x[0] = np.array([[0, 0, 6], + [0, 0, 6], + [5, 5, 5]]) + x[1] = np.array([[6, 6, 0], + [5, 0, 0], + [0, 0, 0]]) + lb = x.copy() + lb[0] = np.array([[BG, BG, 0], + [BG, BG, 0], + [1, 1, 1]]) + lb[1] = np.array([[0, 0, BG], + [1, BG, BG], + [BG, BG, BG]]) + + res = label(x, background=0) + assert_array_equal(res, lb) + + def test_background_one_region_center(self): + x = np.zeros((3, 3, 3), int) + x[1, 1, 1] = 1 + + lb = np.ones_like(x) * BG + lb[1, 1, 1] = 0 + + assert_array_equal(label(x, neighbors=4, background=0), lb) + + def test_return_num(self): + x = np.array([[1, 0, 6], + [0, 0, 6], + [5, 5, 5]]) + + with expected_warnings(['`background`']): + assert_array_equal(label(x, return_num=True)[1], 4) + + assert_array_equal(label(x, background=0, return_num=True)[1], 3) + + def test_1D(self): + x = np.array((0, 1, 2, 2, 1, 1, 0, 0)) + xlen = len(x) + y = np.array((0, 1, 2, 2, 3, 3, 4, 4)) + reshapes = ((xlen,), + (1, xlen), (xlen, 1), + (1, xlen, 1), (xlen, 1, 1), (1, 1, xlen)) + for reshape in reshapes: + x2 = x.reshape(reshape) + with expected_warnings(['`background`']): + labelled = label(x2) + assert_array_equal(y, labelled.flatten()) + + def test_nd(self): + x = np.ones((1, 2, 3, 4)) + np.testing.assert_raises(NotImplementedError, label, x) + + +class TestSupport: + def test_reshape(self): + shapes_in = ((3, 1, 2), (1, 4, 5), (3, 1, 1), (2, 1), (1,)) + for shape in shapes_in: + shape = np.array(shape) + numones = sum(shape == 1) + inp = np.random.random(shape) + + fixed, swaps = ccomp.reshape_array(inp) + shape2 = fixed.shape + # now check that all ones are at the beginning + for i in range(numones): + assert shape2[i] == 1 + + back = ccomp.undo_reshape_array(fixed, swaps) + # check that the undo works as expected + assert_array_equal(inp, back) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index f0099ee5..576404f2 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -2,11 +2,12 @@ import os.path import numpy as np from numpy import testing +from scipy import ndimage import skimage -from skimage import data_dir -from skimage.util import img_as_bool +from skimage import data_dir, img_as_uint from skimage.morphology import grey, selem +from skimage._shared._warnings import expected_warnings lena = np.load(os.path.join(data_dir, 'lena_GRAY_U8.npy')) @@ -119,50 +120,161 @@ class TestEccentricStructuringElements(): tophat = grey.black_tophat(self.white_pixel, s) assert np.all(tophat == 0) +def test_default_selem(): + functions = [grey.erosion, grey.dilation, + grey.opening, grey.closing, + grey.white_tophat, grey.black_tophat] + strel = selem.diamond(radius=1) + image = 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, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 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]], np.uint8) + for function in functions: + im_expected = function(image, strel) + im_test = function(image) + yield testing.assert_array_equal, im_expected, im_test -class TestDTypes(): +def test_3d_fallback_default_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 - def setUp(self): - k = 5 - arrname = '%03i' % k + opened = grey.opening(image) - self.disk = selem.disk(k) + # expect a "hyper-cross" centered in the 5x5x5: + image_expected = np.zeros((7, 7, 7), dtype=bool) + image_expected[2:5, 2:5, 2:5] = ndimage.generate_binary_structure(3, 1) + testing.assert_array_equal(opened, image_expected) - fname_opening = os.path.join(data_dir, "disk-open-matlab-output.npz") - self.expected_opening = np.load(fname_opening)[arrname] +def test_3d_fallback_cube_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 - fname_closing = os.path.join(data_dir, "disk-close-matlab-output.npz") - self.expected_closing = np.load(fname_closing)[arrname] + cube = np.ones((3, 3, 3), dtype=np.uint8) - def _test_image(self, image): - result_opening = grey.opening(image, self.disk) - testing.assert_equal(result_opening, self.expected_opening) + for function in [grey.closing, grey.opening]: + new_image = function(image, cube) + yield testing.assert_array_equal, new_image, image - result_closing = grey.closing(image, self.disk) - testing.assert_equal(result_closing, self.expected_closing) +def test_3d_fallback_white_tophat(): + image = np.zeros((7, 7, 7), dtype=bool) + image[2, 2:4, 2:4] = 1 + image[3, 2:5, 2:5] = 1 + image[4, 3:5, 3:5] = 1 - def test_float(self): - image = skimage.img_as_float(lena) - self._test_image(image) + with expected_warnings(['operator.*deprecated|\A\Z']): + new_image = grey.white_tophat(image) + footprint = ndimage.generate_binary_structure(3,1) + with expected_warnings(['operator.*deprecated|\A\Z']): + image_expected = ndimage.white_tophat(image,footprint=footprint) + testing.assert_array_equal(new_image, image_expected) - @testing.decorators.skipif(True) - def test_int(self): - image = skimage.img_as_int(lena) - self._test_image(image) +def test_3d_fallback_black_tophat(): + image = np.ones((7, 7, 7), dtype=bool) + image[2, 2:4, 2:4] = 0 + image[3, 2:5, 2:5] = 0 + image[4, 3:5, 3:5] = 0 - def test_uint(self): - image = skimage.img_as_uint(lena) - self._test_image(image) + with expected_warnings(['operator.*deprecated|\A\Z']): + new_image = grey.black_tophat(image) + footprint = ndimage.generate_binary_structure(3,1) + with expected_warnings(['operator.*deprecated|\A\Z']): + image_expected = ndimage.black_tophat(image,footprint=footprint) + testing.assert_array_equal(new_image, image_expected) + +def test_2d_ndimage_equivalence(): + image = np.zeros((9, 9), np.uint8) + image[2:-2, 2:-2] = 128 + image[3:-3, 3:-3] = 196 + image[4, 4] = 255 + + opened = grey.opening(image) + closed = grey.closing(image) + + selem = ndimage.generate_binary_structure(2, 1) + ndimage_opened = ndimage.grey_opening(image, footprint=selem) + ndimage_closed = ndimage.grey_closing(image, footprint=selem) + + testing.assert_array_equal(opened, ndimage_opened) + testing.assert_array_equal(closed, ndimage_closed) + +# float test images +im = np.array([[ 0.55, 0.72, 0.6 , 0.54, 0.42], + [ 0.65, 0.44, 0.89, 0.96, 0.38], + [ 0.79, 0.53, 0.57, 0.93, 0.07], + [ 0.09, 0.02, 0.83, 0.78, 0.87], + [ 0.98, 0.8 , 0.46, 0.78, 0.12]]) + +eroded = np.array([[ 0.55, 0.44, 0.54, 0.42, 0.38], + [ 0.44, 0.44, 0.44, 0.38, 0.07], + [ 0.09, 0.02, 0.53, 0.07, 0.07], + [ 0.02, 0.02, 0.02, 0.78, 0.07], + [ 0.09, 0.02, 0.46, 0.12, 0.12]]) + +dilated = np.array([[ 0.72, 0.72, 0.89, 0.96, 0.54], + [ 0.79, 0.89, 0.96, 0.96, 0.96], + [ 0.79, 0.79, 0.93, 0.96, 0.93], + [ 0.98, 0.83, 0.83, 0.93, 0.87], + [ 0.98, 0.98, 0.83, 0.78, 0.87]]) + +opened = np.array([[ 0.55, 0.55, 0.54, 0.54, 0.42], + [ 0.55, 0.44, 0.54, 0.44, 0.38], + [ 0.44, 0.53, 0.53, 0.78, 0.07], + [ 0.09, 0.02, 0.78, 0.78, 0.78], + [ 0.09, 0.46, 0.46, 0.78, 0.12]]) + +closed = np.array([[ 0.72, 0.72, 0.72, 0.54, 0.54], + [ 0.72, 0.72, 0.89, 0.96, 0.54], + [ 0.79, 0.79, 0.79, 0.93, 0.87], + [ 0.79, 0.79, 0.83, 0.78, 0.87], + [ 0.98, 0.83, 0.78, 0.78, 0.78]]) + +def test_float(): + np.testing.assert_allclose(grey.erosion(im), eroded) + np.testing.assert_allclose(grey.dilation(im), dilated) + np.testing.assert_allclose(grey.opening(im), opened) + np.testing.assert_allclose(grey.closing(im), closed) -def test_inplace(): - selem = np.ones((3, 3)) - image = np.zeros((5, 5)) - out = image +def test_uint16(): + im16, eroded16, dilated16, opened16, closed16 = ( + map(img_as_uint, [im, eroded, dilated, opened, closed])) + np.testing.assert_allclose(grey.erosion(im16), eroded16) + np.testing.assert_allclose(grey.dilation(im16), dilated16) + np.testing.assert_allclose(grey.opening(im16), opened16) + np.testing.assert_allclose(grey.closing(im16), closed16) - for f in (grey.erosion, grey.dilation, - grey.white_tophat, grey.black_tophat): - testing.assert_raises(NotImplementedError, f, image, selem, out=out) + +def test_discontiguous_out_array(): + image = np.array([[5, 6, 2], + [7, 2, 2], + [3, 5, 1]], np.uint8) + out_array_big = np.zeros((5, 5), np.uint8) + out_array = out_array_big[::2, ::2] + expected_dilation = np.array([[7, 0, 6, 0, 6], + [0, 0, 0, 0, 0], + [7, 0, 7, 0, 2], + [0, 0, 0, 0, 0], + [7, 0, 5, 0, 5]], np.uint8) + expected_erosion = np.array([[5, 0, 2, 0, 2], + [0, 0, 0, 0, 0], + [2, 0, 2, 0, 1], + [0, 0, 0, 0, 0], + [3, 0, 1, 0, 1]], np.uint8) + grey.dilation(image, out=out_array) + testing.assert_array_equal(out_array_big, expected_dilation) + grey.erosion(image, out=out_array) + testing.assert_array_equal(out_array_big, expected_erosion) if __name__ == '__main__': diff --git a/skimage/morphology/tests/test_misc.py b/skimage/morphology/tests/test_misc.py index 94d27f64..8789b22a 100644 --- a/skimage/morphology/tests/test_misc.py +++ b/skimage/morphology/tests/test_misc.py @@ -1,5 +1,6 @@ import numpy as np -from numpy.testing import assert_array_equal, assert_equal, assert_raises +from numpy.testing import (assert_array_equal, assert_equal, assert_raises, + assert_warns) from skimage.morphology import remove_small_objects test_image = np.array([[0, 0, 0, 1, 0], @@ -55,6 +56,13 @@ def test_uint_image(): assert_array_equal(observed, expected) +def test_single_label_warning(): + image = np.array([[0, 0, 0, 1, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0]], int) + assert_warns(UserWarning, remove_small_objects, image, min_size=6) + + def test_float_input(): float_test = np.random.rand(5, 5) assert_raises(TypeError, remove_small_objects, float_test) diff --git a/skimage/morphology/tests/test_reconstruction.py b/skimage/morphology/tests/test_reconstruction.py index f5678c08..df9ddf8f 100644 --- a/skimage/morphology/tests/test_reconstruction.py +++ b/skimage/morphology/tests/test_reconstruction.py @@ -97,6 +97,12 @@ def test_invalid_selem(): reconstruction(seed, mask, selem=np.ones((3, 3))) +def test_invalid_method(): + seed = np.array([0, 8, 8, 8, 8, 8, 8, 8, 8, 0]) + mask = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0]) + assert_raises(ValueError, reconstruction, seed, mask, method='foo') + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/morphology/tests/test_selem.py b/skimage/morphology/tests/test_selem.py index 7895fa58..ebefdfa1 100644 --- a/skimage/morphology/tests/test_selem.py +++ b/skimage/morphology/tests/test_selem.py @@ -1,12 +1,14 @@ -# Author: Damian Eads +""" +Tests for Morphological structuring elements +(skimage.morphology.selem) +Author: Damian Eads +""" import os.path import numpy as np -from numpy.testing import * +from numpy.testing import assert_equal -from skimage import data_dir -from skimage.io import * from skimage import data_dir from skimage.morphology import selem @@ -14,25 +16,34 @@ from skimage.morphology import selem class TestSElem(): def test_square_selem(self): + """Test square structuring elements""" for k in range(0, 5): actual_mask = selem.square(k) expected_mask = np.ones((k, k), dtype='uint8') assert_equal(expected_mask, actual_mask) def test_rectangle_selem(self): + """Test rectangle structuring elements""" for i in range(0, 5): for j in range(0, 5): actual_mask = selem.rectangle(i, j) expected_mask = np.ones((i, j), dtype='uint8') assert_equal(expected_mask, actual_mask) + def test_cube_selem(self): + """Test cube structuring elements""" + for k in range(0, 5): + actual_mask = selem.cube(k) + expected_mask = np.ones((k, k, k), dtype='uint8') + assert_equal(expected_mask, actual_mask) + def strel_worker(self, fn, func): matlab_masks = np.load(os.path.join(data_dir, fn)) k = 0 for arrname in sorted(matlab_masks): expected_mask = matlab_masks[arrname] actual_mask = func(k) - if (expected_mask.shape == (1,)): + if expected_mask.shape == (1,): expected_mask = expected_mask[:, np.newaxis] assert_equal(expected_mask, actual_mask) k = k + 1 @@ -43,30 +54,35 @@ class TestSElem(): for arrname in sorted(matlab_masks): expected_mask = matlab_masks[arrname] actual_mask = func(k) - if (expected_mask.shape == (1,)): + if expected_mask.shape == (1,): expected_mask = expected_mask[:, np.newaxis] # Test center slice for each dimension. This gives a good # indication of validity without the need for a 3D reference # mask. c = int(expected_mask.shape[0]/2) - assert_equal(expected_mask, actual_mask[c,:,:]) - assert_equal(expected_mask, actual_mask[:,c,:]) - assert_equal(expected_mask, actual_mask[:,:,c]) + assert_equal(expected_mask, actual_mask[c, :, :]) + assert_equal(expected_mask, actual_mask[:, c, :]) + assert_equal(expected_mask, actual_mask[:, :, c]) k = k + 1 def test_selem_disk(self): + """Test disk structuring elements""" self.strel_worker("disk-matlab-output.npz", selem.disk) def test_selem_diamond(self): + """Test diamond structuring elements""" self.strel_worker("diamond-matlab-output.npz", selem.diamond) def test_selem_ball(self): + """Test ball structuring elements""" self.strel_worker_3d("disk-matlab-output.npz", selem.ball) def test_selem_octahedron(self): + """Test octahedron structuring elements""" self.strel_worker_3d("diamond-matlab-output.npz", selem.octahedron) def test_selem_octagon(self): + """Test octagon structuring elements""" expected_mask1 = np.array([[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], @@ -86,7 +102,27 @@ class TestSElem(): assert_equal(expected_mask1, actual_mask1) assert_equal(expected_mask2, actual_mask2) + def test_selem_ellipse(self): + """Test ellipse structuring elements""" + expected_mask1 = np.array([[0, 0, 1, 1, 1, 1, 1, 1, 1, 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], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0]], dtype=np.uint8) + actual_mask1 = selem.ellipse(5, 3) + expected_mask2 = np.array([[1, 1, 1], + [1, 1, 1], + [1, 1, 1]], dtype=np.uint8) + actual_mask2 = selem.ellipse(1, 1) + assert_equal(expected_mask1, actual_mask1) + assert_equal(expected_mask2, actual_mask2) + assert_equal(expected_mask1, selem.ellipse(3, 5).T) + assert_equal(expected_mask2, selem.ellipse(1, 1).T) + def test_selem_star(self): + """Test star structuring elements""" expected_mask1 = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py index 7318ceea..5e4a9156 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -27,7 +27,7 @@ Original author: Lee Kamentsky from _heapq import heappush, heappop import numpy as np import scipy.ndimage -from ..filter import rank_order +from ..filters import rank_order from .._shared.utils import deprecated from . import _watershed @@ -136,7 +136,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): if offset is None: if any([x % 2 == 0 for x in c_connectivity.shape]): raise ValueError("Connectivity array must have an unambiguous " - "center") + "center") # # offset to center of connectivity array # @@ -147,7 +147,8 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): pads = offset def pad(im): - new_im = np.zeros([i + 2 * p for i, p in zip(im.shape, pads)], im.dtype) + new_im = np.zeros( + [i + 2 * p for i, p in zip(im.shape, pads)], im.dtype) new_im[[slice(p, -p, None) for p in pads]] = im return new_im @@ -211,7 +212,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): # If nothing is labeled, the output is empty and we don't have to # do anything c_output = c_output.flatten() - if c_mask == None: + if c_mask is None: c_mask = np.ones(c_image.shape, np.int8).flatten() else: c_mask = c_mask.astype(np.int8).flatten() @@ -220,7 +221,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): c_mask, c_output) c_output = c_output.reshape(c_image.shape)[[slice(1, -1, None)] * - image.ndim] + image.ndim] try: return c_output.astype(markers.dtype) except: diff --git a/skimage/novice/_novice.py b/skimage/novice/_novice.py index b953b0c8..24c2ff0e 100644 --- a/skimage/novice/_novice.py +++ b/skimage/novice/_novice.py @@ -4,11 +4,10 @@ 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 +from .. import io, img_as_ubyte +from ..transform import resize +from ..color import color_dict +from ..io.util import file_or_url_context, is_url import six from six.moves.urllib_parse import urlparse @@ -80,6 +79,7 @@ class Pixel(object): Transparency component (0-255), 255 (opaque) by default """ + def __init__(self, pic, array, x, y, rgb, alpha=255): self._picture = pic self._x = x @@ -239,6 +239,7 @@ class Picture(object): >>> pic[:, pic.height-1] = (255, 0, 0) """ + def __init__(self, path=None, array=None, xy_array=None): self._modified = False self.scale = 1 @@ -280,6 +281,7 @@ class Picture(object): if isinstance(color, six.string_types): color = color_dict[color] rgb_size = tuple(size) + (len(color),) + color = np.array(color, dtype=np.uint8) array = np.ones(rgb_size, dtype=np.uint8) * color # Force RGBA internally (use max alpha) @@ -351,8 +353,9 @@ class Picture(object): 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) + new_array = resize(self.array, new_size, order=0, + preserve_range=True) + self.array = new_array.astype(np.uint8) self._array_modified() diff --git a/skimage/novice/tests/__init__.py b/skimage/novice/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/novice/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/novice/tests/test_novice.py b/skimage/novice/tests/test_novice.py index a1f750ab..36f4a537 100644 --- a/skimage/novice/tests/test_novice.py +++ b/skimage/novice/tests/test_novice.py @@ -7,16 +7,12 @@ from skimage import novice from skimage.novice._novice import (array_to_xy_origin, xy_to_array_origin, rgb_transpose) from skimage import data_dir - +from skimage._shared.utils import all_warnings 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)) @@ -62,7 +58,8 @@ def test_modify(): assert p.blue <= 128 s = pic.size - pic.size = (pic.width / 2, pic.height / 2) + with all_warnings(): # precision loss + pic.size = (pic.width / 2, pic.height / 2) assert_equal(pic.size, (int(s[0] / 2), int(s[1] / 2))) assert pic.modified @@ -89,6 +86,8 @@ def test_pixel_rgb(): pixel.rgb = np.arange(4) assert_equal(pixel.rgb, np.arange(3)) + assert pic.array.dtype == np.uint8 + def test_pixel_rgba(): pic = novice.Picture.from_size((3, 3), color=(10, 10, 10)) @@ -139,7 +138,8 @@ def test_modified_on_set_pixel(): def test_update_on_save(): pic = novice.Picture(array=np.zeros((3, 3, 3))) - pic.size = (6, 6) + with all_warnings(): # precision loss + pic.size = (6, 6) assert pic.modified assert pic.path is None diff --git a/skimage/restoration/__init__.py b/skimage/restoration/__init__.py index 2b593ccf..66beecfa 100644 --- a/skimage/restoration/__init__.py +++ b/skimage/restoration/__init__.py @@ -22,7 +22,7 @@ from .deconvolution import wiener, unsupervised_wiener, richardson_lucy from .unwrap import unwrap_phase from ._denoise import denoise_tv_chambolle, denoise_tv_bregman, \ denoise_bilateral - +from .non_local_means import nl_means_denoising __all__ = ['wiener', 'unsupervised_wiener', @@ -30,4 +30,5 @@ __all__ = ['wiener', 'unwrap_phase', 'denoise_tv_bregman', 'denoise_tv_chambolle', - 'denoise_bilateral'] + 'denoise_bilateral', + 'nl_means_denoising'] diff --git a/skimage/restoration/_denoise.py b/skimage/restoration/_denoise.py index 260b1078..10de6c68 100644 --- a/skimage/restoration/_denoise.py +++ b/skimage/restoration/_denoise.py @@ -1,8 +1,7 @@ # coding: utf-8 import numpy as np -from skimage import img_as_float -from skimage.restoration._denoise_cy import _denoise_bilateral, \ - _denoise_tv_bregman +from .. import img_as_float +from ..restoration._denoise_cy import _denoise_bilateral, _denoise_tv_bregman def denoise_bilateral(image, win_size=5, sigma_range=None, sigma_spatial=1, @@ -71,7 +70,7 @@ def denoise_tv_bregman(image, weight, max_iter=100, eps=1e-3, isotropic=True): ---------- image : ndarray Input data to be denoised (converted using img_as_float`). - weight : float, optional + weight : float 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`. @@ -151,12 +150,12 @@ def _denoise_tv_chambolle_3d(im, weight=100, eps=2.e-4, n_iter_max=200): d[:, :, 1:] += pz[:, :, :-1] out = im + d - E = (d**2).sum() + E = (d ** 2).sum() gx[:-1] = np.diff(out, axis=0) gy[:, :-1] = np.diff(out, axis=1) gz[:, :, :-1] = np.diff(out, axis=2) - norm = np.sqrt(gx**2 + gy**2 + gz**2) + norm = np.sqrt(gx ** 2 + gy ** 2 + gz ** 2) E += weight * norm.sum() norm *= 0.5 / weight norm += 1. @@ -231,10 +230,10 @@ def _denoise_tv_chambolle_2d(im, weight=50, eps=2.e-4, n_iter_max=200): d[:, 1:] += py[:, :-1] out = im + d - E = (d**2).sum() + E = (d ** 2).sum() gx[:-1] = np.diff(out, axis=0) gy[:, :-1] = np.diff(out, axis=1) - norm = np.sqrt(gx**2 + gy**2) + norm = np.sqrt(gx ** 2 + gy ** 2) E += weight * norm.sum() norm *= 0.5 / weight norm += 1 @@ -310,12 +309,12 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, Examples -------- - 2D example on Lena image: + 2D example on astronaut image: >>> from skimage import color, data - >>> 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) + >>> img = color.rgb2gray(data.astronaut())[:50, :50] + >>> img += 0.5 * img.std() * np.random.randn(*img.shape) + >>> denoised_img = denoise_tv_chambolle(img, weight=60) 3D example on synthetic data: diff --git a/skimage/restoration/_denoise_cy.pyx b/skimage/restoration/_denoise_cy.pyx index 34404dce..c3faf355 100644 --- a/skimage/restoration/_denoise_cy.pyx +++ b/skimage/restoration/_denoise_cy.pyx @@ -8,8 +8,8 @@ import numpy as np from libc.math cimport exp, fabs, sqrt from libc.stdlib cimport malloc, free from libc.float cimport DBL_MAX -from skimage._shared.interpolation cimport get_pixel3d -from skimage.util import img_as_float +from .._shared.interpolation cimport get_pixel3d +from ..util import img_as_float cdef inline double _gaussian_weight(double sigma, double value): diff --git a/skimage/restoration/_nl_means_denoising.pyx b/skimage/restoration/_nl_means_denoising.pyx new file mode 100644 index 00000000..089914fc --- /dev/null +++ b/skimage/restoration/_nl_means_denoising.pyx @@ -0,0 +1,712 @@ +import numpy as np +from skimage import util +cimport numpy as np +cimport cython +from libc.math cimport exp + +ctypedef np.float32_t IMGDTYPE + +cdef float DISTANCE_CUTOFF = 5. + +@cython.boundscheck(False) +cdef inline float patch_distance_2d(IMGDTYPE [:, :] p1, + IMGDTYPE [:, :] p2, + IMGDTYPE [:, ::] w, int s): + """ + Compute a Gaussian distance between two image patches. + + Parameters + ---------- + p1 : 2-D array_like + First patch. + p2 : 2-D array_like + Second patch. + w : 2-D array_like + Array of weigths for the different pixels of the patches. + s : int + Linear size of the patches. + + Returns + ------- + distance : float + Gaussian distance between the two patches + + Notes + ----- + The returned distance is given by + + .. math:: \exp( -w (p1 - p2)^2) + """ + cdef int i, j + cdef int center = s / 2 + # Check if central pixel is too different in the 2 patches + cdef float tmp_diff = p1[center, center] - p2[center, center] + cdef float init = w[center, center] * tmp_diff * tmp_diff + if init > 1: + return 0. + cdef float distance = 0 + for i in range(s): + # exp of large negative numbers will be 0, so we'd better stop + if distance > DISTANCE_CUTOFF: + return 0. + for j in range(s): + tmp_diff = p1[i, j] - p2[i, j] + distance += (w[i, j] * tmp_diff * tmp_diff) + distance = exp(-distance) + return distance + + +@cython.boundscheck(False) +cdef inline float patch_distance_2drgb(IMGDTYPE [:, :, :] p1, + IMGDTYPE [:, :, :] p2, + IMGDTYPE [:, ::] w, int s): + """ + Compute a Gaussian distance between two image patches. + + Parameters + ---------- + p1 : 3-D array_like + First patch, 2D image with last dimension corresponding to channels. + p2 : 3-D array_like + Second patch, 2D image with last dimension corresponding to channels. + w : 2-D array_like + Array of weights for the different pixels of the patches. + s : int + Linear size of the patches. + + Returns + ------- + distance : float + Gaussian distance between the two patches + + Notes + ----- + The returned distance is given by + + .. math:: \exp( -w (p1 - p2)^2) + """ + cdef int i, j + cdef int center = s / 2 + cdef int color + cdef float tmp_diff = 0 + cdef float distance = 0 + for i in range(s): + # exp of large negative numbers will be 0, so we'd better stop + if distance > DISTANCE_CUTOFF: + return 0. + for j in range(s): + for color in range(3): + tmp_diff = p1[i, j, color] - p2[i, j, color] + distance += w[i, j] * tmp_diff * tmp_diff + distance = exp(-distance) + return distance + + +@cython.boundscheck(False) +cdef inline float patch_distance_3d(IMGDTYPE [:, :, :] p1, + IMGDTYPE [:, :, :] p2, + IMGDTYPE [:, :, ::] w, int s): + """ + Compute a Gaussian distance between two image patches. + + Parameters + ---------- + p1 : 3-D array_like + First patch. + p2 : 3-D array_like + Second patch. + w : 3-D array_like + Array of weights for the different pixels of the patches. + s : int + Linear size of the patches. + + Returns + ------- + distance : float + Gaussian distance between the two patches + + Notes + ----- + The returned distance is given by + + .. math:: \exp( -w (p1 - p2)^2) + """ + cdef int i, j, k + cdef float distance = 0 + cdef float tmp_diff + for i in range(s): + # exp of large negative numbers will be 0, so we'd better stop + if distance > DISTANCE_CUTOFF: + return 0. + for j in range(s): + for k in range(s): + tmp_diff = p1[i, j, k] - p2[i, j, k] + distance += w[i, j, k] * tmp_diff * tmp_diff + distance = exp(-distance) + return distance + + +@cython.cdivision(True) +@cython.boundscheck(False) +def _nl_means_denoising_2d(image, int s=7, int d=13, float h=0.1): + """ + Perform non-local means denoising on 2-D RGB image + + Parameters + ---------- + image : ndarray + Input RGB image to be denoised + s : int, optional + Size of patches used for denoising + d : int, optional + Maximal distance in pixels where to search patches used for denoising + h : float, optional + Cut-off distance (in gray levels). The higher h, the more permissive + one is in accepting patches. + + Returns + ------- + result : ndarray + Denoised image, of same shape as input image. + """ + if s % 2 == 0: + s += 1 # odd value for symmetric patch + cdef int n_row, n_col, n_ch + n_row, n_col, n_ch = image.shape + cdef int offset = s / 2 + cdef int row, col, i, j, color + cdef int row_start, row_end, col_start, col_end + cdef int row_start_i, row_end_i, col_start_j, col_end_j + cdef IMGDTYPE [::1] new_values = np.zeros(n_ch).astype(np.float32) + cdef IMGDTYPE [:, :, ::1] padded = np.ascontiguousarray(util.pad(image, + ((offset, offset), (offset, offset), (0, 0)), + mode='reflect').astype(np.float32)) + cdef IMGDTYPE [:, :, ::1] result = padded.copy() + cdef float A = ((s - 1.) / 4.) + cdef float new_value + cdef float weight_sum, weight + xg_row, xg_col = np.mgrid[-offset:offset + 1, -offset:offset + 1] + cdef IMGDTYPE [:, ::1] w = np.ascontiguousarray(np.exp( + -(xg_row ** 2 + xg_col ** 2) / (2 * A ** 2)). + astype(np.float32)) + cdef float distance + w = 1. / (n_ch * np.sum(w) * h ** 2) * w + + # Coordinates of central pixel + # Iterate over rows, taking padding into account + for row in range(offset, n_row + offset): + row_start = row - offset + row_end = row + offset + 1 + # Iterate over columns, taking padding into account + for col in range(offset, n_col + offset): + # Initialize per-channel bins + for color in range(n_ch): + new_values[color] = 0 + # Reset weights for each local region + weight_sum = 0 + col_start = col - offset + col_end = col + offset + 1 + + # Iterate over local 2d patch for each pixel + # First rows + for i in range(max(-d, offset - row), + min(d + 1, n_row + offset - row)): + row_start_i = row_start + i + row_end_i = row_end + i + # Local patch columns + for j in range(max(-d, offset - col), + min(d + 1, n_col + offset - col)): + col_start_j = col_start + j + col_end_j = col_end + j + # Shortcut for grayscale, else assume RGB + if n_ch == 1: + weight = patch_distance_2d( + padded[row_start:row_end, + col_start:col_end, 0], + padded[row_start_i:row_end_i, + col_start_j:col_end_j, 0], + w, s) + else: + weight = patch_distance_2drgb( + padded[row_start:row_end, + col_start:col_end, :], + padded[row_start_i:row_end_i, + col_start_j:col_end_j, :], + w, s) + + # Collect results in weight sum + weight_sum += weight + # Apply to each channel multiplicatively + for color in range(n_ch): + new_values[color] += weight * padded[row + i, + col + j, color] + + # Normalize the result + for color in range(n_ch): + result[row, col, color] = new_values[color] / weight_sum + + # Return cropped result, undoing padding + return result[offset:-offset, offset:-offset] + + +@cython.cdivision(True) +@cython.boundscheck(False) +def _nl_means_denoising_3d(image, int s=7, + int d=13, float h=0.1): + """ + Perform non-local means denoising on 3-D array + + Parameters + ---------- + image : ndarray + Input data to be denoised. + s : int, optional + Size of patches used for denoising. + d : int, optional + Maximal distance in pixels where to search patches used for denoising. + h : float, optional + Cut-off distance (in gray levels). + + Returns + ------- + result : ndarray + Denoised image, of same shape as input image. + """ + if s % 2 == 0: + s += 1 # odd value for symmetric patch + cdef int n_pln, n_row, n_col + n_pln, n_row, n_col = image.shape + cdef int offset = s / 2 + # padd the image so that boundaries are denoised as well + cdef IMGDTYPE [:, :, ::1] padded = np.ascontiguousarray(util.pad( + image.astype(np.float32), + offset, mode='reflect')) + cdef IMGDTYPE [:, :, ::1] result = padded.copy() + cdef float A = ((s - 1.) / 4.) + cdef float new_value + cdef float weight_sum, weight + xg_pln, xg_row, xg_col = np.mgrid[-offset: offset + 1, + -offset: offset + 1, + -offset: offset + 1] + cdef IMGDTYPE [:, :, ::1] w = np.ascontiguousarray(np.exp( + -(xg_pln ** 2 + xg_row ** 2 + xg_col ** 2) / + (2 * A ** 2)).astype(np.float32)) + cdef float distance + cdef int pln, row, col, i, j, k + cdef int pln_start, pln_end, row_start, row_end, col_start, col_end + cdef int pln_start_i, pln_end_i, row_start_j, row_end_j, \ + col_start_k, col_end_k + w = 1. / (np.sum(w) * h ** 2) * w + + # Coordinates of central pixel + # Iterate over planes, taking padding into account + for pln in range(offset, n_pln + offset): + pln_start = pln - offset + pln_end = pln + offset + 1 + # Iterate over rows, taking padding into account + for row in range(offset, n_row + offset): + row_start = row - offset + row_end = row + offset + 1 + # Iterate over columns, taking padding into account + for col in range(offset, n_col + offset): + col_start = col - offset + col_end = col + offset + 1 + new_value = 0 + weight_sum = 0 + + # Iterate over local 3d patch for each pixel + # First planes + for i in range(max(-d, offset - pln), + min(d + 1, n_pln + offset - pln)): + pln_start_i = pln_start + i + pln_end_i = pln_end + i + # Rows + for j in range(max(-d, offset - row), + min(d + 1, n_row + offset - row)): + row_start_j = row_start + j + row_end_j = row_end + j + # Columns + for k in range(max(-d, offset - col), + min(d + 1, n_col + offset - col)): + col_start_k = col_start + k + col_end_k = col_end + k + weight = patch_distance_3d( + padded[pln_start:pln_end, + row_start:row_end, + col_start:col_end], + padded[pln_start_i:pln_end_i, + row_start_j:row_end_j, + col_start_k:col_end_k], + w, s) + # Collect results in weight sum + weight_sum += weight + new_value += weight * padded[pln + i, + row + j, col + k] + + # Normalize the result + result[pln, row, col] = new_value / weight_sum + + # Return cropped result, undoing padding + return result[offset:-offset, offset:-offset, offset:-offset] + +#-------------- Accelerated algorithm of Froment 2015 ------------------ + + +@cython.cdivision(True) +@cython.boundscheck(False) +cdef inline float _integral_to_distance_2d(IMGDTYPE [:, ::] integral, + int row, int col, int offset, float h2s2): + """ + References + ---------- + Jacques Froment. Parameter-Free Fast Pixelwise Non-Local Means + Denoising. Image Processing On Line, 2014, vol. 4, p. 300-326. + + Used in _fast_nl_means_denoising_2d + """ + cdef float distance + distance = integral[row + offset, col + offset] + \ + integral[row - offset, col - offset] - \ + integral[row - offset, col + offset] - \ + integral[row + offset, col - offset] + distance /= h2s2 + return distance + + +@cython.cdivision(True) +@cython.boundscheck(False) +cdef inline float _integral_to_distance_3d(IMGDTYPE [:, :, ::] integral, + int pln, int row, int col, int offset, + float s_cube_h_square): + """ + References + ---------- + Jacques Froment. Parameter-Free Fast Pixelwise Non-Local Means + Denoising. Image Processing On Line, 2014, vol. 4, p. 300-326. + + Used in _fast_nl_means_denoising_3d + """ + cdef float distance + distance = (integral[pln + offset, row + offset, col + offset] - + integral[pln - offset, row - offset, col - offset] + + integral[pln - offset, row - offset, col + offset] + + integral[pln - offset, row + offset, col - offset] + + integral[pln + offset, row - offset, col - offset] - + integral[pln - offset, row + offset, col + offset] - + integral[pln + offset, row - offset, col + offset] - + integral[pln + offset, row + offset, col - offset]) + distance /= s_cube_h_square + return distance + + +@cython.cdivision(True) +@cython.boundscheck(False) +cdef inline _integral_image_2d(IMGDTYPE [:, :, ::] padded, + IMGDTYPE [:, ::] integral, int t_row, + int t_col, int n_row, int n_col, int n_ch): + """ + Computes the integral of the squared difference between an image ``padded`` + and the same image shifted by ``(t_row, t_col)``. + + Parameters + ---------- + padded : ndarray of shape (n_row, n_col, n_ch) + Image of interest. + integral : ndarray + Output of the function. The array is filled with integral values. + ``integral`` should have the same shape as ``padded``. + t_row : int + Shift along the row axis. + t_col : int + Shift along the column axis. + n_row : int + n_col : int + n_ch : int + + Notes + ----- + + The integral computation could be performed using + ``transform.integral_image``, but this helper function saves memory + by avoiding copies of ``padded``. + """ + cdef int row, col + cdef float distance + for row in range(max(1, -t_row), min(n_row, n_row - t_row)): + for col in range(max(1, -t_col), min(n_col, n_col - t_col)): + if n_ch == 1: + distance = (padded[row, col, 0] - + padded[row + t_row, col + t_col, 0])**2 + else: + distance = ((padded[row, col, 0] - + padded[row + t_row, col + t_col, 0])**2 + + (padded[row, col, 1] - + padded[row + t_row, col + t_col, 1])**2 + + (padded[row, col, 2] - + padded[row + t_row, col + t_col, 2])**2) + integral[row, col] = distance + \ + integral[row - 1, col] + \ + integral[row, col - 1] - \ + integral[row - 1, col - 1] + +@cython.cdivision(True) +@cython.boundscheck(False) +cdef inline _integral_image_3d(IMGDTYPE [:, :, ::] padded, + IMGDTYPE [:, :, ::] integral, int t_pln, + int t_row, int t_col, int n_pln, int n_row, + int n_col): + """ + Computes the integral of the squared difference between an image ``padded`` + and the same image shifted by ``(t_pln, t_row, t_col)``. + + Parameters + ---------- + padded : ndarray of shape (n_pln, n_row, n_col) + Image of interest. + integral : ndarray + Output of the function. The array is filled with integral values. + ``integral`` should have the same shape as ``padded``. + t_pln : int + Shift along the plane axis. + t_row : int + Shift along the row axis. + t_col : int + Shift along the column axis. + n_pln : int + n_row : int + n_col : int + + Notes + ----- + + The integral computation could be performed using + ``transform.integral_image``, but this helper function saves memory + by avoiding copies of ``padded``. + """ + cdef int pln, row, col + cdef float distance + for pln in range(max(1, -t_pln), min(n_pln, n_pln - t_pln)): + for row in range(max(1, -t_row), min(n_row, n_row - t_row)): + for col in range(max(1, -t_col), min(n_col, n_col - t_col)): + integral[pln, row, col] = \ + ((padded[pln, row, col] - + padded[pln + t_pln, row + t_row, col + t_col])**2 + + integral[pln - 1, row, col] + + integral[pln, row - 1, col] + + integral[pln, row, col - 1] + + integral[pln - 1, row - 1, col - 1] - + integral[pln - 1, row - 1, col] - + integral[pln, row - 1, col - 1] - + integral[pln - 1, row, col - 1]) + + +@cython.cdivision(True) +@cython.boundscheck(False) +def _fast_nl_means_denoising_2d(image, int s=7, int d=13, float h=0.1): + """ + Perform fast non-local means denoising on 2-D array, with the outer + loop on patch shifts in order to reduce the number of operations. + + Parameters + ---------- + image : ndarray + 2-D input data to be denoised, grayscale or RGB. + s : int, optional + Size of patches used for denoising. + d : int, optional + Maximal distance in pixels where to search patches used for denoising. + h : float, optional + Cut-off distance (in gray levels). The higher h, the more permissive + one is in accepting patches. + + Returns + ------- + result : ndarray + Denoised image, of same shape as input image. + """ + if s % 2 == 0: + s += 1 # odd value for symmetric patch + cdef int offset = s / 2 + # Image padding: we need to account for patch size, possible shift, + # + 1 for the boundary effects in finite differences + cdef int pad_size = offset + d + 1 + cdef IMGDTYPE [:, :, ::1] padded = np.ascontiguousarray(util.pad(image, + ((pad_size, pad_size), (pad_size, pad_size), (0, 0)), + mode='reflect').astype(np.float32)) + cdef IMGDTYPE [:, :, ::1] result = np.zeros_like(padded) + cdef IMGDTYPE [:, ::1] weights = np.zeros_like(padded[..., 0], order='C') + cdef IMGDTYPE [:, ::1] integral = np.zeros_like(padded[..., 0], order='C') + cdef int n_row, n_col, n_ch, t_row, t_col, row, col + cdef float weight, distance + cdef float alpha + cdef float h2 = h ** 2. + cdef float s2 = s ** 2. + n_row, n_col, n_ch = image.shape + cdef float h2s2 = n_ch * h2 * s2 + n_row += 2 * pad_size + n_col += 2 * pad_size + + # Outer loops on patch shifts + # With t2 >= 0, reference patch is always on the left of test patch + # Iterate over shifts along the row axis + for t_row in range(-d, d + 1): + # Iterate over shifts along the column axis + for t_col in range(0, d + 1): + # alpha is to account for patches on the same column + # distance is computed twice in this case + if t_col == 0 and t_row is not 0: + alpha = 0.5 + else: + alpha = 1. + # Compute integral image of the squared difference between + # padded and the same image shifted by (t_row, t_col) + integral = np.zeros_like(padded[..., 0], order='C') + _integral_image_2d(padded, integral, t_row, t_col, + n_row, n_col, n_ch) + + # Inner loops on pixel coordinates + # Iterate over rows, taking offset and shift into account + for row in range(max(offset, offset - t_row), + min(n_row - offset, n_row - offset - t_row)): + # Iterate over columns, taking offset and shift into account + for col in range(max(offset, offset - t_col), + min(n_col - offset, n_col - offset - t_col)): + # Compute squared distance between shifted patches + distance = _integral_to_distance_2d(integral, row, col, + offset, h2s2) + # exp of large negative numbers is close to zero + if distance > DISTANCE_CUTOFF: + continue + weight = alpha * exp(-distance) + # Accumulate weights corresponding to different shifts + weights[row, col] += weight + weights[row + t_row, col + t_col] += weight + # Iterate over channels + for ch in range(n_ch): + result[row, col, ch] += weight * \ + padded[row + t_row, col + t_col, ch] + result[row + t_row, col + t_col, ch] += \ + weight * padded[row, col, ch] + + # Normalize pixel values using sum of weights of contributing patches + for row in range(offset, n_row - offset): + for col in range(offset, n_col - offset): + for channel in range(n_ch): + # No risk of division by zero, since the contribution + # of a null shift is strictly positive + result[row, col, channel] /= weights[row, col] + + # Return cropped result, undoing padding + return result[pad_size:-pad_size, pad_size:-pad_size] + + +@cython.cdivision(True) +@cython.boundscheck(False) +def _fast_nl_means_denoising_3d(image, int s=5, int d=7, float h=0.1): + """ + Perform fast non-local means denoising on 3-D array, with the outer + loop on patch shifts in order to reduce the number of operations. + + Parameters + ---------- + image : ndarray + 3-D input data to be denoised. + s : int, optional + Size of patches used for denoising. + d : int, optional + Maximal distance in pixels where to search patches used for denoising. + h : float, optional + cut-off distance (in gray levels). The higher h, the more permissive + one is in accepting patches. + + Returns + ------- + result : ndarray + Denoised image, of same shape as input image. + """ + if s % 2 == 0: + s += 1 # odd value for symmetric patch + cdef int offset = s / 2 + # Image padding: we need to account for patch size, possible shift, + # + 1 for the boundary effects in finite differences + cdef int pad_size = offset + d + 1 + cdef IMGDTYPE [:, :, ::1] padded = np.ascontiguousarray(util.pad(image, + pad_size, mode='reflect').astype(np.float32)) + cdef IMGDTYPE [:, :, ::1] result = np.zeros_like(padded) + cdef IMGDTYPE [:, :, ::1] weights = np.zeros_like(padded) + cdef IMGDTYPE [:, :, ::1] integral = np.zeros_like(padded) + cdef int n_pln, n_row, n_col, t_pln, t_row, t_col, \ + pln, row, col + cdef int pln_dist_min, pln_dist_max, row_dist_min, row_dist_max, \ + col_dist_min, col_dist_max + cdef float weight, distance + cdef float alpha + cdef float h_square = h ** 2. + cdef float s_cube = s ** 3. + cdef float s_cube_h_square = h_square * s_cube + n_pln, n_row, n_col = image.shape + n_pln += 2 * pad_size + n_row += 2 * pad_size + n_col += 2 * pad_size + + # Outer loops on patch shifts + # With t2 >= 0, reference patch is always on the left of test patch + # Iterate over shifts along the plane axis + for t_pln in range(-d, d + 1): + pln_dist_min = max(offset, offset - t_pln) + pln_dist_max = min(n_pln - offset, n_pln - offset - t_pln) + # Iterate over shifts along the row axis + for t_row in range(-d, d + 1): + row_dist_min = max(offset, offset - t_row) + row_dist_max = min(n_row - offset, n_row - offset - t_row) + # Iterate over shifts along the column axis + for t_col in range(0, d + 1): + col_dist_min = max(offset, offset - t_col) + col_dist_max = min(n_col - offset, n_col - offset - t_col) + # alpha is to account for patches on the same column + # distance is computed twice in this case + if t_col == 0 and (t_pln is not 0 or t_row is not 0): + alpha = 0.5 + else: + alpha = 1. + # Compute integral image of the squared difference between + # padded and the same image shifted by (t_pln, t_row, t_col) + integral = np.zeros_like(padded) + _integral_image_3d(padded, integral, t_pln, t_row, t_col, + n_pln, n_row, n_col) + + # Inner loops on pixel coordinates + # Iterate over planes, taking offset and shift into account + for pln in range(pln_dist_min, pln_dist_max): + # Iterate over rows, taking offset and shift into account + for row in range(row_dist_min, row_dist_max): + # Iterate over columns + for col in range(col_dist_min, col_dist_max): + # Compute squared distance between shifted patches + distance = _integral_to_distance_3d(integral, + pln, row, col, offset, s_cube_h_square) + # exp of large negative numbers is close to zero + if distance > DISTANCE_CUTOFF: + continue + weight = alpha * exp(-distance) + # Accumulate weights for the different shifts + weights[pln, row, col] += weight + weights[pln + t_pln, row + t_row, + col + t_col] += weight + result[pln, row, col] += weight * \ + padded[pln + t_pln, row + t_row, + col + t_col] + result[pln + t_pln, row + t_row, + col + t_col] += weight * \ + padded[pln, row, col] + + # Normalize pixel values using sum of weights of contributing patches + for pln in range(offset, n_pln - offset): + for row in range(offset, n_row - offset): + for col in range(offset, n_col - offset): + # No risk of division by zero, since the contribution + # of a null shift is strictly positive + result[pln, row, col] /= weights[pln, row, col] + + # Return cropped result, undoing padding + return result[pad_size:-pad_size, pad_size:-pad_size, pad_size:-pad_size] diff --git a/skimage/restoration/_unwrap_2d.pyx b/skimage/restoration/_unwrap_2d.pyx index 6e889729..040baa03 100644 --- a/skimage/restoration/_unwrap_2d.pyx +++ b/skimage/restoration/_unwrap_2d.pyx @@ -1,16 +1,19 @@ -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) +cdef extern from *: + 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, + char use_seed, unsigned int seed) def unwrap_2d(double[:, ::1] image, unsigned char[:, ::1] mask, double[:, ::1] unwrapped_image, - wrap_around): + wrap_around, + seed): unwrap2D(&image[0, 0], &unwrapped_image[0, 0], &mask[0, 0], image.shape[1], image.shape[0], wrap_around[1], wrap_around[0], - ) + seed is None, 0 if seed is None else seed) diff --git a/skimage/restoration/_unwrap_3d.pyx b/skimage/restoration/_unwrap_3d.pyx index 370d58be..bbae61a3 100644 --- a/skimage/restoration/_unwrap_3d.pyx +++ b/skimage/restoration/_unwrap_3d.pyx @@ -1,16 +1,19 @@ -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) +cdef extern from *: + 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, + char use_seed, unsigned int seed) def unwrap_3d(double[:, :, ::1] image, unsigned char[:, :, ::1] mask, double[:, :, ::1] unwrapped_image, - wrap_around): + wrap_around, + seed): 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], - ) + seed is None, 0 if seed is None else seed) diff --git a/skimage/restoration/deconvolution.py b/skimage/restoration/deconvolution.py index 51c2c705..632d6788 100644 --- a/skimage/restoration/deconvolution.py +++ b/skimage/restoration/deconvolution.py @@ -36,11 +36,11 @@ def wiener(image, psf, balance, reg=None, is_real=True, clip=True): 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). + avoid noise artifacts). 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. + psf. Shape constraint is the same as 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 @@ -49,24 +49,23 @@ def wiener(image, psf, balance, reg=None, is_real=True, clip=True): 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. + True by default. If True, pixel values of the result above 1 or + under -1 are thresholded for skimage pipeline compatibility. Returns ------- im_deconv : (M, N) ndarray - The deconvolved image + The deconvolved image. Examples -------- >>> from skimage import color, data, restoration - >>> lena = color.rgb2gray(data.lena()) + >>> img = color.rgb2gray(data.astronaut()) >>> 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) + >>> img = convolve2d(img, psf, 'same') + >>> img += 0.1 * img.std() * np.random.standard_normal(img.shape) + >>> deconvolved_img = restoration.wiener(img, psf, 1100) Notes ----- @@ -96,8 +95,8 @@ def wiener(image, psf, balance, reg=None, is_real=True, clip=True): 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). + the solution with respect to the data (sometimes called noise + amplification or "explosive" solution). Finally, the use of Fourier space implies a circulant property of :math:`H`, see [Hunt]. @@ -127,10 +126,11 @@ def wiener(image, psf, balance, reg=None, is_real=True, clip=True): else: trans_func = psf - wiener_filter = np.conj(trans_func) / (np.abs(trans_func)**2 + - balance * np.abs(reg)**2) + 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)) + deconv = uft.uirfft2(wiener_filter * uft.urfft2(image), + shape=image.shape) else: deconv = uft.uifft2(wiener_filter * uft.ufft2(image)) @@ -143,7 +143,7 @@ def wiener(image, psf, balance, reg=None, is_real=True, clip=True): def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, clip=True): - """Unsupervised Wiener-Hunt deconvolution + """Unsupervised Wiener-Hunt deconvolution. Return the deconvolution with a Wiener-Hunt approach, where the hyperparameters are automatically estimated. The algorithm is a @@ -153,21 +153,20 @@ def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, Parameters ---------- image : (M, N) ndarray - The input degraded image + 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 + function is automatically recognized 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. + Dictionary of parameters for the Gibbs sampler. 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. + True by default. If true, pixel values of the result above 1 or + under -1 are thresholded for skimage pipeline compatibility. Returns ------- @@ -203,12 +202,12 @@ def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, Examples -------- >>> from skimage import color, data, restoration - >>> lena = color.rgb2gray(data.lena()) + >>> img = color.rgb2gray(data.astronaut()) >>> 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) + >>> img = convolve2d(img, psf, 'same') + >>> img += 0.1 * img.std() * np.random.standard_normal(img.shape) + >>> deconvolved_img = restoration.unsupervised_wiener(img, psf) Notes ----- @@ -217,10 +216,10 @@ def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, 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 + posterior law. The practical idea is to only draw highly probable + images since they have the biggest contribution to the mean. At the + opposite, the less probable images are drawn less often since + their contribution is low. Finally the empirical mean of these samples give us an estimation of the mean, and an exact computation with an infinite sample set. @@ -262,8 +261,8 @@ def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, # 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 + areg2 = np.abs(reg) ** 2 + atf2 = np.abs(trans_fct) ** 2 # The Fourier transfrom may change the image.size attribut, so we # store it. @@ -320,7 +319,7 @@ def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, # Empirical average \approx POSTMEAN Eq. 44 x_postmean = x_postmean / (iteration - params['burnin']) if is_real: - x_postmean = uft.uirfft2(x_postmean) + x_postmean = uft.uirfft2(x_postmean, shape=image.shape) else: x_postmean = uft.uifft2(x_postmean) @@ -337,21 +336,20 @@ def richardson_lucy(image, psf, iterations=50, clip=True): Parameters ---------- image : ndarray - Input degraded image + Input degraded image. psf : ndarray - The point spread function + The point spread function. iterations : int - Number of iterations. This parameter play to role of + Number of iterations. This parameter plays the 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. + under -1 are thresholded for skimage pipeline compatibility. Returns ------- im_deconv : ndarray - The deconvolved image + The deconvolved image. Examples -------- @@ -365,7 +363,7 @@ def richardson_lucy(image, psf, iterations=50, clip=True): References ---------- - .. [2] http://en.wikipedia.org/wiki/Richardson%E2%80%93Lucy_deconvolution + .. [1] http://en.wikipedia.org/wiki/Richardson%E2%80%93Lucy_deconvolution """ image = image.astype(np.float) psf = psf.astype(np.float) diff --git a/skimage/restoration/non_local_means.py b/skimage/restoration/non_local_means.py new file mode 100644 index 00000000..2ba98a11 --- /dev/null +++ b/skimage/restoration/non_local_means.py @@ -0,0 +1,120 @@ +import numpy as np +from skimage.restoration._nl_means_denoising import _nl_means_denoising_2d, \ + _nl_means_denoising_3d, \ + _fast_nl_means_denoising_2d, _fast_nl_means_denoising_3d + +def nl_means_denoising(image, patch_size=7, patch_distance=11, h=0.1, + multichannel=True, fast_mode=True): + """ + Perform non-local means denoising on 2-D or 3-D grayscale images, and + 2-D RGB images. + + Parameters + ---------- + image : 2D or 3D ndarray + Input image to be denoised, which can be 2D or 3D, and grayscale + or RGB (for 2D images only, see ``multichannel`` parameter). + patch_size : int, optional + Size of patches used for denoising. + patch_distance : int, optional + Maximal distance in pixels where to search patches used for denoising. + h : float, optional + Cut-off distance (in gray levels). The higher h, the more permissive + one is in accepting patches. A higher h results in a smoother image, + at the expense of blurring features. For a Gaussian noise of standard + deviation sigma, a rule of thumb is to choose the value of h to be + sigma of slightly less. + multichannel : bool, optional + Whether the last axis of the image is to be interpreted as multiple + channels or another spatial dimension. Set to ``False`` for 3-D images. + fast_mode : bool, optional + If True (default value), a fast version of the non-local means + algorithm is used. If False, the original version of non-local means is + used. See the Notes section for more details about the algorithms. + + Returns + ------- + + result : ndarray + Denoised image, of same shape as `image`. + + See Also + -------- + fast_nl_means_denoising + + Notes + ----- + + The non-local means algorithm is well suited for denoising images with + specific textures. The principle of the algorithm is to average the value + of a given pixel with values of other pixels in a limited neighbourhood, + provided that the *patches* centered on the other pixels are similar enough + to the patch centered on the pixel of interest. + + In the original version of the algorithm [1]_, corresponding to + ``fast=False``, the computational complexity is + + image.size * patch_size ** image.ndim * patch_distance ** image.ndim + + Hence, changing the size of patches or their maximal distance has a + strong effect on computing times, especially for 3-D images. + + However, the default behavior corresponds to ``fast_mode=True``, for which + another version of non-local means [2]_ is used, corresponding to a + complexity of + + image.size * patch_distance ** image.ndim + + The computing time depends only weakly on the patch size, thanks to the + computation of the integral of patches distances for a given shift, that + reduces the number of operations [1]_. Therefore, this algorithm executes + faster than `nl_means_denoising`, at the expense of using twice as much + memory. + + Compared to the classic non-local means algorithm implemented in + `nl_means_denoising`, all pixels of a patch contribute to the distance to + another patch with the same weight, no matter their distance to the center + of the patch. This coarser computation of the distance can result in a + slightly poorer denoising performance. Moreover, for small images (images + with a linear size that is only a few times the patch size), the classic + algorithm can be faster due to boundary effects. + + The image is padded using the `reflect` mode of `skimage.util.pad` + before denoising. + + References + ---------- + .. [1] Buades, A., Coll, B., & Morel, J. M. (2005, June). A non-local + algorithm for image denoising. In CVPR 2005, Vol. 2, pp. 60-65, IEEE. + + .. [2] Jacques Froment. Parameter-Free Fast Pixelwise Non-Local Means + Denoising. Image Processing On Line, 2014, vol. 4, p. 300-326. + + Examples + -------- + >>> a = np.zeros((40, 40)) + >>> a[10:-10, 10:-10] = 1. + >>> a += 0.3*np.random.randn(*a.shape) + >>> denoised_a = nl_means_denoising(a, 7, 5, 0.1) + """ + if image.ndim == 2: + image = image[..., np.newaxis] + multichannel = True + if image.ndim != 3: + raise NotImplementedError("Non-local means denoising is only \ + implemented for 2D grayscale and RGB images or 3-D grayscale images.") + if multichannel: # 2-D images + if fast_mode: + return np.squeeze(np.array(_fast_nl_means_denoising_2d(image, + patch_size, patch_distance, h))) + else: + return np.squeeze(np.array(_nl_means_denoising_2d(image, + patch_size, patch_distance, h))) + else: # 3-D grayscale + if fast_mode: + return np.array(_fast_nl_means_denoising_3d(image, s=patch_size, + d=patch_distance, h=h)) + else: + return np.array(_nl_means_denoising_3d(image, patch_size, + patch_distance, h)) + diff --git a/skimage/restoration/setup.py b/skimage/restoration/setup.py index e20073e0..12638107 100644 --- a/skimage/restoration/setup.py +++ b/skimage/restoration/setup.py @@ -17,6 +17,7 @@ def configuration(parent_package='', top_path=None): cython(['_unwrap_2d.pyx'], working_path=base_path) cython(['_unwrap_3d.pyx'], working_path=base_path) cython(['_denoise_cy.pyx'], working_path=base_path) + cython(['_nl_means_denoising.pyx'], working_path=base_path) config.add_extension('_unwrap_1d', sources=['_unwrap_1d.c'], include_dirs=[get_numpy_include_dirs()]) @@ -28,6 +29,9 @@ def configuration(parent_package='', top_path=None): 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('_nl_means_denoising', + sources=['_nl_means_denoising.c'], + include_dirs=[get_numpy_include_dirs()]) return config diff --git a/skimage/restoration/tests/__init__.py b/skimage/restoration/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/restoration/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/restoration/tests/test_denoise.py b/skimage/restoration/tests/test_denoise.py index d451aa74..e84e1c49 100644 --- a/skimage/restoration/tests/test_denoise.py +++ b/skimage/restoration/tests/test_denoise.py @@ -7,25 +7,27 @@ from skimage import restoration, data, color, img_as_float np.random.seed(1234) -lena = img_as_float(data.lena()[:256, :256]) -lena_gray = color.rgb2gray(lena) +astro = img_as_float(data.astronaut()[:128, :128]) +astro_gray = color.rgb2gray(astro) +checkerboard_gray = img_as_float(data.checkerboard()) +checkerboard = color.gray2rgb(checkerboard_gray) def test_denoise_tv_chambolle_2d(): - # lena image - img = lena_gray - # add noise to lena - img += 0.5 * img.std() * np.random.random(img.shape) + # astronaut image + img = astro_gray.copy() + # add noise to astronaut + img += 0.5 * img.std() * np.random.rand(*img.shape) # clip noise so that it does not exceed allowed range for float images. img = np.clip(img, 0, 1) # denoise - denoised_lena = restoration.denoise_tv_chambolle(img, weight=60.0) + denoised_astro = restoration.denoise_tv_chambolle(img, weight=60.0) # which dtype? - assert denoised_lena.dtype in [np.float, np.float32, np.float64] + assert denoised_astro.dtype in [np.float, np.float32, np.float64] from scipy import ndimage grad = ndimage.morphological_gradient(img, size=((3, 3))) grad_denoised = ndimage.morphological_gradient( - denoised_lena, size=((3, 3))) + denoised_astro, size=((3, 3))) # test if the total variation has decreased assert grad_denoised.dtype == np.float assert (np.sqrt((grad_denoised**2).sum()) @@ -33,22 +35,23 @@ def test_denoise_tv_chambolle_2d(): def test_denoise_tv_chambolle_multichannel(): - denoised0 = restoration.denoise_tv_chambolle(lena[..., 0], weight=60.0) - denoised = restoration.denoise_tv_chambolle(lena, weight=60.0, + denoised0 = restoration.denoise_tv_chambolle(astro[..., 0], weight=60.0) + denoised = restoration.denoise_tv_chambolle(astro, weight=60.0, multichannel=True) assert_equal(denoised[..., 0], denoised0) def test_denoise_tv_chambolle_float_result_range(): - # lena image - img = lena_gray - int_lena = np.multiply(img, 255).astype(np.uint8) - assert np.max(int_lena) > 1 - denoised_int_lena = restoration.denoise_tv_chambolle(int_lena, weight=60.0) + # astronaut image + img = astro_gray + int_astro = np.multiply(img, 255).astype(np.uint8) + assert np.max(int_astro) > 1 + denoised_int_astro = restoration.denoise_tv_chambolle(int_astro, + weight=60.0) # test if the value range of output float data is within [0.0:1.0] - assert denoised_int_lena.dtype == np.float - assert np.max(denoised_int_lena) <= 1.0 - assert np.min(denoised_int_lena) >= 0.0 + assert denoised_int_astro.dtype == np.float + assert np.max(denoised_int_astro) <= 1.0 + assert np.min(denoised_int_astro) >= 0.0 def test_denoise_tv_chambolle_3d(): @@ -57,7 +60,7 @@ def test_denoise_tv_chambolle_3d(): mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 mask = 100 * mask.astype(np.float) mask += 60 - mask += 20 * np.random.random(mask.shape) + mask += 20 * np.random.rand(*mask.shape) mask[mask < 0] = 0 mask[mask > 255] = 255 res = restoration.denoise_tv_chambolle(mask.astype(np.uint8), weight=100) @@ -66,53 +69,53 @@ def test_denoise_tv_chambolle_3d(): # test wrong number of dimensions assert_raises(ValueError, restoration.denoise_tv_chambolle, - np.random.random((8, 8, 8, 8))) + np.random.rand(8, 8, 8, 8)) def test_denoise_tv_bregman_2d(): - img = lena_gray + img = checkerboard_gray.copy() # add some random noise - img += 0.5 * img.std() * np.random.random(img.shape) + img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) 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() - assert out1.std() > out2.std() + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() def test_denoise_tv_bregman_float_result_range(): - # lena image - img = lena_gray - int_lena = np.multiply(img, 255).astype(np.uint8) - assert np.max(int_lena) > 1 - denoised_int_lena = restoration.denoise_tv_bregman(int_lena, weight=60.0) + # astronaut image + img = astro_gray.copy() + int_astro = np.multiply(img, 255).astype(np.uint8) + assert np.max(int_astro) > 1 + denoised_int_astro = restoration.denoise_tv_bregman(int_astro, weight=60.0) # test if the value range of output float data is within [0.0:1.0] - assert denoised_int_lena.dtype == np.float - assert np.max(denoised_int_lena) <= 1.0 - assert np.min(denoised_int_lena) >= 0.0 + assert denoised_int_astro.dtype == np.float + assert np.max(denoised_int_astro) <= 1.0 + assert np.min(denoised_int_astro) >= 0.0 def test_denoise_tv_bregman_3d(): - img = lena + img = checkerboard.copy() # add some random noise - img += 0.5 * img.std() * np.random.random(img.shape) + img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) 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() - assert out1.std() > out2.std() + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() def test_denoise_bilateral_2d(): - img = lena_gray + img = checkerboard_gray.copy() # add some random noise - img += 0.5 * img.std() * np.random.random(img.shape) + img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) out1 = restoration.denoise_bilateral(img, sigma_range=0.1, @@ -120,15 +123,15 @@ def test_denoise_bilateral_2d(): out2 = restoration.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) - # make sure noise is reduced - assert img.std() > out1.std() - assert out1.std() > out2.std() + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() def test_denoise_bilateral_3d(): - img = lena + img = checkerboard.copy() # add some random noise - img += 0.5 * img.std() * np.random.random(img.shape) + img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) out1 = restoration.denoise_bilateral(img, sigma_range=0.1, @@ -136,9 +139,80 @@ def test_denoise_bilateral_3d(): out2 = restoration.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() + + +def test_nl_means_denoising_2d(): + img = np.zeros((40, 40)) + img[10:-10, 10:-10] = 1. + img += 0.3*np.random.randn(*img.shape) + denoised = restoration.nl_means_denoising(img, 7, 5, 0.2, fast_mode=True) # make sure noise is reduced - assert img.std() > out1.std() - assert out1.std() > out2.std() + assert img.std() > denoised.std() + denoised = restoration.nl_means_denoising(img, 7, 5, 0.2, fast_mode=False) + # make sure noise is reduced + assert img.std() > denoised.std() + + +def test_nl_means_denoising_2drgb(): + # reduce image size because nl means is very slow + img = np.copy(astro[:50, :50]) + # add some random noise + img += 0.5 * img.std() * np.random.random(img.shape) + img = np.clip(img, 0, 1) + denoised = restoration.nl_means_denoising(img, 7, 9, 0.3, fast_mode=True) + # make sure noise is reduced + assert img.std() > denoised.std() + denoised = restoration.nl_means_denoising(img, 7, 9, 0.3, fast_mode=False) + # make sure noise is reduced + assert img.std() > denoised.std() + + +def test_nl_means_denoising_3d(): + img = np.zeros((20, 20, 10)) + img[5:-5, 5:-5, 3:-3] = 1. + img += 0.3*np.random.randn(*img.shape) + denoised = restoration.nl_means_denoising(img, 5, 4, 0.2, fast_mode=True, + multichannel=False) + # make sure noise is reduced + assert img.std() > denoised.std() + denoised = restoration.nl_means_denoising(img, 5, 4, 0.2, fast_mode=False, + multichannel=False) + # make sure noise is reduced + assert img.std() > denoised.std() + + +def test_nl_means_denoising_multichannel(): + img = np.zeros((21, 20, 10)) + img[10, 9:11, 2:-2] = 1. + img += 0.3*np.random.randn(*img.shape) + denoised_wrong_multichannel = restoration.nl_means_denoising(img, + 5, 4, 0.1, fast_mode=True, multichannel=True) + denoised_ok_multichannel = restoration.nl_means_denoising(img, + 5, 4, 0.1, fast_mode=True, multichannel=False) + snr_wrong = 10 * np.log10(1. / + ((denoised_wrong_multichannel - img)**2).mean()) + snr_ok = 10 * np.log10(1. / + ((denoised_ok_multichannel - img)**2).mean()) + assert snr_ok > snr_wrong + + +def test_nl_means_denoising_wrong_dimension(): + img = np.zeros((5, 5, 5, 5)) + assert_raises(NotImplementedError, restoration.nl_means_denoising, img) + + +def test_no_denoising_for_small_h(): + img = np.zeros((40, 40)) + img[10:-10, 10:-10] = 1. + img += 0.3*np.random.randn(*img.shape) + # very small h should result in no averaging with other patches + denoised = restoration.nl_means_denoising(img, 7, 5, 0.01, fast_mode=True) + assert np.allclose(denoised, img) + denoised = restoration.nl_means_denoising(img, 7, 5, 0.01, fast_mode=False) + assert np.allclose(denoised, img) if __name__ == "__main__": diff --git a/skimage/restoration/tests/test_restoration.py b/skimage/restoration/tests/test_restoration.py index 915640af..f248f613 100644 --- a/skimage/restoration/tests/test_restoration.py +++ b/skimage/restoration/tests/test_restoration.py @@ -2,6 +2,7 @@ from os.path import abspath, dirname, join as pjoin import numpy as np from scipy.signal import convolve2d +from scipy import ndimage as nd import skimage from skimage.data import camera @@ -53,6 +54,29 @@ def test_unsupervised_wiener(): rtol=1e-3) +def test_image_shape(): + """Test that shape of output image in deconvolution is same as input. + + This addresses issue #1172. + """ + point = np.zeros((5, 5), np.float) + point[2, 2] = 1. + psf = nd.gaussian_filter(point, sigma=1.) + # image shape: (45, 45), as reported in #1172 + image = skimage.img_as_float(camera()[110:155, 225:270]) # just the face + image_conv = nd.convolve(image, psf) + deconv_sup = restoration.wiener(image_conv, psf, 1) + deconv_un = restoration.unsupervised_wiener(image_conv, psf)[0] + # test the shape + np.testing.assert_equal(image.shape, deconv_sup.shape) + np.testing.assert_equal(image.shape, deconv_un.shape) + # test the reconstruction error + sup_relative_error = np.abs(deconv_sup - image) / image + un_relative_error = np.abs(deconv_un - image) / image + np.testing.assert_array_less(np.median(sup_relative_error), 0.1) + np.testing.assert_array_less(np.median(un_relative_error), 0.1) + + def test_richardson_lucy(): psf = np.ones((5, 5)) / 25 data = convolve2d(test_img, psf, 'same') @@ -62,3 +86,8 @@ def test_richardson_lucy(): path = pjoin(dirname(abspath(__file__)), 'camera_rl.npy') np.testing.assert_allclose(deconvolved, np.load(path), rtol=1e-3) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/restoration/tests/test_unwrap.py b/skimage/restoration/tests/test_unwrap.py index fca63724..f5ff2c76 100644 --- a/skimage/restoration/tests/test_unwrap.py +++ b/skimage/restoration/tests/test_unwrap.py @@ -1,12 +1,13 @@ from __future__ import print_function, division import numpy as np -from numpy.testing import (run_module_suite, assert_array_almost_equal, +from numpy.testing import (run_module_suite, assert_array_almost_equal_nulp, assert_almost_equal, assert_array_equal, assert_raises) import warnings from skimage.restoration import unwrap_phase +from skimage._shared._warnings import expected_warnings def assert_phase_almost_equal(a, b, *args, **kwargs): @@ -28,7 +29,7 @@ def assert_phase_almost_equal(a, b, *args, **kwargs): 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) + assert_array_almost_equal_nulp(a + shift, b, *args, **kwargs) def check_unwrap(image, mask=None): @@ -37,7 +38,7 @@ def check_unwrap(image, mask=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) + image_unwrapped = unwrap_phase(image_wrapped, seed=0) assert_phase_almost_equal(image_unwrapped, image) @@ -47,7 +48,7 @@ def test_unwrap_1d(): # 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) + assert_raises(ValueError, unwrap_phase, image, True, seed=0) def test_unwrap_2d(): @@ -83,7 +84,7 @@ def check_wrap_around(ndim, axis): 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) + image_unwrap_no_wrap_around = unwrap_phase(image_wrapped, seed=0) print('endpoints without wrap_around:', image_unwrap_no_wrap_around[index_first], image_unwrap_no_wrap_around[index_last]) @@ -95,7 +96,8 @@ def check_wrap_around(ndim, axis): 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) + image_unwrap_wrap_around = unwrap_phase(image_wrapped, wrap_around, + seed=0) print('endpoints with wrap_around:', image_unwrap_wrap_around[index_first], image_unwrap_wrap_around[index_last]) @@ -128,13 +130,17 @@ def test_mask(): 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]) + assert_array_almost_equal_nulp(image_unwrapped[:, -1], image[i, -1]) + assert np.ma.isMaskedArray(image_unwrapped) # 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]) + with expected_warnings(['length 1 dimension']): + shape = (1,) + image_wrapped.shape + image_wrapped_3d = image_wrapped.reshape(shape) + image_unwrapped_3d = unwrap_phase(image_wrapped_3d) + # remove phase shift + image_unwrapped_3d -= image_unwrapped_3d[0, 0, 0] + assert_array_almost_equal_nulp(image_unwrapped_3d[:, :, -1], image[i, -1]) def test_invalid_input(): @@ -144,5 +150,58 @@ def test_invalid_input(): assert_raises(ValueError, unwrap_phase, np.zeros((1, 1)), 'False') +def test_unwrap_3d_middle_wrap_around(): + # Segmentation fault in 3D unwrap phase with middle dimension connected + # GitHub issue #1171 + image = np.zeros((20, 30, 40), dtype=np.float32) + unwrap = unwrap_phase(image, wrap_around=[False, True, False]) + assert np.all(unwrap == 0) + + +def test_unwrap_2d_compressed_mask(): + # ValueError when image is masked array with a compressed mask (no masked + # elments). GitHub issue #1346 + image = np.ma.zeros((10, 10)) + unwrap = unwrap_phase(image) + assert np.all(unwrap == 0) + + +def test_unwrap_2d_all_masked(): + # Segmentation fault when image is masked array with a all elements masked + # GitHub issue #1347 + # all elements masked + image = np.ma.zeros((10, 10)) + image[:] = np.ma.masked + unwrap = unwrap_phase(image) + assert np.ma.isMaskedArray(unwrap) + assert np.all(unwrap.mask) + + # 1 unmasked element, still zero edges + image = np.ma.zeros((10, 10)) + image[:] = np.ma.masked + image[0, 0] = 0 + unwrap = unwrap_phase(image) + assert np.ma.isMaskedArray(unwrap) + assert np.sum(unwrap.mask) == 99 # all but one masked + assert unwrap[0, 0] == 0 + + +def test_unwrap_3d_all_masked(): + # all elements masked + image = np.ma.zeros((10, 10, 10)) + image[:] = np.ma.masked + unwrap = unwrap_phase(image) + assert np.ma.isMaskedArray(unwrap) + assert np.all(unwrap.mask) + + # 1 unmasked element, still zero edges + image = np.ma.zeros((10, 10, 10)) + image[:] = np.ma.masked + image[0, 0, 0] = 0 + unwrap = unwrap_phase(image) + assert np.ma.isMaskedArray(unwrap) + assert np.sum(unwrap.mask) == 999 # all but one masked + assert unwrap[0, 0, 0] == 0 + if __name__ == "__main__": run_module_suite() diff --git a/skimage/restoration/uft.py b/skimage/restoration/uft.py index fedcc6f6..dfece24f 100644 --- a/skimage/restoration/uft.py +++ b/skimage/restoration/uft.py @@ -3,17 +3,16 @@ """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 +This module implements the unitary fourier transform, also known as +the ortho-normal transform. It is especially useful for convolution +[1], as it respects 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 +so the Fourier transform has the same energy as 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. +last axis for performance (assuming a C-order array input). References ---------- @@ -31,14 +30,14 @@ __keywords__ = "fft, Fourier Transform, orthonormal, unitary" def ufftn(inarray, dim=None): - """N-dim unitary Fourier transform + """N-dimensional unitary Fourier transform. Parameters ---------- inarray : ndarray The array to transform. dim : int, optional - The ``dim`` last axis along wich to compute the transform. All + The last axis along which to compute the transform. All axes by default. Returns @@ -62,14 +61,14 @@ def ufftn(inarray, dim=None): def uifftn(inarray, dim=None): - """N-dim unitary inverse Fourier transform + """N-dimensional 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 + The last axis along which to compute the transform. All axes by default. Returns @@ -93,29 +92,29 @@ def uifftn(inarray, dim=None): def urfftn(inarray, dim=None): - """N-dim real unitary Fourier transform + """N-dimensional real unitary Fourier transform. - This transform consider the Hermitian property of the transform on - real input + This transform considers the Hermitian property of the transform on + real-valued input. Parameters ---------- - inarray : ndarray + inarray : ndarray, shape (M, N, ..., P) The array to transform. dim : int, optional - The ``dim`` last axis along wich to compute the transform. All + The last axis along which to compute the transform. All axes by default. Returns ------- - outarray : ndarray (the last dim as N / 2 + 1 lenght) + outarray : ndarray, shape (M, N, ..., P / 2 + 1) 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. + values. Consequently, the output has a Hermitian property and + redundant values are not computed or returned. Examples -------- @@ -133,22 +132,22 @@ def urfftn(inarray, dim=None): def uirfftn(inarray, dim=None, shape=None): - """N-dim real unitary Fourier transform + """N-dimensional inverse real unitary Fourier transform. - This transform consider the Hermitian property of the transform - from complex to real real input. + This transform considers the Hermitian property of the transform + from complex to real input. Parameters ---------- inarray : ndarray The array to transform. dim : int, optional - The ``dim`` last axis along wich to compute the transform. All + The last axis along which to compute the transform. All axes by default. - shape : tuple of int + shape : tuple of int, optional 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``. + case of odd-valued input shape. In this case, this parameter + should be provided. See ``np.fft.irfftn``. Returns ------- @@ -157,9 +156,9 @@ def uirfftn(inarray, dim=None, shape=None): 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. + The ``uirfft`` function assumes that the output array is + real-valued. Consequently, the input is assumed to have a Hermitian + property and redundant values are implicit. Examples -------- @@ -177,7 +176,7 @@ def uirfftn(inarray, dim=None, shape=None): def ufft2(inarray): - """2-dim unitary Fourier transform + """2-dimensional unitary Fourier transform. Compute the Fourier transform on the last 2 axes. @@ -188,7 +187,7 @@ def ufft2(inarray): Returns ------- - outarray : ndarray (same shape than inarray) + outarray : ndarray (same shape as inarray) The unitary 2-D Fourier transform of ``inarray``. See Also @@ -199,7 +198,8 @@ def ufft2(inarray): -------- >>> input = np.ones((10, 128, 128)) >>> output = ufft2(input) - >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), output[1, 0, 0]) + >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), + ... output[1, 0, 0]) True >>> output.shape (10, 128, 128) @@ -208,7 +208,7 @@ def ufft2(inarray): def uifft2(inarray): - """2-dim inverse unitary Fourier transform + """2-dimensional inverse unitary Fourier transform. Compute the inverse Fourier transform on the last 2 axes. @@ -219,7 +219,7 @@ def uifft2(inarray): Returns ------- - outarray : ndarray (same shape than inarray) + outarray : ndarray (same shape as inarray) The unitary 2-D inverse Fourier transform of ``inarray``. See Also @@ -230,7 +230,8 @@ def uifft2(inarray): -------- >>> input = np.ones((10, 128, 128)) >>> output = uifft2(input) - >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), output[0, 0, 0]) + >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), + ... output[0, 0, 0]) True >>> output.shape (10, 128, 128) @@ -239,20 +240,20 @@ def uifft2(inarray): def urfft2(inarray): - """2-dim real unitary Fourier transform + """2-dimensional 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. + transform considers the Hermitian property of the transform from + complex to real-valued input. Parameters ---------- - inarray : ndarray + inarray : ndarray, shape (M, N, ..., P) The array to transform. Returns ------- - outarray : ndarray (the last dim as (N - 1) *2 lenght) + outarray : ndarray, shape (M, N, ..., 2 * (P - 1)) The unitary 2-D real Fourier transform of ``inarray``. See Also @@ -263,7 +264,8 @@ def urfft2(inarray): -------- >>> input = np.ones((10, 128, 128)) >>> output = urfft2(input) - >>> np.allclose(np.sum(input[1,...]) / np.sqrt(input[1,...].size), output[1, 0, 0]) + >>> np.allclose(np.sum(input[1,...]) / np.sqrt(input[1,...].size), + ... output[1, 0, 0]) True >>> output.shape (10, 128, 65) @@ -272,20 +274,20 @@ def urfft2(inarray): def uirfft2(inarray, shape=None): - """2-dim real unitary Fourier transform + """2-dimensional inverse 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. + This transform considers the Hermitian property of the transform + from complex to real-valued input. Parameters ---------- - inarray : ndarray + inarray : ndarray, shape (M, N, ..., P) The array to transform. Returns ------- - outarray : ndarray (the last dim as (N - 1) *2 lenght) + outarray : ndarray, shape (M, N, ..., 2 * (P - 1)) The unitary 2-D inverse real Fourier transform of ``inarray``. See Also @@ -305,14 +307,16 @@ def uirfft2(inarray, shape=None): def image_quad_norm(inarray): - """Return quadratic norm of images in Fourier space + """Return the quadratic norm of images in Fourier space. - This function detect if the image suppose the hermitian property. + This function detects whether the input image satisfies the + Hermitian property. Parameters ---------- inarray : ndarray - The images are supposed to be in the last two axes + Input image. The image data should reside in the final two + axes. Returns ------- @@ -327,40 +331,40 @@ def image_quad_norm(inarray): >>> image_quad_norm(ufft2(input)) == image_quad_norm(urfft2(input)) True """ - # If there is an hermitian symmetry + # If there is a 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) + 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) + 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 + """Compute the transfer function of an impulse response (IR). - This function make the necessary correct zero-padding, zero - convention, correct fft2 etc... to compute the transfer function + This function makes 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. + The impulse responses. shape : tuple of int - A tuple of integer corresponding to the target shape of the - tranfert function. + A tuple of integer corresponding to the target shape of the + transfer function. dim : int, optional - The ``dim`` last axis along wich to compute the transform. All + The last axis along which 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_real : boolean (optional, default True) + If True, imp_resp is supposed real and the Hermitian property is used with rfftn Fourier transform. Returns ------- y : complex ndarray - The tranfert function of shape ``shape``. + The transfer function of shape ``shape``. See Also -------- @@ -377,11 +381,10 @@ def ir2tf(imp_resp, shape, dim=None, is_real=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. + The input array can be composed of multiple-dimensional IR with + an arbitrary number of IR. The individual IR must be accessed + through the first axes. The last ``dim`` axes contain the space + definition. """ if not dim: dim = imp_resp.ndim @@ -402,27 +405,27 @@ def ir2tf(imp_resp, shape, dim=None, is_real=True): def laplacian(ndim, shape, is_real=True): - """Return the transfer function of the Laplacian + """Return the transfer function of the Laplacian. - Laplacian is the second order difference, on line and column. + Laplacian is the second order difference, on row and column. Parameters ---------- ndim : int - The dimension of the Laplacian + 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. + is_real : boolean (optional, default True) + If True, imp_resp is assumed to be real-valued and + the Hermitian property is used with rfftn Fourier transform + to return the transfer function. Returns ------- tf : array_like, complex - The transfer function + The transfer function. impr : array_like, real - The Laplacian + The Laplacian. Examples -------- diff --git a/skimage/restoration/unwrap.py b/skimage/restoration/unwrap.py index 085bef0b..14f1eafa 100644 --- a/skimage/restoration/unwrap.py +++ b/skimage/restoration/unwrap.py @@ -7,29 +7,34 @@ 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 +def unwrap_phase(image, wrap_around=False, seed=None): + '''Recover the original from a wrapped phase image. + + From an 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 + 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 + `ValueError`. + wrap_around : bool or sequence of bool, optional + 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. + seed : int, optional + Unwrapping 2D or 3D images uses random initialization. This sets the + seed of the PRNG to achieve deterministic behavior. Returns ------- - image_unwrapped : array_like, float32 - Unwrapped image of the same shape as the input. If the input ``image`` + image_unwrapped : array_like, double + Unwrapped image of the same shape as the input. If the input `image` was a masked array, the mask will be preserved. Raises @@ -60,43 +65,46 @@ def unwrap_phase(image, wrap_around=False): International Society for Optics and Photonics. ''' if image.ndim not in (1, 2, 3): - raise ValueError('image must be 1, 2 or 3 dimensional') + 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 ' + 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 ' + 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') + 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 ' + 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']) + mask = np.require(np.ma.getmaskarray(image), 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') + + image_not_masked = np.asarray( + np.ma.getdata(image), dtype=np.double, order='C') + image_unwrapped = np.empty_like(image, dtype=np.double, order='C', + subok=False) 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) + wrap_around, seed) elif image.ndim == 3: unwrap_3d(image_not_masked, mask, image_unwrapped, - wrap_around) + wrap_around, seed) if np.ma.isMaskedArray(image): return np.ma.array(image_unwrapped, mask=mask) diff --git a/skimage/restoration/unwrap_2d_ljmu.c b/skimage/restoration/unwrap_2d_ljmu.c index 9b5c53b8..690a8d77 100644 --- a/skimage/restoration/unwrap_2d_ljmu.c +++ b/skimage/restoration/unwrap_2d_ljmu.c @@ -1,23 +1,30 @@ // 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. +// 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 +#include #ifndef M_PI #define M_PI 3.1415926535897932384626433832795 @@ -26,496 +33,505 @@ #define PI M_PI #define TWOPI (2 * M_PI) -//TODO: remove global variables -//TODO: make thresholds independent +// TODO: remove global variables +// TODO: make thresholds independent #define NOMASK 0 #define MASK 1 -typedef struct -{ +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 +// 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. + 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 + 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 +// 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) +#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; +typedef enum { + yes, + no +} yes_no; -yes_no find_pivot(EDGE *left, EDGE *right, double *pivot_ptr) -{ - EDGE a, b, c, *p; +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); + 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 (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; } - - 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; + } + 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; - } +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; + } + return left; } -void quicker_sort(EDGE *left, EDGE *right) -{ - EDGE *p; - double pivot; +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); - } + 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) -{ +// 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, + char use_seed, unsigned int seed) { 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++; - } + if (use_seed) { + srand(seed); + } + + 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 = 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) -{ +// 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; + 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) -{ +// 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; + 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, 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 + 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; + // 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; } - 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; + } + } - //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++; } - 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++; - } + // 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) -{ +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 *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; + 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; } - 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; + // 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 + 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; - } + 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; + } + } - //calculating the reliability for the right border of the image - pixel_pointer = pixel + 2 * image_width - 1; - WIP = wrappedImage + 2 * image_width - 1; + 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_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; - } + 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++; } - if (params->y_connectivity == 1) - { - //calculating the reliability for the top border of the image - PIXELM *pixel_pointer = pixel + 1; - double *WIP = wrappedImage + 1; + // 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*(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++; - } + 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) -{ +// 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++; - } + 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++; } - //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; - } + 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) -{ +// 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 + 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++; - } + // 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) -{ +// gather the pixels of the image into groups +void gatherPIXELs(EDGE *edge, params_t *params) { int k; PIXELM *PIXEL1; PIXELM *PIXEL2; @@ -524,173 +540,165 @@ void gatherPIXELs(EDGE *edge, params_t *params) 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; + 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 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 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; - } - } + // 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 + // 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++; - } + } // else + } // else + } // if + pointer_edge++; + } } -//unwrap the image -void unwrapImage(PIXELM *pixel, int image_width, int image_height) -{ +// 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; + PIXELM *pixel_pointer = pixel; - for (i = 0; i < image_size; i++) - { - pixel_pointer->value += TWOPI * (double)(pixel_pointer->increment); - pixel_pointer++; - } + 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; +// 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; + unsigned char *IMP = input_mask; // input mask pointer + double min = DBL_MAX; 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; + // 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++; + 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++; + // 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) -{ +// 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++; - } + 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) -{ +// 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, + char use_seed, unsigned int seed) { params_t params = {TWOPI, wrap_around_x, wrap_around_y, 0}; unsigned char *extended_mask; PIXELM *pixel; @@ -698,29 +706,32 @@ unwrap2D(double* wrapped_image, double* UnwrappedImage, unsigned char* input_mas 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)); + 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); + initialisePIXELs(wrapped_image, input_mask, extended_mask, pixel, image_width, + image_height, use_seed, seed); + 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 + if (params.no_of_edges != 0) { + // 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 ? + // 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); diff --git a/skimage/restoration/unwrap_3d_ljmu.c b/skimage/restoration/unwrap_3d_ljmu.c index 88012f18..fdd9847a 100644 --- a/skimage/restoration/unwrap_3d_ljmu.c +++ b/skimage/restoration/unwrap_3d_ljmu.c @@ -1,25 +1,34 @@ // 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. +// 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 +#include #ifndef M_PI #define M_PI 3.1415926535897932384626433832795 @@ -31,8 +40,7 @@ #define NOMASK 0 #define MASK 1 -typedef struct -{ +typedef struct { double mod; int x_connectivity; int y_connectivity; @@ -40,406 +48,420 @@ typedef struct 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 +// 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. + 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 + 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 +// 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) +#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; +typedef enum { + yes, + no +} yes_no; -yes_no find_pivot(EDGE *left, EDGE *right, double *pivot_ptr) -{ - EDGE a, b, c, *p; +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); + 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 (a.reliab < b.reliab) { + *pivot_ptr = b.reliab; + return yes; + } - if (b.reliab < c.reliab) - { - *pivot_ptr = c.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; + 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; +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; +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); - } + 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) -{ +// 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, + char use_seed, unsigned int seed) { 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++; - } - } + if (use_seed) { + srand(seed); + } + + 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 = 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) -{ +// 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; + 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) -{ +// 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; + 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) -{ +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 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 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 + 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; + // 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; } - 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; + } + } - //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; } - 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; + } + } - //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; } - 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; - } + // 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; +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; @@ -448,298 +470,375 @@ void calculate_reliability(double *wrappedVolume, VOXELM *voxel, int volume_widt 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; - } + 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; } - 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; + } + } - //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; } - 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; + } + } - //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; } - 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; - } + // 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) -{ +// 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++; - } + 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; - } - } + } + 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) -{ +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; @@ -747,53 +846,51 @@ void verticalEDGEs(VOXELM *voxel, EDGE *edge, int volume_width, int volume_heig 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; + for (n = 0; n < volume_depth; n++) { + for (i = 0; i < volume_height - 1; i++) { + for (j = 0; j < volume_width; j++) { + if (voxel_pointer->input_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; - } + 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; } + } 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) -{ +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; @@ -802,53 +899,49 @@ void normalEDGEs(VOXELM *voxel, EDGE *edge, int volume_width, int volume_height 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++; - } - } + for (n = 0; n < volume_depth - 1; n++) { + for (i = 0; i < volume_height; i++) { + for (j = 0; j < volume_width; j++) { + if (voxel_pointer->input_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++; - } - } + 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) -{ +// gather the voxels of the volume into groups +void gatherVOXELs(EDGE *edge, params_t *params) { int k; VOXELM *VOXEL1; VOXELM *VOXEL2; @@ -857,171 +950,168 @@ void gatherVOXELs(EDGE *edge, params_t *params) 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; + 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 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 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; - } - } + // 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 + // 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++; - } + } // else + } // else + } // if + pointer_edge++; + } } -//unwrap the volume -void unwrapVolume(VOXELM *voxel, int volume_width, int volume_height, int volume_depth) -{ +// 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; + VOXELM *voxel_pointer = voxel; - for (i = 0; i < volume_size; i++) - { - voxel_pointer->value += TWOPI * (double)(voxel_pointer->increment); - voxel_pointer++; - } + 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; +// 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.; + unsigned char *IMP = input_mask; // input mask pointer + double min = DBL_MAX; 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; + // 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++; + 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++; + // 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) -{ +// 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++; - } + 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) -{ +// 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, char use_seed, unsigned int seed) { params_t params = {TWOPI, wrap_around_x, wrap_around_y, wrap_around_z, 0}; unsigned char *extended_mask; VOXELM *voxel; @@ -1029,28 +1119,39 @@ unwrap3D(double* wrapped_volume, double* unwrapped_volume, unsigned char* input_ 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));; + 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); + 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, use_seed, seed); + 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); + if (params.no_of_edges != 0) { + // 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 + // 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); + // 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); diff --git a/skimage/segmentation/_felzenszwalb.py b/skimage/segmentation/_felzenszwalb.py index 56642f8d..d9b82c2b 100644 --- a/skimage/segmentation/_felzenszwalb.py +++ b/skimage/segmentation/_felzenszwalb.py @@ -45,7 +45,8 @@ def felzenszwalb(image, scale=1, sigma=0.8, min_size=20): if image.ndim == 2: # assume single channel image - return _felzenszwalb_grey(image, scale=scale, sigma=sigma, min_size=min_size) + 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 80afaedb..37229225 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.measure._ccomp cimport find_root, join_trees +from ..measure._ccomp cimport find_root, join_trees from ..util import img_as_float diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index 7b6d2a3e..94769be7 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -1,5 +1,5 @@ import numpy as np -from skimage._shared.utils import deprecated +from .._shared.utils import deprecated def join_segmentations(s1, s2): @@ -124,7 +124,7 @@ def relabel_sequential(label_field, offset=1): if m == len(labels0): # nothing to do, already 1...n labels return label_field, labels, labels forward_map = np.zeros(m + 1, int) - forward_map[labels0] = np.arange(offset, offset + len(labels0) + 1) + forward_map[labels0] = np.arange(offset, offset + len(labels0)) if not (labels == 0).any(): labels = np.concatenate(([0], labels)) inverse_map = np.zeros(offset - 1 + len(labels), dtype=np.intp) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 766cd317..1418e902 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -8,7 +8,7 @@ from cpython cimport bool import numpy as np cimport numpy as cnp -from skimage.util import regular_grid +from ..util import regular_grid def _slic_cython(double[:, :, :, ::1] image_zyx, @@ -32,7 +32,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, 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. @@ -178,7 +178,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, 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: diff --git a/skimage/segmentation/boundaries.py b/skimage/segmentation/boundaries.py index d2633f97..9400e306 100644 --- a/skimage/segmentation/boundaries.py +++ b/skimage/segmentation/boundaries.py @@ -1,40 +1,218 @@ +from __future__ import division + import numpy as np -from ..morphology import dilation, square -from ..util import img_as_float +from scipy import ndimage as nd +from ..morphology import dilation, erosion, square +from ..util import img_as_float, view_as_windows, pad from ..color import gray2rgb -from .._shared.utils import deprecated -def find_boundaries(label_img): - """Return bool array where boundaries between labeled regions are True.""" - boundaries = np.zeros(label_img.shape, dtype=np.bool) - boundaries[1:, :] += label_img[1:, :] != label_img[:-1, :] - boundaries[:, 1:] += label_img[:, 1:] != label_img[:, :-1] +def _find_boundaries_subpixel(label_img): + """See ``find_boundaries(..., mode='subpixel')``. + + Notes + ----- + This function puts in an empty row and column between each *actual* + row and column of the image, for a corresponding shape of $2s - 1$ + for every image dimension of size $s$. These "interstitial" rows + and columns are filled as ``True`` if they separate two labels in + `label_img`, ``False`` otherwise. + + I used ``view_as_windows`` to get the neighborhood of each pixel. + Then I check whether there are two labels or more in that + neighborhood. + """ + ndim = label_img.ndim + max_label = np.iinfo(label_img.dtype).max + + label_img_expanded = np.zeros([(2 * s - 1) for s in label_img.shape], + label_img.dtype) + pixels = [slice(None, None, 2)] * ndim + label_img_expanded[pixels] = label_img + + edges = np.ones(label_img_expanded.shape, dtype=bool) + edges[pixels] = False + label_img_expanded[edges] = max_label + windows = view_as_windows(pad(label_img_expanded, 1, + mode='constant', constant_values=0), + (3,) * ndim) + + boundaries = np.zeros_like(edges) + for index in np.ndindex(label_img_expanded.shape): + if edges[index]: + values = np.unique(windows[index].ravel()) + if len(values) > 2: # single value and max_label + boundaries[index] = True return boundaries -def mark_boundaries(image, label_img, color=(1, 1, 0), outline_color=(0, 0, 0)): +def find_boundaries(label_img, connectivity=1, mode='thick', background=0): + """Return bool array where boundaries between labeled regions are True. + + Parameters + ---------- + label_img : array of int + An array in which different regions are labeled with different + integers. + connectivity: int in {1, ..., `label_img.ndim`}, optional + A pixel is considered a boundary pixel if any of its neighbors + has a different label. `connectivity` controls which pixels are + considered neighbors. A connectivity of 1 (default) means + pixels sharing an edge (in 2D) or a face (in 3D) will be + considered neighbors. A connectivity of `label_img.ndim` means + pixels sharing a corner will be considered neighbors. + mode: string in {'thick', 'inner', 'outer', 'subpixel'} + How to mark the boundaries: + + - thick: any pixel not completely surrounded by pixels of the + same label (defined by `connectivity`) is marked as a boundary. + This results in boundaries that are 2 pixels thick. + - inner: outline the pixels *just inside* of objects, leaving + background pixels untouched. + - outer: outline pixels in the background around object + boundaries. When two objects touch, their boundary is also + marked. + - subpixel: return a doubled image, with pixels *between* the + original pixels marked as boundary where appropriate. + background: int, optional + For modes 'inner' and 'outer', a definition of a background + label is required. See `mode` for descriptions of these two. + + Returns + ------- + boundaries : array of bool, same shape as `label_img` + A bool image where ``True`` represents a boundary pixel. For + `mode` equal to 'subpixel', ``boundaries.shape[i]`` is equal + to ``2 * label_img.shape[i] - 1`` for all ``i`` (a pixel is + inserted in between all other pairs of pixels). + + Examples + -------- + >>> labels = 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, 5, 5, 5, 0, 0], + ... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0], + ... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0], + ... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0], + ... [0, 0, 0, 0, 0, 5, 5, 5, 0, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8) + >>> find_boundaries(labels, mode='thick').astype(np.uint8) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 0, 1, 1, 0], + [0, 1, 1, 0, 1, 1, 0, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 0, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + >>> find_boundaries(labels, mode='inner').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, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 1, 0, 0], + [0, 0, 1, 0, 1, 1, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + >>> find_boundaries(labels, mode='outer').astype(np.uint8) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0, 1, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 1, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 1, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 1, 0], + [0, 0, 1, 1, 1, 1, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + >>> labels_small = labels[::2, ::3] + >>> labels_small + array([[0, 0, 0, 0], + [0, 0, 5, 0], + [0, 1, 5, 0], + [0, 0, 5, 0], + [0, 0, 0, 0]], dtype=uint8) + >>> find_boundaries(labels_small, mode='subpixel').astype(np.uint8) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 0, 1, 0], + [0, 0, 0, 1, 0, 1, 0], + [0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + ndim = label_img.ndim + selem = nd.generate_binary_structure(ndim, connectivity) + if mode != 'subpixel': + boundaries = dilation(label_img, selem) != erosion(label_img, selem) + if mode == 'inner': + foreground_image = (label_img != background) + boundaries &= foreground_image + elif mode == 'outer': + max_label = np.iinfo(label_img.dtype).max + background_image = (label_img == background) + selem = nd.generate_binary_structure(ndim, ndim) + inverted_background = np.array(label_img, copy=True) + inverted_background[background_image] = max_label + adjacent_objects = ((dilation(label_img, selem) != + erosion(inverted_background, selem)) & + ~background_image) + boundaries &= (background_image | adjacent_objects) + return boundaries + else: + boundaries = _find_boundaries_subpixel(label_img) + return boundaries + + +def mark_boundaries(image, label_img, color=(1, 1, 0), + outline_color=None, mode='outer', background_label=0): """Return image with boundaries between labeled regions highlighted. Parameters ---------- image : (M, N[, 3]) array Grayscale or RGB image. - label_img : (M, N) array + label_img : (M, N) array of int Label array where regions are marked by different integer values. - color : length-3 sequence + color : length-3 sequence, optional RGB color of boundaries in the output image. - outline_color : length-3 sequence + outline_color : length-3 sequence, optional RGB color surrounding boundaries in the output image. If None, no outline is drawn. - """ - if image.ndim == 2: - image = gray2rgb(image) - image = img_as_float(image, force_copy=True) + mode : string in {'thick', 'inner', 'outer', 'subpixel'}, optional + The mode for finding boundaries. + background_label : int, optional + Which label to consider background (this is only useful for + modes ``inner`` and ``outer``). - boundaries = find_boundaries(label_img) + Returns + ------- + marked : (M, N, 3) array of float + An image in which the boundaries between labels are + superimposed on the original image. + + See Also + -------- + find_boundaries + """ + marked = img_as_float(image, force_copy=True) + if marked.ndim == 2: + marked = gray2rgb(marked) + if mode == 'subpixel': + # Here, we want to interpose an extra line of pixels between + # each original line - except for the last axis which holds + # the RGB information. ``nd.zoom`` then performs the (cubic) + # interpolation, filling in the values of the interposed pixels + marked = nd.zoom(marked, [2 - 1/s for s in marked.shape[:-1]] + [1], + mode='reflect') + boundaries = find_boundaries(label_img, mode=mode, + background=background_label) if outline_color is not None: - outer_boundaries = dilation(boundaries.astype(np.uint8), square(2)) - image[outer_boundaries != 0, :] = np.array(outline_color) - image[boundaries, :] = np.array(color) - return image + outlines = dilation(boundaries, square(3)) + marked[outlines] = outline_color + marked[boundaries] = color + return marked diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 731518e3..be325fc6 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -39,7 +39,7 @@ except ImportError: amg_loaded = False from scipy.sparse.linalg import cg from ..util import img_as_float -from ..filter import rank_order +from ..filters import rank_order #-----------Laplacian-------------------- @@ -316,7 +316,8 @@ 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)) + >>> np.random.seed(0) + >>> a = np.zeros((10, 10)) + 0.2 * np.random.rand(10, 10) >>> a[5:8, 5:8] += 1 >>> b = np.zeros_like(a) >>> b[3, 3] = 1 # Marker for first phase @@ -453,8 +454,8 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, # Clean up results if return_full_prob: labels = labels.astype(np.float) - X = np.array([_clean_labels_ar(Xline, labels, - copy=True).reshape(dims) for Xline in X]) + X = np.array([_clean_labels_ar(Xline, labels, copy=True).reshape(dims) + for Xline in X]) for i in range(1, int(labels.max()) + 1): mask_i = np.squeeze(labels == i) X[:, mask_i] = 0 diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 7135047e..08f4f828 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -5,13 +5,14 @@ import numpy as np from scipy import ndimage import warnings -from skimage.util import img_as_float, regular_grid -from skimage.segmentation._slic import _slic_cython, _enforce_label_connectivity_cython -from skimage.color import rgb2lab +from ..util import img_as_float, regular_grid +from ..segmentation._slic import (_slic_cython, + _enforce_label_connectivity_cython) +from ..color import rgb2lab def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0, - spacing=None, multichannel=True, convert2lab=True, + spacing=None, multichannel=True, convert2lab=None, 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. @@ -46,8 +47,9 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0, channels or another spatial dimension. convert2lab : bool, optional Whether the input should be converted to Lab colorspace prior to - segmentation. For this purpose, the input is assumed to be RGB. Highly - recommended. + segmentation. The input image *must* be RGB. Highly recommended. + This option defaults to ``True`` when ``multichannel=True`` *and* + ``image.shape[-1] == 3``. enforce_connectivity: bool, optional (default False) Whether the generated segments are connected or not min_size_factor: float, optional @@ -57,7 +59,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0, 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 + Run SLIC-zero, the zero-parameter mode of SLIC. [2]_ Returns ------- @@ -67,9 +69,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0, Raises ------ ValueError - If: - - the image dimension is not 2 or 3 and `multichannel == False`, OR - - the image dimension is not 3 or 4 and `multichannel == True` + If ``convert2lab`` is set to ``True`` but the last array + dimension is not of length 3. Notes ----- @@ -92,12 +93,13 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0, .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to State-of-the-art Superpixel Methods, TPAMI, May 2012. + .. [2] http://ivrg.epfl.ch/research/superpixels#SLICO Examples -------- >>> from skimage.segmentation import slic - >>> from skimage.data import lena - >>> img = lena() + >>> from skimage.data import astronaut + >>> img = astronaut() >>> segments = slic(img, n_segments=100, compactness=10) Increasing the compactness parameter yields more square regions: @@ -139,10 +141,11 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0, sigma = list(sigma) + [0] image = ndimage.gaussian_filter(image, sigma) - if convert2lab and multichannel: - if image.shape[3] != 3: + if multichannel and (convert2lab or convert2lab is None): + if image.shape[-1] != 3 and convert2lab: raise ValueError("Lab colorspace conversion requires a RGB image.") - image = rgb2lab(image) + elif image.shape[-1] == 3: + image = rgb2lab(image) depth, height, width = image.shape[:3] diff --git a/skimage/segmentation/tests/__init__.py b/skimage/segmentation/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/segmentation/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/segmentation/tests/test_boundaries.py b/skimage/segmentation/tests/test_boundaries.py index 2fff52f8..e6e401ac 100644 --- a/skimage/segmentation/tests/test_boundaries.py +++ b/skimage/segmentation/tests/test_boundaries.py @@ -1,19 +1,22 @@ import numpy as np -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_equal, assert_allclose from skimage.segmentation import find_boundaries, mark_boundaries +white = (1, 1, 1) + + def test_find_boundaries(): - image = np.zeros((10, 10)) + image = np.zeros((10, 10), dtype=np.uint8) 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, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) @@ -24,36 +27,63 @@ def test_find_boundaries(): def test_mark_boundaries(): image = np.zeros((10, 10)) - label_image = np.zeros((10, 10)) + label_image = np.zeros((10, 10), dtype=np.uint8) 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, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) - result = mark_boundaries(image, label_image, color=(1, 1, 1)).mean(axis=2) + + marked = mark_boundaries(image, label_image, color=white, mode='thick') + result = np.mean(marked, axis=-1) 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], + ref = np.array([[0, 2, 2, 2, 2, 2, 2, 2, 0, 0], + [2, 2, 1, 1, 1, 1, 1, 2, 2, 0], + [2, 1, 1, 1, 1, 1, 1, 1, 2, 0], + [2, 1, 1, 2, 2, 2, 1, 1, 2, 0], + [2, 1, 1, 2, 0, 2, 1, 1, 2, 0], + [2, 1, 1, 2, 2, 2, 1, 1, 2, 0], + [2, 1, 1, 1, 1, 1, 1, 1, 2, 0], + [2, 2, 1, 1, 1, 1, 1, 2, 2, 0], + [0, 2, 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) + marked = mark_boundaries(image, label_image, color=white, + outline_color=(2, 2, 2), mode='thick') + result = np.mean(marked, axis=-1) assert_array_equal(result, ref) +def test_mark_boundaries_subpixel(): + labels = np.array([[0, 0, 0, 0], + [0, 0, 5, 0], + [0, 1, 5, 0], + [0, 0, 5, 0], + [0, 0, 0, 0]], dtype=np.uint8) + np.random.seed(0) + image = np.round(np.random.rand(*labels.shape), 2) + marked = mark_boundaries(image, labels, color=white, mode='subpixel') + marked_proj = np.round(np.mean(marked, axis=-1), 2) + + ref_result = np.array( + [[ 0.55, 0.63, 0.72, 0.69, 0.6 , 0.55, 0.54], + [ 0.45, 0.58, 0.72, 1. , 1. , 1. , 0.69], + [ 0.42, 0.54, 0.65, 1. , 0.44, 1. , 0.89], + [ 0.69, 1. , 1. , 1. , 0.69, 1. , 0.83], + [ 0.96, 1. , 0.38, 1. , 0.79, 1. , 0.53], + [ 0.89, 1. , 1. , 1. , 0.38, 1. , 0.16], + [ 0.57, 0.78, 0.93, 1. , 0.07, 1. , 0.09], + [ 0.2 , 0.52, 0.92, 1. , 1. , 1. , 0.54], + [ 0.02, 0.35, 0.83, 0.9 , 0.78, 0.81, 0.87]]) + assert_allclose(marked_proj, ref_result, atol=0.01) + + 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 9d30473b..a3a45c8d 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -1,6 +1,9 @@ import numpy as np from skimage.segmentation import random_walker from skimage.transform import resize +from skimage._shared._warnings import expected_warnings + +PYAMG_EXPECTED_WARNING = 'pyamg|\A\Z' def make_2d_syntheticdata(lx, ly=None): @@ -74,11 +77,13 @@ def test_2d_cg(): lx = 70 ly = 100 data, labels = make_2d_syntheticdata(lx, ly) - labels_cg = random_walker(data, labels, beta=90, mode='cg') + with expected_warnings(['"cg" mode']): + labels_cg = random_walker(data, labels, beta=90, mode='cg') assert (labels_cg[25:45, 40:60] == 2).all() assert data.shape == labels.shape - full_prob = random_walker(data, labels, beta=90, mode='cg', - return_full_prob=True) + with expected_warnings(['"cg" mode']): + full_prob = random_walker(data, labels, beta=90, mode='cg', + return_full_prob=True) assert (full_prob[1, 25:45, 40:60] >= full_prob[0, 25:45, 40:60]).all() assert data.shape == labels.shape @@ -89,10 +94,13 @@ def test_2d_cg_mg(): lx = 70 ly = 100 data, labels = make_2d_syntheticdata(lx, ly) - labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') + expected = 'scipy.sparse.sparsetools|%s' % PYAMG_EXPECTED_WARNING + with expected_warnings([expected]): + labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') assert (labels_cg_mg[25:45, 40:60] == 2).all() assert data.shape == labels.shape - full_prob = random_walker(data, labels, beta=90, mode='cg_mg', + with expected_warnings([expected]): + full_prob = random_walker(data, labels, beta=90, mode='cg_mg', return_full_prob=True) assert (full_prob[1, 25:45, 40:60] >= full_prob[0, 25:45, 40:60]).all() @@ -106,7 +114,8 @@ def test_types(): data, labels = make_2d_syntheticdata(lx, ly) data = 255 * (data - data.min()) // (data.max() - data.min()) data = data.astype(np.uint8) - labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') + with expected_warnings([PYAMG_EXPECTED_WARNING]): + labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') assert (labels_cg_mg[25:45, 40:60] == 2).all() assert data.shape == labels.shape return data, labels_cg_mg @@ -139,7 +148,8 @@ def test_3d(): n = 30 lx, ly, lz = n, n, n data, labels = make_3d_syntheticdata(lx, ly, lz) - labels = random_walker(data, labels, mode='cg') + with expected_warnings(['"cg" mode']): + labels = random_walker(data, labels, mode='cg') assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all() assert data.shape == labels.shape return data, labels @@ -152,7 +162,8 @@ def test_3d_inactive(): old_labels = np.copy(labels) labels[5:25, 26:29, 26:29] = -1 after_labels = np.copy(labels) - labels = random_walker(data, labels, mode='cg') + with expected_warnings(['"cg" mode|CObject type']): + labels = random_walker(data, labels, mode='cg') assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all() assert data.shape == labels.shape return data, labels, old_labels, after_labels @@ -162,9 +173,12 @@ def test_multispectral_2d(): lx, ly = 70, 100 data, labels = make_2d_syntheticdata(lx, ly) data = data[..., np.newaxis].repeat(2, axis=-1) # Expect identical output - multi_labels = random_walker(data, labels, mode='cg', multichannel=True) + with expected_warnings(['"cg" mode']): + multi_labels = random_walker(data, labels, mode='cg', + multichannel=True) assert data[..., 0].shape == labels.shape - single_labels = random_walker(data[..., 0], labels, mode='cg') + with expected_warnings(['"cg" mode']): + single_labels = random_walker(data[..., 0], labels, mode='cg') assert (multi_labels.reshape(labels.shape)[25:45, 40:60] == 2).all() assert data[..., 0].shape == labels.shape return data, multi_labels, single_labels, labels @@ -175,9 +189,12 @@ def test_multispectral_3d(): lx, ly, lz = n, n, n data, labels = make_3d_syntheticdata(lx, ly, lz) data = data[..., np.newaxis].repeat(2, axis=-1) # Expect identical output - multi_labels = random_walker(data, labels, mode='cg', multichannel=True) + with expected_warnings(['"cg" mode']): + multi_labels = random_walker(data, labels, mode='cg', + multichannel=True) assert data[..., 0].shape == labels.shape - single_labels = random_walker(data[..., 0], labels, mode='cg') + with expected_warnings(['"cg" mode']): + single_labels = random_walker(data[..., 0], labels, mode='cg') assert (multi_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all() assert (single_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all() assert data[..., 0].shape == labels.shape @@ -203,7 +220,8 @@ def test_spacing_0(): lz // 4 - small_l // 8] = 2 # Test with `spacing` kwarg - labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', + with expected_warnings(['"cg" mode']): + labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', spacing=(1., 1., 0.5)) assert (labels_aniso[13:17, 13:17, 7:9] == 2).all() @@ -230,8 +248,9 @@ def test_spacing_1(): # Test with `spacing` kwarg # First, anisotropic along Y - labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', - spacing=(1., 2., 1.)) + with expected_warnings(['"cg" mode']): + labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', + spacing=(1., 2., 1.)) assert (labels_aniso[13:17, 26:34, 13:17] == 2).all() # Rescale `data` along X axis @@ -249,9 +268,10 @@ def test_spacing_1(): lz // 2 - small_l // 4] = 2 # Anisotropic along X - labels_aniso2 = random_walker(data_aniso, - labels_aniso2, - mode='cg', spacing=(2., 1., 1.)) + with expected_warnings(['"cg" mode']): + labels_aniso2 = random_walker(data_aniso, + labels_aniso2, + mode='cg', spacing=(2., 1., 1.)) assert (labels_aniso2[26:34, 13:17, 13:17] == 2).all() @@ -259,14 +279,17 @@ 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) + + with expected_warnings(["Returning provided labels"]): + 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) + with expected_warnings(["Returning provided labels"]): + test = random_walker(img, labels, return_full_prob=True) np.testing.assert_array_equal(test, expected) diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 7dda66d2..239413d4 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -1,5 +1,4 @@ import itertools as it -import warnings import numpy as np from numpy.testing import assert_equal, assert_raises from skimage.segmentation import slic @@ -14,9 +13,27 @@ def test_color_2d(): 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) + seg = slic(img, n_segments=4, sigma=0, enforce_connectivity=False) + + # 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) + + +def test_multichannel_2d(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 20, 8)) + img[:10, :10, 0:2] = 1 + img[:10, 10:, 2:4] = 1 + img[10:, :10, 4:6] = 1 + img[10:, 10:, 6:8] = 1 + img += 0.01 * rnd.normal(size=img.shape) + img = np.clip(img, 0, 1, out=img) + seg = slic(img, n_segments=4, enforce_connectivity=False) # we expect 4 segments assert_equal(len(np.unique(seg)), 4) @@ -158,9 +175,7 @@ def test_slic_zero(): 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) + seg = slic(img, n_segments=4, sigma=0, slic_zero=True) # we expect 4 segments assert_equal(len(np.unique(seg)), 4) diff --git a/skimage/setup.py b/skimage/setup.py index 962adb97..b33f5e81 100644 --- a/skimage/setup.py +++ b/skimage/setup.py @@ -13,7 +13,8 @@ def configuration(parent_package='', top_path=None): config.add_subpackage('exposure') config.add_subpackage('feature') config.add_subpackage('restoration') - config.add_subpackage('filter') + config.add_subpackage('filters') + config.add_subpackage('future') config.add_subpackage('graph') config.add_subpackage('io') config.add_subpackage('measure') @@ -21,6 +22,7 @@ def configuration(parent_package='', top_path=None): config.add_subpackage('transform') config.add_subpackage('util') config.add_subpackage('segmentation') + config.add_subpackage('external') def add_test_directories(arg, dirname, fnames): if dirname.split(os.path.sep)[-1] == 'tests': diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 877f1614..3049ebb0 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -18,8 +18,6 @@ __all__ = ['hough_circle', 'hough_ellipse', 'hough_line', 'probabilistic_hough_line', - 'probabilistic_hough', - 'hough_peaks', 'hough_line_peaks', 'radon', 'iradon', diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index b3019135..c94e3b76 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -4,11 +4,60 @@ 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 .._shared.utils import get_bound_method_class, safe_as_int +from ..util import img_as_float +from ..exposure import rescale_intensity from ._warps_cy import _warp_fast +def _center_and_normalize_points(points): + """Center and normalize image points. + + The points are transformed in a two-step procedure that is expressed + as a transformation matrix. The matrix of the resulting points is usually + better conditioned than the matrix of the original points. + + Center the image points, such that the new coordinate system has its + origin at the centroid of the image points. + + Normalize the image points, such that the mean distance from the points + to the origin of the coordinate system is sqrt(2). + + Parameters + ---------- + points : (N, 2) array + The coordinates of the image points. + + Returns + ------- + matrix : (3, 3) array + The transformation matrix to obtain the new points. + new_points : (N, 2) array + The transformed image points. + + """ + + centroid = np.mean(points, axis=0) + + rms = math.sqrt(np.sum((points - centroid) ** 2) / points.shape[0]) + + norm_factor = math.sqrt(2) / rms + + matrix = np.array([[norm_factor, 0, -norm_factor * centroid[0]], + [0, norm_factor, -norm_factor * centroid[1]], + [0, 0, 1]]) + + pointsh = np.row_stack([points.T, np.ones((points.shape[0]),)]) + + new_pointsh = np.dot(matrix, pointsh).T + + new_points = new_pointsh[:, :2] + new_points[:, 0] /= new_pointsh[:, 2] + new_points[:, 1] /= new_pointsh[:, 2] + + return matrix, new_points + + class GeometricTransform(object): """Perform geometric transformations on a set of coordinates. @@ -215,6 +264,14 @@ class ProjectiveTransform(GeometricTransform): Destination coordinates. """ + + try: + src_matrix, src = _center_and_normalize_points(src) + dst_matrix, dst = _center_and_normalize_points(dst) + except ZeroDivisionError: + self.params = np.nan * np.empty((3, 3)) + return + xs = src[:, 0] ys = src[:, 1] xd = dst[:, 0] @@ -247,6 +304,9 @@ class ProjectiveTransform(GeometricTransform): H.flat[list(self._coeffs) + [8]] = - V[-1, :-1] / V[-1, -1] H[2, 2] = 1 + # De-center and de-normalize + H = np.dot(np.linalg.inv(dst_matrix), np.dot(H, src_matrix)) + self.params = H def __add__(self, other): @@ -261,6 +321,10 @@ class ProjectiveTransform(GeometricTransform): else: tform = ProjectiveTransform return tform(other.params.dot(self.params)) + elif (hasattr(other, '__name__') + and other.__name__ == 'inverse' + and hasattr(get_bound_method_class(other), '_inv_matrix')): + return ProjectiveTransform(self._inv_matrix.dot(self.params)) else: raise TypeError("Cannot combine transformations of differing " "types.") @@ -268,7 +332,9 @@ class ProjectiveTransform(GeometricTransform): class AffineTransform(ProjectiveTransform): - """2D affine transformation of the form:: + """2D affine transformation of the form: + + ..:math: X = a0*x + a1*y + a2 = = sx*x*cos(rotation) - sy*y*sin(rotation + shear) + a2 @@ -487,7 +553,9 @@ class PiecewiseAffineTransform(GeometricTransform): class SimilarityTransform(ProjectiveTransform): - """2D similarity transformation of the form:: + """2D similarity transformation of the form: + + ..:math: X = a0 * x - b0 * y + a1 = = m * x * cos(rotation) - m * y * sin(rotation) + a1 @@ -591,6 +659,14 @@ class SimilarityTransform(ProjectiveTransform): Destination coordinates. """ + + try: + src_matrix, src = _center_and_normalize_points(src) + dst_matrix, dst = _center_and_normalize_points(dst) + except ZeroDivisionError: + self.params = np.nan * np.empty((3, 3)) + return + xs = src[:, 0] ys = src[:, 1] xd = dst[:, 0] @@ -614,9 +690,15 @@ class SimilarityTransform(ProjectiveTransform): # singular value a0, a1, b0, b1 = - V[-1, :-1] / V[-1, -1] - self.params = np.array([[a0, -b0, a1], - [b0, a0, b1], - [ 0, 0, 1]]) + S = np.array([[a0, -b0, a1], + [b0, a0, b1], + [ 0, 0, 1]]) + + # De-center and de-normalize + S = np.dot(np.linalg.inv(dst_matrix), np.dot(S, src_matrix)) + + self.params = S + @property def scale(self): @@ -637,7 +719,9 @@ class SimilarityTransform(ProjectiveTransform): class PolynomialTransform(GeometricTransform): - """2D transformation of the form:: + """2D transformation of the form: + + ..:math: X = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) Y = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) @@ -789,6 +873,7 @@ TRANSFORMS = { 'projective': ProjectiveTransform, 'polynomial': PolynomialTransform, } + HOMOGRAPHY_TRANSFORMS = ( SimilarityTransform, AffineTransform, @@ -912,14 +997,14 @@ def _stackcopy(a, b): def warp_coords(coord_map, shape, dtype=np.float64): - """Build the source coordinates for the output pixels of an image warp. + """Build the source coordinates for the output of a 2-D image warp. Parameters ---------- coord_map : callable like GeometricTransform.inverse Return input coordinates for given output coordinates. Coordinates are in the shape (P, 2), where P is the number - of coordinates and each element is a ``(x, y)`` pair. + of coordinates and each element is a ``(row, col)`` pair. shape : tuple Shape of output image ``(rows, cols[, bands])``. dtype : np.dtype or string @@ -934,8 +1019,9 @@ def warp_coords(coord_map, shape, dtype=np.float64): Notes ----- - This is a lower-level routine that produces the source coordinates used by - `warp()`. + + This is a lower-level routine that produces the source coordinates for 2-D + images used by `warp()`. It is provided separately from `warp` to give additional flexibility to users who would like, for example, to re-use a particular coordinate @@ -946,7 +1032,7 @@ def warp_coords(coord_map, shape, dtype=np.float64): Examples -------- - Produce a coordinate map that Shifts an image up and to the right: + Produce a coordinate map that shifts an image up and to the right: >>> from skimage import data >>> from scipy.ndimage import map_coordinates @@ -954,7 +1040,7 @@ def warp_coords(coord_map, shape, dtype=np.float64): >>> def shift_up10_left20(xy): ... return xy - np.array([-20, 10])[None, :] >>> - >>> image = data.lena().astype(np.float32) + >>> image = data.astronaut().astype(np.float32) >>> coords = warp_coords(shift_up10_left20, image.shape) >>> warped_image = map_coordinates(image, coords) @@ -966,10 +1052,10 @@ def warp_coords(coord_map, shape, dtype=np.float64): coords_shape.append(shape[2]) coords = np.empty(coords_shape, dtype=dtype) - # Reshape grid coordinates into a (P, 2) array of (x, y) pairs + # Reshape grid coordinates into a (P, 2) array of (row, col) pairs tf_coords = np.indices((cols, rows), dtype=dtype).reshape(2, -1).T - # Map each (x, y) pair to the source image according to + # Map each (row, col) pair to the source image according to # the user-provided mapping tf_coords = coord_map(tf_coords) @@ -988,19 +1074,102 @@ def warp_coords(coord_map, shape, dtype=np.float64): return coords +def _convert_warp_input(image, preserve_range): + """Convert input image to double image with the appropriate range.""" + if preserve_range: + image = image.astype(np.double) + else: + image = img_as_float(image) + return image + + +def _clip_warp_output(input_image, output_image, order, mode, cval, clip): + """Clip output image to range of values of input image. + + Note that this function modifies the values of `output_image` in-place + and it is only modified if ``clip=True``. + + Parameters + ---------- + input_image : ndarray + Input image. + output_image : ndarray + Output image, which is modified in-place. + + Other parameters + ---------------- + order : int, optional + The order of the spline interpolation, default is 1. The order has to + be in the range 0-5. See `skimage.transform.warp` for detail. + mode : string, optional + Points outside the boundaries of the input are filled according + to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + + """ + + if clip and order != 0: + min_val = input_image.min() + max_val = input_image.max() + + preserve_cval = mode == 'constant' and not \ + (min_val <= cval <= max_val) + + if preserve_cval: + cval_mask = output_image == cval + + np.clip(output_image, min_val, max_val, out=output_image) + + if preserve_cval: + output_image[cval_mask] = cval + + def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, - mode='constant', cval=0., reverse_map=None): + mode='constant', cval=0., clip=True, preserve_range=False): """Warp an image according to a given coordinate transformation. Parameters ---------- - image : 2-D or 3-D array + image : ndarray Input image. - inverse_map : transformation object, callable ``xy = f(xy, **kwargs)``, (3, 3) array - Inverse coordinate map. A function that transforms a (N, 2) array of - ``(x, y)`` coordinates in the *output image* into their corresponding - coordinates in the *source image* (e.g. a transformation object or its - inverse). See example section for usage. + inverse_map : transformation object, callable ``cr = f(cr, **kwargs)``, or ndarray + Inverse coordinate map, which transforms coordinates in the output + images into their corresponding coordinates in the input image. + + There are a number of different options to define this map, depending + on the dimensionality of the input image. A 2-D image can have 2 + dimensions for gray-scale images, or 3 dimensions with color + information. + + - For 2-D images, you can directly pass a transformation object, + e.g. `skimage.transform.SimilarityTransform`, or its inverse. + - For 2-D images, you can pass a ``(3, 3)`` homogeneous + transformation matrix, e.g. + `skimage.transform.SimilarityTransform.params`. + - For 2-D images, a function that transforms a ``(M, 2)`` array of + ``(col, row)`` coordinates in the output image to their + corresponding coordinates in the input image. Extra parameters to + the function can be specified through `map_args`. + - For N-D images, you can directly pass an array of coordinates. + The first dimension specifies the coordinates in the input image, + while the subsequent dimensions determine the position in the + output image. E.g. in case of 2-D images, you need to pass an array + of shape ``(2, rows, cols)``, where `rows` and `cols` determine the + shape of the output image, and the first dimension contains the + ``(row, col)`` coordinate in the input image. + See `scipy.ndimage.map_coordinates` for further documentation. + + Note, that a ``(3, 3)`` matrix is interpreted as a homogeneous + transformation matrix, so you cannot interpolate values from a 3-D + input, if the output is of shape ``(3,)``. + + See example section for usage. map_args : dict, optional Keyword arguments passed to `inverse_map`. output_shape : tuple (rows, cols), optional @@ -1009,25 +1178,38 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, 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 - * 1: Bi-linear (default) - * 2: Bi-quadratic - * 3: Bi-cubic - * 4: Bi-quartic - * 5: Bi-quintic + - 0: Nearest-neighbor + - 1: Bi-linear (default) + - 2: Bi-quadratic + - 3: Bi-cubic + - 4: Bi-quartic + - 5: Bi-quintic mode : string, optional Points outside the boundaries of the input are filled according to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + + Returns + ------- + warped : double ndarray + The warped input image. Notes ----- - In case of a `SimilarityTransform`, `AffineTransform` and - `ProjectiveTransform` and `order` in [0, 3] this function uses the - underlying transformation matrix to warp the image with a much faster - routine. + - The input image is converted to a `double` image. + - In case of a `SimilarityTransform`, `AffineTransform` and + `ProjectiveTransform` and `order` in [0, 3] this function uses the + underlying transformation matrix to warp the image with a much faster + routine. Examples -------- @@ -1062,89 +1244,127 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, >>> warped = warp(image, tform.inverse) + For N-D images you can pass a coordinate array, that specifies the + coordinates in the input image for every element in the output image. E.g. + if you want to rescale a 3-D cube, you can do: + + >>> cube_shape = np.array([30, 30, 30]) + >>> cube = np.random.rand(*cube_shape) + + Setup the coordinate array, that defines the scaling: + + >>> scale = 0.1 + >>> output_shape = (scale * cube_shape).astype(int) + >>> coords0, coords1, coords2 = np.mgrid[:output_shape[0], + ... :output_shape[1], :output_shape[2]] + >>> coords = np.array([coords0, coords1, coords2]) + + Assume that the cube contains spatial data, where the first array element + center is at coordinate (0.5, 0.5, 0.5) in real space, i.e. we have to + account for this extra offset when scaling the image: + + >>> coords = (coords + 0.5) / scale - 0.5 + >>> warped = warp(cube, coords) + """ - # 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 or image.ndim > 3: - raise ValueError("Input must have 2 or 3 dimensions.") + image = _convert_warp_input(image, preserve_range) - orig_ndim = image.ndim - image = np.atleast_3d(img_as_float(image)) - ishape = np.array(image.shape) - bands = ishape[2] + input_shape = np.array(image.shape) if output_shape is None: - output_shape = ishape + output_shape = input_shape else: output_shape = safe_as_int(output_shape) + warped = None - out = None + if order == 2: + # When fixing this issue, make sure to fix the branches further + # below in this function + warnings.warn("Bi-quadratic interpolation behavior has changed due " + "to a bug in the implementation of scikit-image. " + "The new version now serves as a wrapper " + "around SciPy's interpolation functions, which itself " + "is not verified to be a correct implementation. Until " + "skimage's implementation is fixed, we recommend " + "to use bi-linear or bi-cubic interpolation instead.") - # use fast Cython version for specific interpolation orders and input - if order in range(4) and not map_args: + if order in (0, 1, 3) and not map_args: + # use fast Cython version for specific interpolation orders and input matrix = None - # inverse_map is a transformation matrix as numpy array if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + # inverse_map is a transformation matrix as numpy array matrix = inverse_map - # inverse_map is a homography elif isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): + # inverse_map is a homography matrix = inverse_map.params - # inverse_map is the inverse of a homography elif (hasattr(inverse_map, '__name__') and inverse_map.__name__ == 'inverse' and get_bound_method_class(inverse_map) \ in HOMOGRAPHY_TRANSFORMS): + # inverse_map is the inverse of a homography matrix = np.linalg.inv(six.get_method_self(inverse_map).params) if matrix is not None: matrix = matrix.astype(np.double) - # transform all bands - dims = [] - for dim in range(image.shape[2]): - dims.append(_warp_fast(image[..., dim], matrix, - output_shape=output_shape, - order=order, mode=mode, cval=cval)) - out = np.dstack(dims) - if orig_ndim == 2: - out = out[..., 0] + if image.ndim == 2: + warped = _warp_fast(image, matrix, + output_shape=output_shape, + order=order, mode=mode, cval=cval) + elif image.ndim == 3: + 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)) + warped = np.dstack(dims) - if out is None: # use ndimage.map_coordinates - rows, cols = output_shape[:2] + if warped is None: + # use ndimage.map_coordinates - # inverse_map is a transformation matrix as numpy array - if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + if (isinstance(inverse_map, np.ndarray) + and inverse_map.shape == (3, 3)): + # inverse_map is a transformation matrix as numpy array, + # this is only used for order >= 4. inverse_map = ProjectiveTransform(matrix=inverse_map) - def coord_map(*args): - return inverse_map(*args, **map_args) + if isinstance(inverse_map, np.ndarray): + # inverse_map is directly given as coordinates + coords = inverse_map + else: + # inverse_map is given as function, that transforms (N, 2) + # destination coordinates to their corresponding source + # coordinates. This is only supported for 2(+1)-D images. - coords = warp_coords(coord_map, (rows, cols, bands)) + if image.ndim < 2 or image.ndim > 3: + raise ValueError("Only 2-D images (grayscale or color) are " + "supported, when providing a callable " + "`inverse_map`.") + + def coord_map(*args): + return inverse_map(*args, **map_args) + + if len(input_shape) == 3 and len(output_shape) == 2: + # Input image is 2D and has color channel, but output_shape is + # given for 2-D images. Automatically add the color channel + # dimensionality. + output_shape = (output_shape[0], output_shape[1], + input_shape[2]) + + coords = warp_coords(coord_map, output_shape) # Pre-filtering not necessary for order 0, 1 interpolation prefilter = order > 1 - out = ndimage.map_coordinates(image, coords, prefilter=prefilter, + + warped = ndimage.map_coordinates(image, coords, prefilter=prefilter, mode=mode, order=order, cval=cval) - # The spline filters sometimes return results outside [0, 1], - # so clip to ensure valid data - clipped = np.clip(out, 0, 1) - if mode == 'constant' and not (0 <= cval <= 1): - clipped[out == cval] = cval + _clip_warp_output(image, warped, order, mode, cval, clip) - out = clipped - - if out.ndim == 3 and orig_ndim == 2: - # remove singleton dimension introduced by atleast_3d - return out[..., 0] - else: - return out + return warped diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 104fed62..e51085cb 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -10,12 +10,12 @@ cimport cython from libc.math cimport abs, fabs, sqrt, ceil, atan2, M_PI from libc.stdlib cimport rand -from skimage.draw import circle_perimeter +from ..draw import circle_perimeter cdef double PI_2 = 1.5707963267948966 cdef double NEG_PI_2 = -PI_2 -from skimage._shared.interpolation cimport round +from .._shared.interpolation cimport round def _hough_circle(cnp.ndarray img, @@ -359,12 +359,9 @@ def probabilistic_hough_line(cnp.ndarray img, int threshold=10, cdef Py_ssize_t width = img.shape[1] # compute the bins and allocate the accumulator array - cdef cnp.ndarray[ndim=2, dtype=cnp.int64_t] accum - cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] ctheta, stheta cdef cnp.ndarray[ndim=2, dtype=cnp.uint8_t] mask = \ np.zeros((height, width), dtype=np.uint8) - cdef cnp.ndarray[ndim=2, dtype=cnp.int32_t] line_end = \ - np.zeros((2, 2), dtype=np.int32) + cdef cnp.int32_t[:, ::1] line_end = np.zeros((2, 2), dtype=np.int32) cdef Py_ssize_t max_distance, offset, num_indexes, index cdef double a, b cdef Py_ssize_t nidxs, i, j, x, y, px, py, accum_idx @@ -378,17 +375,18 @@ def probabilistic_hough_line(cnp.ndarray img, int threshold=10, max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + img.shape[1] * img.shape[1]))) - accum = np.zeros((max_distance, theta.shape[0]), dtype=np.int64) + cdef cnp.int64_t[:, ::1] accum = \ + np.zeros((max_distance, theta.shape[0]), dtype=np.int64) offset = max_distance / 2 nthetas = theta.shape[0] # compute sine and cosine of angles - ctheta = np.cos(theta) - stheta = np.sin(theta) + cdef cnp.double_t[::1] ctheta = np.cos(theta) + cdef cnp.double_t[::1] stheta = np.sin(theta) # find the nonzero indexes y_idxs, x_idxs = np.nonzero(img) - points = list(zip(x_idxs, y_idxs)) + cdef list points = list(zip(x_idxs, y_idxs)) # mask all non-zero indexes mask[y_idxs, x_idxs] = 1 diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 23b56164..5a9c8aa2 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,12 +1,13 @@ import numpy as np from scipy import ndimage -from skimage.transform._geometric import (warp, SimilarityTransform, - AffineTransform) -from skimage.measure import block_reduce +from ..measure import block_reduce +from ._geometric import (warp, SimilarityTransform, AffineTransform, + _convert_warp_input, _clip_warp_output) -def resize(image, output_shape, order=1, mode='constant', cval=0.): +def resize(image, output_shape, order=1, mode='constant', cval=0, clip=True, + preserve_range=False): """Resize image to match a certain size. Performs interpolation to up-size or down-size images. For down-sampling @@ -40,6 +41,13 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. Examples -------- @@ -71,28 +79,38 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): coord_map = np.array([map_rows, map_cols, map_dims]) - out = ndimage.map_coordinates(image, coord_map, order=order, mode=mode, - cval=cval) + image = _convert_warp_input(image, preserve_range) + + out = ndimage.map_coordinates(image, coord_map, order=order, + mode=mode, cval=cval) + + _clip_warp_output(image, out, order, mode, cval, clip) else: # 2-dimensional interpolation - # 3 control points necessary to estimate exact AffineTransform - src_corners = np.array([[1, 1], [1, rows], [cols, rows]]) - 1 - dst_corners = np.zeros(src_corners.shape, dtype=np.double) - # take into account that 0th pixel is at position (0.5, 0.5) - dst_corners[:, 0] = col_scale * (src_corners[:, 0] + 0.5) - 0.5 - dst_corners[:, 1] = row_scale * (src_corners[:, 1] + 0.5) - 0.5 + if rows == 1 and cols == 1: + tform = AffineTransform(translation=(orig_cols / 2.0 - 0.5, + orig_rows / 2.0 - 0.5)) + else: + # 3 control points necessary to estimate exact AffineTransform + src_corners = np.array([[1, 1], [1, rows], [cols, rows]]) - 1 + dst_corners = np.zeros(src_corners.shape, dtype=np.double) + # take into account that 0th pixel is at position (0.5, 0.5) + dst_corners[:, 0] = col_scale * (src_corners[:, 0] + 0.5) - 0.5 + dst_corners[:, 1] = row_scale * (src_corners[:, 1] + 0.5) - 0.5 - tform = AffineTransform() - tform.estimate(src_corners, dst_corners) + tform = AffineTransform() + tform.estimate(src_corners, dst_corners) out = warp(image, tform, output_shape=output_shape, order=order, - mode=mode, cval=cval) + mode=mode, cval=cval, clip=clip, + preserve_range=preserve_range) return out -def rescale(image, scale, order=1, mode='constant', cval=0.): +def rescale(image, scale, order=1, mode='constant', cval=0, clip=True, + preserve_range=False): """Scale image by a certain factor. Performs interpolation to upscale or down-scale images. For down-sampling @@ -124,6 +142,13 @@ def rescale(image, scale, order=1, mode='constant', cval=0.): cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. Examples -------- @@ -147,10 +172,12 @@ def rescale(image, scale, order=1, mode='constant', cval=0.): cols = np.round(col_scale * orig_cols) output_shape = (rows, cols) - return resize(image, output_shape, order=order, mode=mode, cval=cval) + return resize(image, output_shape, order=order, mode=mode, cval=cval, + clip=clip, preserve_range=preserve_range) -def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): +def rotate(image, angle, resize=False, center=None, order=1, mode='constant', + cval=0, clip=True, preserve_range=False): """Rotate image by a certain angle around its center. Parameters @@ -163,6 +190,9 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): Determine whether the shape of the output image will be automatically calculated, so the complete rotated image exactly fits. Default is False. + center : iterable of length 2 + The rotation center. If ``center=None``, the image is rotated around + its center, i.e. ``center=(rows / 2 - 0.5, cols / 2 - 0.5)``. Returns ------- @@ -180,6 +210,13 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. Examples -------- @@ -198,10 +235,13 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): rows, cols = image.shape[0], image.shape[1] # rotation around center - translation = np.array((cols, rows)) / 2. - 0.5 - tform1 = SimilarityTransform(translation=-translation) + if center is None: + center = np.array((cols, rows)) / 2. - 0.5 + else: + center = np.asarray(center) + tform1 = SimilarityTransform(translation=-center) tform2 = SimilarityTransform(rotation=np.deg2rad(angle)) - tform3 = SimilarityTransform(translation=translation) + tform3 = SimilarityTransform(translation=center) tform = tform1 + tform2 + tform3 output_shape = None @@ -223,10 +263,10 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): tform = tform4 + tform return warp(image, tform, output_shape=output_shape, order=order, - mode=mode, cval=cval) + mode=mode, cval=cval, clip=clip, preserve_range=preserve_range) -def downscale_local_mean(image, factors, cval=0): +def downscale_local_mean(image, factors, cval=0, clip=True): """Down-sample N-dimensional image by local averaging. The image is padded with `cval` if it is not perfectly divisible by the @@ -277,8 +317,8 @@ def _swirl_mapping(xy, center, rotation, strength, radius): radius = radius / 5 * np.log(2) theta = rotation + strength * \ - np.exp(-rho / radius) + \ - np.arctan2(y - y0, x - x0) + np.exp(-rho / radius) + \ + np.arctan2(y - y0, x - x0) xy[..., 0] = x0 + rho * np.cos(theta) xy[..., 1] = y0 + rho * np.sin(theta) @@ -287,14 +327,15 @@ def _swirl_mapping(xy, center, rotation, strength, radius): def swirl(image, center=None, strength=1, radius=100, rotation=0, - output_shape=None, order=1, mode='constant', cval=0): + output_shape=None, order=1, mode='constant', cval=0, clip=True, + preserve_range=False): """Perform a swirl transformation. Parameters ---------- image : ndarray Input image. - center : (x,y) tuple or (2,) ndarray, optional + center : (row, column) tuple or (2,) ndarray, optional Center coordinate of transformation. strength : float, optional The amount of swirling applied. @@ -323,6 +364,13 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. """ @@ -335,5 +383,5 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, 'radius': radius} return warp(image, _swirl_mapping, map_args=warp_args, - output_shape=output_shape, - order=order, mode=mode, cval=cval) + output_shape=output_shape, order=order, mode=mode, cval=cval, + clip=clip, preserve_range=preserve_range) diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 433c586d..27b6c95a 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -3,12 +3,11 @@ #cython: nonecheck=False #cython: wraparound=False import numpy as np - cimport numpy as cnp -from skimage._shared.interpolation cimport (nearest_neighbour_interpolation, - bilinear_interpolation, - biquadratic_interpolation, - bicubic_interpolation) +from .._shared.interpolation cimport (nearest_neighbour_interpolation, + bilinear_interpolation, + biquadratic_interpolation, + bicubic_interpolation) cdef inline void _matrix_transform(double x, double y, double* H, double *x_, diff --git a/skimage/transform/finite_radon_transform.py b/skimage/transform/finite_radon_transform.py index dd3bad33..b5bbaae0 100644 --- a/skimage/transform/finite_radon_transform.py +++ b/skimage/transform/finite_radon_transform.py @@ -29,7 +29,7 @@ def frt2(a): Notes ----- - The FRT has a unique inverse iff n is prime. [FRT] + The FRT has a unique inverse if and only if n is prime. [FRT] The idea for this algorithm is due to Vlad Negnevitski. Examples @@ -88,7 +88,7 @@ def ifrt2(a): Notes ----- - The FRT has a unique inverse iff n is prime. + The FRT has a unique inverse if and only if n is prime. See [1]_ for an overview. The idea for this algorithm is due to Vlad Negnevitski. diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index cbb4caa6..0760f5e7 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -1,6 +1,6 @@ import numpy as np from scipy import ndimage -from skimage import measure, morphology +from .. import measure, morphology from ._hough_transform import _hough_circle diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index a68b7a70..435d60b9 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -1,8 +1,8 @@ import math import numpy as np from scipy import ndimage -from skimage.transform import resize -from skimage.util import img_as_float +from ..transform import resize +from ..util import img_as_float def _smooth(image, sigma, mode, cval): @@ -10,7 +10,8 @@ def _smooth(image, sigma, mode, cval): smoothed = np.empty(image.shape, dtype=np.double) - if image.ndim == 3: # apply Gaussian filter to all dimensions independently + # apply Gaussian filter to all dimensions independently + if image.ndim == 3: for dim in range(image.shape[2]): ndimage.gaussian_filter(image[..., dim], sigma, output=smoothed[..., dim], diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 101d09c6..e49496c1 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -63,8 +63,9 @@ def radon(image, theta=None, circle=False): if circle: radius = min(image.shape) // 2 c0, c1 = np.ogrid[0:image.shape[0], 0:image.shape[1]] - reconstruction_circle = ((c0 - image.shape[0] // 2)**2 - + (c1 - image.shape[1] // 2)**2) <= radius**2 + reconstruction_circle = ((c0 - image.shape[0] // 2) ** 2 + + (c1 - image.shape[1] // 2) ** 2) + reconstruction_circle = reconstruction_circle <= radius ** 2 if not np.all(reconstruction_circle | (image == 0)): raise ValueError('Image must be zero outside the reconstruction' ' circle') @@ -189,7 +190,7 @@ def iradon(radon_image, theta=None, output_size=None, if circle: output_size = radon_image.shape[0] else: - output_size = int(np.floor(np.sqrt((radon_image.shape[0])**2 + output_size = int(np.floor(np.sqrt((radon_image.shape[0]) ** 2 / 2.0))) if circle: radon_image = _sinogram_circle_to_square(radon_image) @@ -198,7 +199,7 @@ def iradon(radon_image, theta=None, output_size=None, # resize image to next power of two (but no less than 64) for # Fourier analysis; speeds up Fourier and lessens artifacts projection_size_padded = \ - max(64, int(2**np.ceil(np.log2(2 * radon_image.shape[0])))) + max(64, int(2 ** np.ceil(np.log2(2 * radon_image.shape[0])))) pad_width = ((0, projection_size_padded - radon_image.shape[0]), (0, 0)) img = util.pad(radon_image, pad_width, mode='constant', constant_values=0) @@ -249,7 +250,7 @@ def iradon(radon_image, theta=None, output_size=None, reconstructed += backprojected if circle: radius = output_size // 2 - reconstruction_circle = (xpr**2 + ypr**2) <= radius**2 + reconstruction_circle = (xpr ** 2 + ypr ** 2) <= radius ** 2 reconstructed[~reconstruction_circle] = 0. return reconstructed * np.pi / (2 * len(th)) diff --git a/skimage/transform/tests/__init__.py b/skimage/transform/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/transform/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index 14a9305c..d34e5e8f 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import (assert_equal, assert_array_almost_equal, +from numpy.testing import (assert_equal, assert_almost_equal, assert_raises) from skimage.transform._geometric import _stackcopy from skimage.transform._geometric import GeometricTransform @@ -7,6 +7,7 @@ from skimage.transform import (estimate_transform, matrix_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) +from skimage._shared._warnings import expected_warnings SRC = np.array([ @@ -37,7 +38,7 @@ def test_stackcopy(): y = np.eye(3, 3) _stackcopy(x, y) for i in range(layers): - assert_array_almost_equal(x[..., i], y) + assert_almost_equal(x[..., i], y) def test_estimate_transform(): @@ -49,26 +50,26 @@ def test_estimate_transform(): def test_matrix_transform(): tform = AffineTransform(scale=(0.1, 0.5), rotation=2) - assert_equal(tform(SRC), matrix_transform(SRC, tform._matrix)) + assert_equal(tform(SRC), matrix_transform(SRC, tform.params)) def test_similarity_estimation(): # exact solution tform = estimate_transform('similarity', SRC[:2, :], DST[:2, :]) - assert_array_almost_equal(tform(SRC[:2, :]), DST[:2, :]) + assert_almost_equal(tform(SRC[:2, :]), DST[:2, :]) 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_almost_equal(tform2.inverse(tform2(SRC)), SRC) 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.params, tform2.params) + assert_almost_equal(tform3.params, tform2.params) def test_similarity_init(): @@ -78,15 +79,15 @@ def test_similarity_init(): 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) + assert_almost_equal(tform.scale, scale) + assert_almost_equal(tform.rotation, rotation) + assert_almost_equal(tform.translation, translation) # init with transformation 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) + assert_almost_equal(tform2.scale, scale) + assert_almost_equal(tform2.rotation, rotation) + assert_almost_equal(tform2.translation, translation) # test special case for scale if rotation=0 scale = 0.1 @@ -94,9 +95,9 @@ def test_similarity_init(): 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) + assert_almost_equal(tform.scale, scale) + assert_almost_equal(tform.rotation, rotation) + assert_almost_equal(tform.translation, translation) # test special case for scale if rotation=90deg @@ -105,24 +106,24 @@ def test_similarity_init(): 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) + assert_almost_equal(tform.scale, scale) + assert_almost_equal(tform.rotation, rotation) + assert_almost_equal(tform.translation, translation) def test_affine_estimation(): # exact solution tform = estimate_transform('affine', SRC[:3, :], DST[:3, :]) - assert_array_almost_equal(tform(SRC[:3, :]), DST[:3, :]) + assert_almost_equal(tform(SRC[:3, :]), DST[:3, :]) # over-determined tform2 = estimate_transform('affine', SRC, DST) - assert_array_almost_equal(tform2.inverse(tform2(SRC)), SRC) + assert_almost_equal(tform2.inverse(tform2(SRC)), SRC) # via estimate method tform3 = AffineTransform() tform3.estimate(SRC, DST) - assert_array_almost_equal(tform3.params, tform2.params) + assert_almost_equal(tform3.params, tform2.params) def test_affine_init(): @@ -133,71 +134,71 @@ def test_affine_init(): translation = (1, 1) tform = AffineTransform(scale=scale, rotation=rotation, shear=shear, translation=translation) - assert_array_almost_equal(tform.scale, scale) - assert_array_almost_equal(tform.rotation, rotation) - assert_array_almost_equal(tform.shear, shear) - assert_array_almost_equal(tform.translation, translation) + assert_almost_equal(tform.scale, scale) + assert_almost_equal(tform.rotation, rotation) + assert_almost_equal(tform.shear, shear) + assert_almost_equal(tform.translation, translation) # init with transformation 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) - assert_array_almost_equal(tform2.translation, translation) + assert_almost_equal(tform2.scale, scale) + assert_almost_equal(tform2.rotation, rotation) + assert_almost_equal(tform2.shear, shear) + assert_almost_equal(tform2.translation, translation) def test_piecewise_affine(): tform = PiecewiseAffineTransform() tform.estimate(SRC, DST) # make sure each single affine transform is exactly estimated - assert_array_almost_equal(tform(SRC), DST) - assert_array_almost_equal(tform.inverse(DST), SRC) + assert_almost_equal(tform(SRC), DST) + assert_almost_equal(tform.inverse(DST), SRC) def test_projective_estimation(): # exact solution tform = estimate_transform('projective', SRC[:4, :], DST[:4, :]) - assert_array_almost_equal(tform(SRC[:4, :]), DST[:4, :]) + assert_almost_equal(tform(SRC[:4, :]), DST[:4, :]) # over-determined tform2 = estimate_transform('projective', SRC, DST) - assert_array_almost_equal(tform2.inverse(tform2(SRC)), SRC) + assert_almost_equal(tform2.inverse(tform2(SRC)), SRC) # via estimate method tform3 = ProjectiveTransform() tform3.estimate(SRC, DST) - assert_array_almost_equal(tform3.params, tform2.params) + assert_almost_equal(tform3.params, tform2.params) def test_projective_init(): tform = estimate_transform('projective', SRC, DST) # init with transformation matrix tform2 = ProjectiveTransform(tform.params) - assert_array_almost_equal(tform2.params, tform.params) + assert_almost_equal(tform2.params, tform.params) def test_polynomial_estimation(): # over-determined tform = estimate_transform('polynomial', SRC, DST, order=10) - assert_array_almost_equal(tform(SRC), DST, 6) + assert_almost_equal(tform(SRC), DST, 6) # via estimate method tform2 = PolynomialTransform() tform2.estimate(SRC, DST, order=10) - assert_array_almost_equal(tform2.params, tform.params) + assert_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) + assert_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_almost_equal(tform2.params, tform.params) def test_polynomial_inverse(): @@ -209,15 +210,18 @@ def test_union(): 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_almost_equal(tform.params, tform3.params) 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_almost_equal(tform.params, tform3.params) assert tform.__class__ == ProjectiveTransform + tform = AffineTransform(scale=(0.1, 0.1), rotation=0.3) + assert_almost_equal((tform + tform.inverse).params, np.eye(3)) + def test_union_differing_types(): tform1 = SimilarityTransform() @@ -248,10 +252,28 @@ def test_invalid_input(): def test_deprecated_params_attributes(): for t in ('projective', 'affine', 'similarity'): tform = estimate_transform(t, SRC, DST) - assert_equal(tform._matrix, tform.params) + with expected_warnings(['`_matrix`.*deprecated']): + assert_equal(tform._matrix, tform.params) tform = estimate_transform('polynomial', SRC, DST, order=3) - assert_equal(tform._params, tform.params) + with expected_warnings(['`_params`.*deprecated']): + assert_equal(tform._params, tform.params) + + +def test_degenerate(): + src = dst = np.zeros((10, 2)) + + tform = SimilarityTransform() + tform.estimate(src, dst) + assert np.all(np.isnan(tform.params)) + + tform = AffineTransform() + tform.estimate(src, dst) + assert np.all(np.isnan(tform.params)) + + tform = ProjectiveTransform() + tform.estimate(src, dst) + assert np.all(np.isnan(tform.params)) if __name__ == "__main__": diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index fb19d8c1..884d7957 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -3,6 +3,7 @@ from numpy.testing import assert_almost_equal, assert_equal import skimage.transform as tf from skimage.draw import line, circle_perimeter, ellipse_perimeter +from skimage._shared._warnings import expected_warnings def append_desc(func, description): @@ -67,7 +68,8 @@ def test_hough_line_peaks(): out, angles, d = tf.hough_line(img) - out, theta, dist = tf.hough_line_peaks(out, angles, d) + with expected_warnings(['`background`']): + out, theta, dist = tf.hough_line_peaks(out, angles, d) assert_equal(len(dist), 1) assert_almost_equal(dist[0], 80.723, 1) @@ -79,13 +81,19 @@ def test_hough_line_peaks_dist(): img[:, 30] = True img[:, 40] = True hspace, angles, dists = tf.hough_line(img) - assert len(tf.hough_line_peaks(hspace, angles, dists, - min_distance=5)[0]) == 2 - assert len(tf.hough_line_peaks(hspace, angles, dists, - min_distance=15)[0]) == 1 + with expected_warnings(['`background`']): + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_distance=5)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_distance=15)[0]) == 1 def test_hough_line_peaks_angle(): + with expected_warnings(['`background`']): + check_hough_line_peaks_angle() + + +def check_hough_line_peaks_angle(): img = np.zeros((100, 100), dtype=np.bool_) img[:, 0] = True img[0, :] = True @@ -116,8 +124,9 @@ def test_hough_line_peaks_num(): img[:, 30] = True img[:, 40] = True hspace, angles, dists = tf.hough_line(img) - assert len(tf.hough_line_peaks(hspace, angles, dists, min_distance=0, - min_angle=0, num_peaks=1)[0]) == 1 + with expected_warnings(['`background`']): + assert len(tf.hough_line_peaks(hspace, angles, dists, min_distance=0, + min_angle=0, num_peaks=1)[0]) == 1 def test_hough_circle(): diff --git a/skimage/transform/tests/test_integral.py b/skimage/transform/tests/test_integral.py index 4a641e71..eb92a40b 100644 --- a/skimage/transform/tests/test_integral.py +++ b/skimage/transform/tests/test_integral.py @@ -3,14 +3,15 @@ from numpy.testing import assert_equal from skimage.transform import integral_image, integrate -x = (np.random.random((50, 50)) * 255).astype(np.uint8) +np.random.seed(0) +x = (np.random.rand(50, 50) * 255).astype(np.uint8) s = integral_image(x) def test_validity(): y = np.arange(12).reshape((4, 3)) - y = (np.random.random((50, 50)) * 255).astype(np.uint8) + y = (np.random.rand(50, 50) * 255).astype(np.uint8) assert_equal(integral_image(y)[-1, -1], y.sum()) diff --git a/skimage/transform/tests/test_pyramids.py b/skimage/transform/tests/test_pyramids.py index 6d0609e0..078eb37d 100644 --- a/skimage/transform/tests/test_pyramids.py +++ b/skimage/transform/tests/test_pyramids.py @@ -3,7 +3,7 @@ from skimage import data from skimage.transform import pyramids -image = data.lena() +image = data.astronaut() image_gray = image[..., 0] diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 07054ab7..115f0e6f 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -1,5 +1,5 @@ -from numpy.testing import (assert_array_almost_equal, run_module_suite, - assert_array_equal, assert_raises) +from numpy.testing import (assert_almost_equal, run_module_suite, + assert_equal, assert_raises) import numpy as np from scipy.ndimage import map_coordinates @@ -10,6 +10,10 @@ from skimage.transform import (warp, warp_coords, rotate, resize, rescale, downscale_local_mean) from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray +from skimage._shared._warnings import expected_warnings + + +np.random.seed(0) def test_warp_tform(): @@ -19,10 +23,10 @@ def test_warp_tform(): tform = SimilarityTransform(scale=1, rotation=theta, translation=(0, 4)) x90 = warp(x, tform, order=1) - assert_array_almost_equal(x90, np.rot90(x)) + assert_almost_equal(x90, np.rot90(x)) x90 = warp(x, tform.inverse, order=1) - assert_array_almost_equal(x90, np.rot90(x)) + assert_almost_equal(x90, np.rot90(x)) def test_warp_callable(): @@ -34,7 +38,7 @@ def test_warp_callable(): shift = lambda xy: xy + 1 outx = warp(x, shift, order=1) - assert_array_almost_equal(outx, refx) + assert_almost_equal(outx, refx) def test_warp_matrix(): @@ -47,11 +51,42 @@ def test_warp_matrix(): # _warp_fast outx = warp(x, matrix, order=1) - assert_array_almost_equal(outx, refx) + assert_almost_equal(outx, refx) # check for ndimage.map_coordinates outx = warp(x, matrix, order=5) +def test_warp_nd(): + for dim in range(2, 8): + shape = dim * (5,) + + x = np.zeros(shape, dtype=np.double) + x_c = dim * (2,) + x[x_c] = 1 + refx = np.zeros(shape, dtype=np.double) + refx_c = dim * (1,) + refx[refx_c] = 1 + + coord_grid = dim * (slice(0, 5, 1),) + coords = np.array(np.mgrid[coord_grid]) + 1 + + outx = warp(x, coords, order=0, cval=0) + + assert_almost_equal(outx, refx) + + +def test_warp_clip(): + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 + + outx = rescale(x, 3, order=3, clip=False) + assert outx.min() < 0 + + outx = rescale(x, 3, order=3, clip=True) + assert_almost_equal(outx.min(), 0) + assert_almost_equal(outx.max(), 1) + + def test_homography(): x = np.zeros((5, 5), dtype=np.double) x[1, 1] = 1 @@ -63,49 +98,14 @@ def test_homography(): x90 = warp(x, inverse_map=ProjectiveTransform(M).inverse, order=1) - assert_array_almost_equal(x90, np.rot90(x)) - - -def test_fast_homography(): - img = rgb2gray(data.lena()).astype(np.uint8) - img = img[:, :100] - - theta = np.deg2rad(30) - scale = 0.5 - tx, ty = 50, 50 - - H = np.eye(3) - S = scale * np.sin(theta) - C = scale * np.cos(theta) - - H[:2, :2] = [[C, -S], [S, C]] - H[:2, 2] = [tx, ty] - - tform = ProjectiveTransform(H) - coords = warp_coords(tform.inverse, (img.shape[0], img.shape[1])) - - for order in range(4): - for mode in ('constant', 'reflect', 'wrap', 'nearest'): - p0 = map_coordinates(img, coords, mode=mode, order=order) - p1 = warp(img, tform, mode=mode, order=order) - - # import matplotlib.pyplot as plt - # f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) - # ax0.imshow(img) - # ax1.imshow(p0, cmap=plt.cm.gray) - # ax2.imshow(p1, cmap=plt.cm.gray) - # ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) - # plt.show() - - d = np.mean(np.abs(p0 - p1)) - assert d < 0.001 + assert_almost_equal(x90, np.rot90(x)) def test_rotate(): x = np.zeros((5, 5), dtype=np.double) x[1, 1] = 1 x90 = rotate(x, 90) - assert_array_almost_equal(x90, np.rot90(x)) + assert_almost_equal(x90, np.rot90(x)) def test_rotate_resize(): @@ -119,6 +119,17 @@ def test_rotate_resize(): assert x45.shape == (14, 14) +def test_rotate_center(): + x = np.zeros((10, 10), dtype=np.double) + x[4, 4] = 1 + refx = np.zeros((10, 10), dtype=np.double) + refx[2, 5] = 1 + x20 = rotate(x, 20, order=0, center=(0, 0)) + assert_almost_equal(x20, refx) + x0 = rotate(x20, -20, order=0, center=(0, 0)) + assert_almost_equal(x0, x) + + def test_rescale(): # same scale factor x = np.zeros((5, 5), dtype=np.double) @@ -126,7 +137,7 @@ def test_rescale(): scaled = rescale(x, 2, order=0) ref = np.zeros((10, 10)) ref[2:4, 2:4] = 1 - assert_array_almost_equal(scaled, ref) + assert_almost_equal(scaled, ref) # different scale factors x = np.zeros((5, 5), dtype=np.double) @@ -134,7 +145,7 @@ def test_rescale(): scaled = rescale(x, (2, 1), order=0) ref = np.zeros((10, 5)) ref[2:4, 1] = 1 - assert_array_almost_equal(scaled, ref) + assert_almost_equal(scaled, ref) def test_resize2d(): @@ -143,7 +154,7 @@ def test_resize2d(): resized = resize(x, (10, 10), order=0) ref = np.zeros((10, 10)) ref[2:4, 2:4] = 1 - assert_array_almost_equal(resized, ref) + assert_almost_equal(resized, ref) def test_resize3d_keep(): @@ -153,9 +164,9 @@ def test_resize3d_keep(): resized = resize(x, (10, 10), order=0) ref = np.zeros((10, 10, 3)) ref[2:4, 2:4, :] = 1 - assert_array_almost_equal(resized, ref) + assert_almost_equal(resized, ref) resized = resize(x, (10, 10, 3), order=0) - assert_array_almost_equal(resized, ref) + assert_almost_equal(resized, ref) def test_resize3d_resize(): @@ -165,7 +176,7 @@ def test_resize3d_resize(): resized = resize(x, (10, 10, 1), order=0) ref = np.zeros((10, 10, 1)) ref[2:4, 2:4] = 1 - assert_array_almost_equal(resized, ref) + assert_almost_equal(resized, ref) def test_resize3d_bilinear(): @@ -179,15 +190,17 @@ def test_resize3d_bilinear(): ref[1:5, 2:4, :] = 0.09375 ref[2:4, 1:5, :] = 0.09375 ref[2:4, 2:4, :] = 0.28125 - assert_array_almost_equal(resized, ref) + assert_almost_equal(resized, ref) def test_swirl(): image = img_as_float(data.checkerboard()) swirl_params = {'radius': 80, 'rotation': 0, 'order': 2, 'mode': 'reflect'} - swirled = tf.swirl(image, strength=10, **swirl_params) - unswirled = tf.swirl(swirled, strength=-10, **swirl_params) + + with expected_warnings(['Bi-quadratic.*bug']): + swirled = tf.swirl(image, strength=10, **swirl_params) + unswirled = tf.swirl(swirled, strength=-10, **swirl_params) assert np.mean(np.abs(image - unswirled)) < 0.01 @@ -200,21 +213,21 @@ def test_const_cval_out_of_range(): def test_warp_identity(): - lena = img_as_float(rgb2gray(data.lena())) - assert len(lena.shape) == 2 - assert np.allclose(lena, warp(lena, AffineTransform(rotation=0))) - assert not np.allclose(lena, warp(lena, AffineTransform(rotation=0.1))) - rgb_lena = np.transpose(np.asarray([lena, np.zeros_like(lena), lena]), + img = img_as_float(rgb2gray(data.astronaut())) + assert len(img.shape) == 2 + assert np.allclose(img, warp(img, AffineTransform(rotation=0))) + assert not np.allclose(img, warp(img, AffineTransform(rotation=0.1))) + rgb_img = np.transpose(np.asarray([img, np.zeros_like(img), img]), (1, 2, 0)) - warped_rgb_lena = warp(rgb_lena, AffineTransform(rotation=0.1)) - assert np.allclose(rgb_lena, warp(rgb_lena, AffineTransform(rotation=0))) - assert not np.allclose(rgb_lena, warped_rgb_lena) + warped_rgb_img = warp(rgb_img, AffineTransform(rotation=0.1)) + assert np.allclose(rgb_img, warp(rgb_img, AffineTransform(rotation=0))) + assert not np.allclose(rgb_img, warped_rgb_img) # assert no cross-talk between bands - assert np.all(0 == warped_rgb_lena[:, :, 1]) + assert np.all(0 == warped_rgb_img[:, :, 1]) def test_warp_coords_example(): - image = data.lena().astype(np.float32) + image = data.astronaut().astype(np.float32) assert 3 == image.shape[2] tform = SimilarityTransform(translation=(0, -10)) coords = warp_coords(tform, (30, 30, 3)) @@ -226,17 +239,16 @@ def test_downscale_local_mean(): out1 = downscale_local_mean(image1, (2, 3)) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) - assert_array_equal(expected1, out1) + assert_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) out2 = downscale_local_mean(image2, (4, 5)) expected2 = np.array([[ 14. , 10.8], [ 8.5, 5.7]]) - assert_array_equal(expected2, out2) + assert_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()) @@ -245,11 +257,11 @@ 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)) + assert_equal(warp(image, inverse_tform), warp(image, tform.inverse)) def test_slow_warp_nonint_oshape(): - image = np.random.random((5, 5)) + image = np.random.rand(5, 5) assert_raises(ValueError, warp, image, lambda xy: xy, output_shape=(13.1, 19.5)) @@ -257,5 +269,23 @@ def test_slow_warp_nonint_oshape(): warp(image, lambda xy: xy, output_shape=(13.0001, 19.9999)) +def test_keep_range(): + image = np.linspace(0, 2, 25).reshape(5, 5) + + out = rescale(image, 2, preserve_range=False, clip=True, order=0) + assert out.min() == 0 + assert out.max() == 2 + + out = rescale(image, 2, preserve_range=True, clip=True, order=0) + assert out.min() == 0 + assert out.max() == 2 + + out = rescale(image.astype(np.uint8), 2, preserve_range=False, + clip=True, order=0) + assert out.min() == 0 + assert out.max() == 2 / 255.0 + + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index 5577e46b..4739bd5b 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -3,7 +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 -from .arraypad import pad +from .arraypad import pad, crop from ._regular_grid import regular_grid from .unique import unique_rows @@ -17,6 +17,7 @@ __all__ = ['img_as_float', 'view_as_blocks', 'view_as_windows', 'pad', + 'crop', 'random_noise', 'regular_grid', 'unique_rows'] diff --git a/skimage/util/_regular_grid.py b/skimage/util/_regular_grid.py index 92b32082..f4b77a4a 100644 --- a/skimage/util/_regular_grid.py +++ b/skimage/util/_regular_grid.py @@ -59,9 +59,9 @@ def regular_grid(ar_shape, n_points): if (sorted_dims < stepsizes).any(): for dim in range(ndim): stepsizes[dim] = sorted_dims[dim] - space_size = float(np.prod(sorted_dims[dim+1:])) - stepsizes[dim+1:] = ((space_size / n_points) ** - (1.0 / (ndim - dim - 1))) + space_size = float(np.prod(sorted_dims[dim + 1:])) + stepsizes[dim + 1:] = ((space_size / n_points) ** + (1.0 / (ndim - dim - 1))) if (sorted_dims >= stepsizes).all(): break starts = (stepsizes // 2).astype(int) diff --git a/skimage/util/arraypad.py b/skimage/util/arraypad.py index 0bc92d7b..f8104df5 100644 --- a/skimage/util/arraypad.py +++ b/skimage/util/arraypad.py @@ -5,17 +5,10 @@ of an n-dimensional array. """ from __future__ import division, absolute_import, print_function -from six import integer_types import numpy as np -try: - # Available on 2.x at base, Py3 requires this compatibility import. - # Later versions of NumPy have this for 2.x as well. - from numpy.compat import long -except: - pass -__all__ = ['pad'] +__all__ = ['pad', 'crop'] ############################################################################### @@ -785,7 +778,7 @@ def _pad_ref(arr, pad_amt, method, axis=-1): Notes ----- This algorithm does not pad with repetition, i.e. the edges are not - repeated in the reflection. For that behavior, use `method='symmetric'`. + repeated in the reflection. For that behavior, use `mode='symmetric'`. The modes 'reflect', 'symmetric', and 'wrap' must be padded with a single function, lest the indexing tricks in non-integer multiples of the @@ -870,7 +863,7 @@ def _pad_sym(arr, pad_amt, method, axis=-1): Notes ----- This algorithm DOES pad with repetition, i.e. the edges are repeated. - For a method that does not repeat edges, use `method='reflect'`. + For padding without repeated edges, use `mode='reflect'`. The modes 'reflect', 'symmetric', and 'wrap' must be padded with a single function, lest the indexing tricks in non-integer multiples of the @@ -993,7 +986,7 @@ def _pad_wrap(arr, pad_amt, axis=-1): return np.concatenate((wrap_chunk1, arr, wrap_chunk2), axis=axis) -def _normalize_shape(narray, shape): +def _normalize_shape(ndarray, shape, cast_to_int=True): """ Private function which does some checks and normalizes the possibly much simpler representations of 'pad_width', 'stat_length', @@ -1003,56 +996,77 @@ def _normalize_shape(narray, shape): ---------- narray : ndarray Input ndarray - shape : {sequence, int}, optional - The width of padding (pad_width) or the number of elements on the - edge of the narray used for statistics (stat_length). + shape : {sequence, array_like, float, int}, optional + The width of padding (pad_width), the number of elements on the + edge of the narray used for statistics (stat_length), the constant + value(s) to use when filling padded regions (constant_values), or the + endpoint target(s) for linear ramps (end_values). ((before_1, after_1), ... (before_N, after_N)) unique number of elements for each axis where `N` is rank of `narray`. ((before, after),) yields same before and after constants for each axis. - (constant,) or int is a shortcut for before = after = constant for + (constant,) or val is a shortcut for before = after = constant for all axes. + cast_to_int : bool, optional + Controls if values in ``shape`` will be rounded and cast to int + before being returned. Returns ------- - _normalize_shape : tuple of tuples - int => ((int, int), (int, int), ...) - [[int1, int2], [int3, int4], ...] => ((int1, int2), (int3, int4), ...) - ((int1, int2), (int3, int4), ...) => no change - [[int1, int2], ] => ((int1, int2), (int1, int2), ...) - ((int1, int2), ) => ((int1, int2), (int1, int2), ...) - [[int , ], ] => ((int, int), (int, int), ...) - ((int , ), ) => ((int, int), (int, int), ...) + normalized_shape : tuple of tuples + val => ((val, val), (val, val), ...) + [[val1, val2], [val3, val4], ...] => ((val1, val2), (val3, val4), ...) + ((val1, val2), (val3, val4), ...) => no change + [[val1, val2], ] => ((val1, val2), (val1, val2), ...) + ((val1, val2), ) => ((val1, val2), (val1, val2), ...) + [[val , ], ] => ((val, val), (val, val), ...) + ((val , ), ) => ((val, val), (val, val), ...) """ - normshp = None - shapelen = len(np.shape(narray)) + ndims = ndarray.ndim - if isinstance(shape, np.ndarray): - shape = shape.tolist() + # Shortcut shape=None + if shape is None: + return ((None, None), ) * ndims - if isinstance(shape, (int, float)) or shape is None: - normshp = ((shape, shape), ) * shapelen - elif (isinstance(shape, (tuple, list)) - and isinstance(shape[0], (tuple, list)) - and len(shape) == shapelen): - normshp = shape - for i in normshp: - if len(i) != 2: - fmt = "Unable to create correctly shaped tuple from %s" - raise ValueError(fmt % (normshp,)) - elif (isinstance(shape, (tuple, list)) - and isinstance(shape[0], integer_types + (float,)) - and len(shape) == 1): - normshp = ((shape[0], shape[0]), ) * shapelen - elif (isinstance(shape, (tuple, list)) - and isinstance(shape[0], integer_types + (float,)) - and len(shape) == 2): - normshp = (shape, ) * shapelen - if normshp is None: + # Convert any input `info` to a NumPy array + arr = np.asarray(shape) + + # Switch based on what input looks like + if arr.ndim <= 1: + if arr.shape == () or arr.shape == (1,): + # Single scalar input + # Create new array of ones, multiply by the scalar + arr = np.ones((ndims, 2), dtype=ndarray.dtype) * arr + elif arr.shape == (2,): + # Apply padding (before, after) each axis + # Create new axis 0, repeat along it for every axis + arr = arr[np.newaxis, :].repeat(ndims, axis=0) + else: + fmt = "Unable to create correctly shaped tuple from %s" + raise ValueError(fmt % (shape,)) + + elif arr.ndim == 2: + if arr.shape[1] == 1 and arr.shape[0] == ndims: + # Padded before and after by the same amount + arr = arr.repeat(2, axis=1) + elif arr.shape[0] == ndims: + # Input correctly formatted, pass it on as `arr` + arr = shape + else: + fmt = "Unable to create correctly shaped tuple from %s" + raise ValueError(fmt % (shape,)) + + else: fmt = "Unable to create correctly shaped tuple from %s" raise ValueError(fmt % (shape,)) - return normshp + + # Cast if necessary + if cast_to_int is True: + arr = np.round(arr).astype(int) + + # Convert list of lists to tuple of tuples + return tuple(tuple(axis) for axis in arr.tolist()) def _validate_lengths(narray, number_elements): @@ -1108,38 +1122,49 @@ def pad(array, pad_width, mode=None, **kwargs): ---------- array : array_like of rank N Input array - pad_width : {sequence, int} + pad_width : {sequence, array_like, int} Number of values padded to the edges of each axis. ((before_1, after_1), ... (before_N, after_N)) unique pad widths for each axis. ((before, after),) yields same before and after pad for each axis. (pad,) or int is a shortcut for before = after = pad width for all axes. - mode : {str, function} + mode : str or function One of the following string values or a user supplied function. - 'constant' Pads with a constant value. - 'edge' Pads with the edge values of array. - 'linear_ramp' Pads with the linear ramp between end_value and the - array edge value. - 'maximum' Pads with the maximum value of all or part of the - vector along each axis. - 'mean' Pads with the mean value of all or part of the - vector along each axis. - 'median' Pads with the median value of all or part of the - vector along each axis. - 'minimum' Pads with the minimum value of all or part of the - vector along each axis. - 'reflect' Pads with the reflection of the vector mirrored on - the first and last values of the vector along each - axis. - 'symmetric' Pads with the reflection of the vector mirrored - along the edge of the array. - 'wrap' Pads with the wrap of the vector along the axis. - The first values are used to pad the end and the - end values are used to pad the beginning. - Padding function, see Notes. - stat_length : {sequence, int}, optional + 'constant' + Pads with a constant value. + 'edge' + Pads with the edge values of array. + 'linear_ramp' + Pads with the linear ramp between end_value and the + array edge value. + 'maximum' + Pads with the maximum value of all or part of the + vector along each axis. + 'mean' + Pads with the mean value of all or part of the + vector along each axis. + 'median' + Pads with the median value of all or part of the + vector along each axis. + 'minimum' + Pads with the minimum value of all or part of the + vector along each axis. + 'reflect' + Pads with the reflection of the vector mirrored on + the first and last values of the vector along each + axis. + 'symmetric' + Pads with the reflection of the vector mirrored + along the edge of the array. + 'wrap' + Pads with the wrap of the vector along the axis. + The first values are used to pad the end and the + end values are used to pad the beginning. + + Padding function, see Notes. + stat_length : sequence or int, optional Used in 'maximum', 'mean', 'median', and 'minimum'. Number of values at edge of each axis used to calculate the statistic value. @@ -1153,7 +1178,7 @@ def pad(array, pad_width, mode=None, **kwargs): length for all axes. Default is ``None``, to use the entire axis. - constant_values : {sequence, int}, optional + constant_values : sequence or int, optional Used in 'constant'. The values to set the padded values for each axis. @@ -1167,7 +1192,7 @@ def pad(array, pad_width, mode=None, **kwargs): all axes. Default is 0. - end_values : {sequence, int}, optional + end_values : sequence or int, optional Used in 'linear_ramp'. The values used for the ending value of the linear_ramp and that will form the edge of the padded array. @@ -1181,7 +1206,7 @@ def pad(array, pad_width, mode=None, **kwargs): all axes. Default is 0. - reflect_type : str {'even', 'odd'}, optional + reflect_type : {'even', 'odd'}, optional Used in 'reflect', and 'symmetric'. The 'even' style is the default with an unaltered reflection around the edge value. For the 'odd' style, the extented part of the array is created by @@ -1195,6 +1220,8 @@ def pad(array, pad_width, mode=None, **kwargs): Notes ----- + .. versionadded:: 1.7.0 + For an array with rank greater than 1, some of the padding of later axes is calculated from padding of previous axes. This is easiest to think about with a rank 2 array where the corners of the padded array @@ -1202,7 +1229,7 @@ def pad(array, pad_width, mode=None, **kwargs): The padding function, if used, should return a rank 1 array equal in length to the vector argument with padded values replaced. It has the - following signature: + following signature:: padding_func(vector, iaxis_pad_width, iaxis, **kwargs) @@ -1223,14 +1250,15 @@ def pad(array, pad_width, mode=None, **kwargs): Examples -------- + >>> from skimage.util import pad >>> a = [1, 2, 3, 4, 5] - >>> 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]) - >>> pad(a, (2,3), 'edge') + >>> pad(a, (2, 3), 'edge') array([1, 1, 1, 2, 3, 4, 5, 5, 5, 5]) - >>> 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]) >>> pad(a, (2,), 'maximum') @@ -1242,7 +1270,7 @@ def pad(array, pad_width, mode=None, **kwargs): >>> pad(a, (2,), 'median') array([3, 3, 1, 2, 3, 4, 5, 3, 3]) - >>> a = [[1,2], [3,4]] + >>> a = [[1, 2], [3, 4]] >>> pad(a, ((3, 2), (2, 3)), 'minimum') array([[1, 1, 1, 2, 1, 1, 1], [1, 1, 1, 2, 1, 1, 1], @@ -1253,19 +1281,19 @@ def pad(array, pad_width, mode=None, **kwargs): [1, 1, 1, 2, 1, 1, 1]]) >>> a = [1, 2, 3, 4, 5] - >>> pad(a, (2,3), 'reflect') + >>> pad(a, (2, 3), 'reflect') array([3, 2, 1, 2, 3, 4, 5, 4, 3, 2]) - >>> 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]) - >>> pad(a, (2,3), 'symmetric') + >>> pad(a, (2, 3), 'symmetric') array([2, 1, 1, 2, 3, 4, 5, 5, 4, 3]) - >>> 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]) - >>> 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): @@ -1274,7 +1302,7 @@ def pad(array, pad_width, mode=None, **kwargs): ... return vector >>> a = np.arange(6) - >>> a = a.reshape((2,3)) + >>> a = a.reshape((2, 3)) >>> pad(a, 2, padwithtens) array([[10, 10, 10, 10, 10, 10, 10], @@ -1283,8 +1311,9 @@ def pad(array, pad_width, mode=None, **kwargs): [10, 10, 3, 4, 5, 10, 10], [10, 10, 10, 10, 10, 10, 10], [10, 10, 10, 10, 10, 10, 10]]) - """ + if not np.asarray(pad_width).dtype.kind == 'i': + raise TypeError('`pad_width` must be of integral type.') narray = np.array(array) pad_width = _validate_lengths(narray, pad_width) @@ -1299,13 +1328,15 @@ def pad(array, pad_width, mode=None, **kwargs): 'minimum': ['stat_length'], 'reflect': ['reflect_type'], 'symmetric': ['reflect_type'], - 'wrap': []} + 'wrap': [], + } kwdefaults = { 'stat_length': None, 'constant_values': 0, 'end_values': 0, - 'reflect_type': 'even'} + 'reflect_type': 'even', + } if isinstance(mode, str): # Make sure have allowed kwargs appropriate for mode @@ -1323,7 +1354,8 @@ def pad(array, pad_width, mode=None, **kwargs): if i == 'stat_length': kwargs[i] = _validate_lengths(narray, kwargs[i]) if i in ['end_values', 'constant_values']: - kwargs[i] = _normalize_shape(narray, kwargs[i]) + kwargs[i] = _normalize_shape(narray, kwargs[i], + cast_to_int=False) elif mode is None: raise ValueError('Keyword "mode" must be a function or one of %s.' % (list(allowedkwargs.keys()),)) @@ -1339,7 +1371,7 @@ def pad(array, pad_width, mode=None, **kwargs): pad_width[i][0] + narray.shape[i]) for i in rank] new_shape = np.array(narray.shape) + total_dim_increase - newmat = np.zeros(new_shape).astype(narray.dtype) + newmat = np.zeros(new_shape, narray.dtype) # Insert the original array into the padded array newmat[offset_slices] = narray @@ -1416,7 +1448,6 @@ def pad(array, pad_width, mode=None, **kwargs): method = kwargs['reflect_type'] safe_pad = newmat.shape[axis] - 1 while ((pad_before > safe_pad) or (pad_after > safe_pad)): - offset = 0 pad_iter_b = min(safe_pad, safe_pad * (pad_before // safe_pad)) pad_iter_a = min(safe_pad, safe_pad * (pad_after // safe_pad)) @@ -1424,10 +1455,6 @@ def pad(array, pad_width, mode=None, **kwargs): pad_iter_a), method, axis) pad_before -= pad_iter_b pad_after -= pad_iter_a - if pad_iter_b > 0: - offset += 1 - if pad_iter_a > 0: - offset += 1 safe_pad += pad_iter_b + pad_iter_a newmat = _pad_ref(newmat, (pad_before, pad_after), method, axis) @@ -1469,3 +1496,43 @@ def pad(array, pad_width, mode=None, **kwargs): newmat = _pad_wrap(newmat, (pad_before, pad_after), axis) return newmat + + +def crop(ar, crop_width, copy=False, order='K'): + """Crop array `ar` by `crop_width` along each dimension. + + Parameters + ---------- + ar : array-like of rank N + Input array. + crop_width : {sequence, int} + Number of values to remove from the edges of each axis. + ``((before_1, after_1),`` ... ``(before_N, after_N))`` specifies + unique crop widths at the start and end of each axis. + ``((before, after),)`` specifies a fixed start and end crop + for every axis. + ``(n,)`` or ``n`` for integer ``n`` is a shortcut for + before = after = ``n`` for all axes. + copy : bool, optional + Ensure the returned array is a contiguous copy. Normally, a crop + operation will return a discontiguous view of the underlying + input array. Passing ``copy=True`` will result in a contiguous + copy. + order : {'C', 'F', 'A', 'K'}, optional + If ``copy==True``, control the memory layout of the copy. See + ``np.copy``. + + Returns + ------- + cropped : array + The cropped array. If ``copy=False`` (default), this is a sliced + view of the input array. + """ + ar = np.array(ar, copy=False) + crops = _validate_lengths(ar, crop_width) + slices = [slice(a, ar.shape[i] - b) for i, (a, b) in enumerate(crops)] + if copy: + cropped = np.array(ar[slices], order=order, copy=True) + else: + cropped = ar[slices] + return cropped diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 3f85cc2b..0e7bc060 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -304,7 +304,8 @@ def img_as_uint(image, force_copy=False): Notes ----- - Negative input values will be shifted to the positive domain. + Negative input values will be clipped. + Positive values are scaled between 0 and 65535. """ return convert(image, np.uint16, force_copy) @@ -327,6 +328,7 @@ def img_as_int(image, force_copy=False): Notes ----- + The values are scaled between -32768 and 32767. If the input data-type is positive-only (e.g., uint8), then the output image will still only have positive values. @@ -351,8 +353,8 @@ def img_as_ubyte(image, force_copy=False): Notes ----- - If the input data-type is positive-only (e.g., uint16), then - the output image will still only have positive values. + Negative input values will be clipped. + Positive values are scaled between 0 and 255. """ return convert(image, np.uint8, force_copy) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index 4bc6aca8..fbfef476 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -98,7 +98,7 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=None): if fill == 'mean': fill = arr_in.mean() - n_missing = int((alpha_y * alpha_x) - n_images) + n_missing = int((alpha_y * alpha_x) - n_images) missing = np.ones((n_missing, height, width), dtype=arr_in.dtype) * fill arr_out = np.vstack((arr_in, missing)) diff --git a/skimage/util/noise.py b/skimage/util/noise.py index 9283f537..ff20084a 100644 --- a/skimage/util/noise.py +++ b/skimage/util/noise.py @@ -16,15 +16,15 @@ def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): mode : str One of the following strings, selecting the type of noise to add: - 'gaussian' Gaussian-distributed additive noise. - 'localvar' Gaussian-distributed additive noise, with specified - local variance at each point of `image` - 'poisson' Poisson-distributed noise generated from the data. - 'salt' Replaces random pixels with 1. - 'pepper' Replaces random pixels with 0. - 's&p' Replaces random pixels with 0 or 1. - 'speckle' Multiplicative noise using out = image + n*image, where - n is uniform noise with specified mean & variance. + - 'gaussian' Gaussian-distributed additive noise. + - 'localvar' Gaussian-distributed additive noise, with specified + local variance at each point of `image` + - 'poisson' Poisson-distributed noise generated from the data. + - 'salt' Replaces random pixels with 1. + - 'pepper' Replaces random pixels with 0. + - 's&p' Replaces random pixels with 0 or 1. + - 'speckle' Multiplicative noise using out = image + n*image, where + n is uniform noise with specified mean & variance. seed : int If provided, this will set the random seed before generating noise, for valid pseudo-random comparisons. diff --git a/skimage/util/shape.py b/skimage/util/shape.py index 4633ada6..929ceb6f 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -240,7 +240,7 @@ def view_as_windows(arr_in, window_shape, step=1): arr_in = np.ascontiguousarray(arr_in) new_shape = tuple((arr_shape - window_shape) // step + 1) + \ - tuple(window_shape) + tuple(window_shape) arr_strides = np.array(arr_in.strides) new_strides = np.concatenate((arr_strides * step, arr_strides)) diff --git a/skimage/util/tests/__init__.py b/skimage/util/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/util/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/util/tests/test_arraypad.py b/skimage/util/tests/test_arraypad.py index 3fbe50a1..d6f993f9 100644 --- a/skimage/util/tests/test_arraypad.py +++ b/skimage/util/tests/test_arraypad.py @@ -1,12 +1,55 @@ -"""Tests for the pad functions. +"""Tests for the array padding functions. """ from __future__ import division, absolute_import, print_function -from numpy.testing import TestCase, run_module_suite, assert_array_equal -from numpy.testing import assert_raises, assert_array_almost_equal import numpy as np -from skimage.util import pad +from numpy.testing import (assert_array_equal, assert_raises, assert_allclose, + assert_equal, TestCase) +from skimage.util import pad, crop + + +class TestConditionalShortcuts(TestCase): + def test_zero_padding_shortcuts(self): + test = np.arange(120).reshape(4, 5, 6) + pad_amt = [(0, 0) for axis in test.shape] + modes = ['constant', + 'edge', + 'linear_ramp', + 'maximum', + 'mean', + 'median', + 'minimum', + 'reflect', + 'symmetric', + 'wrap', + ] + for mode in modes: + assert_array_equal(test, pad(test, pad_amt, mode=mode)) + + def test_shallow_statistic_range(self): + test = np.arange(120).reshape(4, 5, 6) + pad_amt = [(1, 1) for axis in test.shape] + modes = ['maximum', + 'mean', + 'median', + 'minimum', + ] + for mode in modes: + assert_array_equal(pad(test, pad_amt, mode='edge'), + pad(test, pad_amt, mode=mode, stat_length=1)) + + def test_clip_statistic_range(self): + test = np.arange(30).reshape(5, 6) + pad_amt = [(3, 3) for axis in test.shape] + modes = ['maximum', + 'mean', + 'median', + 'minimum', + ] + for mode in modes: + assert_array_equal(pad(test, pad_amt, mode=mode), + pad(test, pad_amt, mode=mode, stat_length=30)) class TestStatistic(TestCase): @@ -18,7 +61,7 @@ class TestStatistic(TestCase): 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., + 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., @@ -42,7 +85,7 @@ class TestStatistic(TestCase): 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, + 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, @@ -66,16 +109,40 @@ class TestStatistic(TestCase): 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] + ) + assert_array_equal(a, b) + + def test_check_maximum_stat_length(self): + a = np.arange(100) + 1 + a = pad(a, (25, 20), 'maximum', stat_length=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, + + 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] @@ -86,11 +153,11 @@ class TestStatistic(TestCase): a = np.arange(100) a = pad(a, (25, 20), 'minimum') b = np.array( - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, + [0, 0, 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, + 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, @@ -101,8 +168,8 @@ class TestStatistic(TestCase): 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) @@ -110,11 +177,11 @@ class TestStatistic(TestCase): a = np.arange(100) + 2 a = pad(a, (25, 20), 'minimum') b = np.array( - [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, + [2, 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, + 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, @@ -125,8 +192,32 @@ class TestStatistic(TestCase): 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_minimum_stat_length(self): + a = np.arange(100) + 1 + a = pad(a, (25, 20), 'minimum', stat_length=10) + 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, 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, + + 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, + 91, 91, 91, 91, 91, 91, 91, 91, 91, 91] ) assert_array_equal(a, b) @@ -138,16 +229,16 @@ class TestStatistic(TestCase): 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] @@ -158,13 +249,13 @@ class TestStatistic(TestCase): 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], + [[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) @@ -172,13 +263,39 @@ class TestStatistic(TestCase): 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], + [[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_median_stat_length(self): + a = np.arange(100).astype('f') + a[1] = 2. + a[97] = 96. + a = pad(a, (25, 20), 'median', stat_length=(3, 5)) + 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., + + 0., 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., 96., 98., 99., + + 96., 96., 96., 96., 96., 96., 96., 96., 96., 96., + 96., 96., 96., 96., 96., 96., 96., 96., 96., 96.] ) assert_array_equal(a, b) @@ -186,21 +303,21 @@ class TestStatistic(TestCase): a = [[4, 5, 6]] a = pad(a, (5, 7), 'mean', stat_length=2) b = np.array( - [[4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [[4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [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) @@ -212,16 +329,16 @@ class TestStatistic(TestCase): 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] @@ -238,7 +355,7 @@ class TestConstant(TestCase): 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, + 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, @@ -254,15 +371,15 @@ class TestConstant(TestCase): ) assert_array_equal(a, b) - def test_check_constant_float(self): + def test_check_constant_zeros(self): a = np.arange(100) - a = pad(a, (25, 20), 'constant', constant_values=-1.1) + a = pad(a, (25, 20), 'constant') 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, 0, 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, + 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, @@ -273,12 +390,53 @@ class TestConstant(TestCase): 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] + 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_constant_float(self): + # If input array is int, but constant_values are float, the dtype of + # the array to be padded is kept + arr = np.arange(30).reshape(5, 6) + test = pad(arr, (1, 2), mode='constant', + constant_values=1.1) + expected = np.array( + [[ 1, 1, 1, 1, 1, 1, 1, 1, 1], + + [ 1, 0, 1, 2, 3, 4, 5, 1, 1], + [ 1, 6, 7, 8, 9, 10, 11, 1, 1], + [ 1, 12, 13, 14, 15, 16, 17, 1, 1], + [ 1, 18, 19, 20, 21, 22, 23, 1, 1], + [ 1, 24, 25, 26, 27, 28, 29, 1, 1], + + [ 1, 1, 1, 1, 1, 1, 1, 1, 1], + [ 1, 1, 1, 1, 1, 1, 1, 1, 1]] + ) + assert_allclose(test, expected) + + def test_check_constant_float2(self): + # If input array is float, and constant_values are float, the dtype of + # the array to be padded is kept - here retaining the float constants + arr = np.arange(30).reshape(5, 6) + arr_float = arr.astype(np.float64) + test = pad(arr_float, ((1, 2), (1, 2)), mode='constant', + constant_values=1.1) + expected = np.array( + [[ 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. , 1.1, 1.1], + [ 1.1, 6. , 7. , 8. , 9. , 10. , 11. , 1.1, 1.1], + [ 1.1, 12. , 13. , 14. , 15. , 16. , 17. , 1.1, 1.1], + [ 1.1, 18. , 19. , 20. , 21. , 22. , 23. , 1.1, 1.1], + [ 1.1, 24. , 25. , 26. , 27. , 28. , 29. , 1.1, 1.1], + + [ 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 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_allclose(test, expected) + + def test_check_constant_float3(self): a = np.arange(100, dtype=float) a = pad(a, (25, 20), 'constant', constant_values=(-1.1, -1.2)) b = np.array( @@ -300,7 +458,24 @@ class TestConstant(TestCase): -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) + assert_allclose(a, b) + + def test_check_constant_odd_pad_amount(self): + arr = np.arange(30).reshape(5, 6) + test = pad(arr, ((1,), (2,)), mode='constant', + constant_values=3) + expected = np.array( + [[ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + + [ 3, 3, 0, 1, 2, 3, 4, 5, 3, 3], + [ 3, 3, 6, 7, 8, 9, 10, 11, 3, 3], + [ 3, 3, 12, 13, 14, 15, 16, 17, 3, 3], + [ 3, 3, 18, 19, 20, 21, 22, 23, 3, 3], + [ 3, 3, 24, 25, 26, 27, 28, 29, 3, 3], + + [ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]] + ) + assert_allclose(test, expected) class TestLinearRamp(TestCase): @@ -324,9 +499,23 @@ class TestLinearRamp(TestCase): 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.] + 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) + assert_allclose(a, b, rtol=1e-5, atol=1e-5) + + def test_check_2d(self): + arr = np.arange(20).reshape(4, 5).astype(np.float64) + test = pad(arr, (2, 2), mode='linear_ramp', end_values=(0, 0)) + expected = np.array( + [[0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0.5, 1., 1.5, 2., 1., 0.], + [0., 0., 0., 1., 2., 3., 4., 2., 0.], + [0., 2.5, 5., 6., 7., 8., 9., 4.5, 0.], + [0., 5., 10., 11., 12., 13., 14., 7., 0.], + [0., 7.5, 15., 16., 17., 18., 19., 9.5, 0.], + [0., 3.75, 7.5, 8., 8.5, 9., 9.5, 4.75, 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0.]]) + assert_allclose(test, expected) class TestReflect(TestCase): @@ -335,10 +524,10 @@ class TestReflect(TestCase): 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, + 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, + 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, @@ -354,26 +543,50 @@ class TestReflect(TestCase): ) assert_array_equal(a, b) + def test_check_odd_method(self): + a = np.arange(100) + a = pad(a, (25, 20), 'reflect', reflect_type='odd') + 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, + + 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, + 110, 111, 112, 113, 114, 115, 116, 117, 118, 119] + ) + 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], + [[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) @@ -381,21 +594,21 @@ class TestReflect(TestCase): a = [[4, 5, 6]] a = pad(a, (5, 7), 'reflect') b = np.array( - [[5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [[5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [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) @@ -415,16 +628,16 @@ class TestReflect(TestCase): assert_array_equal(a, b) -class TestWrap(TestCase): +class TestSymmetric(TestCase): def test_check_simple(self): a = np.arange(100) - a = pad(a, (25, 20), 'wrap') + a = pad(a, (25, 20), 'symmetric') 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, + [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, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 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, @@ -435,7 +648,141 @@ class TestWrap(TestCase): 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, + 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, + 89, 88, 87, 86, 85, 84, 83, 82, 81, 80] + ) + assert_array_equal(a, b) + + def test_check_odd_method(self): + a = np.arange(100) + a = pad(a, (25, 20), 'symmetric', reflect_type='odd') + b = np.array( + [-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, + + 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, 100, 101, 102, 103, 104, 105, 106, 107, 108, + 109, 110, 111, 112, 113, 114, 115, 116, 117, 118] + ) + assert_array_equal(a, b) + + def test_check_large_pad(self): + a = [[4, 5, 6], [6, 7, 8]] + a = pad(a, (5, 7), 'symmetric') + b = np.array( + [[5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [7, 8, 8, 7, 6, 6, 7, 8, 8, 7, 6, 6, 7, 8, 8], + [7, 8, 8, 7, 6, 6, 7, 8, 8, 7, 6, 6, 7, 8, 8], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [7, 8, 8, 7, 6, 6, 7, 8, 8, 7, 6, 6, 7, 8, 8], + + [7, 8, 8, 7, 6, 6, 7, 8, 8, 7, 6, 6, 7, 8, 8], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [7, 8, 8, 7, 6, 6, 7, 8, 8, 7, 6, 6, 7, 8, 8], + [7, 8, 8, 7, 6, 6, 7, 8, 8, 7, 6, 6, 7, 8, 8], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6]] + ) + + assert_array_equal(a, b) + + def test_check_large_pad_odd(self): + a = [[4, 5, 6], [6, 7, 8]] + a = pad(a, (5, 7), 'symmetric', reflect_type='odd') + b = np.array( + [[-3, -2, -2, -1, 0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6], + [-3, -2, -2, -1, 0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6], + [-1, 0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8], + [-1, 0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8], + [ 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9, 10, 10], + + [ 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9, 10, 10], + [ 3, 4, 4, 5, 6, 6, 7, 8, 8, 9, 10, 10, 11, 12, 12], + + [ 3, 4, 4, 5, 6, 6, 7, 8, 8, 9, 10, 10, 11, 12, 12], + [ 5, 6, 6, 7, 8, 8, 9, 10, 10, 11, 12, 12, 13, 14, 14], + [ 5, 6, 6, 7, 8, 8, 9, 10, 10, 11, 12, 12, 13, 14, 14], + [ 7, 8, 8, 9, 10, 10, 11, 12, 12, 13, 14, 14, 15, 16, 16], + [ 7, 8, 8, 9, 10, 10, 11, 12, 12, 13, 14, 14, 15, 16, 16], + [ 9, 10, 10, 11, 12, 12, 13, 14, 14, 15, 16, 16, 17, 18, 18], + [ 9, 10, 10, 11, 12, 12, 13, 14, 14, 15, 16, 16, 17, 18, 18]] + ) + assert_array_equal(a, b) + + def test_check_shape(self): + a = [[4, 5, 6]] + a = pad(a, (5, 7), 'symmetric') + b = np.array( + [[5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6], + [5, 6, 6, 5, 4, 4, 5, 6, 6, 5, 4, 4, 5, 6, 6]] + ) + assert_array_equal(a, b) + + def test_check_01(self): + a = pad([1, 2, 3], 2, 'symmetric') + b = np.array([2, 1, 1, 2, 3, 3, 2]) + assert_array_equal(a, b) + + def test_check_02(self): + a = pad([1, 2, 3], 3, 'symmetric') + b = np.array([3, 2, 1, 1, 2, 3, 3, 2, 1]) + assert_array_equal(a, b) + + def test_check_03(self): + a = pad([1, 2, 3], 6, 'symmetric') + b = np.array([1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1, 1, 2, 3]) + assert_array_equal(a, b) + + +class TestWrap(TestCase): + def test_check_simple(self): + a = np.arange(100) + a = pad(a, (25, 20), 'wrap') + b = np.array( + [75, 76, 77, 78, 79, 80, 81, 82, 83, 84, + 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, + 95, 96, 97, 98, 99, + + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] ) assert_array_equal(a, b) @@ -445,58 +792,58 @@ class TestWrap(TestCase): 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], + [[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) @@ -517,19 +864,19 @@ class TestStatLen(TestCase): 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], + [[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) @@ -540,17 +887,17 @@ class TestEdge(TestCase): a = np.reshape(a, (4, 3)) a = pad(a, ((2, 3), (3, 2)), 'edge') b = np.array( - [[0, 0, 0, 0, 1, 2, 2, 2], - [0, 0, 0, 0, 1, 2, 2, 2], + [[0, 0, 0, 0, 1, 2, 2, 2], + [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) @@ -563,6 +910,28 @@ class TestZeroPadWidth(TestCase): assert_array_equal(arr, pad(arr, pad_width, mode='constant')) +class TestLegacyVectorFunction(TestCase): + def test_legacy_vector_functionality(self): + def _padwithtens(vector, pad_width, iaxis, kwargs): + vector[:pad_width[0]] = 10 + vector[-pad_width[1]:] = 10 + return vector + + a = np.arange(6).reshape(2, 3) + a = pad(a, 2, _padwithtens) + b = np.array( + [[10, 10, 10, 10, 10, 10, 10], + [10, 10, 10, 10, 10, 10, 10], + + [10, 10, 0, 1, 2, 10, 10], + [10, 10, 3, 4, 5, 10, 10], + + [10, 10, 10, 10, 10, 10, 10], + [10, 10, 10, 10, 10, 10, 10]] + ) + assert_array_equal(a, b) + + class TestNdarrayPadWidth(TestCase): def test_check_simple(self): a = np.arange(12) @@ -608,16 +977,7 @@ class ValueError1(TestCase): 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) - - -class ValueError3(TestCase): - def test_check_simple(self): + def test_check_negative_pad_amount(self): arr = np.arange(30) arr = np.reshape(arr, (6, 5)) kwargs = dict(mode='mean', stat_length=(3, )) @@ -625,5 +985,107 @@ class ValueError3(TestCase): **kwargs) +class ValueError3(TestCase): + def test_check_kwarg_not_allowed(self): + arr = np.arange(30).reshape(5, 6) + assert_raises(ValueError, pad, arr, 4, mode='mean', + reflect_type='odd') + + def test_mode_not_set(self): + arr = np.arange(30).reshape(5, 6) + assert_raises(ValueError, pad, arr, 4) + + def test_malformed_pad_amount(self): + arr = np.arange(30).reshape(5, 6) + assert_raises(ValueError, pad, arr, (4, 5, 6, 7), mode='constant') + + def test_malformed_pad_amount2(self): + arr = np.arange(30).reshape(5, 6) + assert_raises(ValueError, pad, arr, ((3, 4, 5), (0, 1, 2)), + mode='constant') + + def test_pad_too_many_axes(self): + arr = np.arange(30).reshape(5, 6) + + # Attempt to pad using a 3D array equivalent + bad_shape = (((3,), (4,), (5,)), ((0,), (1,), (2,))) + assert_raises(ValueError, pad, arr, bad_shape, + mode='constant') + + +class TypeError1(TestCase): + def test_float(self): + arr = np.arange(30) + assert_raises(TypeError, pad, arr, ((-2.1, 3), (3, 2))) + assert_raises(TypeError, pad, arr, np.array(((-2.1, 3), (3, 2)))) + + def test_str(self): + arr = np.arange(30) + assert_raises(TypeError, pad, arr, 'foo') + assert_raises(TypeError, pad, arr, np.array('foo')) + + def test_object(self): + class FooBar(object): + pass + arr = np.arange(30) + assert_raises(TypeError, pad, arr, FooBar()) + + def test_complex(self): + arr = np.arange(30) + assert_raises(TypeError, pad, arr, complex(1, -1)) + assert_raises(TypeError, pad, arr, np.array(complex(1, -1))) + + def test_check_wrong_pad_amount(self): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + kwargs = dict(mode='mean', stat_length=(3, )) + assert_raises(TypeError, pad, arr, ((2, 3, 4), (3, 2)), + **kwargs) + + +def test_multi_crop(): + arr = np.arange(45).reshape(9, 5) + out = crop(arr, ((1, 2), (2, 1))) + assert_array_equal(out[0], [7, 8]) + assert_array_equal(out[-1], [32, 33]) + assert_equal(out.shape, (6, 2)) + + +def test_pair_crop(): + arr = np.arange(45).reshape(9, 5) + out = crop(arr, (1, 2)) + assert_array_equal(out[0], [6, 7]) + assert_array_equal(out[-1], [31, 32]) + assert_equal(out.shape, (6, 2)) + + +def test_int_crop(): + arr = np.arange(45).reshape(9, 5) + out = crop(arr, 1) + assert_array_equal(out[0], [6, 7, 8]) + assert_array_equal(out[-1], [36, 37, 38]) + assert_equal(out.shape, (7, 3)) + + +def test_copy_crop(): + arr = np.arange(45).reshape(9, 5) + out0 = crop(arr, 1, copy=True) + assert out0.flags.c_contiguous + out0[0, 0] = 100 + assert not np.any(arr == 100) + assert not np.may_share_memory(arr, out0) + + out1 = crop(arr, 1) + out1[0, 0] = 100 + assert arr[1, 1] == 100 + assert np.may_share_memory(arr, out1) + + +def test_zero_crop(): + arr = np.arange(45).reshape(9, 5) + out = crop(arr, 0) + assert out.shape == (9, 5) + + if __name__ == "__main__": - run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/util/tests/test_dtype.py b/skimage/util/tests/test_dtype.py index ae26cd27..612c43e6 100644 --- a/skimage/util/tests/test_dtype.py +++ b/skimage/util/tests/test_dtype.py @@ -3,6 +3,7 @@ from numpy.testing import assert_equal, assert_raises from skimage import img_as_int, img_as_float, \ img_as_uint, img_as_ubyte from skimage.util.dtype import convert +from skimage._shared._warnings import expected_warnings dtype_range = {np.uint8: (0, 255), @@ -28,7 +29,9 @@ def test_range(): (img_as_float, np.float64), (img_as_uint, np.uint16), (img_as_ubyte, np.ubyte)]: - y = f(x) + + with expected_warnings(['precision loss|sign loss|\A\Z']): + y = f(x) omin, omax = dtype_range[dt] @@ -59,7 +62,10 @@ def test_range_extra_dtypes(): for dtype_in, dt in dtype_pairs: imin, imax = dtype_range_extra[dtype_in] x = np.linspace(imin, imax, 10).astype(dtype_in) - y = convert(x, dt) + + with expected_warnings(['precision loss|sign loss|\A\Z']): + y = convert(x, dt) + omin, omax = dtype_range_extra[dt] yield (_verify_range, "From %s to %s" % (np.dtype(dtype_in), np.dtype(dt)), diff --git a/skimage/util/tests/test_shape.py b/skimage/util/tests/test_shape.py index ee897e7c..38127df8 100644 --- a/skimage/util/tests/test_shape.py +++ b/skimage/util/tests/test_shape.py @@ -3,7 +3,7 @@ from nose.tools import raises 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 +from skimage._shared._warnings import expected_warnings @raises(TypeError) @@ -153,9 +153,8 @@ 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)) + assert_warns(RuntimeWarning, view_as_blocks, A, (2, 2)) + assert_warns(RuntimeWarning, view_as_windows, A, (2, 2)) if __name__ == '__main__': diff --git a/skimage/viewer/__init__.py b/skimage/viewer/__init__.py index 5eed9589..443e5659 100644 --- a/skimage/viewer/__init__.py +++ b/skimage/viewer/__init__.py @@ -1 +1,6 @@ +import warnings from .viewers import ImageViewer, CollectionViewer +from .qt import has_qt + +if not has_qt: + warnings.warn('Viewer requires Qt') diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py index 6fcda9c4..4b1d0c00 100644 --- a/skimage/viewer/canvastools/base.py +++ b/skimage/viewer/canvastools/base.py @@ -1,10 +1,5 @@ import numpy as np - -try: - from matplotlib import lines -except ImportError: - print("Could not import matplotlib -- skimage.viewer not available.") - +from matplotlib import lines __all__ = ['CanvasToolBase', 'ToolHandles'] @@ -18,8 +13,8 @@ class CanvasToolBase(object): Parameters ---------- - ax : :class:`matplotlib.axes.Axes` - Matplotlib axes where tool is displayed. + viewer : :class:`skimage.viewer.Viewer` + Skimage viewer object. on_move : function Function called whenever a control handle is moved. This function must accept the end points of line as the only argument. @@ -27,43 +22,20 @@ class CanvasToolBase(object): Function called whenever the control handle is released. on_enter : function Function called whenever the "enter" key is pressed. - useblit : bool - If True, update canvas by blitting, which is much faster than normal - redrawing (turn off for debugging purposes). """ - def __init__(self, ax, on_move=None, on_enter=None, on_release=None, - useblit=True): - self.ax = ax - self.canvas = ax.figure.canvas - self.img_background = None - self.cids = [] - self._artists = [] - self.active = True - if useblit: - self.connect_event('draw_event', self._blit_on_draw_event) - self.useblit = useblit + def __init__(self, viewer, on_move=None, on_enter=None, on_release=None, + useblit=True): + self.viewer = viewer + self.ax = viewer.ax + self.artists = [] + self.active = True + viewer.add_tool(self) self.callback_on_move = _pass if on_move is None else on_move self.callback_on_enter = _pass if on_enter is None else on_enter self.callback_on_release = _pass if on_release is None else on_release - self.connect_event('key_press_event', self._on_key_press) - - def connect_event(self, event, callback): - """Connect callback with an event. - - This should be used in lieu of `figure.canvas.mpl_connect` since this - function stores call back ids for later clean up. - """ - cid = self.canvas.mpl_connect(event, callback) - self.cids.append(cid) - - def disconnect_events(self): - """Disconnect all events created by this widget.""" - for c in self.cids: - self.canvas.mpl_disconnect(c) - def ignore(self, event): """Return True if event should be ignored. @@ -72,50 +44,41 @@ class CanvasToolBase(object): """ return not self.active - def set_visible(self, val): - for artist in self._artists: - artist.set_visible(val) - - def _blit_on_draw_event(self, event=None): - self.img_background = self.canvas.copy_from_bbox(self.ax.bbox) - self._draw_artists() - - def _draw_artists(self): - for artist in self._artists: - self.ax.draw_artist(artist) - - def remove(self): - """Remove artists and events from axes. - - Note that the naming here mimics the interface of Matplotlib artists. - """ - #TODO: For some reason, RectangleTool doesn't get properly removed - self.disconnect_events() - for a in self._artists: - a.remove() + def hit_test(self, event): + return False def redraw(self): - """Redraw image and canvas artists. + self.viewer.redraw() - This method should be called by subclasses when artists are updated. - """ - if self.useblit and self.img_background is not None: - self.canvas.restore_region(self.img_background) - self._draw_artists() - self.canvas.blit(self.ax.bbox) - else: - self.canvas.draw_idle() + def set_visible(self, val): + for artist in self.artists: + artist.set_visible(val) - def _on_key_press(self, event): + def on_key_press(self, event): if event.key == 'enter': self.callback_on_enter(self.geometry) self.set_visible(False) - self.redraw() + self.viewer.redraw() + + def on_mouse_press(self, event): + pass + + def on_mouse_release(self, event): + pass + + def on_move(self, event): + pass + + def on_scroll(self, event): + pass + + def remove(self): + self.viewer.remove_tool(self) @property def geometry(self): """Geometry information that gets passed to callback functions.""" - raise NotImplementedError + return None class ToolHandles(object): @@ -163,9 +126,6 @@ class ToolHandles(object): def set_animated(self, val): self._markers.set_animated(val) - def draw(self): - self.ax.draw_artist(self._markers) - def closest(self, x, y): """Return index and pixel distance to closest index.""" pts = np.transpose((self.x, self.y)) diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index c32d0ea7..261d5a7e 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -1,11 +1,6 @@ import numpy as np - -try: - from matplotlib import lines -except ImportError: - print("Could not import matplotlib -- skimage.viewer not available.") - -from skimage.viewer.canvastools.base import CanvasToolBase, ToolHandles +from matplotlib import lines +from ...viewer.canvastools.base import CanvasToolBase, ToolHandles __all__ = ['LineTool', 'ThickLineTool'] @@ -16,8 +11,8 @@ class LineTool(CanvasToolBase): Parameters ---------- - ax : :class:`matplotlib.axes.Axes` - Matplotlib axes where tool is displayed. + viewer : :class:`skimage.viewer.Viewer` + Skimage viewer object. on_move : function Function called whenever a control handle is moved. This function must accept the end points of line as the only argument. @@ -29,16 +24,21 @@ class LineTool(CanvasToolBase): Maximum pixel distance allowed when selecting control handle. line_props : dict Properties for :class:`matplotlib.lines.Line2D`. + handle_props : dict + Marker properties for the handles (also see + :class:`matplotlib.lines.Line2D`). Attributes ---------- end_points : 2D array End points of line ((x1, y1), (x2, y2)). """ - def __init__(self, ax, on_move=None, on_release=None, on_enter=None, - maxdist=10, line_props=None): - super(LineTool, self).__init__(ax, on_move=on_move, on_enter=on_enter, - on_release=on_release) + def __init__(self, viewer, on_move=None, on_release=None, on_enter=None, + maxdist=10, line_props=None, handle_props=None, + **kwargs): + super(LineTool, self).__init__(viewer, on_move=on_move, + on_enter=on_enter, + on_release=on_release, **kwargs) props = dict(color='r', linewidth=1, alpha=0.4, solid_capstyle='butt') props.update(line_props if line_props is not None else {}) @@ -51,25 +51,24 @@ class LineTool(CanvasToolBase): self._end_pts = np.transpose([x, y]) self._line = lines.Line2D(x, y, visible=False, animated=True, **props) - ax.add_line(self._line) + self.ax.add_line(self._line) - self._handles = ToolHandles(ax, x, y) + self._handles = ToolHandles(self.ax, x, y, + marker_props=handle_props) self._handles.set_visible(False) - self._artists = [self._line, self._handles.artist] + self.artists = [self._line, self._handles.artist] if on_enter is None: def on_enter(pts): x, y = np.transpose(pts) - print("length = %0.2f" % np.sqrt(np.diff(x)**2 + np.diff(y)**2)) + print("length = %0.2f" % + np.sqrt(np.diff(x)**2 + np.diff(y)**2)) self.callback_on_enter = on_enter - - self.connect_event('button_press_event', self.on_mouse_press) - self.connect_event('button_release_event', self.on_mouse_release) - self.connect_event('motion_notify_event', self.on_move) + viewer.add_tool(self) @property def end_points(self): - return self._end_pts + return self._end_pts.astype(int) @end_points.setter def end_points(self, pts): @@ -82,14 +81,20 @@ class LineTool(CanvasToolBase): self.set_visible(True) self.redraw() - def on_mouse_press(self, event): + def hit_test(self, event): if event.button != 1 or not self.ax.in_axes(event): - return - self.set_visible(True) + return False idx, px_dist = self._handles.closest(event.x, event.y) if px_dist < self.maxdist: self._active_pt = idx + return True else: + self._active_pt = None + return False + + def on_mouse_press(self, event): + self.set_visible(True) + if self._active_pt is None: self._active_pt = 0 x, y = event.xdata, event.ydata self._end_pts = np.array([[x, y], [x, y]]) @@ -99,6 +104,7 @@ class LineTool(CanvasToolBase): return self._active_pt = None self.callback_on_release(self.geometry) + self.redraw() def on_move(self, event): if event.button != 1 or self._active_pt is None: @@ -126,8 +132,8 @@ class ThickLineTool(LineTool): Parameters ---------- - ax : :class:`matplotlib.axes.Axes` - Matplotlib axes where tool is displayed. + viewer : :class:`skimage.viewer.Viewer` + Skimage viewer object. on_move : function Function called whenever a control handle is moved. This function must accept the end points of line as the only argument. @@ -141,6 +147,9 @@ class ThickLineTool(LineTool): Maximum pixel distance allowed when selecting control handle. line_props : dict Properties for :class:`matplotlib.lines.Line2D`. + handle_props : dict + Marker properties for the handles (also see + :class:`matplotlib.lines.Line2D`). Attributes ---------- @@ -148,23 +157,21 @@ class ThickLineTool(LineTool): End points of line ((x1, y1), (x2, y2)). """ - def __init__(self, ax, on_move=None, on_enter=None, on_release=None, - on_change=None, maxdist=10, line_props=None): - super(ThickLineTool, self).__init__(ax, + def __init__(self, viewer, on_move=None, on_enter=None, on_release=None, + on_change=None, maxdist=10, line_props=None, handle_props=None): + super(ThickLineTool, self).__init__(viewer, on_move=on_move, on_enter=on_enter, on_release=on_release, maxdist=maxdist, - line_props=line_props) + line_props=line_props, + handle_props=handle_props) if on_change is None: def on_change(*args): pass self.callback_on_change = on_change - self.connect_event('scroll_event', self.on_scroll) - self.connect_event('key_press_event', self.on_key_press) - def on_scroll(self, event): if not event.inaxes: return @@ -191,17 +198,15 @@ class ThickLineTool(LineTool): self.callback_on_change(self.geometry) -if __name__ == '__main__': - import matplotlib.pyplot as plt - from skimage import data +if __name__ == '__main__': # pragma: no cover + from ... import data + from ...viewer import ImageViewer image = data.camera() - f, ax = plt.subplots() - ax.imshow(image, interpolation='nearest') + viewer = ImageViewer(image) h, w = image.shape - # line_tool = LineTool(ax) - line_tool = ThickLineTool(ax) + line_tool = ThickLineTool(viewer) line_tool.end_points = ([w/3, h/2], [2*w/3, h/2]) - plt.show() + viewer.show() diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index cc5f9705..e8ce8281 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -2,9 +2,8 @@ import numpy as np import matplotlib.pyplot as plt import matplotlib.colors as mcolors LABELS_CMAP = mcolors.ListedColormap(['white', 'red', 'dodgerblue', 'gold', - 'greenyellow', 'blueviolet']) - -from skimage.viewer.canvastools.base import CanvasToolBase + 'greenyellow', 'blueviolet']) +from ...viewer.canvastools.base import CanvasToolBase __all__ = ['PaintTool'] @@ -15,8 +14,8 @@ class PaintTool(CanvasToolBase): Parameters ---------- - ax : :class:`matplotlib.axes.Axes` - Matplotlib axes where tool is displayed. + viewer : :class:`skimage.viewer.Viewer` + Skimage viewer object. overlay_shape : shape tuple 2D shape tuple used to initialize overlay image. alpha : float (between [0, 1]) @@ -39,9 +38,11 @@ class PaintTool(CanvasToolBase): label : int Current paint color. """ - def __init__(self, ax, overlay_shape, radius=5, alpha=0.3, on_move=None, - on_release=None, on_enter=None, rect_props=None): - super(PaintTool, self).__init__(ax, on_move=on_move, on_enter=on_enter, + def __init__(self, viewer, overlay_shape, radius=5, alpha=0.3, + on_move=None, on_release=None, on_enter=None, + rect_props=None): + super(PaintTool, self).__init__(viewer, on_move=on_move, + on_enter=on_enter, on_release=on_release) props = dict(edgecolor='r', facecolor='0.7', alpha=0.5, animated=True) @@ -61,11 +62,8 @@ class PaintTool(CanvasToolBase): self.radius = radius # Note that the order is important: Redraw cursor *after* overlay - self._artists = [self._overlay_plot, self._cursor] - - self.connect_event('button_press_event', self.on_mouse_press) - self.connect_event('button_release_event', self.on_mouse_release) - self.connect_event('motion_notify_event', self.on_move) + self.artists = [self._overlay_plot, self._cursor] + viewer.add_tool(self) @property def label(self): @@ -102,7 +100,7 @@ class PaintTool(CanvasToolBase): self._overlay_plot = None elif self._overlay_plot is None: props = dict(cmap=self.cmap, alpha=self.alpha, - norm=mcolors.no_norm(), animated=True) + norm=mcolors.NoNorm(), animated=True) self._overlay_plot = self.ax.imshow(image, **props) else: self._overlay_plot.set_data(image) @@ -121,7 +119,7 @@ class PaintTool(CanvasToolBase): self.radius = self._radius self.overlay = np.zeros(shape, dtype='uint8') - def _on_key_press(self, event): + def on_key_press(self, event): if event.key == 'enter': self.callback_on_enter(self.geometry) self.redraw() @@ -138,7 +136,7 @@ class PaintTool(CanvasToolBase): self.callback_on_release(self.geometry) def on_move(self, event): - if not self.ax.in_axes(event): + if not self.viewer.ax.in_axes(event): self._cursor.set_visible(False) self.redraw() # make sure cursor is not visible return @@ -198,13 +196,13 @@ class CenteredWindow(object): return [slice(ymin, ymax), slice(xmin, xmax)] -if __name__ == '__main__': +if __name__ == '__main__': # pragma: no cover np.testing.rundocs() - from skimage import data + from ... import data + from ...viewer import ImageViewer image = data.camera() - f, ax = plt.subplots() - ax.imshow(image, interpolation='nearest') - paint_tool = PaintTool(ax, image.shape) - plt.show() + viewer = ImageViewer(image) + paint_tool = PaintTool(viewer, image.shape) + viewer.show() diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index d4ae113e..8a895adf 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -1,11 +1,6 @@ -try: - from matplotlib.widgets import RectangleSelector -except ImportError: - RectangleSelector = object - print("Could not import matplotlib -- skimage.viewer not available.") - -from skimage.viewer.canvastools.base import CanvasToolBase -from skimage.viewer.canvastools.base import ToolHandles +from matplotlib.widgets import RectangleSelector +from ...viewer.canvastools.base import CanvasToolBase +from ...viewer.canvastools.base import ToolHandles __all__ = ['RectangleTool'] @@ -19,8 +14,8 @@ class RectangleTool(CanvasToolBase, RectangleSelector): Parameters ---------- - ax : :class:`matplotlib.axes.Axes` - Matplotlib axes where tool is displayed. + viewer : :class:`skimage.viewer.Viewer` + Skimage viewer object. on_move : function Function called whenever a control handle is moved. This function must accept the rectangle extents as the only argument. @@ -38,20 +33,54 @@ class RectangleTool(CanvasToolBase, RectangleSelector): ---------- extents : tuple Rectangle extents: (xmin, xmax, ymin, ymax). + + Examples + ---------- + >>> from skimage import data + >>> from skimage.viewer import ImageViewer + >>> from skimage.viewer.canvastools import RectangleTool + >>> from skimage.draw import line + >>> from skimage.draw import set_color + + >>> viewer = ImageViewer(data.coffee()) # doctest: +SKIP + + >>> def print_the_rect(extents): + ... global viewer + ... im = viewer.image + ... coord = np.int64(extents) + ... [rr1, cc1] = line(coord[2],coord[0],coord[2],coord[1]) + ... [rr2, cc2] = line(coord[2],coord[1],coord[3],coord[1]) + ... [rr3, cc3] = line(coord[3],coord[1],coord[3],coord[0]) + ... [rr4, cc4] = line(coord[3],coord[0],coord[2],coord[0]) + ... set_color(im, (rr1, cc1), [255, 255, 0]) + ... set_color(im, (rr2, cc2), [0, 255, 255]) + ... set_color(im, (rr3, cc3), [255, 0, 255]) + ... set_color(im, (rr4, cc4), [0, 0, 0]) + ... viewer.image=im + + >>> rect_tool = RectangleTool(viewer.ax, on_enter=print_the_rect) # doctest: +SKIP + >>> viewer.show() # doctest: +SKIP """ - def __init__(self, ax, on_move=None, on_release=None, on_enter=None, + def __init__(self, viewer, on_move=None, on_release=None, on_enter=None, maxdist=10, rect_props=None): - CanvasToolBase.__init__(self, ax, on_move=on_move, - on_enter=on_enter, on_release=on_release) - + self._rect = None props = dict(edgecolor=None, facecolor='r', alpha=0.15) props.update(rect_props if rect_props is not None else {}) if props['edgecolor'] is None: props['edgecolor'] = props['facecolor'] - RectangleSelector.__init__(self, ax, lambda *args: None, - rectprops=props, - useblit=self.useblit) + RectangleSelector.__init__(self, viewer.ax, lambda *args: None, + rectprops=props) + CanvasToolBase.__init__(self, viewer, on_move=on_move, + on_enter=on_enter, on_release=on_release) + + # Events are handled by the viewer + try: + self.disconnect_events() + except AttributeError: + # disconnect the events manually (hack for older mpl versions) + [self.canvas.mpl_disconnect(i) for i in range(10)] + # Alias rectangle attribute, which is initialized in RectangleSelector. self._rect = self.to_draw self._rect.set_animated(True) @@ -68,19 +97,22 @@ class RectangleTool(CanvasToolBase, RectangleSelector): props = dict(mec=props['edgecolor']) self._corner_order = ['NW', 'NE', 'SE', 'SW'] xc, yc = self.corners - self._corner_handles = ToolHandles(ax, xc, yc, marker_props=props) + self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=props) self._edge_order = ['W', 'N', 'E', 'S'] xe, ye = self.edge_centers - self._edge_handles = ToolHandles(ax, xe, ye, marker='s', + self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', marker_props=props) - self._artists = [self._rect, - self._corner_handles.artist, - self._edge_handles.artist] + self.artists = [self._rect, + self._corner_handles.artist, + self._edge_handles.artist] + viewer.add_tool(self) @property def _rect_bbox(self): + if not self._rect: + return 0, 0, 0, 0 x0 = self._rect.get_x() y0 = self._rect.get_y() width = self._rect.get_width() @@ -130,7 +162,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): self.set_visible(True) self.redraw() - def release(self, event): + def on_mouse_release(self, event): if event.button != 1: return if not self.ax.in_axes(event): @@ -143,7 +175,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): self.redraw() self.callback_on_release(self.geometry) - def press(self, event): + def on_mouse_press(self, event): if event.button != 1 or not self.ax.in_axes(event): return self._set_active_handle(event) @@ -178,7 +210,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): y1, y2 = y2, event.ydata self._extents_on_press = x1, x2, y1, y2 - def onmove(self, event): + def on_move(self, event): if self.eventpress is None or not self.ax.in_axes(event): return @@ -201,14 +233,13 @@ class RectangleTool(CanvasToolBase, RectangleSelector): return self.extents -if __name__ == '__main__': - import matplotlib.pyplot as plt - from skimage import data +if __name__ == '__main__': # pragma: no cover + from ...viewer import ImageViewer + from ... import data - f, ax = plt.subplots() - ax.imshow(data.camera(), interpolation='nearest') + viewer = ImageViewer(data.camera()) - rect_tool = RectangleTool(ax) - plt.show() + rect_tool = RectangleTool(viewer) + viewer.show() print("Final selection:") rect_tool.callback_on_enter(rect_tool.extents) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index baded1cb..7b0418b2 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -4,19 +4,11 @@ Base class for Plugins that interact with ImageViewer. from warnings import warn import numpy as np - -from ..qt import QtGui, qt_api -from ..qt.QtCore import Qt, Signal +from ..qt import QtWidgets, QtCore, 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): +class Plugin(QtWidgets.QDialog): """Base class for plugins that interact with an ImageViewer. A plugin connects an image filter (or another function) to an image viewer. @@ -101,7 +93,7 @@ class Plugin(QtGui.QDialog): "then the `image_filter` argument is ignored.") self.setWindowTitle(self.name) - self.layout = QtGui.QGridLayout(self) + self.layout = QtWidgets.QGridLayout(self) self.resize(width, height) self.row = 0 @@ -124,7 +116,7 @@ class Plugin(QtGui.QDialog): the image matches the filtered value specified by attached widgets. """ self.setParent(image_viewer) - self.setWindowFlags(Qt.Dialog) + self.setWindowFlags(QtCore.Qt.Dialog) self.image_viewer = image_viewer self.image_viewer.plugins.append(self) @@ -142,7 +134,7 @@ class Plugin(QtGui.QDialog): plugin += Widget(...) Widgets can adjust required or optional arguments of filter function or - parameters for the plugin. This is specified by the Widget's `ptype'. + parameters for the plugin. This is specified by the Widget's `ptype`. """ if widget.ptype == 'kwarg': name = widget.name.replace(' ', '_') @@ -239,7 +231,8 @@ class Plugin(QtGui.QDialog): def clean_up(self): self.remove_image_artists() - self.image_viewer.plugins.remove(self) + if self in self.image_viewer.plugins: + self.image_viewer.plugins.remove(self) self.image_viewer.reset_image() self.image_viewer.redraw() diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index c2294ba8..84313d02 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,7 +1,7 @@ import numpy as np import skimage -from skimage.filter import canny +from ...feature import canny from .overlayplugin import OverlayPlugin from ..widgets import Slider, ComboBox diff --git a/skimage/viewer/plugins/color_histogram.py b/skimage/viewer/plugins/color_histogram.py index dd54a93d..09415a83 100644 --- a/skimage/viewer/plugins/color_histogram.py +++ b/skimage/viewer/plugins/color_histogram.py @@ -1,8 +1,6 @@ import numpy as np import matplotlib.pyplot as plt - -from skimage import color -from skimage import exposure +from ... import color, exposure from .plotplugin import PlotPlugin from ..canvastools import RectangleTool @@ -19,7 +17,8 @@ class ColorHistogram(PlotPlugin): def attach(self, image_viewer): super(ColorHistogram, self).attach(image_viewer) - self.rect_tool = RectangleTool(self.ax, on_release=self.ab_selected) + self.rect_tool = RectangleTool(image_viewer, + on_release=self.ab_selected) self._on_new_image(image_viewer.image) def _on_new_image(self, image): @@ -70,14 +69,16 @@ class ColorHistogram(PlotPlugin): 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. + The dictionary contains: + + - '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) @@ -90,4 +91,3 @@ def pct_total_area(image, percentile=0.80): idx = int((image.size - 1) * percentile) sorted_pixels = np.sort(image.flat) return sorted_pixels[idx] - diff --git a/skimage/viewer/plugins/crop.py b/skimage/viewer/plugins/crop.py index 61f61034..04a85321 100644 --- a/skimage/viewer/plugins/crop.py +++ b/skimage/viewer/plugins/crop.py @@ -1,6 +1,6 @@ from .base import Plugin from ..canvastools import RectangleTool -from skimage.viewer.widgets import SaveButtons +from ...viewer.widgets import SaveButtons __all__ = ['Crop'] @@ -18,7 +18,7 @@ class Crop(Plugin): def attach(self, image_viewer): super(Crop, self).attach(image_viewer) - self.rect_tool = RectangleTool(self.image_viewer.ax, + self.rect_tool = RectangleTool(image_viewer, maxdist=self.maxdist, on_enter=self.crop) self.artists.append(self.rect_tool) diff --git a/skimage/viewer/plugins/labelplugin.py b/skimage/viewer/plugins/labelplugin.py index 06ba2dd9..fa2b1940 100644 --- a/skimage/viewer/plugins/labelplugin.py +++ b/skimage/viewer/plugins/labelplugin.py @@ -37,7 +37,7 @@ class LabelPainter(Plugin): super(LabelPainter, self).attach(image_viewer) image = image_viewer.original_image - self.paint_tool = PaintTool(self.image_viewer.ax, image.shape, + self.paint_tool = PaintTool(image_viewer, image.shape, on_enter=self.on_enter) self.paint_tool.radius = self.radius self.paint_tool.label = self._label_widget.index = 1 diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index d0011c75..c46b878b 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -1,9 +1,8 @@ from __future__ import division import numpy as np -from skimage.util.dtype import dtype_range -from skimage import draw -from skimage import measure +from ...util.dtype import dtype_range +from ... import draw, measure from .plotplugin import PlotPlugin from ..canvastools import ThickLineTool @@ -46,7 +45,7 @@ class LineProfile(PlotPlugin): if self._limit_type == 'image': self.limits = (np.min(image), np.max(image)) elif self._limit_type == 'dtype': - self._limit_type = dtype_range[image.dtype.type] + self.limits = dtype_range[image.dtype.type] elif self._limit_type is None or len(self._limit_type) == 2: self.limits = self._limit_type else: @@ -59,13 +58,13 @@ class LineProfile(PlotPlugin): x = [w / 3, 2 * w / 3] y = [h / 2] * 2 - self.line_tool = ThickLineTool(self.image_viewer.ax, + self.line_tool = ThickLineTool(self.image_viewer, maxdist=self.maxdist, on_move=self.line_changed, on_change=self.line_changed) self.line_tool.end_points = np.transpose([x, y]) - scan_data = measure.profile_line(image, + scan_data = measure.profile_line(image, *self.line_tool.end_points[:, ::-1]) self.scan_data = scan_data if scan_data.ndim == 1: @@ -91,6 +90,7 @@ class LineProfile(PlotPlugin): profile: list of 1d arrays Profile of intensity values. Length 1 (grayscale) or 3 (rgb). """ + self._update_data() profiles = [data.get_ydata() for data in self.profile] return self.line_tool.end_points, profiles @@ -103,8 +103,15 @@ class LineProfile(PlotPlugin): def line_changed(self, end_points): x, y = np.transpose(end_points) self.line_tool.end_points = end_points - scan = measure.profile_line(self.image_viewer.original_image, - *end_points[:, ::-1], + self._update_data() + self.ax.relim() + + self._autoscale_view() + self.redraw() + + def _update_data(self): + scan = measure.profile_line(self.image_viewer.image, + *self.line_tool.end_points[:, ::-1], linewidth=self.line_tool.linewidth) self.scan_data = scan if scan.ndim == 1: @@ -117,11 +124,6 @@ class LineProfile(PlotPlugin): self.profile[i].set_xdata(np.arange(scan.shape[0])) self.profile[i].set_ydata(scan[:, i]) - self.ax.relim() - - self._autoscale_view() - self.redraw() - def reset_axes(self, scan_data): # Clear lines out for line in self.ax.lines: @@ -147,7 +149,7 @@ class LineProfile(PlotPlugin): 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], + line_image = np.zeros(self.image_viewer.image.shape[:2], np.uint8) width = self.line_tool.linewidth if width > 1: @@ -162,4 +164,3 @@ class LineProfile(PlotPlugin): rr, cc = draw.line(y1, x1, y2, x2) line_image[rr, cc] = 255 return line_image, self.scan_data - diff --git a/skimage/viewer/plugins/measure.py b/skimage/viewer/plugins/measure.py index eeaa13b0..7e29a796 100644 --- a/skimage/viewer/plugins/measure.py +++ b/skimage/viewer/plugins/measure.py @@ -32,7 +32,7 @@ class Measure(Plugin): image = image_viewer.original_image h, w = image.shape - self.line_tool = LineTool(self.image_viewer.ax, + self.line_tool = LineTool(self.image_viewer, maxdist=self.maxdist, on_move=self.line_changed) self.artists.append(self.line_tool) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 373d6bf4..eb94baa3 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -1,21 +1,16 @@ from warnings import warn -from skimage.util.dtype import dtype_range +from ...util.dtype import dtype_range from .base import Plugin from ..utils import ClearColormap, update_axes_image import six +from ..._shared.version_requirements import is_installed __all__ = ['OverlayPlugin'] -def recent_mpl_version(): - import matplotlib - version = matplotlib.__version__.split('.') - return int(version[0]) == 1 and int(version[1]) >= 2 - - class OverlayPlugin(Plugin): """Plugin for ImageViewer that displays an overlay on top of main image. @@ -38,14 +33,14 @@ class OverlayPlugin(Plugin): 'cyan': (0, 1, 1)} def __init__(self, **kwargs): - if not recent_mpl_version(): + if not is_installed('matplotlib', '>=1.2'): msg = "Matplotlib >= 1.2 required for OverlayPlugin." warn(RuntimeWarning(msg)) super(OverlayPlugin, self).__init__(**kwargs) self._overlay_plot = None self._overlay = None self.cmap = None - self.color_names = list(self.colors.keys()) + self.color_names = sorted(list(self.colors.keys())) def attach(self, image_viewer): super(OverlayPlugin, self).attach(image_viewer) @@ -70,6 +65,9 @@ class OverlayPlugin(Plugin): else: update_axes_image(self._overlay_plot, image) + if self.image_viewer.useblit: + self.image_viewer._blit_manager.background = None + self.image_viewer.redraw() @property diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index 5de6be7b..f995efa8 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -1,6 +1,6 @@ import numpy as np -from ..qt import QtGui +from ..qt import QtGui from ..utils import new_plot from .base import Plugin diff --git a/skimage/viewer/qt.py b/skimage/viewer/qt.py new file mode 100644 index 00000000..8f6768a5 --- /dev/null +++ b/skimage/viewer/qt.py @@ -0,0 +1,44 @@ +_qt_version = None +has_qt = True + +try: + from matplotlib.backends.qt_compat import QtGui, QtCore, QtWidgets, QT_RC_MAJOR_VERSION as _qt_version +except ImportError: + try: + from matplotlib.backends.qt4_compat import QtGui, QtCore + QtWidgets = QtGui + _qt_version = 4 + except ImportError: + # Mock objects + class QtGui_cls(object): + QMainWindow = object + QDialog = object + QWidget = object + + class QtCore_cls(object): + class Qt(object): + TopDockWidgetArea = None + BottomDockWidgetArea = None + LeftDockWidgetArea = None + RightDockWidgetArea = None + + def Signal(self, *args, **kwargs): + pass + + QtGui = QtWidgets = QtGui_cls() + QtCore = QtCore_cls() + + has_qt = False + +if _qt_version == 5: + from matplotlib.backends.backend_qt5 import FigureManagerQT + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg +elif _qt_version == 4: + from matplotlib.backends.backend_qt4 import FigureManagerQT + from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg +else: + FigureManagerQT = object + FigureCanvasQTAgg = object + +Qt = QtCore.Qt +Signal = QtCore.Signal diff --git a/skimage/viewer/qt/QtCore.py b/skimage/viewer/qt/QtCore.py deleted file mode 100644 index 19b92a53..00000000 --- a/skimage/viewer/qt/QtCore.py +++ /dev/null @@ -1,22 +0,0 @@ -from . import qt_api - -if qt_api == 'pyside': - from PySide.QtCore import * -elif qt_api == 'pyqt': - from PyQt4.QtCore import * - # Use pyside names for signals and slots - Signal = pyqtSignal - Slot = pyqtSlot -else: - # Mock objects for buildbot (which doesn't have Qt, but imports viewer). - class Qt(object): - TopDockWidgetArea = None - BottomDockWidgetArea = None - LeftDockWidgetArea = None - RightDockWidgetArea = None - - def Signal(*args, **kwargs): - pass - - def Slot(*args, **kwargs): - pass diff --git a/skimage/viewer/qt/QtGui.py b/skimage/viewer/qt/QtGui.py deleted file mode 100644 index 12e9837f..00000000 --- a/skimage/viewer/qt/QtGui.py +++ /dev/null @@ -1,11 +0,0 @@ -from . import qt_api - -if qt_api == 'pyside': - from PySide.QtGui import * -elif qt_api == 'pyqt': - from PyQt4.QtGui import * -else: - # Mock objects - QMainWindow = object - QDialog = object - QWidget = object diff --git a/skimage/viewer/qt/README.rst b/skimage/viewer/qt/README.rst deleted file mode 100644 index 993ae1a5..00000000 --- a/skimage/viewer/qt/README.rst +++ /dev/null @@ -1,5 +0,0 @@ -This qt subpackage provides a wrapper to allow use of either PySide or PyQt4. -In addition, if neither package is available, some mock objects are created to -prevent errors in the TravisCI build. Only the objects used in the global -namespace need to be mocked (e.g., a Qt object that gets subclassed is used -in the global namespace). diff --git a/skimage/viewer/qt/__init__.py b/skimage/viewer/qt/__init__.py deleted file mode 100644 index 8e7ab939..00000000 --- a/skimage/viewer/qt/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -import warnings - -qt_api = os.environ.get('QT_API') - -if qt_api is None: - try: - import PySide - qt_api = 'pyside' - except ImportError: - try: - import PyQt4 - qt_api = 'pyqt' - except ImportError: - qt_api = None - # Note that we don't want to raise an error because that would - # cause the TravisCI build to fail. - warnings.warn("Could not import PyQt4: ImageViewer not available!") - - -if qt_api is not None: - os.environ['QT_API'] = qt_api diff --git a/skimage/viewer/tests/__init__.py b/skimage/viewer/tests/__init__.py new file mode 100644 index 00000000..3df53221 --- /dev/null +++ b/skimage/viewer/tests/__init__.py @@ -0,0 +1,9 @@ +from skimage._shared.testing import setup_test, teardown_test + + +def setup(): + setup_test() + + +def teardown(): + teardown_test() diff --git a/skimage/viewer/tests/test_plugins.py b/skimage/viewer/tests/test_plugins.py new file mode 100644 index 00000000..8dcf7855 --- /dev/null +++ b/skimage/viewer/tests/test_plugins.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +import numpy as np +import skimage +import skimage.data as data +from skimage.filters.rank import median +from skimage.morphology import disk +from skimage.viewer import ImageViewer, has_qt +from skimage.viewer.plugins.base import Plugin +from skimage.viewer.widgets import Slider +from skimage.viewer.plugins import ( + LineProfile, Measure, CannyPlugin, LabelPainter, Crop, ColorHistogram, + PlotPlugin) +from numpy.testing import assert_equal, assert_allclose, assert_almost_equal +from numpy.testing.decorators import skipif +from skimage._shared._warnings import expected_warnings + + +def setup_line_profile(image, limits='image'): + viewer = ImageViewer(skimage.img_as_float(image)) + plugin = LineProfile(limits=limits) + viewer += plugin + return plugin + + +@skipif(not has_qt) +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.9176, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.2812, rtol=1e-3) + + +@skipif(not has_qt) +def test_line_profile_rgb(): + """ Test a line profile using an ndim=3 image""" + plugin = setup_line_profile(data.chelsea(), limits=None) + 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, 750) + assert_equal(line_image[line_image == 255].size, 151) + assert_equal(line_image.shape, (300, 451)) + assert_equal(scan_data.shape, (151, 3)) + assert_allclose(scan_data.max(), 0.772, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.4359, rtol=1e-3) + + +@skipif(not has_qt) +def test_line_profile_dynamic(): + """Test a line profile updating after an image transform""" + image = data.coins()[:-50, :] # shave some off to make the line lower + image = skimage.img_as_float(image) + viewer = ImageViewer(image) + + lp = LineProfile(limits='dtype') + viewer += lp + + line = lp.get_profiles()[-1][0] + assert line.size == 129 + assert_almost_equal(np.std(viewer.image), 0.208, 3) + assert_almost_equal(np.std(line), 0.229, 3) + assert_almost_equal(np.max(line) - np.min(line), 0.725, 1) + + with expected_warnings(['precision loss']): + viewer.image = skimage.img_as_float(median(image, + selem=disk(radius=3))) + + line = lp.get_profiles()[-1][0] + assert_almost_equal(np.std(viewer.image), 0.198, 3) + assert_almost_equal(np.std(line), 0.220, 3) + assert_almost_equal(np.max(line) - np.min(line), 0.639, 1) + + +@skipif(not has_qt) +def test_measure(): + image = data.camera() + viewer = ImageViewer(image) + m = Measure() + viewer += m + + m.line_changed([(0, 0), (10, 10)]) + assert_equal(str(m._length.text), '14.1') + assert_equal(str(m._angle.text[:5]), '135.0') + + +@skipif(not has_qt) +def test_canny(): + image = data.camera() + viewer = ImageViewer(image) + c = CannyPlugin() + viewer += c + + canny_edges = viewer.show(False) + viewer.close() + edges = canny_edges[0][0] + assert edges.sum() == 2852 + + +@skipif(not has_qt) +def test_label_painter(): + image = data.camera() + moon = data.moon() + viewer = ImageViewer(image) + lp = LabelPainter() + viewer += lp + + assert_equal(lp.radius, 5) + lp.label = 1 + assert_equal(str(lp.label), '1') + lp.label = 2 + assert_equal(str(lp.paint_tool.label), '2') + assert_equal(lp.paint_tool.radius, 5) + lp._on_new_image(moon) + assert_equal(lp.paint_tool.shape, moon.shape) + + +@skipif(not has_qt) +def test_crop(): + image = data.camera() + viewer = ImageViewer(image) + c = Crop() + viewer += c + + c.crop((0, 100, 0, 100)) + assert_equal(viewer.image.shape, (101, 101)) + + +@skipif(not has_qt) +def test_color_histogram(): + image = skimage.img_as_float(data.load('color.png')) + viewer = ImageViewer(image) + ch = ColorHistogram(dock='right') + viewer += ch + + assert_almost_equal(viewer.image.std(), 0.352, 3), + ch.ab_selected((0, 100, 0, 100)), + assert_almost_equal(viewer.image.std(), 0.325, 3) + + +@skipif(not has_qt) +def test_plot_plugin(): + viewer = ImageViewer(data.moon()) + plugin = PlotPlugin(image_filter=lambda x: x) + viewer += plugin + + assert_equal(viewer.image, data.moon()) + plugin._update_original_image(data.coins()) + assert_equal(viewer.image, data.coins()) + viewer.close() + + +@skipif(not has_qt) +def test_plugin(): + img = skimage.img_as_float(data.moon()) + viewer = ImageViewer(img) + + def median_filter(img, radius=3): + with expected_warnings(['precision loss']): + return median(img, selem=disk(radius=radius)) + + plugin = Plugin(image_filter=median_filter) + viewer += plugin + + plugin += Slider('radius', 1, 5) + + assert_almost_equal(np.std(viewer.image), 12.556, 3) + + plugin.filter_image() + + assert_almost_equal(np.std(viewer.image), 12.931, 3) + + plugin.show() + plugin.close() + plugin.clean_up() + img, _ = plugin.output() + assert_equal(img, viewer.image) diff --git a/skimage/viewer/tests/test_tools.py b/skimage/viewer/tests/test_tools.py new file mode 100644 index 00000000..b79a40e2 --- /dev/null +++ b/skimage/viewer/tests/test_tools.py @@ -0,0 +1,209 @@ +from collections import namedtuple + +import numpy as np +from numpy.testing import assert_equal +from numpy.testing.decorators import skipif +from skimage import data +from skimage.viewer import ImageViewer, has_qt +from skimage.viewer.canvastools import ( + LineTool, ThickLineTool, RectangleTool, PaintTool) +from skimage.viewer.canvastools.base import CanvasToolBase +try: + from matplotlib.testing.decorators import cleanup +except ImportError: + def cleanup(func): + return func + + +def get_end_points(image): + h, w = image.shape[0:2] + x = [w / 3, 2 * w / 3] + y = [h / 2] * 2 + return np.transpose([x, y]) + + +def do_event(viewer, etype, button=1, xdata=0, ydata=0, key=None): + """ + *name* + the event name + + *canvas* + the FigureCanvas instance generating the event + + *guiEvent* + the GUI event that triggered the matplotlib event + + *x* + x position - pixels from left of canvas + + *y* + y position - pixels from bottom of canvas + + *inaxes* + the :class:`~matplotlib.axes.Axes` instance if mouse is over axes + + *xdata* + x coord of mouse in data coords + + *ydata* + y coord of mouse in data coords + + *button* + button pressed None, 1, 2, 3, 'up', 'down' (up and down are used + for scroll events) + + *key* + the key depressed when the mouse event triggered (see + :class:`KeyEvent`) + + *step* + number of scroll steps (positive for 'up', negative for 'down') + """ + ax = viewer.ax + event = namedtuple('Event', + ('name canvas guiEvent x y inaxes xdata ydata ' + 'button key step')) + event.button = button + event.x, event.y = ax.transData.transform((xdata, ydata)) + event.xdata, event.ydata = xdata, ydata + event.inaxes = ax + event.canvas = ax.figure.canvas + event.key = key + event.step = 1 + event.guiEvent = None + event.name = 'Custom' + + func = getattr(viewer._event_manager, 'on_%s' % etype) + func(event) + + +@cleanup +@skipif(not has_qt) +def test_line_tool(): + img = data.camera() + viewer = ImageViewer(img) + + tool = LineTool(viewer, maxdist=10, line_props=dict(linewidth=3), + handle_props=dict(markersize=5)) + tool.end_points = get_end_points(img) + assert_equal(tool.end_points, np.array([[170, 256], [341, 256]])) + + # grab a handle and move it + do_event(viewer, 'mouse_press', xdata=170, ydata=256) + do_event(viewer, 'move', xdata=180, ydata=260) + do_event(viewer, 'mouse_release') + + assert_equal(tool.geometry, np.array([[180, 260], [341, 256]])) + + # create a new line + do_event(viewer, 'mouse_press', xdata=10, ydata=10) + do_event(viewer, 'move', xdata=100, ydata=100) + do_event(viewer, 'mouse_release') + + assert_equal(tool.geometry, np.array([[100, 100], [10, 10]])) + + +@cleanup +@skipif(not has_qt) +def test_thick_line_tool(): + img = data.camera() + viewer = ImageViewer(img) + + tool = ThickLineTool(viewer, maxdist=10, line_props=dict(color='red'), + handle_props=dict(markersize=5)) + tool.end_points = get_end_points(img) + + do_event(viewer, 'scroll', button='up') + assert_equal(tool.linewidth, 2) + + do_event(viewer, 'scroll', button='down') + + assert_equal(tool.linewidth, 1) + + do_event(viewer, 'key_press', key='+') + assert_equal(tool.linewidth, 2) + + do_event(viewer, 'key_press', key='-') + assert_equal(tool.linewidth, 1) + + +@cleanup +@skipif(not has_qt) +def test_rect_tool(): + img = data.camera() + viewer = ImageViewer(img) + + tool = RectangleTool(viewer, maxdist=10) + tool.extents = (100, 150, 100, 150) + + assert_equal(tool.corners, + ((100, 150, 150, 100), (100, 100, 150, 150))) + assert_equal(tool.extents, (100, 150, 100, 150)) + assert_equal(tool.edge_centers, + ((100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150))) + assert_equal(tool.geometry, (100, 150, 100, 150)) + + # grab a corner and move it + do_event(viewer, 'mouse_press', xdata=100, ydata=100) + do_event(viewer, 'move', xdata=120, ydata=120) + do_event(viewer, 'mouse_release') + assert_equal(tool.geometry, [120, 150, 120, 150]) + + # create a new line + do_event(viewer, 'mouse_press', xdata=10, ydata=10) + do_event(viewer, 'move', xdata=100, ydata=100) + do_event(viewer, 'mouse_release') + assert_equal(tool.geometry, [10, 100, 10, 100]) + + +@cleanup +@skipif(not has_qt) +def test_paint_tool(): + img = data.moon() + viewer = ImageViewer(img) + + tool = PaintTool(viewer, img.shape) + + tool.radius = 10 + assert_equal(tool.radius, 10) + tool.label = 2 + assert_equal(tool.label, 2) + assert_equal(tool.shape, img.shape) + + do_event(viewer, 'mouse_press', xdata=100, ydata=100) + do_event(viewer, 'move', xdata=110, ydata=110) + do_event(viewer, 'mouse_release') + + assert_equal(tool.overlay[tool.overlay == 2].size, 761) + + tool.label = 5 + do_event(viewer, 'mouse_press', xdata=20, ydata=20) + do_event(viewer, 'move', xdata=40, ydata=40) + do_event(viewer, 'mouse_release') + + assert_equal(tool.overlay[tool.overlay == 5].size, 881) + assert_equal(tool.overlay[tool.overlay == 2].size, 761) + + do_event(viewer, 'key_press', key='enter') + + tool.overlay = tool.overlay * 0 + assert_equal(tool.overlay.sum(), 0) + + +@cleanup +@skipif(not has_qt) +def test_base_tool(): + img = data.moon() + viewer = ImageViewer(img) + + tool = CanvasToolBase(viewer) + tool.set_visible(False) + tool.set_visible(True) + + do_event(viewer, 'key_press', key='enter') + + tool.redraw() + tool.remove() + + tool = CanvasToolBase(viewer, useblit=False) + tool.redraw() diff --git a/skimage/viewer/tests/test_utils.py b/skimage/viewer/tests/test_utils.py new file mode 100644 index 00000000..ae158a90 --- /dev/null +++ b/skimage/viewer/tests/test_utils.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from skimage.viewer import utils +from skimage.viewer.utils import dialogs +from skimage.viewer.qt import QtCore, QtGui, has_qt +from numpy.testing.decorators import skipif + + +@skipif(not has_qt) +def test_event_loop(): + utils.init_qtapp() + timer = QtCore.QTimer() + timer.singleShot(10, QtGui.QApplication.quit) + utils.start_qtapp() + + +@skipif(not has_qt) +def test_format_filename(): + fname = dialogs._format_filename(('apple', 2)) + assert fname == 'apple' + fname = dialogs._format_filename('') + assert fname is None + + +@skipif(not has_qt) +def test_open_file_dialog(): + utils.init_qtapp() + timer = QtCore.QTimer() + timer.singleShot(100, lambda: QtGui.QApplication.quit()) + filename = dialogs.open_file_dialog() + assert filename is None + + +@skipif(not has_qt) +def test_save_file_dialog(): + utils.init_qtapp() + timer = QtCore.QTimer() + timer.singleShot(100, lambda: QtGui.QApplication.quit()) + filename = dialogs.save_file_dialog() + assert filename is None diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py index 7fa4e374..548565a4 100644 --- a/skimage/viewer/tests/test_viewer.py +++ b/skimage/viewer/tests/test_viewer.py @@ -1,48 +1,81 @@ -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 skimage import data + +from skimage.viewer.qt import QtGui, QtCore, has_qt +from skimage.viewer import ImageViewer, CollectionViewer +from skimage.viewer.plugins import OverlayPlugin + +from skimage.transform import pyramid_gaussian +from skimage.filters import sobel +from numpy.testing import assert_equal from numpy.testing.decorators import skipif +from skimage._shared.version_requirements import is_installed +from skimage._shared._warnings import expected_warnings -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(not has_qt) +def test_viewer(): + astro = data.astronaut() + coins = data.coins() + + view = ImageViewer(astro) + import tempfile + _, filename = tempfile.mkstemp(suffix='.png') + + view.show(False) + view.close() + view.save_to_file(filename) + view.open_file(filename) + assert_equal(view.image, astro) + view.image = coins + assert_equal(view.image, coins), + view.save_to_file(filename), + view.open_file(filename), + view.reset_image(), + assert_equal(view.image, coins) -@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) +def make_key_event(key): + return QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key, + QtCore.Qt.NoModifier) -@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) +@skipif(not has_qt) +def test_collection_viewer(): + + img = data.astronaut() + img_collection = tuple(pyramid_gaussian(img)) + + view = CollectionViewer(img_collection) + make_key_event(48) + + view.update_index('', 2), + assert_equal(view.image, img_collection[2]) + view.keyPressEvent(make_key_event(53)) + assert_equal(view.image, img_collection[5]) + view._format_coord(10, 10) -if __name__ == "__main__": - from numpy.testing import run_module_suite - run_module_suite() +@skipif(not has_qt) +@skipif(not is_installed('matplotlib', '>=1.2')) +def test_viewer_with_overlay(): + img = data.coins() + ov = OverlayPlugin(image_filter=sobel) + viewer = ImageViewer(img) + viewer += ov + + import tempfile + _, filename = tempfile.mkstemp(suffix='.png') + + ov.color = 3 + assert_equal(ov.color, 'yellow') + + with expected_warnings(['precision loss']): + viewer.save_to_file(filename) + ov.display_filtered_image(img) + assert_equal(ov.overlay, img) + ov.overlay = None + assert_equal(ov.overlay, None) + ov.overlay = img + assert_equal(ov.overlay, img) + assert_equal(ov.filtered_image, img) diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py new file mode 100644 index 00000000..66b86fa4 --- /dev/null +++ b/skimage/viewer/tests/test_widgets.py @@ -0,0 +1,127 @@ + +import os +from skimage import data, img_as_float, io, img_as_uint + +from skimage.viewer import ImageViewer +from skimage.viewer.qt import QtGui, QtCore, has_qt +from skimage.viewer.widgets import ( + Slider, OKCancelButtons, SaveButtons, ComboBox, CheckBox, Text) +from skimage.viewer.plugins.base import Plugin + +from numpy.testing import assert_almost_equal, assert_equal +from numpy.testing.decorators import skipif +from skimage._shared._warnings import expected_warnings + + +def get_image_viewer(): + image = data.coins() + viewer = ImageViewer(img_as_float(image)) + viewer += Plugin() + return viewer + + +@skipif(not has_qt) +def test_check_box(): + viewer = get_image_viewer() + cb = CheckBox('hello', value=True, alignment='left') + viewer.plugins[0] += cb + + assert_equal(cb.val, True) + cb.val = False + assert_equal(cb.val, False) + cb.val = 1 + assert_equal(cb.val, True) + cb.val = 0 + assert_equal(cb.val, False) + + +@skipif(not has_qt) +def test_combo_box(): + viewer = get_image_viewer() + cb = ComboBox('hello', ('a', 'b', 'c')) + viewer.plugins[0] += cb + + assert_equal(str(cb.val), 'a') + assert_equal(cb.index, 0) + cb.index = 2 + assert_equal(str(cb.val), 'c'), + assert_equal(cb.index, 2) + + +@skipif(not has_qt) +def test_text_widget(): + viewer = get_image_viewer() + txt = Text('hello', 'hello, world!') + viewer.plugins[0] += txt + + assert_equal(str(txt.text), 'hello, world!') + txt.text = 'goodbye, world!' + assert_equal(str(txt.text), 'goodbye, world!') + + +@skipif(not has_qt) +def test_slider_int(): + viewer = get_image_viewer() + sld = Slider('radius', 2, 10, value_type='int') + viewer.plugins[0] += sld + + assert_equal(sld.val, 4) + sld.val = 6 + assert_equal(sld.val, 6) + sld.editbox.setText('5') + sld._on_editbox_changed() + assert_equal(sld.val, 5) + + +@skipif(not has_qt) +def test_slider_float(): + viewer = get_image_viewer() + sld = Slider('alpha', 2.1, 3.1, value=2.1, value_type='float', + orientation='vertical', update_on='move') + viewer.plugins[0] += sld + + assert_equal(sld.val, 2.1) + sld.val = 2.5 + assert_almost_equal(sld.val, 2.5, 2) + sld.editbox.setText('0.1') + sld._on_editbox_changed() + assert_almost_equal(sld.val, 2.5, 2) + + +@skipif(not has_qt) +def test_save_buttons(): + viewer = get_image_viewer() + sv = SaveButtons() + viewer.plugins[0] += sv + + import tempfile + fid, filename = tempfile.mkstemp(suffix='.png') + os.close(fid) + + timer = QtCore.QTimer() + timer.singleShot(100, QtGui.QApplication.quit) + + sv.save_to_stack() + with expected_warnings(['precision loss']): + sv.save_to_file(filename) + + img = data.imread(filename) + + with expected_warnings(['precision loss']): + assert_almost_equal(img, img_as_uint(viewer.image)) + + img = io.pop() + assert_almost_equal(img, viewer.image) + + os.remove(filename) + + +@skipif(not has_qt) +def test_ok_buttons(): + viewer = get_image_viewer() + ok = OKCancelButtons() + viewer.plugins[0] += ok + + ok.update_original_image(), + ok.close_plugin() + diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 95176fcd..524e9b61 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -1,28 +1,15 @@ import warnings import numpy as np +from skimage.viewer.qt import QtWidgets, has_qt, FigureManagerQT, FigureCanvasQTAgg +import matplotlib as mpl +from matplotlib.figure import Figure +from matplotlib import _pylab_helpers +from matplotlib.colors import LinearSegmentedColormap -from ..qt import qt_api - -try: - import matplotlib as mpl - from matplotlib.figure import Figure - from matplotlib import _pylab_helpers - from matplotlib.colors import LinearSegmentedColormap - if qt_api is None: - raise ImportError - 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 - print("Could not import matplotlib -- skimage.viewer not available.") - -from ..qt import QtGui +if has_qt and 'agg' not in mpl.get_backend().lower(): + warnings.warn("Recommended matplotlib backend is `Agg` for full " + "skimage.viewer functionality.") __all__ = ['init_qtapp', 'start_qtapp', 'RequiredAttr', 'figimage', @@ -39,9 +26,9 @@ def init_qtapp(): The QApplication needs to be initialized before creating any QWidgets """ global QApp - QApp = QtGui.QApplication.instance() + QApp = QtWidgets.QApplication.instance() if QApp is None: - QApp = QtGui.QApplication([]) + QApp = QtWidgets.QApplication([]) return QApp @@ -72,16 +59,13 @@ class RequiredAttr(object): instances = dict() - def __init__(self, msg='Required attribute not set', init_val=None): + def __init__(self, init_val=None): self.instances[self, None] = init_val - self.msg = msg def __get__(self, obj, objtype): value = self.instances[self, obj] if value is None: - # Should raise an error but that causes issues with the buildbot. - warnings.warn(self.msg) - self.__set__(obj, self.init_val) + raise AttributeError('Required attribute not set') return value def __set__(self, obj, value): @@ -131,8 +115,8 @@ class FigureCanvas(FigureCanvasQTAgg): self.fig = figure FigureCanvasQTAgg.__init__(self, self.fig) FigureCanvasQTAgg.setSizePolicy(self, - QtGui.QSizePolicy.Expanding, - QtGui.QSizePolicy.Expanding) + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) FigureCanvasQTAgg.updateGeometry(self) def resizeEvent(self, event): @@ -206,6 +190,7 @@ def figimage(image, scale=1, dpi=None, **kwargs): ax.set_axis_off() ax.imshow(image, **kwargs) + ax.figure.canvas.draw() return fig, ax diff --git a/skimage/viewer/utils/dialogs.py b/skimage/viewer/utils/dialogs.py index f160531d..20dd0e16 100644 --- a/skimage/viewer/utils/dialogs.py +++ b/skimage/viewer/utils/dialogs.py @@ -1,6 +1,6 @@ import os -from ..qt import QtGui +from skimage.viewer.qt import QtGui __all__ = ['open_file_dialog', 'save_file_dialog'] diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 32c10cd4..50a69603 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -1,22 +1,15 @@ """ ImageViewer class for viewing and interacting with images. """ -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 ... import io, img_as_float +from ...util.dtype import dtype_range +from ...exposure import rescale_intensity +from ..qt import QtWidgets, Qt, Signal from ..widgets import Slider -from ..utils import dialogs +from ..utils import (dialogs, init_qtapp, figimage, start_qtapp, + update_axes_image) from ..plugins.base import Plugin @@ -51,13 +44,118 @@ def mpl_image_to_rgba(mpl_image): return img_as_float(image) -class ImageViewer(QtGui.QMainWindow): +class BlitManager(object): + """Object that manages blits on an axes""" + def __init__(self, ax): + self.ax = ax + self.canvas = ax.figure.canvas + self.canvas.mpl_connect('draw_event', self.on_draw_event) + self.ax = ax + self.background = None + self.artists = [] + + def add_artists(self, artists): + self.artists.extend(artists) + self.redraw() + + def remove_artists(self, artists): + for artist in artists: + self.artists.remove(artist) + + def on_draw_event(self, event=None): + self.background = self.canvas.copy_from_bbox(self.ax.bbox) + self.draw_artists() + + def redraw(self): + if self.background is not None: + self.canvas.restore_region(self.background) + self.draw_artists() + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw_idle() + + def draw_artists(self): + for artist in self.artists: + self.ax.draw_artist(artist) + + +class EventManager(object): + """Object that manages events on a canvas""" + def __init__(self, ax): + self.canvas = ax.figure.canvas + self.connect_event('button_press_event', self.on_mouse_press) + self.connect_event('key_press_event', self.on_key_press) + self.connect_event('button_release_event', self.on_mouse_release) + self.connect_event('motion_notify_event', self.on_move) + self.connect_event('scroll_event', self.on_scroll) + + self.tools = [] + self.active_tool = None + + def connect_event(self, name, handler): + self.canvas.mpl_connect(name, handler) + + def attach(self, tool): + self.tools.append(tool) + self.active_tool = tool + + def detach(self, tool): + self.tools.remove(tool) + if self.tools: + self.active_tool = self.tools[-1] + else: + self.active_tool = None + + def on_mouse_press(self, event): + for tool in self.tools: + if not tool.ignore(event) and tool.hit_test(event): + self.active_tool = tool + break + if self.active_tool and not self.active_tool.ignore(event): + self.active_tool.on_mouse_press(event) + return + for tool in reversed(self.tools): + if not tool.ignore(event): + self.active_tool = tool + tool.on_mouse_press(event) + return + + def on_key_press(self, event): + tool = self._get_tool(event) + if not tool is None: + tool.on_key_press(event) + + def _get_tool(self, event): + if not self.tools or self.active_tool.ignore(event): + return None + return self.active_tool + + def on_mouse_release(self, event): + tool = self._get_tool(event) + if not tool is None: + tool.on_mouse_release(event) + + def on_move(self, event): + tool = self._get_tool(event) + if not tool is None: + tool.on_move(event) + + def on_scroll(self, event): + tool = self._get_tool(event) + if not tool is None: + tool.on_scroll(event) + + +class ImageViewer(QtWidgets.QMainWindow): """Viewer for displaying images. This viewer is a simple container object that holds a Matplotlib axes for showing images. `ImageViewer` doesn't subclass the Matplotlib axes (or figure) because of the high probability of name collisions. + Subclasses and plugins will likely extend the `update_image` method to add + custom overlays or filter the displayed image. + Parameters ---------- image : array @@ -91,9 +189,9 @@ class ImageViewer(QtGui.QMainWindow): # Signal that the original image has been changed original_image_changed = Signal(np.ndarray) - def __init__(self, image): + def __init__(self, image, useblit=True): # Start main loop - utils.init_qtapp() + init_qtapp() super(ImageViewer, self).__init__() #TODO: Add ImageViewer to skimage.io window manager @@ -101,7 +199,7 @@ class ImageViewer(QtGui.QMainWindow): self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle("Image Viewer") - self.file_menu = QtGui.QMenu('&File', self) + self.file_menu = QtWidgets.QMenu('&File', self) self.file_menu.addAction('Open file', self.open_file, Qt.CTRL + Qt.Key_O) self.file_menu.addAction('Save to file', self.save_to_file, @@ -110,7 +208,7 @@ class ImageViewer(QtGui.QMainWindow): Qt.CTRL + Qt.Key_Q) self.menuBar().addMenu(self.file_menu) - self.main_widget = QtGui.QWidget() + self.main_widget = QtWidgets.QWidget() self.setCentralWidget(self.main_widget) if isinstance(image, Plugin): @@ -120,17 +218,22 @@ class ImageViewer(QtGui.QMainWindow): # When plugin is started, start plugin._started.connect(self._show) - self.fig, self.ax = utils.figimage(image) + self.fig, self.ax = figimage(image) self.canvas = self.fig.canvas self.canvas.setParent(self) - self.ax.autoscale(enable=False) + self._tools = [] + self.useblit = useblit + if useblit: + self._blit_manager = BlitManager(self.ax) + self._event_manager = EventManager(self.ax) + self._image_plot = self.ax.images[0] self._update_original_image(image) self.plugins = [] - self.layout = QtGui.QVBoxLayout(self.main_widget) + self.layout = QtWidgets.QVBoxLayout(self.main_widget) self.layout.addWidget(self.canvas) status_bar = self.statusBar() @@ -149,7 +252,7 @@ class ImageViewer(QtGui.QMainWindow): if plugin.dock: location = self.dock_areas[plugin.dock] dock_location = Qt.DockWidgetArea(location) - dock = QtGui.QDockWidget() + dock = QtWidgets.QDockWidget() dock.setWidget(plugin) dock.setWindowTitle(plugin.name) self.addDockWidget(dock_location, dock) @@ -174,20 +277,29 @@ class ImageViewer(QtGui.QMainWindow): h = viewer_size.height() self.resize(w + dx, h + dy) - def open_file(self): + def open_file(self, filename=None): """Open image file and display in viewer.""" - filename = dialogs.open_file_dialog() + if filename is None: + filename = dialogs.open_file_dialog() if filename is None: return image = io.imread(filename) self._update_original_image(image) + def update_image(self, image): + """Update displayed image. + + This method can be overridden or extended in subclasses and plugins to + react to image changes. + """ + self._update_original_image(image) + def _update_original_image(self, image): self.original_image = image # update saved image self.image = image.copy() # update displayed image self.original_image_changed.emit(image) - def save_to_file(self): + def save_to_file(self, filename=None): """Save current image to file. The current behavior is not ideal: It saves the image displayed on @@ -195,7 +307,8 @@ class ImageViewer(QtGui.QMainWindow): not preserved (resizing the viewer window will alter the size of the saved image). """ - filename = dialogs.save_file_dialog() + if filename is None: + filename = dialogs.save_file_dialog() if filename is None: return if len(self.ax.images) == 1: @@ -233,11 +346,14 @@ class ImageViewer(QtGui.QMainWindow): """ self._show() if main_window: - utils.start_qtapp() + start_qtapp() return [p.output() for p in self.plugins] def redraw(self): - self.canvas.draw_idle() + if self.useblit: + self._blit_manager.redraw() + else: + self.canvas.draw_idle() @property def image(self): @@ -246,7 +362,7 @@ class ImageViewer(QtGui.QMainWindow): @image.setter def image(self, image): self._img = image - utils.update_axes_image(self._image_plot, image) + update_axes_image(self._image_plot, image) # update display (otherwise image doesn't fill the canvas) h, w = image.shape[:2] @@ -259,6 +375,9 @@ class ImageViewer(QtGui.QMainWindow): clim = (0, clim[1]) self._image_plot.set_clim(clim) + if self.useblit: + self._blit_manager.background = None + self.redraw() def reset_image(self): @@ -279,6 +398,18 @@ class ImageViewer(QtGui.QMainWindow): else: self.status_message('') + def add_tool(self, tool): + if self.useblit: + self._blit_manager.add_artists(tool.artists) + self._tools.append(tool) + self._event_manager.attach(tool) + + def remove_tool(self, tool): + if self.useblit: + self._blit_manager.remove_artists(tool.artists) + self._tools.remove(tool) + self._event_manager.detach(tool) + def _format_coord(self, x, y): # callback function to format coordinate display in status bar x = int(x + 0.5) @@ -303,9 +434,6 @@ class CollectionViewer(ImageViewer): home/end keys First/last image in collection. - Subclasses and plugins will likely extend the `update_image` method to add - custom overlays or filter the displayed image. - Parameters ---------- image_collection : list of images @@ -352,16 +480,8 @@ class CollectionViewer(ImageViewer): self.slider.val = index self.update_image(self.image_collection[index]) - def update_image(self, image): - """Update displayed image. - - This method can be overridden or extended in subclasses and plugins to - react to image changes. - """ - self._update_original_image(image) - def keyPressEvent(self, event): - if type(event) == QtGui.QKeyEvent: + if type(event) == QtWidgets.QKeyEvent: key = event.key() # Number keys (code: 0 = key 48, 9 = key 57) move to deciles if 48 <= key < 58: diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 2bbf53d2..ae8be4e7 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -15,17 +15,15 @@ parameter type specified by its `ptype` attribute, which can be: property of the same name that updates the display. """ -from ..qt import QtGui -from ..qt import QtCore -from ..qt.QtCore import Qt - +from ..qt import QtWidgets, QtCore, Qt from ..utils import RequiredAttr -__all__ = ['BaseWidget', 'Slider', 'ComboBox', 'Text'] +__all__ = ['BaseWidget', 'Slider', 'ComboBox', 'CheckBox', 'Text'] -class BaseWidget(QtGui.QWidget): + +class BaseWidget(QtWidgets.QWidget): plugin = RequiredAttr("Widget is not attached to a Plugin.") @@ -49,11 +47,11 @@ class Text(BaseWidget): def __init__(self, name=None, text=''): super(Text, self).__init__(name) - self._label = QtGui.QLabel() + self._label = QtWidgets.QLabel() self.text = text - self.layout = QtGui.QHBoxLayout(self) + self.layout = QtWidgets.QHBoxLayout(self) if name is not None: - name_label = QtGui.QLabel() + name_label = QtWidgets.QLabel() name_label.setText(name) self.layout.addWidget(name_label) self.layout.addWidget(self._label) @@ -81,16 +79,17 @@ class Slider(BaseWidget): Range of slider values. value : float Default slider value. If None, use midpoint between `low` and `high`. - value_type : {'float' | 'int'} + value_type : {'float' | 'int'}, optional Numeric type of slider value. - ptype : {'arg' | 'kwarg' | 'plugin'} + ptype : {'kwarg' | 'arg' | 'plugin'}, optional Parameter type. - callback : function - Callback function called in response to slider changes. This function - is typically set when the widget is added to a plugin. - orientation : {'horizontal' | 'vertical'} + callback : callable f(widget_name, value), optional + Callback function called in response to slider changes. + *Note:* This function is typically set (overridden) when the widget is + added to a plugin. + orientation : {'horizontal' | 'vertical'}, optional Slider orientation. - update_on : {'release' | 'move'} + update_on : {'release' | 'move'}, optional Control when callback function is called: on slider move or release. """ def __init__(self, name, low=0.0, high=1.0, value=None, value_type='float', @@ -104,17 +103,17 @@ class Slider(BaseWidget): # Set widget orientation #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if orientation == 'vertical': - self.slider = QtGui.QSlider(Qt.Vertical) + self.slider = QtWidgets.QSlider(Qt.Vertical) alignment = QtCore.Qt.AlignHCenter align_text = QtCore.Qt.AlignHCenter align_value = QtCore.Qt.AlignHCenter - self.layout = QtGui.QVBoxLayout(self) + self.layout = QtWidgets.QVBoxLayout(self) elif orientation == 'horizontal': - self.slider = QtGui.QSlider(Qt.Horizontal) + self.slider = QtWidgets.QSlider(Qt.Horizontal) alignment = QtCore.Qt.AlignVCenter align_text = QtCore.Qt.AlignLeft align_value = QtCore.Qt.AlignRight - self.layout = QtGui.QHBoxLayout(self) + self.layout = QtWidgets.QHBoxLayout(self) else: msg = "Unexpected value %s for 'orientation'" raise ValueError(msg % orientation) @@ -150,11 +149,11 @@ class Slider(BaseWidget): raise ValueError("Unexpected value %s for 'update_on'" % update_on) self.slider.setFocusPolicy(QtCore.Qt.StrongFocus) - self.name_label = QtGui.QLabel() + self.name_label = QtWidgets.QLabel() self.name_label.setText(self.name) self.name_label.setAlignment(align_text) - self.editbox = QtGui.QLineEdit() + self.editbox = QtWidgets.QLineEdit() self.editbox.setMaximumWidth(max_edit_width) self.editbox.setText(self.value_fmt % self.val) self.editbox.setAlignment(align_value) @@ -211,40 +210,39 @@ class ComboBox(BaseWidget): Parameters ---------- name : str - Name of slider parameter. If this parameter is passed as a keyword + Name of ComboBox parameter. If this parameter is passed as a keyword argument, it must match the name of that keyword argument (spaces are replaced with underscores). In addition, this name is displayed as the - name of the slider. - items: list + name of the ComboBox. + items: list of str Allowed parameter values. - ptype : {'arg' | 'kwarg' | 'plugin'} + ptype : {'arg' | 'kwarg' | 'plugin'}, optional Parameter type. - callback : function - Callback function called in response to slider changes. This function - is typically set when the widget is added to a plugin. + callback : callable f(widget_name, value), optional + Callback function called in response to combobox changes. + *Note:* This function is typically set (overridden) when the widget is + added to a plugin. """ def __init__(self, name, items, ptype='kwarg', callback=None): super(ComboBox, self).__init__(name, ptype, callback) - self.name_label = QtGui.QLabel() + self.name_label = QtWidgets.QLabel() self.name_label.setText(self.name) self.name_label.setAlignment(QtCore.Qt.AlignLeft) - self._combo_box = QtGui.QComboBox() + self._combo_box = QtWidgets.QComboBox() self._combo_box.addItems(list(items)) - self.layout = QtGui.QHBoxLayout(self) + self.layout = QtWidgets.QHBoxLayout(self) self.layout.addWidget(self.name_label) self.layout.addWidget(self._combo_box) self._combo_box.currentIndexChanged.connect(self._value_changed) - # self.connect(self._combo_box, - # SIGNAL("currentIndexChanged(int)"), self.updateUi) @property def val(self): - return self._combo_box.value() + return self._combo_box.currentText() @property def index(self): @@ -253,3 +251,56 @@ class ComboBox(BaseWidget): @index.setter def index(self, i): self._combo_box.setCurrentIndex(i) + + +class CheckBox(BaseWidget): + """CheckBox widget + + Parameters + ---------- + name : str + Name of CheckBox parameter. If this parameter is passed as a keyword + argument, it must match the name of that keyword argument (spaces are + replaced with underscores). In addition, this name is displayed as the + name of the CheckBox. + value: {False, True}, optional + Initial state of the CheckBox. + alignment: {'center','left','right'}, optional + Checkbox alignment + ptype : {'arg' | 'kwarg' | 'plugin'}, optional + Parameter type + callback : callable f(widget_name, value), optional + Callback function called in response to checkbox changes. + *Note:* This function is typically set (overridden) when the widget is + added to a plugin. + """ + + def __init__(self, name, value=False, alignment='center', ptype='kwarg', + callback=None): + super(CheckBox, self).__init__(name, ptype, callback) + + self._check_box = QtWidgets.QCheckBox() + self._check_box.setChecked(value) + self._check_box.setText(self.name) + + self.layout = QtWidgets.QHBoxLayout(self) + if alignment == 'center': + self.layout.setAlignment(QtCore.Qt.AlignCenter) + elif alignment == 'left': + self.layout.setAlignment(QtCore.Qt.AlignLeft) + elif alignment == 'right': + self.layout.setAlignment(QtCore.Qt.AlignRight) + else: + raise ValueError("Unexpected value %s for 'alignment'" % alignment) + + self.layout.addWidget(self._check_box) + + self._check_box.stateChanged.connect(self._value_changed) + + @property + def val(self): + return self._check_box.isChecked() + + @val.setter + def val(self, i): + self._check_box.setChecked(i) diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py index ce20b559..3b7f58b0 100644 --- a/skimage/viewer/widgets/history.py +++ b/skimage/viewer/widgets/history.py @@ -1,12 +1,10 @@ from textwrap import dedent -from ..qt import QtGui -from ..qt import QtCore - +from ..qt import QtGui, QtCore import numpy as np import skimage -from skimage import io +from ... import io, img_as_ubyte from .core import BaseWidget from ..utils import dialogs @@ -81,14 +79,15 @@ class SaveButtons(BaseWidget): NOTE: The io stack only works in interactive sessions.''') notify(msg) - def save_to_file(self): - filename = dialogs.save_file_dialog() + def save_to_file(self, filename=None): + if filename is None: + filename = dialogs.save_file_dialog() if filename is None: return image = self.plugin.filtered_image if image.dtype == np.bool: #TODO: This check/conversion should probably be in `imsave`. - image = skimage.img_as_ubyte(image) + image = img_as_ubyte(image) io.imsave(filename, image) diff --git a/tools/appveyor/install.ps1 b/tools/appveyor/install.ps1 new file mode 100644 index 00000000..0f165d8b --- /dev/null +++ b/tools/appveyor/install.ps1 @@ -0,0 +1,180 @@ +# Sample script to install Python and pip under Windows +# Authors: Olivier Grisel, Jonathan Helmus and Kyle Kastner +# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ + +$MINICONDA_URL = "http://repo.continuum.io/miniconda/" +$BASE_URL = "https://www.python.org/ftp/python/" +$GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" +$GET_PIP_PATH = "C:\get-pip.py" + + +function DownloadPython ($python_version, $platform_suffix) { + $webclient = New-Object System.Net.WebClient + $filename = "python-" + $python_version + $platform_suffix + ".msi" + $url = $BASE_URL + $python_version + "/" + $filename + + $basedir = $pwd.Path + "\" + $filepath = $basedir + $filename + if (Test-Path $filename) { + Write-Host "Reusing" $filepath + return $filepath + } + + # Download and retry up to 3 times in case of network transient errors. + Write-Host "Downloading" $filename "from" $url + $retry_attempts = 2 + for($i=0; $i -lt $retry_attempts; $i++){ + try { + $webclient.DownloadFile($url, $filepath) + break + } + Catch [Exception]{ + Start-Sleep 1 + } + } + if (Test-Path $filepath) { + Write-Host "File saved at" $filepath + } else { + # Retry once to get the error message if any at the last try + $webclient.DownloadFile($url, $filepath) + } + return $filepath +} + + +function InstallPython ($python_version, $architecture, $python_home) { + Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home + if (Test-Path $python_home) { + Write-Host $python_home "already exists, skipping." + return $false + } + if ($architecture -eq "32") { + $platform_suffix = "" + } else { + $platform_suffix = ".amd64" + } + $msipath = DownloadPython $python_version $platform_suffix + Write-Host "Installing" $msipath "to" $python_home + $install_log = $python_home + ".log" + $install_args = "/qn /log $install_log /i $msipath TARGETDIR=$python_home" + $uninstall_args = "/qn /x $msipath" + RunCommand "msiexec.exe" $install_args + if (-not(Test-Path $python_home)) { + Write-Host "Python seems to be installed else-where, reinstalling." + RunCommand "msiexec.exe" $uninstall_args + RunCommand "msiexec.exe" $install_args + } + if (Test-Path $python_home) { + Write-Host "Python $python_version ($architecture) installation complete" + } else { + Write-Host "Failed to install Python in $python_home" + Get-Content -Path $install_log + Exit 1 + } +} + +function RunCommand ($command, $command_args) { + Write-Host $command $command_args + Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru +} + + +function InstallPip ($python_home) { + $pip_path = $python_home + "\Scripts\pip.exe" + $python_path = $python_home + "\python.exe" + if (-not(Test-Path $pip_path)) { + Write-Host "Installing pip..." + $webclient = New-Object System.Net.WebClient + $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) + Write-Host "Executing:" $python_path $GET_PIP_PATH + Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru + } else { + Write-Host "pip already installed." + } +} + + +function DownloadMiniconda ($python_version, $platform_suffix) { + $webclient = New-Object System.Net.WebClient + if ($python_version -eq "3.4") { + $filename = "Miniconda3-3.5.5-Windows-" + $platform_suffix + ".exe" + } else { + $filename = "Miniconda-3.5.5-Windows-" + $platform_suffix + ".exe" + } + $url = $MINICONDA_URL + $filename + + $basedir = $pwd.Path + "\" + $filepath = $basedir + $filename + if (Test-Path $filename) { + Write-Host "Reusing" $filepath + return $filepath + } + + # Download and retry up to 3 times in case of network transient errors. + Write-Host "Downloading" $filename "from" $url + $retry_attempts = 2 + for($i=0; $i -lt $retry_attempts; $i++){ + try { + $webclient.DownloadFile($url, $filepath) + break + } + Catch [Exception]{ + Start-Sleep 1 + } + } + if (Test-Path $filepath) { + Write-Host "File saved at" $filepath + } else { + # Retry once to get the error message if any at the last try + $webclient.DownloadFile($url, $filepath) + } + return $filepath +} + + +function InstallMiniconda ($python_version, $architecture, $python_home) { + Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home + if (Test-Path $python_home) { + Write-Host $python_home "already exists, skipping." + return $false + } + if ($architecture -eq "32") { + $platform_suffix = "x86" + } else { + $platform_suffix = "x86_64" + } + $filepath = DownloadMiniconda $python_version $platform_suffix + Write-Host "Installing" $filepath "to" $python_home + $install_log = $python_home + ".log" + $args = "/S /D=$python_home" + Write-Host $filepath $args + Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru + if (Test-Path $python_home) { + Write-Host "Python $python_version ($architecture) installation complete" + } else { + Write-Host "Failed to install Python in $python_home" + Get-Content -Path $install_log + Exit 1 + } +} + + +function InstallMinicondaPip ($python_home) { + $pip_path = $python_home + "\Scripts\pip.exe" + $conda_path = $python_home + "\Scripts\conda.exe" + if (-not(Test-Path $pip_path)) { + Write-Host "Installing pip..." + $args = "install --yes pip" + Write-Host $conda_path $args + Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru + } else { + Write-Host "pip already installed." + } +} + +function main () { + InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON + InstallPip $env:PYTHON +} + +main diff --git a/tools/appveyor/requirements.txt b/tools/appveyor/requirements.txt new file mode 100644 index 00000000..9cc4022f --- /dev/null +++ b/tools/appveyor/requirements.txt @@ -0,0 +1,17 @@ +# Fetch numpy and scipy wheels from the sklearn rackspace wheelhouse. +# Those wheels were generated by @ogrisel by calling `wheel convert` on +# the binaries from http://www.lfd.uci.edu/~gohlke/pythonlibs/ +# This is a temporary solution. As soon as numpy, scipy and cython provide +# official wheels for windows we can delete this --find-links line. +--find-links http://28daf2247a33ed269873-7b1aad3fab3cc330e1fd9d109892382a.r6.cf2.rackcdn.com/appveyor_pip_index.html + +# fix the versions of binary packages to force the use of the whl +# of the rackspace folder instead of trying to install from PyPI +# wheels are preferred for a given version +numpy==1.8.1 +scipy==0.14.0 +cython==0.20.2 +matplotlib==1.4.2 +pillow==2.6.1 +wheel +nose diff --git a/tools/appveyor/run_with_env.cmd b/tools/appveyor/run_with_env.cmd new file mode 100644 index 00000000..0c70d633 --- /dev/null +++ b/tools/appveyor/run_with_env.cmd @@ -0,0 +1,47 @@ +:: To build extensions for 64 bit Python 3, we need to configure environment +:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: +:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) +:: +:: To build extensions for 64 bit Python 2, we need to configure environment +:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: +:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) +:: +:: 32 bit builds do not require specific environment configurations. +:: +:: Note: this script needs to be run with the /E:ON and /V:ON flags for the +:: cmd interpreter, at least for (SDK v7.0) +:: +:: More details at: +:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows +:: http://stackoverflow.com/a/13751649/163740 +:: +:: Author: Olivier Grisel +:: License: BSD 3 clause +@ECHO OFF + +SET COMMAND_TO_RUN=%* +SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows + +SET MAJOR_PYTHON_VERSION="%PYTHON_VERSION:~0,1%" +IF %MAJOR_PYTHON_VERSION% == "2" ( + SET WINDOWS_SDK_VERSION="v7.0" +) ELSE IF %MAJOR_PYTHON_VERSION% == "3" ( + SET WINDOWS_SDK_VERSION="v7.1" +) ELSE ( + ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" + EXIT 1 +) + +IF "%PYTHON_ARCH%"=="64" ( + ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture + SET DISTUTILS_USE_SDK=1 + SET MSSdk=1 + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release + ECHO Executing: %COMMAND_TO_RUN% + call %COMMAND_TO_RUN% || EXIT 1 +) ELSE ( + ECHO Using default MSVC build environment for 32 bit architecture + ECHO Executing: %COMMAND_TO_RUN% + call %COMMAND_TO_RUN% || EXIT 1 +) diff --git a/tools/build_versions.py b/tools/build_versions.py index 2c0e32ba..af1faac2 100755 --- a/tools/build_versions.py +++ b/tools/build_versions.py @@ -6,7 +6,14 @@ import numpy as np import scipy as sp import matplotlib as mpl import six +from PIL import Image +import Cython +import networkx -for m in (np, sp, mpl, six): - print(m.__name__.rjust(10), ' ', m.__version__) +for m in (np, sp, mpl, six, Image, networkx, Cython): + if m is Image: + version = m.VERSION + else: + version = m.__version__ + print(m.__name__.rjust(10), ' ', version) diff --git a/tools/header.py b/tools/header.py index d7a4c50b..01e2069f 100755 --- a/tools/header.py +++ b/tools/header.py @@ -9,6 +9,7 @@ print('*' * screen_width) if len(sys.argv) > 1: header = ' '.join(sys.argv[1:]) + header = header.replace('.', ' ') print('*', header.center(screen_width - 4), '*') print('*' * screen_width) diff --git a/tools/osx_wheel_upload.sh b/tools/osx_wheel_upload.sh new file mode 100755 index 00000000..7d757379 --- /dev/null +++ b/tools/osx_wheel_upload.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Script to download / check and upload scikit_image wheels for release +RACKSPACE_URL=http://wheels.scikit-image.org +if [ "`which twine`" == "" ]; then + echo "twine not on path; need to pip install twine?" + exit 1 +fi +SK_VERSION=`git describe --tags` +if [ "${SK_VERSION:0:1}" != 'v' ]; then + echo "scikit image version $SK_VERSION does not start with 'v'" + exit 1 +fi +WHEEL_HEAD="scikit_image-${SK_VERSION:1}" +WHEEL_TAIL="macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.whl" +mkdir -p wheels +cd wheels +rm -rf *.whl +for py_tag in cp27-none cp33-cp33m cp34-cp34m +do + wheel_name="$WHEEL_HEAD-$py_tag-$WHEEL_TAIL" + wheel_url="${RACKSPACE_URL}/${wheel_name}" + echo "Fetching $wheel_url" + curl -O $wheel_url + if [ "$?" != "0" ]; then + echo "Failed downloading $wheel_url; check travis build?" + exit $? + fi +done +cd .. +twine upload wheels/*.whl diff --git a/tools/travis_before_install.sh b/tools/travis_before_install.sh new file mode 100755 index 00000000..f98e48de --- /dev/null +++ b/tools/travis_before_install.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -ex + +export WHEELHOUSE="--no-index --find-links=http://travis-wheels.scikit-image.org/" +export COVERALLS_REPO_TOKEN=7LdFN9232ZbSY3oaXHbQIzLazrSf6w2pQ +export PIP_DEFAULT_TIMEOUT=60 +sh -e /etc/init.d/xvfb start +export DISPLAY=:99.0 +export PYTHONWARNINGS="all" +export TEST_ARGS="--exe --ignore-files=^_test -v --with-doctest --ignore-files=^setup.py$" + + +retry () { + # https://gist.github.com/fungusakafungus/1026804 + local retry_max=3 + local count=$retry_max + while [ $count -gt 0 ]; do + "$@" && break + count=$(($count - 1)) + sleep 1 + done + + [ $count -eq 0 ] && { + echo "Retry failed [$retry_max]: $@" >&2 + return 1 + } + return 0 +} + + +# on Python 2.7, use the system versions of numpy, scipy, and matplotlib +# and the minimum version of cython and networkx +if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + virtualenv --system-site-packages ~/venv + sudo apt-get install python-scipy python-matplotlib python-imaging + sed -i 's/cython>=/cython==/g' requirements.txt + sed -i 's/networkx>=/networkx==/g' requirements.txt + sed -i '/pillow/d' requirements.txt +else + virtualenv -p python --system-site-packages ~/venv +fi + +source ~/venv/bin/activate +retry pip install wheel flake8 coveralls nose + +# install system tk for matplotlib +sudo apt-get install python-tk + + +# on Python 3.2, use matplotlib 1.3.1 +if [[ $TRAVIS_PYTHON_VERSION == 3.2 ]]; then + sed -i 's/matplotlib>=*.*.*/matplotlib==1.3.1/g' requirements.txt +fi + +retry pip install $WHEELHOUSE -r requirements.txt + +# clean up disk space +sudo apt-get clean +sudo rm -rf /tmp/* + + +section () { + echo -en "travis_fold:start:$1\r" + tools/header.py $1 +} + +section_end () { + echo -en "travis_fold:end:$1\r" +} + +export -f section +export -f section_end +export -f retry + +set +ex diff --git a/tools/travis_notes.txt b/tools/travis_notes.txt new file mode 100644 index 00000000..84271821 --- /dev/null +++ b/tools/travis_notes.txt @@ -0,0 +1,24 @@ + +- Use http://yaml-online-parser.appspot.com/ to make sure +the yaml file is valid. + http://lint.travis-ci.org/ is recommended elsewhere but does not give helpful + error reports. +- Make sure all of your "-" lines start on the same column +- Use bash scripts for `before_install` and `script` or any part that + has conditional statements + - Make sure they are "executable" (chmod +x) + - Use the following header: + + ``` + #!/usr/bin/env bash + set -ex + ``` + +- Use the `retry` bash function from `before_install.sh` before a command to have it try 3 times before +failing (useful for installing from third party sources). +- Use the `section` function to start a folded section of the script. +Section names must have underscores or dots instead of spaces and must be +accompanied by a corresponding `section_end` call. +- Feel free to cancel a build rather than waiting for it to go to completion + if you have made a change to that branch. +- A VM with 64bit Ubuntu 12.04 is a huge help for debugging. diff --git a/tools/travis_script.sh b/tools/travis_script.sh new file mode 100755 index 00000000..8dc2bb4e --- /dev/null +++ b/tools/travis_script.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +set -ex + +PY=$TRAVIS_PYTHON_VERSION + +section "Test.with.min.requirements" +nosetests $TEST_ARGS skimage +section_end "Test.with.min.requirements" + +section "Build.docs" +if [[ ($PY != 2.6) && ($PY != 3.2) ]]; then + sudo apt-get install -qq texlive texlive-latex-extra dvipng + make html +fi +section_end "Build.docs" + +section "Flake8.test" +flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples +section_end "Flake8.test" + + +section "Install.optional.dependencies" + +# Install Qt and then update the Matplotlib settings +if [[ $PY == 2.7* ]]; then + sudo apt-get install -q python-qt4 + + # http://stackoverflow.com/a/9716100 + LIBS=( PyQt4 sip.so ) + + VAR=( $(which -a python$PY) ) + + GET_PYTHON_LIB_CMD="from distutils.sysconfig import get_python_lib; print (get_python_lib())" + LIB_VIRTUALENV_PATH=$(python -c "$GET_PYTHON_LIB_CMD") + LIB_SYSTEM_PATH=$(${VAR[-1]} -c "$GET_PYTHON_LIB_CMD") + + for LIB in ${LIBS[@]} + do + sudo ln -sf $LIB_SYSTEM_PATH/$LIB $LIB_VIRTUALENV_PATH/$LIB + done + +else + sudo apt-get install -q libqt4-dev + retry pip install -q PySide $WHEELHOUSE + python ~/venv/bin/pyside_postinstall.py -install +fi + +# imread does NOT support py3.2 +if [[ $PY != 3.2 ]]; then + sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools + retry pip install -q imread +fi + +# Install SimpleITK from wheelhouse if available (not 3.2 or 3.4) +if [[ $PY =~ 3\.[24] ]]; then + echo "SimpleITK unavailable on $PY" +else + retry pip install -q SimpleITK $WHEELHOUSE +fi + +sudo apt-get install -q libfreeimage3 +retry pip install -q astropy $WHEELHOUSE + +if [[ $PY == 2.* ]]; then + retry pip install -q pyamg +fi + +retry pip install -q tifffile + +section_end "Install.optional.dependencies" + + +section "Run.doc.examples" + +# Matplotlib settings - do not show figures during doc examples +if [[ $PY == 2.7* ]]; then + MPL_DIR=$HOME/.matplotlib +else + MPL_DIR=$HOME/.config/matplotlib +fi + +mkdir -p $MPL_DIR +touch $MPL_DIR/matplotlibrc +echo 'backend : Template' > $MPL_DIR/matplotlibrc + + +for f in doc/examples/*.py; do + python "$f" + if [ $? -ne 0 ]; then + exit 1 + fi +done + +section_end "Run.doc.examples" + + +section "Run.doc.applications" + +for f in doc/examples/applications/*.py; do + python "$f" + if [ $? -ne 0 ]; then + exit 1 + fi +done + +# Now configure Matplotlib to use Qt4 +if [[ $PY == 2.7* ]]; then + MPL_QT_API=PyQt4 + export QT_API=pyqt +else + MPL_QT_API=PySide + export QT_API=pyside +fi +echo 'backend: Agg' > $MPL_DIR/matplotlibrc +echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc + +section_end "Run.doc.applications" + + +section "Test.with.optional.dependencies" + +# run tests again with optional dependencies to get more coverage +if [[ $PY == 3.3 ]]; then + TEST_ARGS="$TEST_ARGS --with-cov --cover-package skimage" +fi +nosetests $TEST_ARGS + +section_end "Test.with.optional.dependencies" diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 02feddba..00000000 --- a/tox.ini +++ /dev/null @@ -1,10 +0,0 @@ -[testenv] -deps = nose - coverage - cython - numpy - scipy -commands= - nosetests {posargs:--with-coverage --cover-package=skimage} - coverage xml - diff --git a/viewer_examples/plugins/canny_simple.py b/viewer_examples/plugins/canny_simple.py index 641ca138..faa92afd 100644 --- a/viewer_examples/plugins/canny_simple.py +++ b/viewer_examples/plugins/canny_simple.py @@ -1,5 +1,5 @@ from skimage import data -from skimage.filter import canny +from skimage.feature import canny from skimage.viewer import ImageViewer from skimage.viewer.widgets import Slider diff --git a/viewer_examples/plugins/collection_plugin.py b/viewer_examples/plugins/collection_plugin.py index 65ff1f21..83c0223a 100644 --- a/viewer_examples/plugins/collection_plugin.py +++ b/viewer_examples/plugins/collection_plugin.py @@ -8,7 +8,7 @@ Demo of a CollectionViewer for viewing collections of images with the """ from skimage import data -from skimage.filter import rank +from skimage.filters import rank from skimage.morphology import disk from skimage.viewer import CollectionViewer diff --git a/viewer_examples/plugins/median_filter.py b/viewer_examples/plugins/median_filter.py index 36593c3d..5a4549a1 100644 --- a/viewer_examples/plugins/median_filter.py +++ b/viewer_examples/plugins/median_filter.py @@ -1,10 +1,13 @@ from skimage import data -from skimage.filter import median_filter +from skimage.filters.rank import median +from skimage.morphology import disk from skimage.viewer import ImageViewer from skimage.viewer.widgets import Slider, OKCancelButtons, SaveButtons from skimage.viewer.plugins.base import Plugin +def median_filter(image, radius): + return median(image, selem=disk(radius)) image = data.coins() viewer = ImageViewer(image) diff --git a/viewer_examples/plugins/tv_denoise.py b/viewer_examples/plugins/tv_denoise.py new file mode 100644 index 00000000..5ad527ec --- /dev/null +++ b/viewer_examples/plugins/tv_denoise.py @@ -0,0 +1,27 @@ +from skimage import data +from skimage.restoration import denoise_tv_chambolle +from skimage.util import img_as_float +from numpy import random, clip + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import (Slider, CheckBox, OKCancelButtons, + SaveButtons) +from skimage.viewer.plugins.base import Plugin + + +image = img_as_float(data.chelsea()) +sigma = 30/255. + +image = image + random.normal(loc=0, scale=sigma, size=image.shape) +image = clip(image, 0, 1) +viewer = ImageViewer(image) + +plugin = Plugin(image_filter=denoise_tv_chambolle) +plugin += Slider('weight', 0.01, 5, value=0.3, value_type='float') +plugin += Slider('n_iter_max', 1, 100, value=20, value_type='int') +plugin += CheckBox('multichannel', value=True) +plugin += SaveButtons() +plugin += OKCancelButtons() + +viewer += plugin +viewer.show() diff --git a/viewer_examples/plugins/watershed_demo.py b/viewer_examples/plugins/watershed_demo.py index 612ec6c9..2f043f9d 100644 --- a/viewer_examples/plugins/watershed_demo.py +++ b/viewer_examples/plugins/watershed_demo.py @@ -1,7 +1,7 @@ import matplotlib.pyplot as plt from skimage import data -from skimage import filter +from skimage import filters from skimage import morphology from skimage.viewer import ImageViewer from skimage.viewer.widgets import history diff --git a/viewer_examples/viewers/collection_viewer.py b/viewer_examples/viewers/collection_viewer.py index 61df3dd9..6083e9b1 100644 --- a/viewer_examples/viewers/collection_viewer.py +++ b/viewer_examples/viewers/collection_viewer.py @@ -23,7 +23,7 @@ from skimage.viewer import CollectionViewer from skimage.transform import pyramid_gaussian -img = data.lena() +img = data.astronaut() img_collection = tuple(pyramid_gaussian(img)) view = CollectionViewer(img_collection)