diff --git a/.travis.yml b/.travis.yml index a47dae84..91b9b5fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,10 +21,12 @@ install: - sudo apt-get install libfreeimage3 - if [[ $PYVER == '2.7' ]]; then sudo apt-get install $PYTHON-matplotlib; fi - if [[ $PYVER == '3.2' ]]; then sudo pip-$PYVER install git+git://github.com/matplotlib/matplotlib.git@v1.2.x; fi - - sudo pip-$PYVER install flake8 --use-mirrors + - sudo pip-$PYVER install flake8 - $PYTHON setup.py build - sudo $PYTHON setup.py install script: + # Check if setup.py's match bento.info + - $PYTHON check_bento_build.py # Change into an innocuous directory and find tests from installation - mkdir $HOME/.matplotlib - "echo 'backend : Agg' > $HOME/.matplotlib/matplotlibrc" @@ -35,4 +37,6 @@ script: # Change back to repository root directory and run all doc examples - cd .. - for f in doc/examples/*.py; do $PYTHON "$f"; if [ $? -ne 0 ]; then exit 1; fi done - - flake8 --exit-zero skimage doc/examples viewer_examples + - for f in doc/examples/applications/*.py; do $PYTHON "$f"; if [ $? -ne 0 ]; then exit 1; fi done + # Run pep8 and flake tests + - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 38ec8e4b..331b4ed8 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -154,6 +154,26 @@ detailing the test coverage:: skimage/filter/__init__ 1 1 100% ... +Activate Travis-CI for your fork (optional) +------------------------------------------- + +Travis-CI checks all unittests in the project to prevent breakage. + +Before sending a pull request, you may want to check that Travis-CI +successfully passes all tests. To do so, + + * Go to `Travis-CI `__ and follow the Sign In link at the top + + * Go to your `profile page `__ and switch on your + scikit-image fork + +It corresponds to steps one and two in +`Travis-CI documentation `__ +(Step three is already done in scikit-image). + +Thus, as soon as you push your code to your fork, it will trigger Travis-CI, +and you will receive an email notification when the process is done. + Bugs ---- diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index e03271b8..f8f78c52 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -114,7 +114,7 @@ - Joshua Warner Multichannel random walker segmentation, unified peak finder backend, - n-dimensional array padding, bug and doc fixes. + n-dimensional array padding, marching cubes, bug and doc fixes. - Petter Strandmark Perimeter calculation in regionprops. @@ -132,7 +132,8 @@ Dense DAISY feature description, circle perimeter drawing. - François Boulogne - Drawing: Andres Method for circle perimeter, ellipse perimeter drawing, Bezier curve. + Drawing: Andres Method for circle perimeter, ellipse perimeter drawing, + Bezier curve, anti-aliasing. Circular and elliptical Hough Transforms Various fixes @@ -144,3 +145,16 @@ - Jostein Bø Fløystad Reconstruction circle mode for Radon transform + Simultaneous Algebraic Reconstruction Technique for inverse Radon transform + +- Matt Terry + Color difference functions + +- Eugene Dvoretsky + Yen threshold implementation. + +- Riaan van den Dool + skimage.io plugin: GDAL + +- Fedor Morozov + Drawing: Wu's anti-aliased circle diff --git a/DEPENDS.txt b/DEPENDS.txt index b2858256..04b745c2 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -2,11 +2,15 @@ Build Requirements ------------------ * `Python >= 2.5 `__ * `Numpy >= 1.6 `__ -* `Cython >= 0.15 `__ +* `Cython >= 0.17 `__ `Matplotlib >= 1.0 `__ is needed to generate the examples in the documentation. +You can use pip to automatically install the base dependencies as follows:: + + $ pip install -r requirements.txt + Runtime requirements -------------------- * `SciPy >= 0.10 `__ @@ -31,10 +35,20 @@ Optional Requirements You can use this scikit with the basic requirements listed above, but some functionality is only available with the following installed: -`PyQt4 `__ +* `PyQt4 `__ The ``qt`` plugin that provides ``imshow(x, fancy=True)`` and `skivi`. -`FreeImage `__ +* `FreeImage `__ The ``freeimage`` plugin provides support for reading various types of image file formats, including multi-page TIFFs. +* `PyAMG `__ + The ``pyamg`` module is used for the fast `cg_mg` mode of random + walker segmentation. + +Testing requirements +-------------------- +* `Nose `__ + A Python Unit Testing Framework +* `Coverage.py `__ + A tool that generates a unit test code coverage report diff --git a/RELEASE.txt b/RELEASE.txt index 80bf4cab..d29143aa 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -1,6 +1,8 @@ How to make a new release of ``skimage`` ======================================== +- Check ``TODO.txt`` for any outstanding tasks. + - Update release notes. - To show a list contributors, run ``doc/release/contributors.sh ``, diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 00000000..9bc05571 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,20 @@ +Version 0.10 +------------ +* Remove deprecated functions in `skimage.filter.rank.*` +* Remove deprecated parameter `epsilon` of `skimage.viewer.LineProfile` +* Remove backwards-compatability of `skimage.measure.regionprops` +* Remove {`ratio`, `sigma`} deprecation warnings of `skimage.segmentation.slic` +* Change default mode of random_walker segmentation to 'cg_mg' > 'cg' > 'bf', + depending on which optional dependencies are available. +* Remove deprecated `out` parameter of `skimage.morphology.binary_*` +* Remove deprecated parameter `depth` in `skimage.segmentation.random_walker` +* Remove deprecated logger function in ``skimage/__init__.py`` + +Version 0.9 +----------- +* Remove deprecated functions + - `skimage.filter.denoise_tv_chambolle` + - `skimage.morphology.is_local_maximum` + - `skimage.transform.hough` + - `skimage.transform.probabilistic_hough` + - `skimage.transform.hough_peaks` diff --git a/bento.info b/bento.info index 4cfa0173..b1169244 100644 --- a/bento.info +++ b/bento.info @@ -51,6 +51,9 @@ Library: Extension: skimage.measure._moments Sources: skimage/measure/_moments.pyx + Extension: skimage.measure._marching_cubes_cy + Sources: + skimage/measure/_marching_cubes_cy.pyx Extension: skimage.graph._mcp Sources: skimage/graph/_mcp.pyx @@ -90,6 +93,12 @@ Library: Extension: skimage.morphology._greyreconstruct Sources: skimage/morphology/_greyreconstruct.pyx + Extension: skimage.feature.censure_cy + Sources: + skimage/feature/censure_cy.pyx + Extension: skimage.feature._brief_cy + Sources: + skimage/feature/_brief_cy.pyx Extension: skimage.feature.corner_cy Sources: skimage/feature/corner_cy.pyx @@ -108,6 +117,9 @@ Library: Extension: skimage.morphology._skeletonize_cy Sources: skimage/morphology/_skeletonize_cy.pyx + Extension: skimage.transform._radon_transform + Sources: + skimage/transform/_radon_transform.pyx Extension: skimage.transform._warps_cy Sources: skimage/transform/_warps_cy.pyx @@ -120,36 +132,18 @@ Library: Extension: skimage._shared.geometry Sources: skimage/_shared/geometry.pyx - Extension: skimage.filter.rank._core16 + Extension: skimage.filter.rank.generic_cy Sources: - skimage/filter/rank/_core16.pyx - Extension: skimage.filter.rank._crank8 + skimage/filter/rank/generic_cy.pyx + Extension: skimage.filter.rank.percentile_cy Sources: - skimage/filter/rank/_crank8.pyx - Extension: skimage.filter.rank._crank16 + skimage/filter/rank/percentile_cy.pyx + Extension: skimage.filter.rank.core_cy Sources: - skimage/filter/rank/_crank16.pyx - Extension: skimage.filter.rank._core8 + skimage/filter/rank/core_cy.pyx + Extension: skimage.filter.rank.bilateral_cy Sources: - skimage/filter/rank/_core8.pyx - Extension: skimage.filter.rank.rank - Sources: - skimage/filter/rank/rank.pyx - Extension: skimage.filter.rank.bilateral_rank - Sources: - skimage/filter/rank/bilateral_rank.pyx - Extension: skimage.filter.rank._crank16_percentiles - Sources: - skimage/filter/rank/_crank16_percentiles.pyx - Extension: skimage.filter.rank.percentile_rank - Sources: - skimage/filter/rank/percentile_rank.pyx - Extension: skimage.filter.rank._crank8_percentiles - Sources: - skimage/filter/rank/_crank8_percentiles.pyx - Extension: skimage.filter.rank._crank16_bilateral - Sources: - skimage/filter/rank/_crank16_bilateral.pyx + skimage/filter/rank/bilateral_cy.pyx Executable: skivi Module: skimage.scripts.skivi diff --git a/check_bento_build.py b/check_bento_build.py index 1b069056..bf3c5771 100644 --- a/check_bento_build.py +++ b/check_bento_build.py @@ -3,6 +3,7 @@ Check that Cython extensions in setup.py files match those in bento.info. """ import os import re +import sys RE_CYTHON = re.compile("config.add_extension\(\s*['\"]([\S]+)['\"]") @@ -93,3 +94,6 @@ if __name__ == '__main__': cy_bento, cy_setup = remove_common_extensions(cy_bento, cy_setup) print_results(cy_bento, cy_setup) + + if cy_setup or cy_bento: + sys.exit(1) diff --git a/doc/Makefile b/doc/Makefile index 311ce3ca..a593aa56 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -2,9 +2,10 @@ # # You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = +PYTHON ?= python +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +PAPER ?= # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 @@ -36,14 +37,14 @@ clean: -find ./source/auto_examples/* -type f | grep -v blank | xargs rm -f api: @mkdir -p source/api - python tools/build_modref_templates.py + $(PYTHON) tools/build_modref_templates.py @echo "Build API docs...done." random_gallery: - @cd source && python random_gallery.py + @cd source && $(PYTHON) random_gallery.py coveragetable: - @cd source && python coverage_generator.py + @cd source && $(PYTHON) coverage_generator.py html: api coveragetable random_gallery $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DEST)/html @@ -120,10 +121,10 @@ doctest: "results in build/doctest/output.txt." gh-pages: - python gh-pages.py + $(PYTHON) gh-pages.py gitwash: - python tools/gitwash/gitwash_dumper.py source scikit-image \ + $(PYTHON) tools/gitwash/gitwash_dumper.py source scikit-image \ --project-url=http://scikit-image.org \ --project-ml-url=http://groups.google.com/group/scikit-image \ --repo-name=scikit-image \ diff --git a/doc/examples/applications/plot_rank_filters.py b/doc/examples/applications/plot_rank_filters.py index 31284226..87dad0d0 100644 --- a/doc/examples/applications/plot_rank_filters.py +++ b/doc/examples/applications/plot_rank_filters.py @@ -3,11 +3,11 @@ Rank filters ============ -Rank filters are non-linear filters using the local greylevels ordering to +Rank filters are non-linear filters using the local gray-level ordering to compute the filtered value. This ensemble of filters share a common base: the -local grey-level histogram extraction computed on the neighborhood of a pixel -(defined by a 2D structuring element). If the filtered value is taken as the -middle value of the histogram, we get the classical median filter. +local gray-level histogram is computed on the neighborhood of a pixel (defined +by a 2-D structuring element). If the filtered value is taken as the middle +value of the histogram, we get the classical median filter. Rank filters can be used for several purposes such as: @@ -26,11 +26,9 @@ Rank filters can be used for several purposes such as: Some well known filters are specific cases of rank filters [1]_ e.g. morphological dilation, morphological erosion, median filters. -The different implementation availables in `skimage` are compared. - -In this example, we will see how to filter a greylevel image using some of the -linear and non-linear filters availables in skimage. We use the `camera` -image from `skimage.data`. +In this example, we will see how to filter a gray-level image using some of the +linear and non-linear filters available in skimage. We use the `camera` image +from `skimage.data` for all comparisons. .. [1] Pierre Soille, On morphological operators based on rank filters, Pattern Recognition 35 (2002) 527-535. @@ -40,18 +38,19 @@ image from `skimage.data`. import numpy as np import matplotlib.pyplot as plt +from skimage import img_as_ubyte from skimage import data -ima = data.camera() -hist = np.histogram(ima, bins=np.arange(0, 256)) +noisy_image = img_as_ubyte(data.camera()) +hist = np.histogram(noisy_image, bins=np.arange(0, 256)) plt.figure(figsize=(8, 3)) plt.subplot(1, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray, interpolation='nearest') +plt.imshow(noisy_image, interpolation='nearest') plt.axis('off') plt.subplot(1, 2, 2) plt.plot(hist[1][:-1], hist[0], lw=2) -plt.title('histogram of grey values') +plt.title('Histogram of grey values') """ @@ -65,50 +64,56 @@ randomly set to 0. The **median** filter is applied to remove the noise. .. note:: - there are different implementations of median filter : + There are different implementations of median filter: `skimage.filter.median_filter` and `skimage.filter.rank.median` """ -noise = np.random.random(ima.shape) -nima = data.camera() -nima[noise > 0.99] = 255 -nima[noise < 0.01] = 0 - from skimage.filter.rank import median from skimage.morphology import disk -fig = plt.figure(figsize=[10, 7]) +noise = np.random.random(noisy_image.shape) +noisy_image = img_as_ubyte(data.camera()) +noisy_image[noise > 0.99] = 255 +noisy_image[noise < 0.01] = 0 + +fig = plt.figure(figsize=(10, 7)) -lo = median(nima, disk(1)) -hi = median(nima, disk(5)) -ext = median(nima, disk(20)) plt.subplot(2, 2, 1) -plt.imshow(nima, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('noised image') +plt.imshow(noisy_image, vmin=0, vmax=255) +plt.title('Noisy image') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(lo, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('median $r=1$') +plt.imshow(median(noisy_image, disk(1)), vmin=0, vmax=255) +plt.title('Median $r=1$') +plt.axis('off') + plt.subplot(2, 2, 3) -plt.imshow(hi, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('median $r=5$') +plt.imshow(median(noisy_image, disk(5)), vmin=0, vmax=255) +plt.title('Median $r=5$') +plt.axis('off') + plt.subplot(2, 2, 4) -plt.imshow(ext, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('median $r=20$') +plt.imshow(median(noisy_image, disk(20)), vmin=0, vmax=255) +plt.title('Median $r=20$') +plt.axis('off') """ .. image:: PLOT2RST.current_figure -The added noise is efficiently removed, as the image defaults are small (1 pixel -wide), a small filter radius is sufficient. As the radius is increasing, objects -with a bigger size are filtered as well, such as the camera tripod. The median -filter is commonly used for noise removal because borders are preserved. +The added noise is efficiently removed, as the image defaults are small (1 +pixel wide), a small filter radius is sufficient. As the radius is increasing, +objects with bigger sizes are filtered as well, such as the camera tripod. The +median filter is often used for noise removal because borders are preserved and +e.g. salt and pepper noise typically does not distort the gray-level. Image smoothing ================ -The example hereunder shows how a local **mean** smoothes the camera man image. +The example hereunder shows how a local **mean** filter smooths the camera man +image. """ @@ -116,13 +121,17 @@ from skimage.filter.rank import mean fig = plt.figure(figsize=[10, 7]) -loc_mean = mean(nima, disk(10)) +loc_mean = mean(noisy_image, disk(10)) + plt.subplot(1, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('original') +plt.imshow(noisy_image, vmin=0, vmax=255) +plt.title('Original') +plt.axis('off') + plt.subplot(1, 2, 2) -plt.imshow(loc_mean, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('local mean $r=10$') +plt.imshow(loc_mean, vmin=0, vmax=255) +plt.title('Local mean $r=10$') +plt.axis('off') """ @@ -130,35 +139,41 @@ plt.xlabel('local mean $r=10$') One may be interested in smoothing an image while preserving important borders (median filters already achieved this), here we use the **bilateral** filter -that restricts the local neighborhood to pixel having a greylevel similar to +that restricts the local neighborhood to pixel having a gray-level similar to the central one. .. note:: - a different implementation is available for color images in + A different implementation is available for color images in `skimage.filter.denoise_bilateral`. """ from skimage.filter.rank import bilateral_mean -ima = data.camera() -selem = disk(10) +noisy_image = img_as_ubyte(data.camera()) -bilat = bilateral_mean(ima.astype(np.uint16), disk(20), s0=10, s1=10) +bilat = bilateral_mean(noisy_image.astype(np.uint16), disk(20), s0=10, s1=10) -# display results fig = plt.figure(figsize=[10, 7]) + plt.subplot(2, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray) -plt.xlabel('original') +plt.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(bilat, cmap=plt.cm.gray) -plt.xlabel('bilateral mean') +plt.title('Bilateral mean') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(bilat[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') """ @@ -175,7 +190,7 @@ We compare here how the global histogram equalization is applied locally. The equalized image [2]_ has a roughly linear cumulative distribution function for each pixel neighborhood. The local version [3]_ of the histogram -equalization emphasizes every local greylevel variations. +equalization emphasizes every local gray-level variations. .. [2] http://en.wikipedia.org/wiki/Histogram_equalization .. [3] http://en.wikipedia.org/wiki/Adaptive_histogram_equalization @@ -185,101 +200,112 @@ equalization emphasizes every local greylevel variations. from skimage import exposure from skimage.filter import rank -ima = data.camera() +noisy_image = img_as_ubyte(data.camera()) + # equalize globally and locally -glob = exposure.equalize(ima) * 255 -loc = rank.equalize(ima, disk(20)) +glob = exposure.equalize(noisy_image) * 255 +loc = rank.equalize(noisy_image, disk(20)) # extract histogram for each image -hist = np.histogram(ima, bins=np.arange(0, 256)) +hist = np.histogram(noisy_image, bins=np.arange(0, 256)) glob_hist = np.histogram(glob, bins=np.arange(0, 256)) loc_hist = np.histogram(loc, bins=np.arange(0, 256)) plt.figure(figsize=(10, 10)) + plt.subplot(321) -plt.imshow(ima, cmap=plt.cm.gray, interpolation='nearest') +plt.imshow(noisy_image, interpolation='nearest') plt.axis('off') + plt.subplot(322) plt.plot(hist[1][:-1], hist[0], lw=2) -plt.title('histogram of grey values') +plt.title('Histogram of gray values') + plt.subplot(323) -plt.imshow(glob, cmap=plt.cm.gray, interpolation='nearest') +plt.imshow(glob, interpolation='nearest') plt.axis('off') + plt.subplot(324) plt.plot(glob_hist[1][:-1], glob_hist[0], lw=2) -plt.title('histogram of grey values') +plt.title('Histogram of gray values') + plt.subplot(325) -plt.imshow(loc, cmap=plt.cm.gray, interpolation='nearest') +plt.imshow(loc, interpolation='nearest') plt.axis('off') + plt.subplot(326) plt.plot(loc_hist[1][:-1], loc_hist[0], lw=2) -plt.title('histogram of grey values') +plt.title('Histogram of gray values') """ .. image:: PLOT2RST.current_figure -another way to maximize the number of greylevels used for an image is to apply -a local autoleveling, i.e. here a pixel greylevel is proportionally remapped -between local minimum and local maximum. +Another way to maximize the number of gray-levels used for an image is to apply +a local auto-leveling, i.e. the gray-value of a pixel is proportionally +remapped between local minimum and local maximum. -The following example shows how local autolevel enhances the camara man picture. +The following example shows how local auto-level enhances the camara man +picture. """ from skimage.filter.rank import autolevel -ima = data.camera() -selem = disk(10) +noisy_image = img_as_ubyte(data.camera()) -auto = autolevel(ima.astype(np.uint16), disk(20)) +auto = autolevel(noisy_image.astype(np.uint16), disk(20)) -# display results fig = plt.figure(figsize=[10, 7]) + plt.subplot(1, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray) -plt.xlabel('original') +plt.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(1, 2, 2) plt.imshow(auto, cmap=plt.cm.gray) -plt.xlabel('local autolevel') +plt.title('Local autolevel') +plt.axis('off') """ .. image:: PLOT2RST.current_figure -This filter is very sensitive to local outlayers, see the little white spot in -the sky left part. This is due to a local maximum which is very high comparing -to the rest of the neighborhood. One can moderate this using the percentile -version of the autolevel filter which uses given percentiles (one inferior, -one superior) in place of local minimum and maximum. The example below -illustrates how the percentile parameters influence the local autolevel result. +This filter is very sensitive to local outliers, see the little white spot in +the left part of the sky. This is due to a local maximum which is very high +comparing to the rest of the neighborhood. One can moderate this using the +percentile version of the auto-level filter which uses given percentiles (one +inferior, one superior) in place of local minimum and maximum. The example +below illustrates how the percentile parameters influence the local auto-level +result. """ -from skimage.filter.rank import percentile_autolevel +from skimage.filter.rank import autolevel_percentile image = data.camera() selem = disk(20) loc_autolevel = autolevel(image, selem=selem) -loc_perc_autolevel0 = percentile_autolevel(image, selem=selem, p0=.00, p1=1.0) -loc_perc_autolevel1 = percentile_autolevel(image, selem=selem, p0=.01, p1=.99) -loc_perc_autolevel2 = percentile_autolevel(image, selem=selem, p0=.05, p1=.95) -loc_perc_autolevel3 = percentile_autolevel(image, selem=selem, p0=.1, p1=.9) +loc_perc_autolevel0 = autolevel_percentile(image, selem=selem, p0=.00, p1=1.0) +loc_perc_autolevel1 = autolevel_percentile(image, selem=selem, p0=.01, p1=.99) +loc_perc_autolevel2 = autolevel_percentile(image, selem=selem, p0=.05, p1=.95) +loc_perc_autolevel3 = autolevel_percentile(image, selem=selem, p0=.1, p1=.9) fig, axes = plt.subplots(nrows=3, figsize=(7, 8)) ax0, ax1, ax2 = axes plt.gray() ax0.imshow(np.hstack((image, loc_autolevel))) -ax0.set_title('original / autolevel') +ax0.set_title('Original / auto-level') ax1.imshow( np.hstack((loc_perc_autolevel0, loc_perc_autolevel1)), vmin=0, vmax=255) -ax1.set_title('percentile autolevel 0%,1%') +ax1.set_title('Percentile auto-level 0%,1%') ax2.imshow( np.hstack((loc_perc_autolevel2, loc_perc_autolevel3)), vmin=0, vmax=255) -ax2.set_title('percentile autolevel 5% and 10%') +ax2.set_title('Percentile auto-level 5% and 10%') for ax in axes: ax.axis('off') @@ -289,29 +315,35 @@ for ax in axes: .. image:: PLOT2RST.current_figure The morphological contrast enhancement filter replaces the central pixel by the -local maximum if the original pixel value is closest to local maximum, otherwise -by the minimum local. +local maximum if the original pixel value is closest to local maximum, +otherwise by the minimum local. """ -from skimage.filter.rank import morph_contr_enh +from skimage.filter.rank import enhance_contrast -ima = data.camera() +noisy_image = img_as_ubyte(data.camera()) -enh = morph_contr_enh(ima, disk(5)) +enh = enhance_contrast(noisy_image, disk(5)) -# display results fig = plt.figure(figsize=[10, 7]) plt.subplot(2, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray) -plt.xlabel('original') +plt.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(enh, cmap=plt.cm.gray) -plt.xlabel('local morphlogical contrast enhancement') +plt.title('Local morphological contrast enhancement') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(enh[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') """ @@ -322,24 +354,30 @@ percentile *p0* and *p1* instead of the local minimum and maximum. """ -from skimage.filter.rank import percentile_morph_contr_enh +from skimage.filter.rank import enhance_contrast_percentile -ima = data.camera() +noisy_image = img_as_ubyte(data.camera()) -penh = percentile_morph_contr_enh(ima, disk(5), p0=.1, p1=.9) +penh = enhance_contrast_percentile(noisy_image, disk(5), p0=.1, p1=.9) -# display results fig = plt.figure(figsize=[10, 7]) plt.subplot(2, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray) -plt.xlabel('original') +plt.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(penh, cmap=plt.cm.gray) -plt.xlabel('local percentile morphlogical\n contrast enhancement') +plt.title('Local percentile morphological\n contrast enhancement') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(penh[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') """ @@ -348,18 +386,18 @@ plt.imshow(penh[200:350, 350:450], cmap=plt.cm.gray) Image threshold =============== -The Otsu's threshold [1]_ method can be applied locally using the local -greylevel distribution. In the example below, for each pixel, an "optimal" -threshold is determined by maximizing the variance between two classes of pixels -of the local neighborhood defined by a structuring element. +The Otsu threshold [1]_ method can be applied locally using the local gray- +level distribution. In the example below, for each pixel, an "optimal" +threshold is determined by maximizing the variance between two classes of +pixels of the local neighborhood defined by a structuring element. The example compares the local threshold with the global threshold `skimage.filter.threshold_otsu`. .. note:: - Local thresholding is much slower than global one. There exists a function - for global Otsu thresholding: `skimage.filter.threshold_otsu`. + Local is much slower than global thresholding. A function for global Otsu + thresholding can be found in : `skimage.filter.threshold_otsu`. .. [4] http://en.wikipedia.org/wiki/Otsu's_method @@ -382,27 +420,35 @@ t_glob_otsu = threshold_otsu(p8) glob_otsu = p8 >= t_glob_otsu plt.figure() + plt.subplot(2, 2, 1) plt.imshow(p8, cmap=plt.cm.gray) -plt.xlabel('original') +plt.title('Original') plt.colorbar() +plt.axis('off') + plt.subplot(2, 2, 2) plt.imshow(t_loc_otsu, cmap=plt.cm.gray) -plt.xlabel('local Otsu ($radius=%d$)' % radius) +plt.title('Local Otsu ($r=%d$)' % radius) plt.colorbar() +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(p8 >= t_loc_otsu, cmap=plt.cm.gray) -plt.xlabel('original>=local Otsu' % t_glob_otsu) +plt.title('Original >= local Otsu' % t_glob_otsu) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(glob_otsu, cmap=plt.cm.gray) -plt.xlabel('global Otsu ($t=%d$)' % t_glob_otsu) +plt.title('Global Otsu ($t=%d$)' % t_glob_otsu) +plt.axis('off') """ .. image:: PLOT2RST.current_figure -The following example shows how local Otsu's threshold handles a global level -shift applied to a synthetic image . +The following example shows how local Otsu thresholding handles a global level +shift applied to a synthetic image. """ @@ -413,13 +459,18 @@ m = (np.tile(x, (n, 1)) * np.linspace(0.1, 1, n) * 128 + 128).astype(np.uint8) radius = 10 t = rank.otsu(m, disk(radius)) + plt.figure() + plt.subplot(1, 2, 1) plt.imshow(m) -plt.xlabel('original') +plt.title('Original') +plt.axis('off') + plt.subplot(1, 2, 2) plt.imshow(m >= t, interpolation='nearest') -plt.xlabel('local Otsu ($radius=%d$)' % radius) +plt.title('Local Otsu ($r=%d$)' % radius) +plt.axis('off') """ @@ -428,7 +479,7 @@ plt.xlabel('local Otsu ($radius=%d$)' % radius) Image morphology ================ -Local maximum and local minimum are the base operators for greylevel +Local maximum and local minimum are the base operators for gray-level morphology. .. note:: @@ -436,33 +487,41 @@ morphology. `skimage.dilate` and `skimage.erode` are equivalent filters (see below for comparison). -Here is an example of the classical morphological greylevel filters: opening, +Here is an example of the classical morphological gray-level filters: opening, closing and morphological gradient. """ from skimage.filter.rank import maximum, minimum, gradient -ima = data.camera() +noisy_image = img_as_ubyte(data.camera()) -closing = maximum(minimum(ima, disk(5)), disk(5)) -opening = minimum(maximum(ima, disk(5)), disk(5)) -grad = gradient(ima, disk(5)) +closing = maximum(minimum(noisy_image, disk(5)), disk(5)) +opening = minimum(maximum(noisy_image, disk(5)), disk(5)) +grad = gradient(noisy_image, disk(5)) # display results fig = plt.figure(figsize=[10, 7]) + plt.subplot(2, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray) -plt.xlabel('original') +plt.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 2) plt.imshow(closing, cmap=plt.cm.gray) -plt.xlabel('greylevel closing') +plt.title('Gray-level closing') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(opening, cmap=plt.cm.gray) -plt.xlabel('greylevel opening') +plt.title('Gray-level opening') +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(grad, cmap=plt.cm.gray) -plt.xlabel('morphological gradient') +plt.title('Morphological gradient') +plt.axis('off') """ @@ -471,13 +530,14 @@ plt.xlabel('morphological gradient') Feature extraction =================== -Local histogram can be exploited to compute local entropy, which is related to +Local histograms can be exploited to compute local entropy, which is related to the local image complexity. Entropy is computed using base 2 logarithm i.e. the -filter returns the minimum number of bits needed to encode local greylevel +filter returns the minimum number of bits needed to encode local gray-level distribution. -`skimage.rank.entropy` returns local entropy on a given structuring element. -The following example shows this filter applied on 8- and 16- bit images. +`skimage.rank.entropy` returns the local entropy on a given structuring +element. The following example shows applies this filter on 8- and 16-bit +images. .. note:: @@ -492,47 +552,36 @@ from skimage.morphology import disk import numpy as np import matplotlib.pyplot as plt -# defining a 8- and a 16-bit test images -a8 = data.camera() -a16 = data.camera().astype(np.uint16) * 4 +image = data.camera() -ent8 = entropy(a8, disk(5)) # pixel value contain 10x the local entropy -ent16 = entropy(a16, disk(5)) # pixel value contain 1000x the local entropy +plt.figure(figsize=(10, 4)) -# display results -plt.figure(figsize=(10, 10)) - -plt.subplot(2, 2, 1) -plt.imshow(a8, cmap=plt.cm.gray) -plt.xlabel('8-bit image') +plt.subplot(1, 2, 1) +plt.imshow(image, cmap=plt.cm.gray) +plt.title('Image') plt.colorbar() +plt.axis('off') -plt.subplot(2, 2, 2) -plt.imshow(ent8, cmap=plt.cm.jet) -plt.xlabel('entropy*10') -plt.colorbar() - -plt.subplot(2, 2, 3) -plt.imshow(a16, cmap=plt.cm.gray) -plt.xlabel('16-bit image') -plt.colorbar() - -plt.subplot(2, 2, 4) -plt.imshow(ent16, cmap=plt.cm.jet) -plt.xlabel('entropy*1000') +plt.subplot(1, 2, 2) +plt.imshow(entropy(image, disk(5)), cmap=plt.cm.jet) +plt.title('Entropy') plt.colorbar() +plt.axis('off') """ .. image:: PLOT2RST.current_figure Implementation -================ +============== -The central part of the `skimage.rank` filters is build on a sliding window that -update local greylevel histogram. This approach limits the algorithm complexity -to O(n) where n is the number of image pixels. The complexity is also limited -with respect to the structuring element size. +The central part of the `skimage.rank` filters is build on a sliding window +that updates the local gray-level histogram. This approach limits the algorithm +complexity to O(n) where n is the number of image pixels. The complexity is +also limited with respect to the structuring element size. + +In the following we compare the performance of different implementations +available in `skimage`. """ @@ -583,10 +632,10 @@ def ndi_med(image, n): Comparison between -* `rank.maximum` -* `cmorph.dilate` +* `filter.rank.maximum` +* `morphology.dilate` -on increasing structuring element size +on increasing structuring element size: """ @@ -603,18 +652,18 @@ for r in e_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing element size') -plt.ylabel('time (ms)') -plt.xlabel('element radius') +plt.title('Performance with respect to element size') +plt.ylabel('Time (ms)') +plt.title('Element radius') plt.plot(e_range, rec) -plt.legend(['crank.maximum', 'cmorph.dilate']) +plt.legend(['filter.rank.maximum', 'morphology.dilate']) """ -and increasing image size - .. image:: PLOT2RST.current_figure +and increasing image size: + """ r = 9 @@ -623,7 +672,7 @@ elem = disk(r + 1) rec = [] s_range = range(100, 1000, 100) for s in s_range: - a = (np.random.random((s, s)) * 256).astype('uint8') + a = (np.random.random((s, s)) * 256).astype(np.uint8) (rc, ms_rc) = cr_max(a, elem) (rcm, ms_rcm) = cm_dil(a, elem) rec.append((ms_rc, ms_rcm)) @@ -631,11 +680,11 @@ for s in s_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing image size') -plt.ylabel('time (ms)') -plt.xlabel('image size') +plt.title('Performance with respect to image size') +plt.ylabel('Time (ms)') +plt.title('Image size') plt.plot(s_range, rec) -plt.legend(['crank.maximum', 'cmorph.dilate']) +plt.legend(['filter.rank.maximum', 'morphology.dilate']) """ @@ -644,11 +693,11 @@ plt.legend(['crank.maximum', 'cmorph.dilate']) Comparison between: -* `rank.median` -* `ctmf.median_filter` -* `ndimage.percentile` +* `filter.rank.median` +* `filter.median_filter` +* `scipy.ndimage.percentile` -on increasing structuring element size +on increasing structuring element size: """ @@ -666,27 +715,29 @@ for r in e_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing element size') +plt.title('Performance with respect to element size') plt.plot(e_range, rec) -plt.legend(['rank.median', 'ctmf.median_filter', 'ndimage.percentile']) -plt.ylabel('time (ms)') -plt.xlabel('element radius') +plt.legend(['filter.rank.median', 'filter.median_filter', + 'scipy.ndimage.percentile']) +plt.ylabel('Time (ms)') +plt.title('Element radius') """ .. image:: PLOT2RST.current_figure -comparison of outcome of the three methods +Comparison of outcome of the three methods: """ plt.figure() plt.imshow(np.hstack((rc, rctmf, rndi))) -plt.xlabel('rank.median vs ctmf.median_filter vs ndimage.percentile') +plt.title('filter.rank.median vs filtermedian_filter vs scipy.ndimage.percentile') +plt.axis('off') """ .. image:: PLOT2RST.current_figure -and increasing image size +and increasing image size: """ @@ -696,7 +747,7 @@ elem = disk(r + 1) rec = [] s_range = [100, 200, 500, 1000] for s in s_range: - a = (np.random.random((s, s)) * 256).astype('uint8') + a = (np.random.random((s, s)) * 256).astype(np.uint8) (rc, ms_rc) = cr_med(a, elem) rctmf, ms_rctmf = ctmf_med(a, r) rndi, ms_ndi = ndi_med(a, r) @@ -705,11 +756,12 @@ for s in s_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing image size') +plt.title('Performance with respect to image size') plt.plot(s_range, rec) -plt.legend(['rank.median', 'ctmf.median_filter', 'ndimage.percentile']) -plt.ylabel('time (ms)') -plt.xlabel('image size') +plt.legend(['filter.rank.median', 'filter.median_filter', + 'scipy.ndimage.percentile']) +plt.ylabel('Time (ms)') +plt.title('Image size') """ .. image:: PLOT2RST.current_figure diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py new file mode 100755 index 00000000..7fb67046 --- /dev/null +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -0,0 +1,148 @@ +""" +======================================== +Circular and Elliptical Hough Transforms +======================================== + +The Hough transform in its simplest form is a `method to detect +straight lines `__ +but it can also be used to detect circles or ellipses. +The algorithm assumes that the edge is detected and it is robust against +noise or missing points. + +Circle detection +================ + +In the following example, the Hough transform is used to detect +coin positions and match their edges. We provide a range of +plausible radii. For each radius, two circles are extracted and +we finally keep the five most prominent candidates. +The result shows that coin positions are well-detected. + + +Algorithm overview +------------------ + +Given a black circle on a white background, we first guess its +radius (or a range of radii) to construct a new circle. +This circle is applied on each black pixel of the original picture +and the coordinates of this circle are voting in an accumulator. +From this geometrical construction, the original circle center +position receives the highest score. + +Note that the accumulator size is built to be larger than the +original picture in order to detect centers outside the frame. +Its size is extended by two times the larger radius. + +""" +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data, filter, color +from skimage.transform import hough_circle +from skimage.feature import peak_local_max +from skimage.draw import circle_perimeter +from skimage.util import img_as_ubyte + + +# Load picture and detect edges +image = img_as_ubyte(data.coins()[0:95, 70:370]) +edges = filter.canny(image, sigma=3, low_threshold=10, high_threshold=50) + +fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) + +# Detect two radii +hough_radii = np.arange(15, 30, 2) +hough_res = hough_circle(edges, hough_radii) + +centers = [] +accums = [] +radii = [] + +for radius, h in zip(hough_radii, hough_res): + # For each radius, extract two circles + peaks = peak_local_max(h, num_peaks=2) + centers.extend(peaks) + accums.extend(h[peaks[:, 0], peaks[:, 1]]) + radii.extend([radius, radius]) + +# Draw the most prominent 5 circles +image = color.gray2rgb(image) +for idx in np.argsort(accums)[::-1][:5]: + center_x, center_y = centers[idx] + radius = radii[idx] + cx, cy = circle_perimeter(center_y, center_x, radius) + image[cy, cx] = (220, 20, 20) + +ax.imshow(image, cmap=plt.cm.gray) + + +""" +Ellipse detection +================= + +In this second example, the aim is to detect the edge of a coffee cup. +Basically, this is a projection of a circle, i.e. an ellipse. +The problem to solve is much more difficult because five parameters have to be +determined, instead of three for circles. + + +Algorithm overview +------------------ + +The algorithm takes two different points belonging to the ellipse. It assumes +that it is the main axis. A loop on all the other points determines how much +an ellipse passes to them. A good match corresponds to high accumulator values. + +A full description of the algorithm can be found in reference [1]_. + +References +---------- +.. [1] Xie, Yonghong, and Qiang Ji. "A new efficient ellipse detection + method." Pattern Recognition, 2002. Proceedings. 16th International + Conference on. Vol. 2. IEEE, 2002 +""" + +import matplotlib.pyplot as plt + +from skimage import data, filter, color +from skimage.transform import hough_ellipse +from skimage.draw import ellipse_perimeter + +# Load picture, convert to grayscale and detect edges +image_rgb = data.coffee()[0:220, 160:420] +image_gray = color.rgb2gray(image_rgb) +edges = filter.canny(image_gray, sigma=2.0, + low_threshold=0.55, high_threshold=0.8) + +# Perform a Hough Transform +# The accuracy corresponds to the bin size of a major axis. +# The value is chosen in order to get a single high accumulator. +# The threshold eliminates low accumulators +result = hough_ellipse(edges, accuracy=20, threshold=250, + min_size=100, max_size=120) +result.sort(order='accumulator') + +# Estimated parameters for the ellipse +best = result[-1] +yc = int(best[1]) +xc = int(best[2]) +a = int(best[3]) +b = int(best[4]) +orientation = best[5] + +# Draw the ellipse on the original image +cy, cx = ellipse_perimeter(yc, xc, a, b, orientation) +image_rgb[cy, cx] = (0, 0, 255) +# Draw the edge (white) and the resulting ellipse (red) +edges = color.gray2rgb(edges) +edges[cy, cx] = (250, 0, 0) + +fig2, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(10, 6)) + +ax1.set_title('Original picture') +ax1.imshow(image_rgb) + +ax2.set_title('Edge (white) and result (red)') +ax2.imshow(edges) + +plt.show() diff --git a/doc/examples/plot_circular_hough_transform.py b/doc/examples/plot_circular_hough_transform.py deleted file mode 100755 index fe4bea26..00000000 --- a/doc/examples/plot_circular_hough_transform.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -======================== -Circular Hough Transform -======================== - -The Hough transform in its simplest form is a `method to detect -straight lines `__ -but it can also be used to detect circles. - -In the following example, the Hough transform is used to detect -coin positions and match their edges. We provide a range of -plausible radii. For each radius, two circles are extracted and -we finally keep the five most prominent candidates. -The result shows that coin positions are well-detected. - - -Algorithm overview ------------------- - -Given a black circle on a white background, we first guess its -radius (or a range of radii) to construct a new circle. -This circle is applied on each black pixel of the original picture -and the coordinates of this circle are voting in an accumulator. -From this geometrical construction, the original circle center -position receives the highest score. - -Note that the accumulator size is built to be larger than the -original picture in order to detect centers outside the frame. -Its size is extended by two times the larger radius. - -""" -import numpy as np -import matplotlib.pyplot as plt - -from skimage import data, filter, color -from skimage.transform import hough_circle -from skimage.feature import peak_local_max -from skimage.draw import circle_perimeter -from skimage.util import img_as_ubyte - - -# Load picture and detect edges -image = img_as_ubyte(data.coins()[0:95, 70:370]) -edges = filter.canny(image, sigma=3, low_threshold=10, high_threshold=50) - -fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) - -# Detect two radii -hough_radii = np.arange(15, 30, 2) -hough_res = hough_circle(edges, hough_radii) - -centers = [] -accums = [] -radii = [] - -for radius, h in zip(hough_radii, hough_res): - # For each radius, extract two circles - peaks = peak_local_max(h, num_peaks=2) - centers.extend(peaks) - accums.extend(h[peaks[:, 0], peaks[:, 1]]) - radii.extend([radius, radius]) - -# Draw the most prominent 5 circles -image = color.gray2rgb(image) -for idx in np.argsort(accums)[::-1][:5]: - center_x, center_y = centers[idx] - radius = radii[idx] - cx, cy = circle_perimeter(center_y, center_x, radius) - image[cy, cx] = (220, 20, 20) - -ax.imshow(image, cmap=plt.cm.gray) -plt.show() diff --git a/doc/examples/plot_convex_hull.py b/doc/examples/plot_convex_hull.py index c5505295..31398e6d 100644 --- a/doc/examples/plot_convex_hull.py +++ b/doc/examples/plot_convex_hull.py @@ -27,9 +27,24 @@ image = np.array( [0, 1, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=float) -chull = convex_hull_image(image) -image[chull] += 1.7 -image -= -1.7 +original_image = np.copy(image) +chull = convex_hull_image(image) +image[chull] += 1 +# image is now: +#[[ 0. 0. 0. 0. 0. 0. 0. 0. 0.] +# [ 0. 0. 0. 0. 2. 0. 0. 0. 0.] +# [ 0. 0. 0. 2. 1. 2. 0. 0. 0.] +# [ 0. 0. 2. 1. 1. 1. 2. 0. 0.] +# [ 0. 2. 1. 1. 1. 1. 1. 2. 0.] +# [ 0. 0. 0. 0. 0. 0. 0. 0. 0.]] + + +fig = plt.subplots(figsize=(10, 6)) +plt.subplot(1, 2, 1) +plt.title('Original picture') +plt.imshow(original_image, cmap=plt.cm.gray, interpolation='nearest') +plt.subplot(1, 2, 2) +plt.title('Transformed picture') plt.imshow(image, cmap=plt.cm.gray, interpolation='nearest') plt.show() diff --git a/doc/examples/plot_entropy.py b/doc/examples/plot_entropy.py index db230d8b..9f208c73 100644 --- a/doc/examples/plot_entropy.py +++ b/doc/examples/plot_entropy.py @@ -3,8 +3,10 @@ Entropy ======= +Image entropy is a quantity which is used to describe the amount of information +coded in an image. + """ -import numpy as np import matplotlib.pyplot as plt from skimage import data @@ -13,33 +15,18 @@ from skimage.morphology import disk from skimage.util import img_as_ubyte -# defining a 8- and a 16-bit test images -a8 = img_as_ubyte(data.camera()) -a16 = a8.astype(np.uint16) * 4 +image = img_as_ubyte(data.camera()) -ent8 = entropy(a8, disk(5)) # pixel value contain 10x the local entropy -ent16 = entropy(a16, disk(5)) # pixel value contain 1000x the local entropy +fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10, 4)) -# display results -plt.figure(figsize=(10, 10)) +img0 = ax0.imshow(image, cmap=plt.cm.gray) +ax0.set_title('Image') +ax0.axis('off') +plt.colorbar(img0, ax=ax0) -plt.subplot(2,2,1) -plt.imshow(a8, cmap=plt.cm.gray) -plt.xlabel('8-bit image') -plt.colorbar() +img1 = ax1.imshow(entropy(image, disk(5)), cmap=plt.cm.jet) +ax1.set_title('Entropy') +ax1.axis('off') +plt.colorbar(img1, ax=ax1) -plt.subplot(2,2,2) -plt.imshow(ent8, cmap=plt.cm.jet) -plt.xlabel('entropy*10') -plt.colorbar() - -plt.subplot(2,2,3) -plt.imshow(a16, cmap=plt.cm.gray) -plt.xlabel('16-bit image') -plt.colorbar() - -plt.subplot(2,2,4) -plt.imshow(ent16, cmap=plt.cm.jet) -plt.xlabel('entropy*1000') -plt.colorbar() plt.show() diff --git a/doc/examples/plot_join_segmentations.py b/doc/examples/plot_join_segmentations.py index 2cafab15..8ccc5038 100644 --- a/doc/examples/plot_join_segmentations.py +++ b/doc/examples/plot_join_segmentations.py @@ -18,27 +18,23 @@ from skimage.filter import sobel from skimage.segmentation import slic, join_segmentations from skimage.morphology import watershed from skimage.color import label2rgb -from skimage import data +from skimage import data, img_as_float - -coins = data.coins() +coins = img_as_float(data.coins()) # make segmentation using edge-detection and watershed edges = sobel(coins) markers = np.zeros_like(coins) foreground, background = 1, 2 -markers[coins < 30] = background -markers[coins > 150] = foreground +markers[coins < 30.0 / 255] = background +markers[coins > 150.0 / 255] = foreground ws = watershed(edges, markers) seg1 = nd.label(ws == foreground)[0] # make segmentation using SLIC superpixels - -# make the RGB equivalent of `coins` -coins_colour = np.tile(coins[..., np.newaxis], (1, 1, 3)) -seg2 = slic(coins_colour, n_segments=30, max_iter=160, sigma=1, ratio=9, - convert2lab=False) +seg2 = slic(coins, n_segments=117, max_iter=160, sigma=1, compactness=0.75, + multichannel=False) # combine the two segj = join_segmentations(seg1, seg2) diff --git a/doc/examples/plot_hough_transform.py b/doc/examples/plot_line_hough_transform.py similarity index 97% rename from doc/examples/plot_hough_transform.py rename to doc/examples/plot_line_hough_transform.py index 985d97ec..cd0ae008 100644 --- a/doc/examples/plot_hough_transform.py +++ b/doc/examples/plot_line_hough_transform.py @@ -1,7 +1,7 @@ -""" -=============== -Hough transform -=============== +r""" +============================= +Straight line Hough transform +============================= The Hough transform in its simplest form is a `method to detect straight lines `__. diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index 8c9cabae..c0169cc1 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -4,27 +4,159 @@ Local Binary Pattern for texture classification =============================================== In this example, we will see how to classify textures based on LBP (Local -Binary Pattern). The histogram of the LBP result is a good measure to classify -textures. For simplicity the histogram distributions are then tested against -each other using the Kullback-Leibler-Divergence. +Binary Pattern). LBP looks at points surrounding a central point and tests +whether the surrounding points are greater than or less than the central point +(i.e. gives a binary result). +Before trying out LBP on an image, it helps to look at a schematic of LBPs. +The below code is just used to plot the schematic. """ from __future__ import print_function - import numpy as np -import matplotlib import matplotlib.pyplot as plt + +METHOD = 'uniform' +plt.rcParams['font.size'] = 9 + + +def plot_circle(ax, center, radius, color): + circle = plt.Circle(center, radius, facecolor=color, edgecolor='0.5') + ax.add_patch(circle) + + +def plot_lbp_model(ax, binary_values): + """Draw the schematic for a local binary pattern.""" + # Geometry spec + theta = np.deg2rad(45) + R = 1 + r = 0.15 + w = 1.5 + gray = '0.5' + + # Draw the central pixel. + plot_circle(ax, (0, 0), radius=r, color=gray) + # Draw the surrounding pixels. + for i, facecolor in enumerate(binary_values): + x = R * np.cos(i * theta) + y = R * np.sin(i * theta) + plot_circle(ax, (x, y), radius=r, color=str(facecolor)) + + # Draw the pixel grid. + for x in np.linspace(-w, w, 4): + ax.axvline(x, color=gray) + ax.axhline(x, color=gray) + + # Tweak the layout. + ax.axis('image') + ax.axis('off') + size = w + 0.2 + ax.set_xlim(-size, size) + ax.set_ylim(-size, size) + + +fig, axes = plt.subplots(ncols=5, figsize=(7, 2)) + +titles = ['flat', 'flat', 'edge', 'corner', 'non-uniform'] + +binary_patterns = [np.zeros(8), + np.ones(8), + np.hstack([np.ones(4), np.zeros(4)]), + np.hstack([np.zeros(3), np.ones(5)]), + [1, 0, 0, 1, 1, 1, 0, 0]] + +for ax, values, name in zip(axes, binary_patterns, titles): + plot_lbp_model(ax, values) + ax.set_title(name) + +""" +.. image:: PLOT2RST.current_figure + +The figure above shows example results with black (or white) representing +pixels that are less (or more) intense than the central pixel. When surrounding +pixels are all black or all white, then that image region is flat (i.e. +featureless). Groups of continuous black or white pixels are considered +"uniform" patterns that can be interpreted as corners or edges. If pixels +switch back-and-forth between black and white pixels, the pattern is considered +"non-uniform". + +When using LBP to detect texture, you measure a collection of LBPs over an +image patch and look at the distribution of these LBPs. Lets apply LBP to +a brick texture. +""" + from skimage.transform import rotate from skimage.feature import local_binary_pattern from skimage import data - +from skimage.color import label2rgb # settings for LBP -METHOD = 'uniform' -P = 16 -R = 2 -matplotlib.rcParams['font.size'] = 9 +radius = 3 +n_points = 8 * radius + + +def overlay_labels(image, lbp, labels): + mask = np.logical_or.reduce([lbp == each for each in labels]) + return label2rgb(mask, image=image, bg_label=0, alpha=0.5) + + +def highlight_bars(bars, indexes): + for i in indexes: + bars[i].set_facecolor('r') + + +image = data.load('brick.png') +lbp = local_binary_pattern(image, n_points, radius, METHOD) + +def hist(ax, lbp): + n_bins = lbp.max() + 1 + return ax.hist(lbp.ravel(), normed=True, bins=n_bins, range=(0, n_bins), + facecolor='0.5') + +# plot histograms of LBP of textures +fig, (ax_img, ax_hist) = plt.subplots(nrows=2, ncols=3, figsize=(9, 6)) +plt.gray() + +titles = ('edge', 'flat', 'corner') +w = width = radius - 1 +edge_labels = range(n_points // 2 - w, n_points // 2 + w + 1) +flat_labels = list(range(0, w + 1)) + list(range(n_points - w, n_points + 2)) +i_14 = n_points // 4 # 1/4th of the histogram +i_34 = 3 * (n_points // 4) # 3/4th of the histogram +corner_labels = (list(range(i_14 - w, i_14 + w + 1)) + + list(range(i_34 - w, i_34 + w + 1))) + +label_sets = (edge_labels, flat_labels, corner_labels) + +for ax, labels in zip(ax_img, label_sets): + ax.imshow(overlay_labels(image, lbp, labels)) + +for ax, labels, name in zip(ax_hist, label_sets, titles): + counts, _, bars = hist(ax, lbp) + highlight_bars(bars, labels) + ax.set_ylim(ymax=np.max(counts[:-1])) + ax.set_xlim(xmax=n_points + 2) + ax.set_title(name) + +ax_hist[0].set_ylabel('Percentage') +for ax in ax_img: + ax.axis('off') + + +""" +.. image:: PLOT2RST.current_figure + +The above plot highlights flat, edge-like, and corner-like regions of the +image. + +The histogram of the LBP result is a good measure to classify textures. Here, +we test the histogram distributions against each other using the +Kullback-Leibler-Divergence. +""" + +# settings for LBP +radius = 2 +n_points = 8 * radius def kullback_leibler_divergence(p, q): @@ -37,11 +169,12 @@ def kullback_leibler_divergence(p, q): def match(refs, img): best_score = 10 best_name = None - lbp = local_binary_pattern(img, P, R, METHOD) - hist, _ = np.histogram(lbp, normed=True, bins=P + 2, range=(0, P + 2)) + lbp = local_binary_pattern(img, n_points, radius, METHOD) + n_bins = lbp.max() + 1 + hist, _ = np.histogram(lbp, normed=True, bins=n_bins, range=(0, n_bins)) for name, ref in refs.items(): - ref_hist, _ = np.histogram(ref, normed=True, bins=P + 2, - range=(0, P + 2)) + ref_hist, _ = np.histogram(ref, normed=True, bins=n_bins, + range=(0, n_bins)) score = kullback_leibler_divergence(hist, ref_hist) if score < best_score: best_score = score @@ -54,19 +187,19 @@ grass = data.load('grass.png') wall = data.load('rough-wall.png') refs = { - 'brick': local_binary_pattern(brick, P, R, METHOD), - 'grass': local_binary_pattern(grass, P, R, METHOD), - 'wall': local_binary_pattern(wall, P, R, METHOD) + 'brick': local_binary_pattern(brick, n_points, radius, METHOD), + 'grass': local_binary_pattern(grass, n_points, radius, METHOD), + 'wall': local_binary_pattern(wall, n_points, radius, METHOD) } # classify rotated textures print('Rotated images matched against references using LBP:') -print('original: brick, rotated: 30deg, match result: ', end='') -print(match(refs, rotate(brick, angle=30, resize=False))) -print('original: brick, rotated: 70deg, match result: ', end='') -print(match(refs, rotate(brick, angle=70, resize=False))) -print('original: grass, rotated: 145deg, match result: ', end='') -print(match(refs, rotate(grass, angle=145, resize=False))) +print('original: brick, rotated: 30deg, match result: ', + match(refs, rotate(brick, angle=30, resize=False))) +print('original: brick, rotated: 70deg, match result: ', + match(refs, rotate(brick, angle=70, resize=False))) +print('original: grass, rotated: 145deg, match result: ', + match(refs, rotate(grass, angle=145, resize=False))) # plot histograms of LBP of textures fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(nrows=2, ncols=3, @@ -75,16 +208,20 @@ plt.gray() ax1.imshow(brick) ax1.axis('off') -ax4.hist(refs['brick'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +hist(ax4, refs['brick']) ax4.set_ylabel('Percentage') ax2.imshow(grass) ax2.axis('off') -ax5.hist(refs['grass'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +hist(ax5, refs['grass']) ax5.set_xlabel('Uniform LBP values') ax3.imshow(wall) ax3.axis('off') -ax6.hist(refs['wall'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +hist(ax6, refs['wall']) + +""" +.. image:: PLOT2RST.current_figure +""" plt.show() diff --git a/doc/examples/plot_marching_cubes.py b/doc/examples/plot_marching_cubes.py new file mode 100644 index 00000000..5dad1680 --- /dev/null +++ b/doc/examples/plot_marching_cubes.py @@ -0,0 +1,55 @@ +""" +============== +Marching Cubes +============== + +Marching cubes is an algorithm to extract a 2D surface mesh from a 3D volume. +This can be conceptualized as a 3D generalization of isolines on topographical +or weather maps. It works by iterating across the volume, looking for regions +which cross the level of interest. If such regions are found, triangulations +are generated and added to an output mesh. The final result is a set of +vertices and a set of triangular faces. + +The algorithm requires a data volume and an isosurface value. For example, in +CT imaging Hounsfield units of +700 to +3000 represent bone. So, one potential +input would be a reconstructed CT set of data and the value +700, to extract +a mesh for regions of bone or bone-like density. + +This implementation also works correctly on anisotropic datasets, where the +voxel spacing is not equal for every spatial dimension, through use of the +`spacing` kwarg. + +""" +import numpy as np +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d.art3d import Poly3DCollection + +from skimage import measure +from skimage.draw import ellipsoid + +# Generate a level set about zero of two identical ellipsoids in 3D +ellip_base = ellipsoid(6, 10, 16, levelset=True) +ellip_double = np.concatenate((ellip_base[:-1, ...], + ellip_base[2:, ...]), axis=0) + +# Use marching cubes to obtain the surface mesh of these ellipsoids +verts, faces = measure.marching_cubes(ellip_double, 0) + +# Display resulting triangular mesh using Matplotlib. This can also be done +# with mayavi (see skimage.measure.marching_cubes docstring). +fig = plt.figure(figsize=(10, 12)) +ax = fig.add_subplot(111, projection='3d') + +# Fancy indexing: `verts[faces]` to generate a collection of triangles +mesh = Poly3DCollection(verts[faces]) +ax.add_collection3d(mesh) + +ax.set_xlabel("x-axis: a = 6 per ellipsoid") +ax.set_ylabel("y-axis: b = 10") +ax.set_zlabel("z-axis: c = 16") + +ax.set_xlim(0, 24) # a = 6 (times two for 2nd ellipsoid) +ax.set_ylim(0, 20) # b = 10 +ax.set_zlim(0, 32) # c = 16 + +plt.show() diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index b4b9721d..0dab82e6 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -3,55 +3,197 @@ Radon transform =============== -The radon transform is a technique widely used in tomography to -reconstruct an object from different projections. A projection is, for -example, the scattering data obtained as the output of a tomographic -scan. +In computed tomography, the tomography reconstruction problem is to obtain +a tomographic slice image from a set of projections [1]_. A projection is formed +by drawing a set of parallel rays through the 2D object of interest, assigning +the integral of the object's contrast along each ray to a single pixel in the +projection. A single projection of a 2D object is one dimensional. To +enable computed tomography reconstruction of the object, several projections +must be acquired, each of them corresponding to a different angle between the +rays with respect to the object. A collection of projections at several angles +is called a sinogram, which is a linear transform of the original image. -For more information see: +The inverse Radon transform is used in computed tomography to reconstruct +a 2D image from the measured projections (the sinogram). A practical, exact +implementation of the inverse Radon transform does not exist, but there are +several good approximate algorithms available. - - http://en.wikipedia.org/wiki/Radon_transform - - http://www.clear.rice.edu/elec431/projects96/DSP/bpanalysis.html +As the inverse Radon transform reconstructs the object from a set of +projections, the (forward) Radon transform can be used to simulate a +tomography experiment. -This script performs the radon transform, and reconstructs the -input image based on the resulting sinogram. +This script performs the Radon transform to simulate a tomography experiment +and reconstructs the input image based on the resulting sinogram formed by +the simulation. Two methods for performing the inverse Radon transform +and reconstructing the original image are compared: The Filtered Back +Projection (FBP) and the Simultaneous Algebraic Reconstruction +Technique (SART). +.. seealso:: + + - AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", + http://www.slaney.org/pct/pct-toc.html + - http://en.wikipedia.org/wiki/Radon_transform + +The forward transform +===================== + +As our original image, we will use the Shepp-Logan phantom. When calculating +the Radon transform, we need to decide how many projection angles we wish +to use. As a rule of thumb, the number of projections should be about the +same as the number of pixels there are across the object (to see why this +is so, consider how many unknown pixel values must be determined in the +reconstruction process and compare this to the number of measurements +provided by the projections), and we follow that rule here. Below is the +original image and its Radon transform, often known as its _sinogram_: """ +from __future__ import print_function, division + +import numpy as np import matplotlib.pyplot as plt from skimage.io import imread from skimage import data_dir -from skimage.transform import radon, iradon, rescale - +from skimage.transform import radon, rescale image = imread(data_dir + "/phantom.png", as_grey=True) image = rescale(image, scale=0.4) -plt.figure(figsize=(8, 8.5)) +plt.figure(figsize=(8, 4.5)) -plt.subplot(221) +plt.subplot(121) plt.title("Original") plt.imshow(image, cmap=plt.cm.Greys_r) -plt.subplot(222) -projections = radon(image, theta=[0, 45, 90]) -plt.plot(projections) -plt.title("Projections at\n0, 45 and 90 degrees") -plt.xlabel("Projection axis") -plt.ylabel("Intensity") - -projections = radon(image) -plt.subplot(223) +theta = np.linspace(0., 180., max(image.shape), endpoint=True) +sinogram = radon(image, theta=theta, circle=True) +plt.subplot(122) plt.title("Radon transform\n(Sinogram)") -plt.xlabel("Projection angle (degrees)") -plt.ylabel("Projection axis") -plt.imshow(projections, aspect='auto') - -reconstruction = iradon(projections) -plt.subplot(224) -plt.title("Reconstruction\nfrom sinogram") -plt.imshow(reconstruction, cmap=plt.cm.Greys_r) +plt.xlabel("Projection angle (deg)") +plt.ylabel("Projection position (pixels)") +plt.imshow(sinogram, cmap=plt.cm.Greys_r, + extent=(0, 180, 0, sinogram.shape[0]), aspect='auto') plt.subplots_adjust(hspace=0.4, wspace=0.5) plt.show() + +""" +.. image:: PLOT2RST.current_figure + +Reconstruction with the Filtered Back Projection (FBP) +====================================================== + +The mathematical foundation of the filtered back projection is the Fourier +slice theorem [2]_. It uses Fourier transform of the projection and +interpolation in Fourier space to obtain the 2D Fourier transform of the image, +which is then inverted to form the reconstructed image. The filtered back +projection is among the fastest methods of performing the inverse Radon +transform. The only tunable parameter for the FBP is the filter, which is +applied to the Fourier transformed projections. It may be used to suppress +high frequency noise in the reconstruction. ``skimage`` provides a few +different options for the filter. + +""" + +from skimage.transform import iradon + +reconstruction_fbp = iradon(sinogram, theta=theta, circle=True) +error = reconstruction_fbp - image +print('FBP rms reconstruction error: %.3g' % np.sqrt(np.mean(error**2))) + +imkwargs = dict(vmin=-0.2, vmax=0.2) +plt.figure(figsize=(8, 4.5)) +plt.subplot(121) +plt.title("Reconstruction\nFiltered back projection") +plt.imshow(reconstruction_fbp, cmap=plt.cm.Greys_r) +plt.subplot(122) +plt.title("Reconstruction error\nFiltered back projection") +plt.imshow(reconstruction_fbp - image, cmap=plt.cm.Greys_r, **imkwargs) +plt.show() + +""" +.. image:: PLOT2RST.current_figure + +Reconstruction with the Simultaneous Algebraic Reconstruction Technique +======================================================================= + +Algebraic reconstruction techniques for tomography are based on a +straightforward idea: for a pixelated image the value of a single ray in a +particular projection is simply a sum of all the pixels the ray passes through +on its way through the object. This is a way of expressing the forward Radon +transform. The inverse Radon transform can then be formulated as a (large) set +of linear equations. As each ray passes through a small fraction of the pixels +in the image, this set of equations is sparse, allowing iterative solvers for +sparse linear systems to tackle the system of equations. One iterative method +has been particularly popular, namely Kaczmarz' method [3]_, which has the +property that the solution will approach a least-squares solution of the +equation set. + +The combination of the formulation of the reconstruction problem as a set +of linear equations and an iterative solver makes algebraic techniques +relatively flexible, hence some forms of prior knowledge can be incorporated +with relative ease. + +``skimage`` provides one of the more popular variations of the algebraic +reconstruction techniques: the Simultaneous Algebraic Reconstruction Technique +(SART) [1]_ [4]_. It uses Kaczmarz' method [3]_ as the iterative solver. A good +reconstruction is normally obtained in a single iteration, making the method +computationally effective. Running one or more extra iterations will normally +improve the reconstruction of sharp, high frequency features and reduce the +mean squared error at the expense of increased high frequency noise (the user +will need to decide on what number of iterations is best suited to the problem +at hand. The implementation in ``skimage`` allows prior information of the +form of a lower and upper threshold on the reconstructed values to be supplied +to the reconstruction. + +""" + +from skimage.transform import iradon_sart + +reconstruction_sart = iradon_sart(sinogram, theta=theta) +error = reconstruction_sart - image +print('SART (1 iteration) rms reconstruction error: %.3g' + % np.sqrt(np.mean(error**2))) + +plt.figure(figsize=(8, 8.5)) + +plt.subplot(221) +plt.title("Reconstruction\nSART") +plt.imshow(reconstruction_sart, cmap=plt.cm.Greys_r) +plt.subplot(222) +plt.title("Reconstruction error\nSART") +plt.imshow(reconstruction_sart - image, cmap=plt.cm.Greys_r, **imkwargs) + +# Run a second iteration of SART by supplying the reconstruction +# from the first iteration as an initial estimate +reconstruction_sart2 = iradon_sart(sinogram, theta=theta, + image=reconstruction_sart) +error = reconstruction_sart2 - image +print('SART (2 iterations) rms reconstruction error: %.3g' + % np.sqrt(np.mean(error**2))) + +plt.subplot(223) +plt.title("Reconstruction\nSART, 2 iterations") +plt.imshow(reconstruction_sart2, cmap=plt.cm.Greys_r) +plt.subplot(224) +plt.title("Reconstruction error\nSART, 2 iterations") +plt.imshow(reconstruction_sart2 - image, cmap=plt.cm.Greys_r, **imkwargs) +plt.show() + +""" +.. image:: PLOT2RST.current_figure + + +.. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", + IEEE Press 1988. http://www.slaney.org/pct/pct-toc.html +.. [2] Wikipedia, Radon transform, + http://en.wikipedia.org/wiki/Radon_transform#Relationship_with_the_Fourier_transform +.. [3] S Kaczmarz, "Angenaeherte Aufloesung von Systemen linearer + Gleichungen", Bulletin International de l'Academie Polonaise des + Sciences et des Lettres 35 pp 355--357 (1937) +.. [4] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique + (SART): a superior implementation of the ART algorithm", Ultrasonic + Imaging 6 pp 81--94 (1984) + +""" diff --git a/doc/examples/plot_rank_mean.py b/doc/examples/plot_rank_mean.py index e23beb18..6f16c440 100644 --- a/doc/examples/plot_rank_mean.py +++ b/doc/examples/plot_rank_mean.py @@ -6,9 +6,9 @@ Mean filters This example compares the following mean filters of the rank filter package: * **local mean**: all pixels belonging to the structuring element to compute - average gray level + average gray level. * **percentile mean**: only use values between percentiles p0 and p1 - (here 10% and 90%) + (here 10% and 90%). * **bilateral mean**: only use pixels of the structuring element having a gray level situated inside g-s0 and g+s1 (here g-500 and g+500) @@ -23,23 +23,30 @@ import matplotlib.pyplot as plt from skimage import data from skimage.morphology import disk -import skimage.filter.rank as rank +from skimage.filter import rank -a16 = (data.coins()).astype(np.uint16) * 16 + +image = (data.coins()).astype(np.uint16) * 16 selem = disk(20) -f1 = rank.percentile_mean(a16, selem=selem, p0=.1, p1=.9) -f2 = rank.bilateral_mean(a16, selem=selem, s0=500, s1=500) -f3 = rank.mean(a16, selem=selem) +percentile_result = rank.mean_percentile(image, selem=selem, p0=.1, p1=.9) +bilateral_result = rank.mean_bilateral(image, selem=selem, s0=500, s1=500) +normal_result = rank.mean(image, selem=selem) -# display results -fig, axes = plt.subplots(nrows=3, figsize=(15, 10)) + +fig, axes = plt.subplots(nrows=3, figsize=(8, 10)) ax0, ax1, ax2 = axes -ax0.imshow(np.hstack((a16, f1))) -ax0.set_title('percentile mean') -ax1.imshow(np.hstack((a16, f2))) -ax1.set_title('bilateral mean') -ax2.imshow(np.hstack((a16, f3))) -ax2.set_title('local mean') +ax0.imshow(np.hstack((image, percentile_result))) +ax0.set_title('Percentile mean') +ax0.axis('off') + +ax1.imshow(np.hstack((image, bilateral_result))) +ax1.set_title('Bilateral mean') +ax1.axis('off') + +ax2.imshow(np.hstack((image, normal_result))) +ax2.set_title('Local mean') +ax2.axis('off') + plt.show() diff --git a/doc/examples/plot_ransac.py b/doc/examples/plot_ransac.py index 26fd78c0..2de76619 100644 --- a/doc/examples/plot_ransac.py +++ b/doc/examples/plot_ransac.py @@ -21,7 +21,7 @@ y = 0.2 * x + 20 data = np.column_stack([x, y]) # add faulty data -faulty = np.array(30 * [(180, -100)]) +faulty = np.array(30 * [(180., -100)]) faulty += 5 * np.random.normal(size=faulty.shape) data[:faulty.shape[0]] = faulty diff --git a/doc/examples/plot_regionprops.py b/doc/examples/plot_regionprops.py index f96b3b8c..f675d11c 100644 --- a/doc/examples/plot_regionprops.py +++ b/doc/examples/plot_regionprops.py @@ -24,29 +24,23 @@ image[rr,cc] = 1 image = rotate(image, angle=15, order=0) label_img = label(image) -props = regionprops(label_img, [ - 'BoundingBox', - 'Centroid', - 'Orientation', - 'MajorAxisLength', - 'MinorAxisLength' -]) +regions = regionprops(label_img) plt.imshow(image) -for prop in props: - x0 = prop['Centroid'][1] - y0 = prop['Centroid'][0] - x1 = x0 + math.cos(prop['Orientation']) * 0.5 * prop['MajorAxisLength'] - y1 = y0 - math.sin(prop['Orientation']) * 0.5 * prop['MajorAxisLength'] - x2 = x0 - math.sin(prop['Orientation']) * 0.5 * prop['MinorAxisLength'] - y2 = y0 - math.cos(prop['Orientation']) * 0.5 * prop['MinorAxisLength'] +for props in regions: + y0, x0 = props.centroid + orientation = props.orientation + x1 = x0 + math.cos(orientation) * 0.5 * props.major_axis_length + y1 = y0 - math.sin(orientation) * 0.5 * props.major_axis_length + x2 = x0 - math.sin(orientation) * 0.5 * props.minor_axis_length + y2 = y0 - math.cos(orientation) * 0.5 * props.minor_axis_length plt.plot((x0, x1), (y0, y1), '-r', linewidth=2.5) plt.plot((x0, x2), (y0, y2), '-r', linewidth=2.5) plt.plot(x0, y0, '.g', markersize=15) - minr, minc, maxr, maxc = prop['BoundingBox'] + minr, minc, maxr, maxc = props.bbox bx = (minc, maxc, maxc, minc, minc) by = (minr, minr, maxr, maxr, minr) plt.plot(bx, by, '-b', linewidth=2.5) diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 507452ef..2ae842a5 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -1,29 +1,34 @@ """ -=========== -Fill shapes -=========== - -This example shows how to fill several different shapes: +====== +Shapes +====== +This example shows how to draw several different shapes: * line +* Bezier curve * polygon * circle * ellipse """ +import math import numpy as np import matplotlib.pyplot as plt -from skimage.draw import line, polygon, circle, circle_perimeter, \ - ellipse, ellipse_perimeter -import numpy as np -import math +from skimage.draw import (line, polygon, circle, + circle_perimeter, + ellipse, ellipse_perimeter, + bezier_curve) -img = np.zeros((500, 500, 3), dtype=np.uint8) + +fig, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(10, 6)) + + +img = np.zeros((500, 500, 3), dtype=np.double) # draw line rr, cc = line(120, 123, 20, 400) -img[rr,cc,0] = 255 +img[rr, cc, 0] = 255 # fill polygon poly = np.array(( @@ -33,28 +38,61 @@ poly = np.array(( (220, 590), (300, 300), )) -rr, cc = polygon(poly[:,0], poly[:,1], img.shape) -img[rr,cc,1] = 255 +rr, cc = polygon(poly[:, 0], poly[:, 1], img.shape) +img[rr, cc, 1] = 1 # fill circle rr, cc = circle(200, 200, 100, img.shape) -img[rr,cc,:] = (255, 255, 0) +img[rr, cc, :] = (1, 1, 0) # fill ellipse rr, cc = ellipse(300, 300, 100, 200, img.shape) -img[rr,cc,2] = 255 +img[rr, cc, 2] = 1 # circle rr, cc = circle_perimeter(120, 400, 15) -img[rr, cc, :] = (255, 0, 0) +img[rr, cc, :] = (1, 0, 0) + +# Bezier curve +rr, cc = bezier_curve(70, 100, 10, 10, 150, 100, 1) +img[rr, cc, :] = (1, 0, 0) # ellipses rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 4.) -img[rr, cc, :] = (255, 0, 255) +img[rr, cc, :] = (1, 0, 1) rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=-math.pi / 4.) -img[rr, cc, :] = (0, 0, 255) +img[rr, cc, :] = (0, 0, 1) rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 2.) -img[rr, cc, :] = (255, 255, 255) +img[rr, cc, :] = (1, 1, 1) + +ax1.imshow(img) +ax1.set_title('No anti-aliasing') +ax1.axis('off') + +""" + +Anti-aliased drawing for: +* line +* circle + +""" + +from skimage.draw import line_aa, circle_perimeter_aa + + +img = np.zeros((100, 100), dtype=np.double) + +# anti-aliased line +rr, cc, val = line_aa(12, 12, 20, 50) +img[rr, cc] = val + +# anti-aliased circle +rr, cc, val = circle_perimeter_aa(60, 40, 30) +img[rr, cc] = val + + +ax2.imshow(img, cmap=plt.cm.gray, interpolation='nearest') +ax2.set_title('Anti-aliasing') +ax2.axis('off') -plt.imshow(img) plt.show() diff --git a/doc/examples/plot_watershed.py b/doc/examples/plot_watershed.py index 88f54ba7..f003c122 100644 --- a/doc/examples/plot_watershed.py +++ b/doc/examples/plot_watershed.py @@ -52,8 +52,11 @@ fig, axes = plt.subplots(ncols=3, figsize=(8, 2.7)) ax0, ax1, ax2 = axes ax0.imshow(image, cmap=plt.cm.gray, interpolation='nearest') +ax0.set_title('Overlapping objects') ax1.imshow(-distance, cmap=plt.cm.jet, interpolation='nearest') +ax1.set_title('Distances') ax2.imshow(labels, cmap=plt.cm.spectral, interpolation='nearest') +ax2.set_title('Separated objects') for ax in axes: ax.axis('off') diff --git a/doc/gh-pages.py b/doc/gh-pages.py index dd798c02..af992158 100644 --- a/doc/gh-pages.py +++ b/doc/gh-pages.py @@ -85,6 +85,10 @@ if __name__ == '__main__': for l in setup_lines: if l.startswith('VERSION'): tag = l.split("'")[1] + + # Rename to, e.g., 0.9.x + tag = '.'.join(tag.split('.')[:-1] + ['x']) + break if "dev" in tag: diff --git a/doc/source/_static/docversions.js b/doc/source/_static/docversions.js index fde9437b..ab333671 100644 --- a/doc/source/_static/docversions.js +++ b/doc/source/_static/docversions.js @@ -1,17 +1,21 @@ -function insert_version_links() { - var labels = ['dev', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; +var versions = ['dev', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; - for (i = 0; i < labels.length; i++){ +function insert_version_links() { + for (i = 0; i < versions.length; i++){ open_list = '
  • ' if (typeof(DOCUMENTATION_OPTIONS) !== 'undefined') { - if ((DOCUMENTATION_OPTIONS['VERSION'] == labels[i]) || + if ((DOCUMENTATION_OPTIONS['VERSION'] == versions[i]) || (DOCUMENTATION_OPTIONS['VERSION'].match(/dev$/) && (i == 0))) { open_list = '
  • ' } } document.write(open_list); document.write('skimage VERSION
  • \n' - .replace('VERSION', labels[i]) - .replace('URL', 'http://scikit-image.org/docs/' + labels[i])); + .replace('VERSION', versions[i]) + .replace('URL', 'http://scikit-image.org/docs/' + versions[i])); } } + +function stable_version() { + return versions[1]; +} diff --git a/doc/source/api_changes.txt b/doc/source/api_changes.txt index 57486723..c7293bc5 100644 --- a/doc/source/api_changes.txt +++ b/doc/source/api_changes.txt @@ -1,9 +1,18 @@ +Version 0.9 +----------- +- No longer wrap ``imread`` output in an ``Image`` class +- Change default value of `sigma` parameter in ``skimage.segmentation.slic`` + to 0 +- ``hough_circle`` now returns a stack of arrays that are the same size as the + input image. Set the ``full_output`` flag to True for the old behavior. + +Version 0.4 +----------- +- Switch mask and radius arguments for ``median_filter`` + Version 0.3 ----------- - Remove ``as_grey``, ``dtype`` keyword from ImageCollection - Remove ``dtype`` from imread - Generalise ImageCollection to accept a load_func -Version 0.4 ------------ -- Switch mask and radius arguments for median_filter diff --git a/doc/source/install.txt b/doc/source/install.txt index 1186479e..b78c1269 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -59,4 +59,27 @@ in-place:: python setup.py build_ext -i +Building with bento +------------------- + +``scikit-image`` can also be built using `bento +`__. Bento depends on `WAF +`__ for compilation. + +Follow the `Bento installation instructions +`__ and `download the WAF +source `__. + +Tell Bento where to find WAF by setting the ``WAFDIR`` environment variable:: + + export WAFDIR= + +From the ``scikit-image`` source directory:: + + bentomaker configure + bentomaker build -j # (add -i for in-place build) + bentomaker install # (when not builing in-place) + +Depending on file permissions, the install commands may need to be run as sudo. + .. include:: ../../DEPENDS.txt diff --git a/doc/source/themes/scikit-image/static/css/custom.css b/doc/source/themes/scikit-image/static/css/custom.css index 18ee7a34..0a2d3136 100644 --- a/doc/source/themes/scikit-image/static/css/custom.css +++ b/doc/source/themes/scikit-image/static/css/custom.css @@ -79,6 +79,10 @@ dt { padding-left: 15px; } +#current { + font-weight: bold; +} + .headerlink { margin-left: 10px; color: #ddd; diff --git a/doc/tools/plot_pr.py b/doc/tools/plot_pr.py index 08f4fc0e..5f9b4aa6 100644 --- a/doc/tools/plot_pr.py +++ b/doc/tools/plot_pr.py @@ -1,14 +1,15 @@ -import urllib import json -import copy +import urllib +import dateutil.parser from collections import OrderedDict +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +import numpy as np import matplotlib.pyplot as plt from matplotlib.ticker import FuncFormatter +from matplotlib.transforms import blended_transform_factory -import dateutil.parser -from dateutil.relativedelta import relativedelta -from datetime import datetime, timedelta cache = '_pr_cache.txt' @@ -22,16 +23,16 @@ cache = '_pr_cache.txt' releases = OrderedDict([ #('0.1', u'2009-10-07 13:52:19 +0200'), #('0.2', u'2009-11-12 14:48:45 +0200'), - ('0.3', u'2011-10-10 03:28:47 -0700'), + #('0.3', u'2011-10-10 03:28:47 -0700'), ('0.4', u'2011-12-03 14:31:32 -0800'), ('0.5', u'2012-02-26 21:00:51 -0800'), - ('0.6', u'2012-06-24 21:37:05 -0700')]) + ('0.6', u'2012-06-24 21:37:05 -0700'), + ('0.7', u'2012-09-29 18:08:49 -0700'), + ('0.8', u'2013-03-04 20:46:09 +0100')]) month_duration = 24 -for r in releases: - releases[r] = dateutil.parser.parse(releases[r]) def fetch_PRs(user='scikit-image', repo='scikit-image', state='open'): params = {'state': state, @@ -46,12 +47,12 @@ def fetch_PRs(user='scikit-image', repo='scikit-image', state='open'): 'repo': repo, 'params': urllib.urlencode(params)} - fetch_status = 'Fetching page %(page)d (state=%(state)s)' % params + \ - ' from %(user)s/%(repo)s...' % config + fetch_status = ('Fetching page %(page)d (state=%(state)s)' % params + + ' from %(user)s/%(repo)s...' % config) print(fetch_status) f = urllib.urlopen( - 'https://api.github.com/repos/%(user)s/%(repo)s/pulls?%(params)s' \ + 'https://api.github.com/repos/%(user)s/%(repo)s/pulls?%(params)s' % config ) @@ -67,6 +68,31 @@ def fetch_PRs(user='scikit-image', repo='scikit-image', state='open'): return data + +def seconds_from_epoch(dates): + seconds = [(dt - epoch).total_seconds() for dt in dates] + return seconds + + +def get_month_bins(dates): + now = datetime.now(tz=dates[0].tzinfo) + this_month = datetime(year=now.year, month=now.month, day=1, + tzinfo=dates[0].tzinfo) + + bins = [this_month - relativedelta(months=i) + for i in reversed(range(-1, month_duration))] + return seconds_from_epoch(bins) + + +def date_formatter(value, _): + dt = epoch + timedelta(seconds=value) + return dt.strftime('%Y/%m') + + +for r in releases: + releases[r] = dateutil.parser.parse(releases[r]) + + try: PRs = json.loads(open(cache, 'r').read()) print('Loaded PRs from cache...') @@ -87,47 +113,41 @@ dates = [dateutil.parser.parse(pr['created_at']) for pr in PRs] epoch = datetime(2009, 1, 1, tzinfo=dates[0].tzinfo) -def seconds_from_epoch(dates): - seconds = [(dt - epoch).total_seconds() for dt in dates] - return seconds - dates_f = seconds_from_epoch(dates) +bins = get_month_bins(dates) -def date_formatter(value, _): - dt = epoch + timedelta(seconds=value) - return dt.strftime('%Y/%m') +fig, ax = plt.subplots(figsize=(7, 5)) -plt.figure(figsize=(7, 5)) +n, bins, _ = ax.hist(dates_f, bins=bins, color='blue', alpha=0.6) -now = datetime.now(tz=dates[0].tzinfo) -this_month = datetime(year=now.year, month=now.month, day=1, - tzinfo=dates[0].tzinfo) - -bins = [this_month - relativedelta(months=i) \ - for i in reversed(range(-1, month_duration))] -bins = seconds_from_epoch(bins) -plt.hist(dates_f, bins=bins) - -ax = plt.gca() ax.xaxis.set_major_formatter(FuncFormatter(date_formatter)) -ax.set_xticks(bins[:-1]) +ax.set_xticks(bins[2:-1:3]) # Date label every 3 months. labels = ax.get_xticklabels() for l in labels: l.set_rotation(40) l.set_size(10) +mixed_transform = blended_transform_factory(ax.transData, ax.transAxes) for version, date in releases.items(): date = seconds_from_epoch([date])[0] - plt.axvline(date, color='r', label=version) + ax.axvline(date, color='black', linestyle=':', label=version) + ax.text(date, 1, version, color='r', va='bottom', ha='center', + transform=mixed_transform) -plt.title('Pull request activity').set_y(1.05) -plt.xlabel('Date') -plt.ylabel('PRs created') -plt.legend(loc=2, title='Release') -plt.subplots_adjust(top=0.875, bottom=0.225) +ax.set_title('Pull request activity').set_y(1.05) +ax.set_xlabel('Date') +ax.set_ylabel('PRs per month', color='blue') +fig.subplots_adjust(top=0.875, bottom=0.225) -plt.savefig('PRs.png') +cumulative = np.cumsum(n) +cumulative += len(dates) - cumulative[-1] + +ax2 = ax.twinx() +ax2.plot(bins[1:], cumulative, color='black', linewidth=2) +ax2.set_ylabel('Total PRs', color='black') + +fig.savefig('PRs.png') plt.show() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..4a4aaa53 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +cython>=0.17 +matplotlib>=1.0 +numpy>=1.6 diff --git a/setup.py b/setup.py index d62a0d86..da76b819 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ VERSION = '0.9dev' PYTHON_VERSION = (2, 5) DEPENDENCIES = { 'numpy': (1, 6), - 'Cython': (0, 15), + 'Cython': (0, 17), } @@ -30,10 +30,7 @@ import sys import re import setuptools from numpy.distutils.core import setup -try: - from distutils.command.build_py import build_py_2to3 as build_py -except ImportError: - from distutils.command.build_py import build_py +from distutils.command.build_py import build_py def configuration(parent_package='', top_path=None): diff --git a/skimage/__init__.py b/skimage/__init__.py index daac238c..c73a79b7 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -38,8 +38,6 @@ util Utility Functions ----------------- -get_log - Returns the ``skimage`` log. Use this to print debug output. img_as_float Convert an image to floating point format, with values in [0, 1]. img_as_uint @@ -54,6 +52,7 @@ img_as_ubyte import os.path as _osp import imp as _imp import functools as _functools +from skimage._shared.utils import deprecated as _deprecated pkg_dir = _osp.abspath(_osp.dirname(__file__)) data_dir = _osp.join(pkg_dir, 'data') @@ -62,6 +61,7 @@ try: from .version import version as __version__ except ImportError: __version__ = "unbuilt-dev" +del version try: @@ -88,6 +88,56 @@ test_verbose = _functools.partial(test, verbose=True) test_verbose.__doc__ = test.__doc__ +class _Log(Warning): + pass + + +class _FakeLog(object): + def __init__(self, name): + """ + Parameters + ---------- + name : str + Name of the log. + repeat : bool + Whether to print repeating messages more than once (False by + default). + """ + self._name = name + + import warnings + warnings.simplefilter("always", _Log) + + self._warnings = warnings + + def _warn(self, msg, wtype): + self._warnings.warn('%s: %s' % (wtype, msg), _Log) + + def debug(self, msg): + self._warn(msg, 'DEBUG') + + def info(self, msg): + self._warn(msg, 'INFO') + + def warning(self, msg): + self._warn(msg, 'WARNING') + + warn = warning + + def error(self, msg): + self._warn(msg, 'ERROR') + + def critical(self, msg): + self._warn(msg, 'CRITICAL') + + def addHandler(*args): + pass + + def setLevel(*args): + pass + + +@_deprecated() def get_log(name=None): """Return a console logger. @@ -105,39 +155,12 @@ def get_log(name=None): http://docs.python.org/library/logging.html """ - import logging - if name is None: name = 'skimage' else: name = 'skimage.' + name - log = logging.getLogger(name) - return log + return _FakeLog(name) -def _setup_log(): - """Configure root logger. - - """ - import logging - import sys - - formatter = logging.Formatter( - '%(name)s: %(levelname)s: %(message)s' - ) - - try: - handler = logging.StreamHandler(stream=sys.stdout) - except TypeError: - handler = logging.StreamHandler(strm=sys.stdout) - handler.setFormatter(formatter) - - log = get_log() - log.addHandler(handler) - log.setLevel(logging.WARNING) - log.propagate = False - -_setup_log() - from .util.dtype import * diff --git a/skimage/_shared/six.py b/skimage/_shared/six.py new file mode 100644 index 00000000..8a877b17 --- /dev/null +++ b/skimage/_shared/six.py @@ -0,0 +1,423 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2013 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.3.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" + _iterlists = "lists" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + _iterlists = "iterlists" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +def iterkeys(d, **kw): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)(**kw)) + +def itervalues(d, **kw): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)(**kw)) + +def iteritems(d, **kw): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)(**kw)) + +def iterlists(d, **kw): + """Return an iterator over the (key, [values]) pairs of a dictionary.""" + return iter(getattr(d, _iterlists)(**kw)) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + unichr = chr + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + unichr = unichr + int2byte = chr + def byte2int(bs): + return ord(bs[0]) + def indexbytes(buf, i): + return ord(buf[i]) + def iterbytes(buf): + return (ord(byte) for byte in buf) + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + return meta("NewBase", bases, {}) diff --git a/skimage/_shared/transform.pxd b/skimage/_shared/transform.pxd index ccb16ff3..4978e843 100644 --- a/skimage/_shared/transform.pxd +++ b/skimage/_shared/transform.pxd @@ -1,5 +1,5 @@ cimport numpy as cnp -cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, - Py_ssize_t r0, Py_ssize_t c0, Py_ssize_t r1, Py_ssize_t c1) +cdef float integrate(float[:, ::1] sat, Py_ssize_t r0, Py_ssize_t c0, + Py_ssize_t r1, Py_ssize_t c1) diff --git a/skimage/_shared/transform.pyx b/skimage/_shared/transform.pyx index 8ce2ab67..9bdc6824 100644 --- a/skimage/_shared/transform.pyx +++ b/skimage/_shared/transform.pyx @@ -5,8 +5,8 @@ cimport numpy as cnp -cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, - Py_ssize_t r0, Py_ssize_t c0, Py_ssize_t r1, Py_ssize_t c1): +cdef float integrate(float[:, ::1] sat, Py_ssize_t r0, Py_ssize_t c0, + Py_ssize_t r1, Py_ssize_t c1): """ Using a summed area table / integral image, calculate the sum over a given window. diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index e1642102..b7071521 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -1,19 +1,19 @@ import warnings import functools +import sys + +from . import six -__all__ = ['deprecated', 'is_str'] +__all__ = ['deprecated', 'get_bound_method_class'] -try: - isinstance("", basestring) - def is_str(s): - """Return True if `s` is a string. Safe for Python 2 and 3.""" - return isinstance(s, basestring) -except NameError: - def is_str(s): - """Return True if `s` is a string. Safe for Python 2 and 3.""" - return isinstance(s, str) +class skimage_deprecation(Warning): + """Create our own deprecation class, since Python >= 2.7 + silences deprecations by default. + + """ + pass class deprecated(object): @@ -46,12 +46,14 @@ class deprecated(object): @functools.wraps(func) def wrapped(*args, **kwargs): if self.behavior == 'warn': + func_code = six.get_function_code(func) + warnings.simplefilter('always', skimage_deprecation) warnings.warn_explicit(msg, - category=DeprecationWarning, - filename=func.func_code.co_filename, - lineno=func.func_code.co_firstlineno + 1) + category=skimage_deprecation, + filename=func_code.co_filename, + lineno=func_code.co_firstlineno + 1) elif self.behavior == 'raise': - raise DeprecationWarning(msg) + raise skimage_deprecation(msg) return func(*args, **kwargs) # modify doc string to display deprecation warning @@ -62,3 +64,10 @@ class deprecated(object): wrapped.__doc__ = doc + '\n\n ' + wrapped.__doc__ return wrapped + + +def get_bound_method_class(m): + """Return the class for a bound method. + + """ + return m.im_class if sys.version < '3' else m.__self__.__class__ diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index 9202bee9..1c61020d 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -1,4 +1,5 @@ from .colorconv import (convert_colorspace, + guess_spatial_dimensions, rgb2hsv, hsv2rgb, rgb2xyz, @@ -14,6 +15,8 @@ from .colorconv import (convert_colorspace, rgb2lab, rgb2hed, hed2rgb, + lab2lch, + lch2lab, separate_stains, combine_stains, rgb_from_hed, @@ -43,8 +46,15 @@ from .colorconv import (convert_colorspace, from .colorlabel import color_dict, label2rgb +from .delta_e import (deltaE_cie76, + deltaE_ciede94, + deltaE_ciede2000, + deltaE_cmc, + ) + __all__ = ['convert_colorspace', + 'guess_spatial_dimensions', 'rgb2hsv', 'hsv2rgb', 'rgb2xyz', @@ -60,6 +70,8 @@ __all__ = ['convert_colorspace', 'rgb2lab', 'rgb2hed', 'hed2rgb', + 'lab2lch', + 'lch2lab', 'separate_stains', 'combine_stains', 'rgb_from_hed', @@ -87,4 +99,9 @@ __all__ = ['convert_colorspace', 'is_rgb', 'is_gray', 'color_dict', - 'label2rgb'] + 'label2rgb', + 'deltaE_cie76', + 'deltaE_ciede94', + 'deltaE_ciede2000', + 'deltaE_cmc', + ] diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 1ef5a6c9..7562b650 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -26,10 +26,17 @@ Supported color spaces Derived from the RGB CIE color space. Chosen such that ``x == y == z == 1/3`` at the whitepoint, and all color matching functions are greater than zero everywhere. +* LAB CIE : Lightness, a, b + Colorspace derived from XYZ CIE that is intended to be more + perceptually uniform +* LCH CIE : Lightness, Chroma, Hue + Defined in terms of LAB CIE. C and H are the polar representation of + a and b. The polar angle C is defined to be on (0, 2*pi) :author: Nicolas Pinto (rgb2hsv) :author: Ralf Gommers (hsv2rgb) :author: Travis Oliphant (XYZ and RGB CIE functions) +:author: Matt Terry (lab2lch) :license: modified BSD @@ -49,6 +56,37 @@ from ..util import dtype from skimage._shared.utils import deprecated +def guess_spatial_dimensions(image): + """Make an educated guess about whether an image has a channels dimension. + + Parameters + ---------- + image : ndarray + The input image. + + Returns + ------- + spatial_dims : int or None + The number of spatial dimensions of `image`. If ambiguous, the value + is `None`. + + Raises + ------ + ValueError + If the image array has less than two or more than four dimensions. + """ + if image.ndim == 2: + return 2 + if image.ndim == 3 and image.shape[-1] != 3: + return 3 + if image.ndim == 3 and image.shape[-1] == 3: + return None + if image.ndim == 4 and image.shape[-1] == 3: + return 3 + else: + raise ValueError("Expected 2D, 3D, or 4D array, got %iD." % image.ndim) + + @deprecated() def is_rgb(image): """Test whether the image is RGB or RGBA. @@ -72,7 +110,7 @@ def is_gray(image): Input image. """ - return np.squeeze(image).ndim == 2 + return image.ndim in (2, 3) and not is_rgb(image) def convert_colorspace(arr, fromspace, tospace): @@ -129,8 +167,9 @@ def _prepare_colorarray(arr): """ arr = np.asanyarray(arr) - if arr.ndim != 3 or arr.shape[2] != 3: - msg = "the input array must be have a shape == (.,.,3))" + if arr.ndim not in [3, 4] or arr.shape[-1] != 3: + msg = ("the input array must be have a shape == (.., ..,[ ..,] 3)), " + + "got (" + (", ".join(map(str, arr.shape))) + ")") raise ValueError(msg) return dtype.img_as_float(arr) @@ -413,12 +452,12 @@ def _convert(matrix, arr): The converted array. """ arr = _prepare_colorarray(arr) - arr = np.swapaxes(arr, 0, 2) + arr = np.swapaxes(arr, 0, -1) oldshape = arr.shape arr = np.reshape(arr, (3, -1)) out = np.dot(matrix, arr) out.shape = oldshape - out = np.swapaxes(out, 2, 0) + out = np.swapaxes(out, -1, 0) return np.ascontiguousarray(out) @@ -473,17 +512,19 @@ def rgb2xyz(rgb): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Returns ------- out : ndarray - The image in XYZ format, in a 3-D array of shape (.., .., 3). + The image in XYZ format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). Notes ----- @@ -628,23 +669,24 @@ def gray2rgb(image): Parameters ---------- image : array_like - Input image of shape ``(M, N)``. + Input image of shape ``(M, N [, P])``. Returns ------- rgb : ndarray - RGB image of shape ``(M, N, 3)``. + RGB image of shape ``(M, N, [, P], 3)``. Raises ------ ValueError - If the input is not 2-dimensional. + If the input is not a 2- or 3-dimensional image. """ if np.squeeze(image).ndim == 3 and image.shape[2] in (3, 4): return image - elif image.ndim == 2 or np.squeeze(image).ndim == 2: - return np.dstack((image, image, image)) + elif image.ndim != 1 and np.squeeze(image).ndim in (1, 2, 3): + image = image[..., np.newaxis] + return np.concatenate(3 * (image,), axis=-1) else: raise ValueError("Input image expected to be RGB, RGBA or gray.") @@ -655,17 +697,19 @@ def xyz2lab(xyz): Parameters ---------- xyz : array_like - The image in XYZ format, in a 3-D array of shape (.., .., 3). + The image in XYZ format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Returns ------- out : ndarray - The image in CIE-LAB format, in a 3-D array of shape (.., .., 3). + The image in CIE-LAB format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Raises ------ ValueError - If `xyz` is not a 3-D array of shape (.., .., 3). + If `xyz` is not a 3-D array of shape (.., ..,[ ..,] 3). Notes ----- @@ -695,14 +739,14 @@ def xyz2lab(xyz): arr[mask] = np.power(arr[mask], 1. / 3.) arr[~mask] = 7.787 * arr[~mask] + 16. / 116. - x, y, z = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + x, y, z = arr[..., 0], arr[..., 1], arr[..., 2] # Vector scaling L = (116. * y) - 16. a = 500.0 * (x - y) b = 200.0 * (y - z) - return np.dstack([L, a, b]) + return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1) def lab2xyz(lab): @@ -759,17 +803,19 @@ def rgb2lab(rgb): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Returns ------- out : ndarray - The image in Lab format, in a 3-D array of shape (.., .., 3). + The image in Lab format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). Notes ----- @@ -987,3 +1033,105 @@ def combine_stains(stains, conv_matrix): logrgb2 = np.dot(-np.reshape(stains, (-1, 3)), conv_matrix) rgb2 = np.exp(logrgb2) return rescale_intensity(np.reshape(rgb2 - 2, stains.shape), in_range=(-1, 1)) + + +def lab2lch(lab): + """CIE-LAB to CIE-LCH color space conversion. + + LCH is the cylindrical representation of the LAB (Cartesian) colorspace + + Parameters + ---------- + lab : array_like + The N-D image in CIE-LAB format. The last (`N+1`th) dimension must have + at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` color + channels. Subsequent elements are copied. + + Returns + ------- + out : ndarray + The image in LCH format, in a N-D array with same shape as input `lab`. + + Raises + ------ + ValueError + If `lch` does not have at least 3 color channels (i.e. l, a, b). + + Notes + ----- + The Hue is expressed as an angle between (0, 2*pi) + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2lab, lab2lch + >>> lena = data.lena() + >>> lena_lab = rgb2lab(lena) + >>> lena_lch = lab2lch(lena_lab) + """ + lch = _prepare_lab_array(lab) + + a, b = lch[..., 1], lch[..., 2] + lch[..., 1], lch[..., 2] = _cart2polar_2pi(a, b) + return lch + + +def _cart2polar_2pi(x, y): + """convert cartesian coordiantes to polar (uses non-standard theta range!) + + NON-STANDARD RANGE! Maps to (0, 2*pi) rather than usual (-pi, +pi) + """ + r, t = np.hypot(x, y), np.arctan2(y, x) + t += np.where(t < 0., 2 * np.pi, 0) + return r, t + + +def lch2lab(lch): + """CIE-LCH to CIE-LAB color space conversion. + + LCH is the cylindrical representation of the LAB (Cartesian) colorspace + + Parameters + ---------- + lch : array_like + The N-D image in CIE-LCH format. The last (`N+1`th) dimension must have + at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` color + channels. Subsequent elements are copied. + + Returns + ------- + out : ndarray + The image in LAB format, with same shape as input `lch`. + + Raises + ------ + ValueError + If `lch` does not have at least 3 color channels (i.e. l, c, h). + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2lab, lch2lab + >>> lena = data.lena() + >>> lena_lab = rgb2lab(lena) + >>> lena_lch = lab2lch(lena_lab) + >>> lena_lab2 = lch2lab(lena_lch) + """ + lch = _prepare_lab_array(lch) + + c, h = lch[..., 1], lch[..., 2] + lch[..., 1], lch[..., 2] = c * np.cos(h), c * np.sin(h) + return lch + + +def _prepare_lab_array(arr): + """Ensure input for lab2lch, lch2lab are well-posed. + + Arrays must be in floating point and have at least 3 elements in + last dimension. Return a new array. + """ + arr = np.asarray(arr) + shape = arr.shape + if shape[-1] < 3: + raise ValueError('Input array has less than 3 color channels') + return dtype.img_as_float(arr, force_copy=True) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index f1c91d67..8d7787aa 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -4,7 +4,8 @@ import itertools import numpy as np from skimage import img_as_float -from skimage._shared.utils import is_str +from skimage._shared import six +from skimage._shared.six.moves import zip from .colorconv import rgb2gray, gray2rgb from . import rgb_colors @@ -30,10 +31,34 @@ def _rgb_vector(color): color : str or array Color name in `color_dict` or RGB float values between [0, 1]. """ - if is_str(color): + if isinstance(color, six.string_types): color = color_dict[color] - # slice to handle RGBA colors - return np.array(color[:3]).reshape(1, 3) + # Slice to handle RGBA colors. + return np.array(color[:3]) + + +def _match_label_with_color(label, colors, bg_label, bg_color): + """Return `unique_labels` and `color_cycle` for label array and color list. + + Colors are cycled for normal labels, but the background color should only + be used for the background. + """ + # Temporarily set background color; it will be removed later. + if bg_color is None: + bg_color = (0, 0, 0) + bg_color = _rgb_vector([bg_color]) + + unique_labels = list(set(label.flat)) + # Ensure that the background label is in front to match call to `chain`. + if bg_label in unique_labels: + unique_labels.remove(bg_label) + unique_labels.insert(0, bg_label) + + # Modify labels and color cycle so background color is used only once. + color_cycle = itertools.cycle(colors) + color_cycle = itertools.chain(bg_color, color_cycle) + + return unique_labels, color_cycle def label2rgb(label, image=None, colors=None, alpha=0.3, @@ -65,7 +90,7 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, colors = [_rgb_vector(c) for c in colors] if image is None: - colorized = np.zeros(label.shape + (3,), dtype=np.float64) + image = np.zeros(label.shape + (3,), dtype=np.float64) # Opacity doesn't make sense if no image exists. alpha = 1 else: @@ -76,20 +101,34 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, warnings.warn("Negative intensities in `image` are not supported") image = img_as_float(rgb2gray(image)) - colorized = gray2rgb(image) * image_alpha + (1 - image_alpha) + image = gray2rgb(image) * image_alpha + (1 - image_alpha) - labels = list(set(label.flat)) - color_cycle = itertools.cycle(colors) + # Ensure that all labels are non-negative so we can index into + # `label_to_color` correctly. + offset = min(label.min(), bg_label) + if offset != 0: + label = label - offset # Make sure you don't modify the input array. + bg_label -= offset - if bg_label in labels: - labels.remove(bg_label) - if bg_color is not None: - labels.insert(0, bg_label) - bg_color = _rgb_vector(bg_color) - color_cycle = itertools.chain(bg_color, color_cycle) + new_type = np.min_scalar_type(label.max()) + if new_type == np.bool: + new_type = np.uint8 + label = label.astype(new_type) - for c, i in itertools.izip(color_cycle, labels): - mask = (label == i) - colorized[mask] = c * alpha + colorized[mask] * (1 - alpha) + unique_labels, color_cycle = _match_label_with_color(label, colors, + bg_label, bg_color) - return colorized + if len(unique_labels) == 0: + return image + + dense_labels = range(max(unique_labels) + 1) + label_to_color = np.array([c for i, c in zip(dense_labels, color_cycle)]) + + result = label_to_color[label] * alpha + image * (1 - alpha) + + # Remove background label if its color was not specified. + remove_background = bg_label in unique_labels and bg_color is None + if remove_background: + result[label == bg_label] = image[label == bg_label] + + return result diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py new file mode 100644 index 00000000..9119ecf4 --- /dev/null +++ b/skimage/color/delta_e.py @@ -0,0 +1,339 @@ +""" +Functions for calculating the "distance" between colors. + +Implicit in these definitions of "distance" is the notion of "Just Noticeable +Distance" (JND). This represents the distance between colors where a human can +perceive different colors. Humans are more sensitive to certain colors than +others, which different deltaE metrics correct for with varying degrees of +sophistication. + +The literature often mentions 1 as the minimum distance for visual +differentiation, but more recent studies (Mahy 1994) peg JND at 2.3 + +The delta-E notation comes from the German word for "Sensation" (Empfindung). + +Reference +--------- +http://en.wikipedia.org/wiki/Color_difference + +""" +from __future__ import division + +import numpy as np + +from skimage.color.colorconv import lab2lch, _cart2polar_2pi + + +def deltaE_cie76(lab1, lab2): + """Euclidean distance between two points in Lab color space + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + + Returns + ------- + dE : array_like + distance between colors `lab1` and `lab2` + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] A. R. Robertson, "The CIE 1976 color-difference formulae," + Color Res. Appl. 2, 7-11 (1977). + """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + L1, a1, b1 = np.rollaxis(lab1, -1)[:3] + L2, a2, b2 = np.rollaxis(lab2, -1)[:3] + return np.sqrt((L2 - L1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2) + + +def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): + """Color difference according to CIEDE 94 standard + + Accommodates perceptual non-uniformities through the use of application + specific scale factors (`kH`, `kC`, `kL`, `k1`, and `k2`). + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + kH : float, optional + Hue scale + kC : float, optional + Chroma scale + kL : float, optional + Lightness scale + k1 : float, optional + first scale parameter + k2 : float, optional + second scale parameter + + Returns + ------- + dE : array_like + color difference between `lab1` and `lab2` + + Notes + ----- + deltaE_ciede94 is not symmetric with respect to lab1 and lab2. CIEDE94 + defines the scales for the lightness, hue, and chroma in terms of the first + color. Consequently, the first color should be regarded as the "reference" + color. + + `kL`, `k1`, `k2` depend on the application and default to the values + suggested for graphic arts + + ========== ============== ========== + Parameter Graphic Arts Textiles + ========== ============== ========== + `kL` 1.000 2.000 + `k1` 0.045 0.048 + `k2` 0.015 0.014 + ========== ============== ========== + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + """ + L1, C1 = np.rollaxis(lab2lch(lab1), -1)[:2] + L2, C2 = np.rollaxis(lab2lch(lab2), -1)[:2] + + dL = L1 - L2 + dC = C1 - C2 + dH2 = get_dH2(lab1, lab2) + + SL = 1 + SC = 1 + k1 * C1 + SH = 1 + k2 * C1 + + dE2 = (dL / (kL * SL)) ** 2 + dE2 += (dC / (kC * SC)) ** 2 + dE2 += dH2 / (kH * SH) ** 2 + return np.sqrt(dE2) + + +def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): + """Color difference as given by the CIEDE 2000 standard. + + CIEDE 2000 is a major revision of CIDE94. The perceptual calibration is + largely based on experience with automotive paint on smooth surfaces. + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + kL : float (range), optional + lightness scale factor, 1 for "acceptably close"; 2 for "imperceptible" + see deltaE_cmc + kC : float (range), optional + chroma scale factor, usually 1 + kH : float (range), optional + hue scale factor, usually 1 + + Returns + ------- + deltaE : array_like + The distance between `lab1` and `lab2` + + Notes + ----- + CIEDE 2000 assumes parametric weighting factors for the lightness, chroma, + and hue (`kL`, `kC`, `kH` respectively). These default to 1. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf + (doi:10.1364/AO.33.008069) + .. [3] M. Melgosa, J. Quesada, and E. Hita, "Uniformity of some recent + color metrics tested with an accurate color-difference tolerance + dataset," Appl. Opt. 33, 8069-8077 (1994). + """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + unroll = False + if lab1.ndim == 1 and lab2.ndim == 1: + unroll = True + if lab1.ndim == 1: + lab1 = lab1[None, :] + if lab2.ndim == 1: + lab2 = lab2[None, :] + L1, a1, b1 = np.rollaxis(lab1, -1)[:3] + L2, a2, b2 = np.rollaxis(lab2, -1)[:3] + + # distort `a` based on average chroma + # then convert to lch coordines from distorted `a` + # all subsequence calculations are in the new coordiantes + # (often denoted "prime" in the literature) + Cbar = 0.5 * (np.hypot(a1, b1) + np.hypot(a2, b2)) + c7 = Cbar ** 7 + G = 0.5 * (1 - np.sqrt(c7 / (c7 + 25 ** 7))) + scale = 1 + G + C1, h1 = _cart2polar_2pi(a1 * scale, b1) + C2, h2 = _cart2polar_2pi(a2 * scale, b2) + # recall that c, h are polar coordiantes. c==r, h==theta + + # cide2000 has four terms to delta_e: + # 1) Luminance term + # 2) Hue term + # 3) Chroma term + # 4) hue Rotation term + + # lightness term + Lbar = 0.5 * (L1 + L2) + tmp = (Lbar - 50) ** 2 + SL = 1 + 0.015 * tmp / np.sqrt(20 + tmp) + L_term = (L2 - L1) / (kL * SL) + + # chroma term + Cbar = 0.5 * (C1 + C2) # new coordiantes + SC = 1 + 0.045 * Cbar + C_term = (C2 - C1) / (kC * SC) + + # hue term + h_diff = h2 - h1 + h_sum = h1 + h2 + CC = C1 * C2 + + dH = h_diff.copy() + dH[h_diff > np.pi] -= 2 * np.pi + dH[h_diff < -np.pi] += 2 * np.pi + dH[CC == 0.] = 0. # if r == 0, dtheta == 0 + dH_term = 2 * np.sqrt(CC) * np.sin(dH / 2) + + Hbar = h_sum.copy() + mask = np.logical_and(CC != 0., np.abs(h_diff) > np.pi) + Hbar[mask * (h_sum < 2 * np.pi)] += 2 * np.pi + Hbar[mask * (h_sum >= 2 * np.pi)] -= 2 * np.pi + Hbar[CC == 0.] *= 2 + Hbar *= 0.5 + + T = (1 - + 0.17 * np.cos(Hbar - np.deg2rad(30)) + + 0.24 * np.cos(2 * Hbar) + + 0.32 * np.cos(3 * Hbar + np.deg2rad(6)) - + 0.20 * np.cos(4 * Hbar - np.deg2rad(63)) + ) + SH = 1 + 0.015 * Cbar * T + + H_term = dH_term / (kH * SH) + + # hue rotation + c7 = Cbar ** 7 + Rc = 2 * np.sqrt(c7 / (c7 + 25 ** 7)) + dtheta = np.deg2rad(30) * np.exp(-((np.rad2deg(Hbar) - 275) / 25) ** 2) + R_term = -np.sin(2 * dtheta) * Rc * C_term * H_term + + # put it all together + dE2 = L_term ** 2 + dE2 += C_term ** 2 + dE2 += H_term ** 2 + dE2 += R_term + ans = np.sqrt(dE2) + if unroll: + ans = ans[0] + return ans + + +def deltaE_cmc(lab1, lab2, kL=1, kC=1): + """Color difference from the CMC l:c standard. + + This color difference was developed by the Colour Measurement Committee + (CMC) of the Society of Dyers and Colourists (United Kingdom). It is + intended for use in the textile industry. + + The scale factors `kL`, `kC` set the weight given to differences in + lightness and chroma relative to differences in hue. The usual values are + ``kL=2``, ``kC=1`` for "acceptability" and ``kL=1``, ``kC=1`` for + "imperceptibility". Colors with ``dE > 1`` are "different" for the given + scale factors. + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + + Returns + ------- + dE : array_like + distance between colors `lab1` and `lab2` + + Notes + ----- + deltaE_cmc the defines the scales for the lightness, hue, and chroma + in terms of the first color. Consequently + ``deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1)`` + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + .. [3] F. J. J. Clarke, R. McDonald, and B. Rigg, "Modification to the + JPC79 colour-difference formula," J. Soc. Dyers Colour. 100, 128-132 + (1984). + """ + L1, C1, h1 = np.rollaxis(lab2lch(lab1), -1)[:3] + L2, C2, h2 = np.rollaxis(lab2lch(lab2), -1)[:3] + + dC = C1 - C2 + dL = L1 - L2 + dH2 = get_dH2(lab1, lab2) + + T = np.where(np.logical_and(np.rad2deg(h1) >= 164, np.rad2deg(h1) <= 345), + 0.56 + 0.2 * np.abs(np.cos(h1 + np.deg2rad(168))), + 0.36 + 0.4 * np.abs(np.cos(h1 + np.deg2rad(35))) + ) + c1_4 = C1 ** 4 + F = np.sqrt(c1_4 / (c1_4 + 1900)) + + SL = np.where(L1 < 16, 0.511, 0.040975 * L1 / (1. + 0.01765 * L1)) + SC = 0.638 + 0.0638 * C1 / (1. + 0.0131 * C1) + SH = SC * (F * T + 1 - F) + + dE2 = (dL / (kL * SL)) ** 2 + dE2 += (dC / (kC * SC)) ** 2 + dE2 += dH2 / (SH ** 2) + return np.sqrt(dE2) + + +def get_dH2(lab1, lab2): + """squared hue difference term occurring in deltaE_cmc and deltaE_ciede94 + + Despite its name, "dH" is not a simple difference of hue values. We avoid + working directly with the hue value, since differencing angles is + troublesome. The hue term is usually written as: + c1 = sqrt(a1**2 + b1**2) + c2 = sqrt(a2**2 + b2**2) + term = (a1-a2)**2 + (b1-b2)**2 - (c1-c2)**2 + dH = sqrt(term) + + However, this has poor roundoff properties when a or b is dominant. + Instead, ab is a vector with elements a and b. The same dH term can be + re-written as: + |ab1-ab2|**2 - (|ab1| - |ab2|)**2 + and then simplified to: + 2*|ab1|*|ab2| - 2*dot(ab1, ab2) + """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + a1, b1 = np.rollaxis(lab1, -1)[1:3] + a2, b2 = np.rollaxis(lab2, -1)[1:3] + + # magnitude of (a, b) is the chroma + C1 = np.hypot(a1, b1) + C2 = np.hypot(a2, b2) + + term = (C1 * C2) - (a1 * a2 + b1 * b2) + return 2*term diff --git a/skimage/color/tests/ciede2000_test_data.txt b/skimage/color/tests/ciede2000_test_data.txt new file mode 100644 index 00000000..b7e3fd57 --- /dev/null +++ b/skimage/color/tests/ciede2000_test_data.txt @@ -0,0 +1,38 @@ +# input, intermediate, and output values for CIEDE2000 dE function +# data taken from "The CIEDE2000 Color-Difference Formula: Implementation Notes, ..." http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf +# tab delimited data +# pair 1 L1 a1 b1 ap1 cp1 hp1 hbar1 G T SL SC SH RT dE 2 L2 a2 b2 ap2 cp2 hp2 +1 1 50.0000 2.6772 -79.7751 2.6774 79.8200 271.9222 270.9611 0.0001 0.6907 1.0000 4.6578 1.8421 -1.7042 2.0425 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +2 1 50.0000 3.1571 -77.2803 3.1573 77.3448 272.3395 271.1698 0.0001 0.6843 1.0000 4.6021 1.8216 -1.7070 2.8615 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +3 1 50.0000 2.8361 -74.0200 2.8363 74.0743 272.1944 271.0972 0.0001 0.6865 1.0000 4.5285 1.8074 -1.7060 3.4412 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +4 1 50.0000 -1.3802 -84.2814 -1.3803 84.2927 269.0618 269.5309 0.0001 0.7357 1.0000 4.7584 1.9217 -1.6809 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +5 1 50.0000 -1.1848 -84.8006 -1.1849 84.8089 269.1995 269.5997 0.0001 0.7335 1.0000 4.7700 1.9218 -1.6822 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +6 1 50.0000 -0.9009 -85.5211 -0.9009 85.5258 269.3964 269.6982 0.0001 0.7303 1.0000 4.7862 1.9217 -1.6840 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +7 1 50.0000 0.0000 0.0000 0.0000 0.0000 0.0000 126.8697 0.5000 1.2200 1.0000 1.0562 1.0229 0.0000 2.3669 2 50.0000 -1.0000 2.0000 -1.5000 2.5000 126.8697 +8 1 50.0000 -1.0000 2.0000 -1.5000 2.5000 126.8697 126.8697 0.5000 1.2200 1.0000 1.0562 1.0229 0.0000 2.3669 2 50.0000 0.0000 0.0000 0.0000 0.0000 0.0000 +9 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 269.9854 0.4998 0.7212 1.0000 1.1681 1.0404 -0.0022 7.1792 2 50.0000 -2.4900 0.0009 -3.7346 3.7346 179.9862 +10 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 269.9847 0.4998 0.7212 1.0000 1.1681 1.0404 -0.0022 7.1792 2 50.0000 -2.4900 0.0010 -3.7346 3.7346 179.9847 +11 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 89.9839 0.4998 0.6175 1.0000 1.1681 1.0346 0.0000 7.2195 2 50.0000 -2.4900 0.0011 -3.7346 3.7346 179.9831 +12 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 89.9831 0.4998 0.6175 1.0000 1.1681 1.0346 0.0000 7.2195 2 50.0000 -2.4900 0.0012 -3.7346 3.7346 179.9816 +13 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 180.0328 0.4998 0.9779 1.0000 1.1121 1.0365 0.0000 4.8045 2 50.0000 0.0009 -2.4900 0.0013 2.4900 270.0311 +14 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 180.0345 0.4998 0.9779 1.0000 1.1121 1.0365 0.0000 4.8045 2 50.0000 0.0010 -2.4900 0.0015 2.4900 270.0345 +15 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 0.0362 0.4998 1.3197 1.0000 1.1121 1.0493 0.0000 4.7461 2 50.0000 0.0011 -2.4900 0.0016 2.4900 270.0380 +16 1 50.0000 2.5000 0.0000 3.7496 3.7496 0.0000 315.0000 0.4998 0.8454 1.0000 1.1406 1.0396 -0.0001 4.3065 2 50.0000 0.0000 -2.5000 0.0000 2.5000 270.0000 +17 1 50.0000 2.5000 0.0000 3.4569 3.4569 0.0000 346.2470 0.3827 1.4453 1.1608 1.9547 1.4599 -0.0003 27.1492 2 73.0000 25.0000 -18.0000 34.5687 38.9743 332.4939 +18 1 50.0000 2.5000 0.0000 3.4954 3.4954 0.0000 51.7766 0.3981 0.6447 1.0640 1.7498 1.1612 0.0000 22.8977 2 61.0000 -5.0000 29.0000 -6.9907 29.8307 103.5532 +19 1 50.0000 2.5000 0.0000 3.5514 3.5514 0.0000 272.2362 0.4206 0.6521 1.0251 1.9455 1.2055 -0.8219 31.9030 2 56.0000 -27.0000 -3.0000 -38.3556 38.4728 184.4723 +20 1 50.0000 2.5000 0.0000 3.5244 3.5244 0.0000 11.9548 0.4098 1.1031 1.0400 1.9120 1.3353 0.0000 19.4535 2 58.0000 24.0000 15.0000 33.8342 37.0102 23.9095 +21 1 50.0000 2.5000 0.0000 3.7494 3.7494 0.0000 3.5056 0.4997 1.2616 1.0000 1.1923 1.0808 0.0000 1.0000 2 50.0000 3.1736 0.5854 4.7596 4.7954 7.0113 +22 1 50.0000 2.5000 0.0000 3.7493 3.7493 0.0000 0.0000 0.4997 1.3202 1.0000 1.1956 1.0861 0.0000 1.0000 2 50.0000 3.2972 0.0000 4.9450 4.9450 0.0000 +23 1 50.0000 2.5000 0.0000 3.7497 3.7497 0.0000 5.8190 0.4999 1.2197 1.0000 1.1486 1.0604 0.0000 1.0000 2 50.0000 1.8634 0.5757 2.7949 2.8536 11.6380 +24 1 50.0000 2.5000 0.0000 3.7493 3.7493 0.0000 1.9603 0.4997 1.2883 1.0000 1.1946 1.0836 0.0000 1.0000 2 50.0000 3.2592 0.3350 4.8879 4.8994 3.9206 +25 1 60.2574 -34.0099 36.2677 -34.0678 49.7590 133.2085 132.0835 0.0017 1.3010 1.1427 3.2946 1.9951 0.0000 1.2644 2 60.4626 -34.1751 39.4387 -34.2333 52.2238 130.9584 +26 1 63.0109 -31.0961 -5.8663 -32.6194 33.1427 190.1951 188.8221 0.0490 0.9402 1.1831 2.4549 1.4560 0.0000 1.2630 2 62.8187 -29.7946 -4.0864 -31.2542 31.5202 187.4490 +27 1 61.2901 3.7196 -5.3901 5.5668 7.7487 315.9240 310.0313 0.4966 0.6952 1.1586 1.3092 1.0717 -0.0032 1.8731 2 61.4292 2.2480 -4.9620 3.3644 5.9950 304.1385 +28 1 35.0831 -44.1164 3.7933 -44.3939 44.5557 175.1161 176.4290 0.0063 1.0168 1.2148 2.9105 1.6476 0.0000 1.8645 2 35.0232 -40.0716 1.5901 -40.3237 40.3550 177.7418 +29 1 22.7233 20.0904 -46.6940 20.1424 50.8532 293.3339 291.3809 0.0026 0.3636 1.4014 3.1597 1.2617 -1.2537 2.0373 2 23.0331 14.9730 -42.5619 15.0118 45.1317 289.4279 +30 1 36.4612 47.8580 18.3852 47.9197 51.3256 20.9901 21.8781 0.0013 0.9239 1.1943 3.3888 1.7357 0.0000 1.4146 2 36.2715 50.5065 21.2231 50.5716 54.8444 22.7660 +31 1 90.8027 -2.0831 1.4410 -3.1245 3.4408 155.2410 167.1011 0.4999 1.1546 1.6110 1.1329 1.0511 0.0000 1.4441 2 91.1528 -1.6435 0.0447 -2.4651 2.4655 178.9612 +32 1 90.9257 -0.5406 -0.9208 -0.8109 1.2270 228.6315 218.4363 0.5000 1.3916 1.5930 1.0620 1.0288 0.0000 1.5381 2 88.6381 -0.8985 -0.7239 -1.3477 1.5298 208.2412 +33 1 6.7747 -0.2908 -2.4247 -0.4362 2.4636 259.8025 263.0049 0.4999 0.9556 1.6517 1.1057 1.0337 -0.0004 0.6377 2 5.8714 -0.0985 -2.2286 -0.1477 2.2335 266.2073 +34 1 2.0776 0.0795 -1.1350 0.1192 1.1412 275.9978 268.0910 0.5000 0.7826 1.7246 1.0383 1.0100 0.0000 0.9082 2 0.9033 -0.0636 -0.5514 -0.0954 0.5596 260.18421 diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 05ab6915..fbec9ba6 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -33,7 +33,9 @@ from skimage.color import (rgb2hsv, hsv2rgb, rgb2grey, gray2rgb, xyz2lab, lab2xyz, lab2rgb, rgb2lab, - is_rgb, is_gray + is_rgb, is_gray, + lab2lch, lch2lab, + guess_spatial_dimensions ) from skimage import data_dir, data @@ -41,6 +43,19 @@ from skimage import data_dir, data import colorsys +def test_guess_spatial_dimensions(): + im1 = np.zeros((5, 5)) + im2 = np.zeros((5, 5, 5)) + im3 = np.zeros((5, 5, 3)) + im4 = np.zeros((5, 5, 5, 3)) + im5 = np.zeros((5,)) + assert_equal(guess_spatial_dimensions(im1), 2) + assert_equal(guess_spatial_dimensions(im2), 3) + assert_equal(guess_spatial_dimensions(im3), None) + assert_equal(guess_spatial_dimensions(im4), 3) + assert_raises(ValueError, guess_spatial_dimensions, im5) + + class TestColorconv(TestCase): img_rgb = imread(os.path.join(data_dir, 'color.png')) @@ -235,6 +250,43 @@ class TestColorconv(TestCase): img_rgb = img_as_float(self.img_rgb) assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb) + def test_lab_lch_roundtrip(self): + rgb = img_as_float(self.img_rgb) + lab = rgb2lab(rgb) + lab2 = lch2lab(lab2lch(lab)) + assert_array_almost_equal(lab2, lab) + + def test_rgb_lch_roundtrip(self): + rgb = img_as_float(self.img_rgb) + lab = rgb2lab(rgb) + lch = lab2lch(lab) + lab2 = lch2lab(lch) + rgb2 = lab2rgb(lab2) + assert_array_almost_equal(rgb, rgb2) + + def test_lab_lch_0d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch2 = lab2lch(lab0[None, None, :]) + assert_array_almost_equal(lch0, lch2[0, 0, :]) + + def test_lab_lch_1d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch1 = lab2lch(lab0[None, :]) + assert_array_almost_equal(lch0, lch1[0, :]) + + def test_lab_lch_3d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch3 = lab2lch(lab0[None, None, None, :]) + assert_array_almost_equal(lch0, lch3[0, 0, 0, :]) + + def _get_lab0(self): + rgb = img_as_float(self.img_rgb[:1, :1, :]) + return rgb2lab(rgb)[0, 0, :] + + def test_gray2rgb(): x = np.array([0, 0.5, 1]) assert_raises(ValueError, gray2rgb, x) diff --git a/skimage/color/tests/test_colorlabel.py b/skimage/color/tests/test_colorlabel.py index fa6ffcf3..dcfbe4ea 100644 --- a/skimage/color/tests/test_colorlabel.py +++ b/skimage/color/tests/test_colorlabel.py @@ -3,7 +3,8 @@ import itertools import numpy as np from numpy import testing from skimage.color.colorlabel import label2rgb -from numpy.testing import assert_array_almost_equal as assert_close +from numpy.testing import (assert_array_almost_equal as assert_close, + assert_array_equal) def test_shape_mismatch(): @@ -69,6 +70,26 @@ def test_bg_and_color_cycle(): assert_close(pixel, color) +def test_label_consistency(): + """Assert that the same labels map to the same colors.""" + label_1 = np.arange(5).reshape(1, -1) + label_2 = np.array([2, 4]) + colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (1, 0, 1)] + # Set alphas just in case the defaults change + rgb_1 = label2rgb(label_1, colors=colors) + rgb_2 = label2rgb(label_2, colors=colors) + for label_id in label_2.flat: + assert_close(rgb_1[label_1 == label_id], rgb_2[label_2 == label_id]) + +def test_leave_labels_alone(): + labels = np.array([-1, 0, 1]) + labels_saved = labels.copy() + + label2rgb(labels) + label2rgb(labels, bg_label=1) + assert_array_equal(labels, labels_saved) + + if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/color/tests/test_delta_e.py b/skimage/color/tests/test_delta_e.py new file mode 100644 index 00000000..84f13b48 --- /dev/null +++ b/skimage/color/tests/test_delta_e.py @@ -0,0 +1,167 @@ +"""Test for correctness of color distance functions""" +from os.path import abspath, dirname, join as pjoin + +import numpy as np +from numpy.testing import assert_allclose + +from skimage.color import (deltaE_cie76, + deltaE_ciede94, + deltaE_ciede2000, + deltaE_cmc) + + +def test_ciede2000_dE(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_ciede2000(lab1, lab2) + + assert_allclose(dE2, data['dE'], rtol=1.e-4) + + +def load_ciede2000_data(): + dtype = [('pair', int), + ('1', int), + ('L1', float), + ('a1', float), + ('b1', float), + ('a1_prime', float), + ('C1_prime', float), + ('h1_prime', float), + ('hbar_prime', float), + ('G', float), + ('T', float), + ('SL', float), + ('SC', float), + ('SH', float), + ('RT', float), + ('dE', float), + ('2', int), + ('L2', float), + ('a2', float), + ('b2', float), + ('a2_prime', float), + ('C2_prime', float), + ('h2_prime', float), + ] + + # note: ciede_test_data.txt contains several intermediate quantities + path = pjoin(dirname(abspath(__file__)), 'ciede2000_test_data.txt') + return np.loadtxt(path, dtype=dtype) + + +def test_cie76(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_cie76(lab1, lab2) + oracle = np.array([ + 4.00106328, 6.31415011, 9.1776999, 2.06270077, 2.36957073, + 2.91529271, 2.23606798, 2.23606798, 4.98000036, 4.9800004, + 4.98000044, 4.98000049, 4.98000036, 4.9800004, 4.98000044, + 3.53553391, 36.86800781, 31.91002977, 30.25309901, 27.40894015, + 0.89242934, 0.7972, 0.8583065, 0.82982507, 3.1819238, + 2.21334297, 1.53890382, 4.60630929, 6.58467989, 3.88641412, + 1.50514845, 2.3237848, 0.94413208, 1.31910843 + ]) + assert_allclose(dE2, oracle, rtol=1.e-8) + + +def test_ciede94(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_ciede94(lab1, lab2) + oracle = np.array([ + 1.39503887, 1.93410055, 2.45433566, 0.68449187, 0.6695627, + 0.69194527, 2.23606798, 2.03163832, 4.80069441, 4.80069445, + 4.80069449, 4.80069453, 4.80069441, 4.80069445, 4.80069449, + 3.40774352, 34.6891632, 29.44137328, 27.91408781, 24.93766082, + 0.82213163, 0.71658427, 0.8048753, 0.75284394, 1.39099471, + 1.24808929, 1.29795787, 1.82045088, 2.55613309, 1.42491303, + 1.41945261, 2.3225685, 0.93853308, 1.30654464 + ]) + assert_allclose(dE2, oracle, rtol=1.e-8) + + +def test_cmc(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_cmc(lab1, lab2) + oracle = np.array([ + 1.73873611, 2.49660844, 3.30494501, 0.85735576, 0.88332927, + 0.97822692, 3.50480874, 2.87930032, 6.5783807, 6.57838075, + 6.5783808, 6.57838086, 6.67492321, 6.67492326, 6.67492331, + 4.66852997, 42.10875485, 39.45889064, 38.36005919, 33.93663807, + 1.14400168, 1.00600419, 1.11302547, 1.05335328, 1.42822951, + 1.2548143, 1.76838061, 2.02583367, 3.08695508, 1.74893533, + 1.90095165, 1.70258148, 1.80317207, 2.44934417 + ]) + + assert_allclose(dE2, oracle, rtol=1.e-8) + + +def test_single_color_cie76(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_cie76(lab1, lab2) + + +def test_single_color_ciede94(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_ciede94(lab1, lab2) + + +def test_single_color_ciede2000(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_ciede2000(lab1, lab2) + + +def test_single_color_cmc(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_cmc(lab1, lab2) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index 864e3638..ecd261f7 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -23,7 +23,8 @@ __all__ = ['load', 'horse', 'clock', 'immunohistochemistry', - 'chelsea'] + 'chelsea', + 'coffee'] def load(f): @@ -184,3 +185,18 @@ def chelsea(): """ return load("chelsea.png") + + +def coffee(): + """Coffee cup. + + This photograph is courtesy of Pikolo Espresso Bar. + It contains several elliptical shapes as well as varying texture (smooth + porcelain to course wood grain). + + Notes + ----- + No copyright restrictions. CC0 by the photographer (Rachel Michetti). + + """ + return load("coffee.png") diff --git a/skimage/data/coffee.png b/skimage/data/coffee.png new file mode 100644 index 00000000..f8350bf7 Binary files /dev/null and b/skimage/data/coffee.png differ diff --git a/skimage/data/tests/test_data.py b/skimage/data/tests/test_data.py index 94fe3a5e..49cd4b5a 100644 --- a/skimage/data/tests/test_data.py +++ b/skimage/data/tests/test_data.py @@ -44,6 +44,11 @@ def test_chelsea(): data.chelsea() +def test_coffee(): + """ Test that "coffee" image can be loaded. """ + data.coffee() + + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/draw/__init__.py b/skimage/draw/__init__.py index 38c114f7..5f788ea7 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -1,11 +1,18 @@ from .draw import circle, ellipse, set_color -from ._draw import line, polygon, ellipse_perimeter, circle_perimeter, \ - bezier_segment +from .draw3d import ellipsoid, ellipsoid_stats +from ._draw import (line, line_aa, polygon, ellipse_perimeter, + circle_perimeter, circle_perimeter_aa, + _bezier_segment, bezier_curve) __all__ = ['line', + 'line_aa', + 'bezier_curve', 'polygon', 'ellipse', 'ellipse_perimeter', + 'ellipsoid', + 'ellipsoid_stats', 'circle', 'circle_perimeter', + 'circle_perimeter_aa', 'set_color'] diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index e95a2366..c22600a4 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -6,7 +6,7 @@ import math import numpy as np cimport numpy as cnp -from libc.math cimport sqrt, sin, cos, floor +from libc.math cimport sqrt, sin, cos, floor, ceil from skimage._shared.geometry cimport point_in_polygon @@ -27,6 +27,10 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. + Notes + ----- + Anti-aliased line generator is available with `line_aa`. + Examples -------- >>> from skimage.draw import line @@ -47,8 +51,6 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): """ - cdef cnp.ndarray[cnp.intp_t, ndim=1, mode="c"] rr, cc - cdef char steep = 0 cdef Py_ssize_t dx = abs(x2 - x) cdef Py_ssize_t dy = abs(y2 - y) @@ -69,8 +71,8 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): sx, sy = sy, sx d = (2 * dy) - dx - rr = np.zeros(int(dx) + 1, dtype=np.intp) - cc = np.zeros(int(dx) + 1, dtype=np.intp) + cdef Py_ssize_t[:] rr = np.zeros(int(dx) + 1, dtype=np.intp) + cdef Py_ssize_t[:] cc = np.zeros(int(dx) + 1, dtype=np.intp) for i in range(dx): if steep: @@ -88,7 +90,100 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): rr[dx] = y2 cc[dx] = x2 - return rr, cc + return np.asarray(rr), np.asarray(cc) + + +def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): + """Generate anti-aliased line pixel coordinates. + + Parameters + ---------- + y1, x1 : int + Starting position (row, column). + y2, x2 : int + End position (row, column). + + Returns + ------- + rr, cc, val : (N,) ndarray (int, int, float) + Indices of pixels (`rr`, `cc`) and intensity values (`val`). + ``img[rr, cc] = val``. + + References + ---------- + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf + + Examples + -------- + >>> from skimage.draw import line_aa + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc, val = line_aa(1, 1, 8, 8) + >>> img[rr, cc] = val * 255 + >>> img + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 255, 56, 0, 0, 0, 0, 0, 0, 0], + [ 0, 56, 255, 56, 0, 0, 0, 0, 0, 0], + [ 0, 0, 56, 255, 56, 0, 0, 0, 0, 0], + [ 0, 0, 0, 56, 255, 56, 0, 0, 0, 0], + [ 0, 0, 0, 0, 56, 255, 56, 0, 0, 0], + [ 0, 0, 0, 0, 0, 56, 255, 56, 0, 0], + [ 0, 0, 0, 0, 0, 0, 56, 255, 56, 0], + [ 0, 0, 0, 0, 0, 0, 0, 56, 255, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + cdef list rr = list() + cdef list cc = list() + cdef list val = list() + + cdef int dx = abs(x1 - x2) + cdef int dy = abs(y1 - y2) + cdef int err = dx - dy + cdef int x, y, e, ed, sign_x, sign_y + + if x1 < x2: + sign_x = 1 + else: + sign_x = -1 + + if y1 < y2: + sign_y = 1 + else: + sign_y = -1 + + if dx + dy == 0: + ed = 1 + else: + ed = (sqrt(dx*dx + dy*dy)) + + x, y = x1, y1 + while True: + cc.append(x) + rr.append(y) + val.append(1. * abs(err - dx + dy) / (ed)) + e = err + if 2 * e >= -dx: + if x == x2: + break + if e + dy < ed: + cc.append(x) + rr.append(y + sign_y) + val.append(1. * abs(e + dy) / (ed)) + err -= dy + x += sign_x + if 2 * e <= dy: + if y == y2: + break + if dx - e < ed: + cc.append(x) + rr.append(y) + val.append(abs(dx - e) / (ed)) + err += dx + y += sign_y + + return (np.array(rr, dtype=np.intp), + np.array(cc, dtype=np.intp), + 1. - np.array(val, dtype=np.float)) def polygon(y, x, shape=None): @@ -136,9 +231,9 @@ def polygon(y, x, shape=None): cdef Py_ssize_t nr_verts = x.shape[0] cdef Py_ssize_t minr = int(max(0, y.min())) - cdef Py_ssize_t maxr = int(math.ceil(y.max())) + cdef Py_ssize_t maxr = int(ceil(y.max())) cdef Py_ssize_t minc = int(max(0, x.min())) - cdef Py_ssize_t maxc = int(math.ceil(x.max())) + cdef Py_ssize_t maxc = int(ceil(x.max())) # make sure output coordinates do not exceed image size if shape is not None: @@ -149,8 +244,8 @@ def polygon(y, x, shape=None): # make contigous arrays for r, c coordinates cdef cnp.ndarray contiguous_rdata, contiguous_cdata - contiguous_rdata = np.ascontiguousarray(y, 'double') - contiguous_cdata = np.ascontiguousarray(x, 'double') + contiguous_rdata = np.ascontiguousarray(y, dtype=np.double) + contiguous_cdata = np.ascontiguousarray(x, dtype=np.double) cdef cnp.double_t* rptr = contiguous_rdata.data cdef cnp.double_t* cptr = contiguous_cdata.data @@ -184,6 +279,7 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, Returns ------- rr, cc : (N,) ndarray of int + Bresenham and Andres' method: Indices of pixels that belong to the circle perimeter. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. @@ -194,13 +290,14 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, circles create a disc whereas Bresenham can make holes. There is also less distortions when Andres circles are rotated. Bresenham method is also known as midpoint circle algorithm. + Anti-aliased circle generator is available with `circle_perimeter_aa`. References ---------- .. [1] J.E. Bresenham, "Algorithm for computer control of a digital - plotter", 4 (1965) 25-30. - .. [2] E. Andres, "Discrete circles, rings and spheres", - 18 (1994) 695-706. + plotter", IBM Systems journal, 4 (1965) 25-30. + .. [2] E. Andres, "Discrete circles, rings and spheres", Computers & + Graphics, 18 (1994) 695-706. Examples -------- @@ -228,6 +325,10 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, cdef Py_ssize_t x = 0 cdef Py_ssize_t y = radius cdef Py_ssize_t d = 0 + + cdef double dceil = 0 + cdef double dceil_prev = 0 + cdef char cmethod if method == 'bresenham': d = 3 - 2 * radius @@ -260,8 +361,84 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, d = d + 2 * (y - x - 1) y = y - 1 x = x + 1 + return (np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx) - return np.array(rr, dtype=np.intp) + cy, np.array(cc, dtype=np.intp) + cx + +def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): + """Generate anti-aliased circle perimeter coordinates. + + Parameters + ---------- + cy, cx : int + Centre coordinate of circle. + radius: int + Radius of circle. + + Returns + ------- + rr, cc, val : (N,) ndarray (int, int, float) + Indices of pixels (`rr`, `cc`) and intensity values (`val`). + ``img[rr, cc] = val``. + + Notes + ----- + Wu's method draws anti-aliased circle. This implementation doesn't use + lookup table optimization. + + References + ---------- + .. [1] X. Wu, "An efficient antialiasing technique", In ACM SIGGRAPH + Computer Graphics, 25 (1991) 143-152. + + Examples + -------- + >>> from skimage.draw import circle_perimeter_aa + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc, val = circle_perimeter_aa(4, 4, 3) + >>> img[rr, cc] = val * 255 + >>> img + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 60, 211, 255, 211, 60, 0, 0, 0], + [ 0, 60, 194, 43, 0, 43, 194, 60, 0, 0], + [ 0, 211, 43, 0, 0, 0, 43, 211, 0, 0], + [ 0, 255, 0, 0, 0, 0, 0, 255, 0, 0], + [ 0, 211, 43, 0, 0, 0, 43, 211, 0, 0], + [ 0, 60, 194, 43, 0, 43, 194, 60, 0, 0], + [ 0, 0, 60, 211, 255, 211, 60, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + + cdef Py_ssize_t x = 0 + cdef Py_ssize_t y = radius + cdef Py_ssize_t d = 0 + + cdef double dceil = 0 + cdef double dceil_prev = 0 + + cdef list rr = [y, x, y, x, -y, -x, -y, -x] + cdef list cc = [x, y, -x, -y, x, y, -x, -y] + cdef list val = [1] * 8 + + while y > x + 1: + x += 1 + dceil = sqrt(radius**2 - x**2) + dceil = ceil(dceil) - dceil + if dceil < dceil_prev: + y -= 1 + rr.extend([y, y - 1, x, x, y, y - 1, x, x]) + cc.extend([x, x, y, y - 1, -x, -x, -y, 1 - y]) + + rr.extend([-y, 1 - y, -x, -x, -y, 1 - y, -x, -x]) + cc.extend([x, x, y, y - 1, -x, -x, -y, 1 - y]) + + val.extend([1 - dceil, dceil] * 8) + dceil_prev = dceil + + return (np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx, + np.array(val, dtype=np.float)) def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, @@ -272,9 +449,9 @@ def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, ---------- cy, cx : int Centre coordinate of ellipse. - yradius, xradius: int + yradius, xradius : int Minor and major semi-axes. ``(x/xradius)**2 + (y/yradius)**2 = 1``. - orientation: double, optional (default 0) + orientation : double, optional (default 0) Major axis orientation in clockwise direction as radians. Returns @@ -384,38 +561,38 @@ def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, iyd = int(floor(ya * w + 0.5)) # Draw the 4 quadrants - rr, cc = bezier_segment(iy0 + iyd, ix0, iy0, ix0, iy0, ix0 + ixd, 1-w) + rr, cc = _bezier_segment(iy0 + iyd, ix0, iy0, ix0, iy0, ix0 + ixd, 1-w) py.extend(rr) px.extend(cc) - rr, cc = bezier_segment(iy0 + iyd, ix0, iy1, ix0, iy1, ix1 - ixd, w) + rr, cc = _bezier_segment(iy0 + iyd, ix0, iy1, ix0, iy1, ix1 - ixd, w) py.extend(rr) px.extend(cc) - rr, cc = bezier_segment(iy1 - iyd, ix1, iy1, ix1, iy1, ix1 - ixd, 1-w) + rr, cc = _bezier_segment(iy1 - iyd, ix1, iy1, ix1, iy1, ix1 - ixd, 1-w) py.extend(rr) px.extend(cc) - rr, cc = bezier_segment(iy1 - iyd, ix1, iy0, ix1, iy0, ix0 + ixd, w) + rr, cc = _bezier_segment(iy1 - iyd, ix1, iy0, ix1, iy0, ix0 + ixd, w) py.extend(rr) px.extend(cc) return np.array(py, dtype=np.intp), np.array(px, dtype=np.intp) -def bezier_segment(Py_ssize_t y0, Py_ssize_t x0, - Py_ssize_t y1, Py_ssize_t x1, - Py_ssize_t y2, Py_ssize_t x2, - double weight): +def _bezier_segment(Py_ssize_t y0, Py_ssize_t x0, + Py_ssize_t y1, Py_ssize_t x1, + Py_ssize_t y2, Py_ssize_t x2, + double weight): """Generate Bezier segment coordinates. Parameters ---------- y0, x0 : int - Coordinates of the first point + Coordinates of the first control point. y1, x1 : int - Coordinates of the middle point + Coordinates of the middle control point. y2, x2 : int - Coordinates of the last point + Coordinates of the last control point. weight : double - Middle point weight, it describes the line tension. + Middle control point weight, it describes the line tension. Returns ------- @@ -427,7 +604,7 @@ def bezier_segment(Py_ssize_t y0, Py_ssize_t x0, Notes ----- The algorithm is the rational quadratic algorithm presented in - reference [1]. + reference [1]_. References ---------- @@ -494,8 +671,8 @@ def bezier_segment(Py_ssize_t y0, Py_ssize_t x0, sy = floor((y0 + 2 * weight * y1 + y2) * xy * 0.5 + 0.5) dx = floor((weight * x1 + x0) * xy + 0.5) dy = floor((y1 * weight + y0) * xy + 0.5) - return bezier_segment(y0, x0, (dy), (dx), - (sy), (sx), cur) + return _bezier_segment(y0, x0, (dy), (dx), + (sy), (sx), cur) err = dx + dy - xy while dy <= xy and dx >= xy: @@ -528,6 +705,137 @@ def bezier_segment(Py_ssize_t y0, Py_ssize_t x0, return np.array(py, dtype=np.intp), np.array(px, dtype=np.intp) +def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, + Py_ssize_t y1, Py_ssize_t x1, + Py_ssize_t y2, Py_ssize_t x2, + double weight): + """Generate Bezier curve coordinates. + + Parameters + ---------- + y0, x0 : int + Coordinates of the first control point. + y1, x1 : int + Coordinates of the middle control point. + y2, x2 : int + Coordinates of the last control point. + weight : double + Middle control point weight, it describes the line tension. + + Returns + ------- + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the Bezier curve. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Notes + ----- + The algorithm is the rational quadratic algorithm presented in + reference [1]_. + + References + ---------- + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf + + Examples + -------- + >>> import numpy as np + >>> from skimage.draw import bezier_curve + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = bezier_curve(1, 5, 5, -2, 8, 8, 2) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + # Pixels + cdef list px = list() + cdef list py = list() + + cdef int x, y + cdef double xx, yy, ww, t, q + x = x0 - 2 * x1 + x2 + y = y0 - 2 * y1 + y2 + + xx = x0 - x1 + yy = y0 - y1 + + if xx * (x2 - x1) > 0: + if yy * (y2 - y1): + if abs(xx * y) > abs(yy * x): + x0 = x2 + x2 = (xx + x1) + y0 = y2 + y2 = (yy + y1) + if (x0 == x2) or (weight == 1.): + t = (x0 - x1) / x + else: + q = sqrt(4. * weight * weight * (x0 - x1) * (x2 - x1) + (x2 - x0) * floor(x2 - x0)) + if (x1 < x0): + q = -q + t = (2. * weight * (x0 - x1) - x0 + x2 + q) / (2. * (1. - weight) * (x2 - x0)) + + q = 1. / (2. * t * (1. - t) * (weight - 1.) + 1.0) + xx = (t * t * (x0 - 2. * weight * x1 + x2) + 2. * t * (weight * x1 - x0) + x0) * q + yy = (t * t * (y0 - 2. * weight * y1 + y2) + 2. * t * (weight * y1 - y0) + y0) * q + ww = t * (weight - 1.) + 1. + ww *= ww * q + weight = ((1. - t) * (weight - 1.) + 1.) * sqrt(q) + x = (xx + 0.5) + y = (yy + 0.5) + yy = (xx - x0) * (y1 - y0) / (x1 - x0) + y0 + + rr, cc = _bezier_segment(y0, x0, (yy + 0.5), x, y, x, ww) + px.extend(rr) + py.extend(cc) + + yy = (xx - x2) * (y1 - y2) / (x1 - x2) + y2 + y1 = (yy + 0.5) + x0 = x1 = x + y0 = y + if (y0 - y1) * floor(y2 - y1) > 0: + if (y0 == y2) or (weight == 1): + t = (y0 - y1) / (y0 - 2. * y1 + y2) + else: + q = sqrt(4. * weight * weight * (y0 - y1) * (y2 - y1) + (y2 - y0) * floor(y2 - y0)) + if y1 < y0: + q = -q + t = (2. * weight * (y0 - y1) - y0 + y2 + q) / (2. * (1. - weight) * (y2 - y0)) + q = 1. / (2. * t * (1. - t) * (weight - 1.) + 1.) + xx = (t * t * (x0 - 2. * weight * x1 + x2) + 2. * t * (weight * x1 - x0) + x0) * q + yy = (t * t * (y0 - 2. * weight * y1 + y2) + 2. * t * (weight * y1 - y0) + y0) * q + ww = t * (weight - 1.) + 1. + ww *= ww * q + weight = ((1. - t) * (weight - 1.) + 1.) * sqrt(q) + x = (xx + 0.5) + y = (yy + 0.5) + xx = (x1 - x0) * (yy - y0) / (y1 - y0) + x0 + + rr, cc = _bezier_segment(y0, x0, y, (xx + 0.5), y, x, ww) + px.extend(rr) + py.extend(cc) + + xx = (x1 - x2) * (yy - y2) / (y1 - y2) + x2 + x1 = (xx + 0.5) + x0 = x + y0 = y1 = y + + rr, cc = _bezier_segment(y0, x0, y1, x1, y2, x2, weight * weight) + px.extend(rr) + py.extend(cc) + return np.array(px, dtype=np.intp), np.array(py, dtype=np.intp) + + def set_color(img, coords, color): """Set pixel color in the image at the given coordinates. diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py index d01bc2b0..cbf3ced2 100644 --- a/skimage/draw/draw.py +++ b/skimage/draw/draw.py @@ -52,7 +52,7 @@ def ellipse(cy, cx, yradius, xradius, shape=None): dc = 1 / float(xradius) r, c = np.ogrid[-1:1:dr, -1:1:dc] - rr, cc = np.nonzero(r ** 2 + c ** 2 < 1) + rr, cc = np.nonzero(r ** 2 + c ** 2 < 1) rr.flags.writeable = True cc.flags.writeable = True diff --git a/skimage/draw/draw3d.py b/skimage/draw/draw3d.py new file mode 100644 index 00000000..db2f0d39 --- /dev/null +++ b/skimage/draw/draw3d.py @@ -0,0 +1,115 @@ +# coding: utf-8 +import numpy as np +from scipy.special import (ellipkinc as ellip_F, ellipeinc as ellip_E) + + +def ellipsoid(a, b, c, spacing=(1., 1., 1.), levelset=False): + """ + Generates ellipsoid with semimajor axes aligned with grid dimensions + on grid with specified `spacing`. + + Parameters + ---------- + a : float + Length of semimajor axis aligned with x-axis. + b : float + Length of semimajor axis aligned with y-axis. + c : float + Length of semimajor axis aligned with z-axis. + spacing : tuple of floats, length 3 + Spacing in (x, y, z) spatial dimensions. + levelset : bool + If True, returns the level set for this ellipsoid (signed level + set about zero, with positive denoting interior) as np.float64. + False returns a binarized version of said level set. + + Returns + ------- + ellip : (N, M, P) array + Ellipsoid centered in a correctly sized array for given `spacing`. + Boolean dtype unless `levelset=True`, in which case a float array is + returned with the level set above 0.0 representing the ellipsoid. + + """ + if (a <= 0) or (b <= 0) or (c <= 0): + raise ValueError('Parameters a, b, and c must all be > 0') + + offset = np.r_[1, 1, 1] * np.r_[spacing] + + # Calculate limits, and ensure output volume is odd & symmetric + low = np.ceil((- np.r_[a, b, c] - offset)) + high = np.floor((np.r_[a, b, c] + offset + 1)) + + for dim in range(3): + if (high[dim] - low[dim]) % 2 == 0: + low[dim] -= 1 + num = np.arange(low[dim], high[dim], spacing[dim]) + if 0 not in num: + low[dim] -= np.max(num[num < 0]) + + # Generate (anisotropic) spatial grid + x, y, z = np.mgrid[low[0]:high[0]:spacing[0], + low[1]:high[1]:spacing[1], + low[2]:high[2]:spacing[2]] + + if not levelset: + arr = ((x / float(a)) ** 2 + + (y / float(b)) ** 2 + + (z / float(c)) ** 2) <= 1 + else: + arr = ((x / float(a)) ** 2 + + (y / float(b)) ** 2 + + (z / float(c)) ** 2) - 1 + + return arr + + +def ellipsoid_stats(a, b, c): + """ + Calculates analytical surface area and volume for ellipsoid with + semimajor axes aligned with grid dimensions of specified `spacing`. + + Parameters + ---------- + a : float + Length of semimajor axis aligned with x-axis. + b : float + Length of semimajor axis aligned with y-axis. + c : float + Length of semimajor axis aligned with z-axis. + + Returns + ------- + vol : float + Calculated volume of ellipsoid. + surf : float + Calculated surface area of ellipsoid. + + """ + if (a <= 0) or (b <= 0) or (c <= 0): + raise ValueError('Parameters a, b, and c must all be > 0') + + # Calculate volume & surface area + # Surface calculation requires a >= b >= c and a != c. + abc = [a, b, c] + abc.sort(reverse=True) + a = abc[0] + b = abc[1] + c = abc[2] + + # Volume + vol = 4 / 3. * np.pi * a * b * c + + # Analytical ellipsoid surface area + phi = np.arcsin((1. - (c ** 2 / (a ** 2.))) ** 0.5) + d = float((a ** 2 - c ** 2) ** 0.5) + m = (a ** 2 * (b ** 2 - c ** 2) / + float(b ** 2 * (a ** 2 - c ** 2))) + F = ellip_F(phi, m) + E = ellip_E(phi, m) + + surf = 2 * np.pi * (c ** 2 + + b * c ** 2 / d * F + + b * d * E) + + return vol, surf diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index f15fba01..d3cab811 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -1,8 +1,11 @@ -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_equal, assert_equal import numpy as np -from skimage.draw import line, polygon, circle, circle_perimeter, \ - ellipse, ellipse_perimeter, bezier_segment +from skimage.draw import (line, line_aa, polygon, + circle, circle_perimeter, circle_perimeter_aa, + ellipse, ellipse_perimeter, + _bezier_segment, bezier_curve, + ) def test_line_horizontal(): @@ -52,6 +55,43 @@ def test_line_diag(): assert_array_equal(img, img_) +def test_line_aa_horizontal(): + img = np.zeros((10, 10)) + + rr, cc, val = line_aa(0, 0, 0, 9) + img[rr, cc] = val + + img_ = np.zeros((10, 10)) + img_[0, :] = 1 + + assert_array_equal(img, img_) + + +def test_line_aa_vertical(): + img = np.zeros((10, 10)) + + rr, cc, val = line_aa(0, 0, 9, 0) + img[rr, cc] = val + + img_ = np.zeros((10, 10)) + img_[:, 0] = 1 + + assert_array_equal(img, img_) + + +def test_line_aa_diagonal(): + img = np.zeros((10, 10)) + + rr, cc, val = line_aa(0, 0, 9, 6) + img[rr, cc] = 1 + + # Check that each pixel belonging to line, + # also belongs to line_aa + r, c = line(0, 0, 9, 6) + for x, y in zip(r, c): + assert_equal(img[r, c], 1) + + def test_polygon_rectangle(): img = np.zeros((10, 10), 'uint8') poly = np.array(((1, 1), (4, 1), (4, 4), (1, 4), (1, 1))) @@ -215,6 +255,38 @@ def test_circle_perimeter_andres(): assert_array_equal(img, img_) +def test_circle_perimeter_aa(): + img = np.zeros((15, 15), 'uint8') + rr, cc, val = circle_perimeter_aa(7, 7, 0) + img[rr, cc] = 1 + assert(np.sum(img) == 1) + assert(img[7][7] == 1) + + img = np.zeros((17, 17), 'uint8') + rr, cc, val = circle_perimeter_aa(8, 8, 7) + img[rr, cc] = val * 255 + img_ = np.array( + [[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 82, 180, 236, 255, 236, 180, 82, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 189, 172, 74, 18, 0, 18, 74, 172, 189, 0, 0, 0, 0], + [ 0, 0, 0, 229, 25, 0, 0, 0, 0, 0, 0, 0, 25, 229, 0, 0, 0], + [ 0, 0, 189, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 189, 0, 0], + [ 0, 82, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 172, 82, 0], + [ 0, 180, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 180, 0], + [ 0, 236, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 236, 0], + [ 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0], + [ 0, 236, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 236, 0], + [ 0, 180, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 180, 0], + [ 0, 82, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 172, 82, 0], + [ 0, 0, 189, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 189, 0, 0], + [ 0, 0, 0, 229, 25, 0, 0, 0, 0, 0, 0, 0, 25, 229, 0, 0, 0], + [ 0, 0, 0, 0, 189, 172, 74, 18, 0, 18, 74, 172, 189, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 82, 180, 236, 255, 236, 180, 82, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] + ) + assert_array_equal(img, img_) + + def test_ellipse(): img = np.zeros((15, 15), 'uint8') @@ -360,18 +432,21 @@ def test_bezier_segment_straight(): y1 = 50 x2 = 150 y2 = 150 - rr, cc = bezier_segment(x0, y0, x1, y1, x2, y2, 0) - image [rr, cc] = 1 + rr, cc = _bezier_segment(x0, y0, x1, y1, x2, y2, 0) + image[rr, cc] = 1 image2 = np.zeros((200, 200), dtype=int) rr, cc = line(x0, y0, x2, y2) - image2 [rr, cc] = 1 + image2[rr, cc] = 1 assert_array_equal(image, image2) def test_bezier_segment_curved(): img = np.zeros((25, 25), 'uint8') - rr, cc = bezier_segment(20, 20, 20, 2, 2, 2, 1) + x1, y1 = 20, 20 + x2, y2 = 20, 2 + x3, y3 = 2, 2 + rr, cc = _bezier_segment(x1, y1, x2, y2, x3, y3, 1) img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -400,9 +475,101 @@ def test_bezier_segment_curved(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] ) + assert_equal(img[x1, y1], 1) + assert_equal(img[x3, y3], 1) assert_array_equal(img, img_) +def test_bezier_curve_straight(): + image = np.zeros((200, 200), dtype=int) + x0 = 50 + y0 = 50 + x1 = 150 + y1 = 50 + x2 = 150 + y2 = 150 + rr, cc = bezier_curve(x0, y0, x1, y1, x2, y2, 0) + image [rr, cc] = 1 + + image2 = np.zeros((200, 200), dtype=int) + rr, cc = line(x0, y0, x2, y2) + image2 [rr, cc] = 1 + assert_array_equal(image, image2) + + +def test_bezier_curved_weight_eq_1(): + img = np.zeros((23, 8), 'uint8') + x1, y1 = (1, 1) + x2, y2 = (11, 11) + x3, y3 = (21, 1) + rr, cc = bezier_curve(x1, y1, x2, y2, x3, y3, 1) + img[rr, cc] = 1 + assert_equal(img[x1, y1], 1) + assert_equal(img[x3, y3], 1) + img_ = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0]] + ) + assert_equal(img, img_) + + +def test_bezier_curved_weight_neq_1(): + img = np.zeros((23, 10), 'uint8') + x1, y1 = (1, 1) + x2, y2 = (11, 11) + x3, y3 = (21, 1) + rr, cc = bezier_curve(x1, y1, x2, y2, x3, y3, 2) + img[rr, cc] = 1 + assert_equal(img[x1, y1], 1) + assert_equal(img[x3, y3], 1) + img_ = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] + ) + assert_equal(img, img_) + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/draw/tests/test_draw3d.py b/skimage/draw/tests/test_draw3d.py new file mode 100644 index 00000000..2e1198eb --- /dev/null +++ b/skimage/draw/tests/test_draw3d.py @@ -0,0 +1,104 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_allclose + +from skimage.draw import ellipsoid, ellipsoid_stats + + +def test_ellipsoid_bool(): + test = ellipsoid(2, 2, 2)[1:-1, 1:-1, 1:-1] + test_anisotropic = ellipsoid(2, 2, 4, spacing=(1., 1., 2.)) + test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1] + + expected = np.array([[[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + + [[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], + + [[0, 0, 1, 0, 0], + [0, 1, 1, 1, 0], + [1, 1, 1, 1, 1], + [0, 1, 1, 1, 0], + [0, 0, 1, 0, 0]], + + [[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], + + [[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]]) + + assert_array_equal(test, expected.astype(bool)) + assert_array_equal(test_anisotropic, expected.astype(bool)) + + +def test_ellipsoid_levelset(): + test = ellipsoid(2, 2, 2, levelset=True)[1:-1, 1:-1, 1:-1] + test_anisotropic = ellipsoid(2, 2, 4, spacing=(1., 1., 2.), + levelset=True) + test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1] + + expected = np.array([[[ 2. , 1.25, 1. , 1.25, 2. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 1. , 0.25, 0. , 0.25, 1. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 2. , 1.25, 1. , 1.25, 2. ]], + + [[ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25]], + + [[ 1. , 0.25, 0. , 0.25, 1. ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0. , -0.75, -1. , -0.75, 0. ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 1. , 0.25, 0. , 0.25, 1. ]], + + [[ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25]], + + [[ 2. , 1.25, 1. , 1.25, 2. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 1. , 0.25, 0. , 0.25, 1. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 2. , 1.25, 1. , 1.25, 2. ]]]) + + assert_allclose(test, expected) + assert_allclose(test_anisotropic, expected) + + +def test_ellipsoid_stats(): + # Test comparison values generated by Wolfram Alpha + vol, surf = ellipsoid_stats(6, 10, 16) + assert(round(1280 * np.pi, 4) == round(vol, 4)) + assert(1383.28 == round(surf, 2)) + + # Test when a <= b <= c does not hold + vol, surf = ellipsoid_stats(16, 6, 10) + assert(round(1280 * np.pi, 4) == round(vol, 4)) + assert(1383.28 == round(surf, 2)) + + # Larger test to ensure reliability over broad range + vol, surf = ellipsoid_stats(17, 27, 169) + assert(round(103428 * np.pi, 4) == round(vol, 4)) + assert(37426.3 == round(surf, 1)) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 9c50ab7d..fd5d53dd 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -287,7 +287,7 @@ def adjust_log(image, gain=1, inv=False): inv : float If True, it performs inverse logarithmic correction, else correction will be logarithmic. Defaults to False. - + Returns ------- out : ndarray diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 09ac4540..4a6518d6 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -2,8 +2,9 @@ from ._daisy import daisy from ._hog import hog from .texture import greycomatrix, greycoprops, local_binary_pattern from .peak import peak_local_max -from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, - corner_foerstner, corner_subpix, corner_peaks) +from .corner import (corner_kitchen_rosenfeld, corner_harris, + corner_shi_tomasi, corner_foerstner, corner_subpix, + corner_peaks) from .corner_cy import corner_moravec from .template import match_template diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py new file mode 100644 index 00000000..ecc2ec11 --- /dev/null +++ b/skimage/feature/_brief.py @@ -0,0 +1,228 @@ +import numpy as np +from scipy.ndimage.filters import gaussian_filter + +from ..util import img_as_float +from .util import _mask_border_keypoints, pairwise_hamming_distance + +from ._brief_cy import _brief_loop + + +def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, + sample_seed=1, variance=2): + """**Experimental function**. + + Extract BRIEF Descriptor about given keypoints for a given image. + + Parameters + ---------- + image : 2D ndarray + Input image. + keypoints : (P, 2) ndarray + Array of keypoint locations in the format (row, col). + descriptor_size : int + Size of BRIEF descriptor about each keypoint. Sizes 128, 256 and 512 + preferred by the authors. Default is 256. + mode : string + Probability distribution for sampling location of decision pixel-pairs + around keypoints. Default is 'normal' otherwise uniform. + patch_size : int + Length of the two dimensional square patch sampling region around + the keypoints. Default is 49. + sample_seed : int + Seed for sampling the decision pixel-pairs. From a square window with + length patch_size, pixel pairs are sampled using the `mode` parameter + to build the descriptors using intensity comparison. The value of + `sample_seed` should be the same for the images to be matched while + building the descriptors. Default is 1. + variance : float + Variance of the Gaussian Low Pass filter applied on the image to + alleviate noise sensitivity. Default is 2. + + Returns + ------- + descriptors : (Q, `descriptor_size`) ndarray of dtype bool + 2D ndarray of binary descriptors of size `descriptor_size` about Q + keypoints after filtering out border keypoints with value at an index + (i, j) either being True or False representing the outcome + of Intensity comparison about ith keypoint on jth decision pixel-pair. + keypoints : (Q, 2) ndarray + Location i.e. (row, col) of keypoints after removing out those that + are near border. + + References + ---------- + .. [1] Michael Calonder, Vincent Lepetit, Christoph Strecha and Pascal Fua + "BRIEF : Binary robust independent elementary features", + http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf + + Examples + -------- + >>> import numpy as np + >>> from skimage.feature.corner import corner_peaks, corner_harris + >>> from skimage.feature import pairwise_hamming_distance, brief, match_keypoints_brief + >>> square1 = np.zeros([8, 8], dtype=np.int32) + >>> square1[2:6, 2:6] = 1 + >>> square1 + array([[0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) + >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) + >>> keypoints1 + array([[2, 2], + [2, 5], + [5, 2], + [5, 5]]) + >>> descriptors1, keypoints1 = brief(square1, keypoints1, patch_size=5) + >>> keypoints1 + array([[2, 2], + [2, 5], + [5, 2], + [5, 5]]) + >>> square2 = np.zeros([9, 9], dtype=np.int32) + >>> square2[2:7, 2:7] = 1 + >>> square2 + array([[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) + >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) + >>> keypoints2 + array([[2, 2], + [2, 6], + [6, 2], + [6, 6]]) + >>> descriptors2, keypoints2 = brief(square2, keypoints2, patch_size=5) + >>> keypoints2 + array([[2, 2], + [2, 6], + [6, 2], + [6, 6]]) + >>> pairwise_hamming_distance(descriptors1, descriptors2) + array([[ 0.03125 , 0.3203125, 0.3671875, 0.6171875], + [ 0.3203125, 0.03125 , 0.640625 , 0.375 ], + [ 0.375 , 0.6328125, 0.0390625, 0.328125 ], + [ 0.625 , 0.3671875, 0.34375 , 0.0234375]]) + >>> match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2) + array([[[ 2, 2], + [ 2, 2]], + + [[ 2, 5], + [ 2, 6]], + + [[ 5, 2], + [ 6, 2]], + + [[ 5, 5], + [ 6, 6]]]) + + """ + np.random.seed(sample_seed) + + image = np.squeeze(image) + if image.ndim != 2: + raise ValueError("Only 2-D gray-scale images supported.") + + image = img_as_float(image) + + # Gaussian Low pass filtering to alleviate noise + # sensitivity + image = gaussian_filter(image, variance) + + image = np.ascontiguousarray(image) + + keypoints = np.array(keypoints + 0.5, dtype=np.intp, order='C') + + # Removing keypoints that are within (patch_size / 2) distance from the + # image border + keypoints = keypoints[_mask_border_keypoints(image, keypoints, patch_size // 2)] + keypoints = np.ascontiguousarray(keypoints) + + descriptors = np.zeros((keypoints.shape[0], descriptor_size), dtype=bool, + order='C') + + # Sampling pairs of decision pixels in patch_size x patch_size window + if mode == 'normal': + + samples = (patch_size / 5.0) * np.random.randn(descriptor_size * 8) + samples = np.array(samples, dtype=np.int32) + samples = samples[(samples < (patch_size // 2)) + & (samples > - (patch_size - 2) // 2)] + + pos1 = samples[:descriptor_size * 2] + pos1 = pos1.reshape(descriptor_size, 2) + pos2 = samples[descriptor_size * 2:descriptor_size * 4] + pos2 = pos2.reshape(descriptor_size, 2) + + else: + + samples = np.random.randint(-(patch_size - 2) // 2, + (patch_size // 2) + 1, + (descriptor_size * 2, 2)) + pos1, pos2 = np.split(samples, 2) + + pos1 = np.ascontiguousarray(pos1) + pos2 = np.ascontiguousarray(pos2) + + _brief_loop(image, descriptors.view(np.uint8), keypoints, pos1, pos2) + + return descriptors, keypoints + + +def match_keypoints_brief(keypoints1, descriptors1, keypoints2, + descriptors2, threshold=0.15): + """**Experimental function**. + + Match keypoints described using BRIEF descriptors in one image to + those in second image. + + Parameters + ---------- + keypoints1 : (M, 2) ndarray + M Keypoints from the first image described using skimage.feature.brief + descriptors1 : (M, P) ndarray + BRIEF descriptors of size P about M keypoints in the first image. + keypoints2 : (N, 2) ndarray + N Keypoints from the second image described using skimage.feature.brief + descriptors2 : (N, P) ndarray + BRIEF descriptors of size P about N keypoints in the second image. + threshold : float in range [0, 1] + Maximum allowable hamming distance between descriptors of two keypoints + in separate images to be regarded as a match. Default is 0.15. + + Returns + ------- + match_keypoints_brief : (Q, 2, 2) ndarray + Location of Q matched keypoint pairs from two images. + + """ + if (keypoints1.shape[0] != descriptors1.shape[0] + or keypoints2.shape[0] != descriptors2.shape[0]): + raise ValueError("The number of keypoints and number of described " + "keypoints do not match. Make the optional parameter " + "return_keypoints True to get described keypoints.") + + if descriptors1.shape[1] != descriptors2.shape[1]: + raise ValueError("Descriptor sizes for matching keypoints in both " + "the images should be equal.") + + # Get hamming distances between keeypoints1 and keypoints2 + distance = pairwise_hamming_distance(descriptors1, descriptors2) + + temp = distance > threshold + row_check = np.any(~temp, axis=1) + matched_keypoints2 = keypoints2[np.argmin(distance, axis=1)] + matched_keypoint_pairs = np.zeros((np.sum(row_check), 2, 2), dtype=np.intp) + matched_keypoint_pairs[:, 0, :] = keypoints1[row_check] + matched_keypoint_pairs[:, 1, :] = matched_keypoints2[row_check] + + return matched_keypoint_pairs diff --git a/skimage/feature/_brief_cy.pyx b/skimage/feature/_brief_cy.pyx new file mode 100644 index 00000000..c53d85fc --- /dev/null +++ b/skimage/feature/_brief_cy.pyx @@ -0,0 +1,24 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp + + +def _brief_loop(double[:, ::1] image, char[:, ::1] descriptors, + Py_ssize_t[:, ::1] keypoints, + int[:, ::1] pos0, int[:, ::1] pos1): + + cdef Py_ssize_t k, d, kr, kc, pr0, pr1, pc0, pc1 + + for p in range(pos0.shape[0]): + pr0 = pos0[p, 0] + pc0 = pos0[p, 1] + pr1 = pos1[p, 0] + pc1 = pos1[p, 1] + for k in range(keypoints.shape[0]): + kr = keypoints[k, 0] + kc = keypoints[k, 1] + if image[kr + pr0, kc + pc0] < image[kr + pr1, kc + pc1]: + descriptors[k, p] = True diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index a2ece2f0..431a5986 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -117,9 +117,9 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), #create new integral image for this orientation # isolate orientations in this range - temp_ori = np.where(orientation < 180 / orientations * (i + 1), + temp_ori = np.where(orientation < 180.0 / orientations * (i + 1), orientation, -1) - temp_ori = np.where(orientation >= 180 / orientations * i, + temp_ori = np.where(orientation >= 180.0 / orientations * i, temp_ori, -1) # select magnitudes for those orientations cond2 = temp_ori > -1 diff --git a/skimage/feature/_template.pyx b/skimage/feature/_template.pyx index 03695959..855ece23 100644 --- a/skimage/feature/_template.pyx +++ b/skimage/feature/_template.pyx @@ -50,9 +50,9 @@ from skimage.transform import integral def match_template(cnp.ndarray[float, ndim=2, mode="c"] image, cnp.ndarray[float, ndim=2, mode="c"] template): - cdef cnp.ndarray[float, ndim=2, mode="c"] corr - cdef cnp.ndarray[float, ndim=2, mode="c"] image_sat - cdef cnp.ndarray[float, ndim=2, mode="c"] image_sqr_sat + cdef float[:, ::1] corr + cdef float[:, ::1] image_sat + cdef float[:, ::1] image_sqr_sat cdef float template_mean = np.mean(template) cdef float template_ssd cdef float inv_area @@ -94,4 +94,4 @@ def match_template(cnp.ndarray[float, ndim=2, mode="c"] image, den = sqrt((window_sqr_sum - window_mean_sqr) * template_ssd) corr[r, c] /= den - return corr + return np.asarray(corr) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index f98ed4ca..6caa7ea3 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -8,15 +8,9 @@ from libc.math cimport sin, cos, abs from skimage._shared.interpolation cimport bilinear_interpolation -def _glcm_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] image, - cnp.ndarray[dtype=cnp.float64_t, ndim=1, - negative_indices=False, mode='c'] distances, - cnp.ndarray[dtype=cnp.float64_t, ndim=1, - negative_indices=False, mode='c'] angles, - int levels, - cnp.ndarray[dtype=cnp.uint32_t, ndim=4, - negative_indices=False, mode='c'] out): +def _glcm_loop(cnp.uint8_t[:, ::1] image, double[:] distances, + double[:] angles, Py_ssize_t levels, + cnp.uint32_t[:, :, :, ::1] out): """Perform co-occurrence matrix accumulation. Parameters @@ -81,7 +75,7 @@ cdef inline int _bit_rotate_right(int value, int length): return (value >> 1) | ((value & 1) << (length - 1)) -def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, +def _local_binary_pattern(double[:, ::1] image, int P, float R, char method='D'): """Gray scale and rotation invariant LBP (Local Binary Patterns). @@ -92,16 +86,17 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, image : (N, M) double array Graylevel image. P : int - Number of circularly symmetric neighbour set points (quantization of the - angular space). + Number of circularly symmetric neighbour set points (quantization of + the angular space). R : float Radius of circle (spatial resolution of the operator). - method : {'D', 'R', 'U', 'V'} + method : {'D', 'R', 'U', 'N', 'V'} Method to determine the pattern. * 'D': 'default' * 'R': 'ror' * 'U': 'uniform' + * 'N': 'nri_uniform' * 'V': 'var' Returns @@ -111,30 +106,35 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, """ # texture weights - cdef cnp.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype=np.int32) + cdef int[:] weights = 2 ** np.arange(P, dtype=np.int32) # local position of texture elements - rp = - R * np.sin(2 * np.pi * np.arange(P, dtype=np.double) / P) - cp = R * np.cos(2 * np.pi * np.arange(P, dtype=np.double) / P) - cdef cnp.ndarray[double, ndim=2] coords = np.round(np.vstack([rp, cp]).T, 5) + rr = - R * np.sin(2 * np.pi * np.arange(P, dtype=np.double) / P) + cc = R * np.cos(2 * np.pi * np.arange(P, dtype=np.double) / P) + cdef double[:] rp = np.round(rr, 5) + cdef double[:] cp = np.round(cc, 5) - # pre allocate arrays for computation - cdef cnp.ndarray[double, ndim=1] texture = np.zeros(P, np.double) - cdef cnp.ndarray[char, ndim=1] signed_texture = np.zeros(P, np.int8) - cdef cnp.ndarray[int, ndim=1] rotation_chain = np.zeros(P, np.int32) + # pre-allocate arrays for computation + cdef double[:] texture = np.zeros(P, dtype=np.double) + cdef char[:] signed_texture = np.zeros(P, dtype=np.int8) + cdef int[:] rotation_chain = np.zeros(P, dtype=np.int32) output_shape = (image.shape[0], image.shape[1]) - cdef cnp.ndarray[double, ndim=2] output = np.zeros(output_shape, np.double) + cdef double[:, ::1] output = np.zeros(output_shape, dtype=np.double) cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] cdef double lbp cdef Py_ssize_t r, c, changes, i + cdef Py_ssize_t rot_index, n_ones + cdef cnp.int8_t first_zero, first_one + for r in range(image.shape[0]): for c in range(image.shape[1]): for i in range(P): - texture[i] = bilinear_interpolation(image.data, - rows, cols, r + coords[i, 0], c + coords[i, 1], 'C', 0) + texture[i] = bilinear_interpolation(&image[0, 0], rows, cols, + r + rp[i], c + cp[i], + 'C', 0) # signed / thresholded texture for i in range(P): if texture[i] - image[r, c] >= 0: @@ -145,24 +145,83 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, lbp = 0 # if method == 'uniform' or method == 'var': - if method == 'U' or method == 'V': + if method == 'U' or method == 'N' or method == 'V': # determine number of 0 - 1 changes changes = 0 for i in range(P - 1): changes += abs(signed_texture[i] - signed_texture[i + 1]) + if method == 'N': + # Uniform local binary patterns are defined as patterns + # with at most 2 value changes (from 0 to 1 or from 1 to + # 0). Uniform patterns can be caraterized by their number + # `n_ones` of 1. The possible values for `n_ones` range + # from 0 to P. + # Here is an example for P = 4: + # n_ones=0: 0000 + # n_ones=1: 0001, 1000, 0100, 0010 + # n_ones=2: 0011, 1001, 1100, 0110 + # n_ones=3: 0111, 1011, 1101, 1110 + # n_ones=4: 1111 + # + # For a pattern of size P there are 2 constant patterns + # corresponding to n_ones=0 and n_ones=P. For each other + # value of `n_ones` , i.e n_ones=[1..P-1], there are P + # possible patterns which are related to each other through + # circular permutations. The total number of uniform + # patterns is thus (2 + P * (P - 1)). + # Given any pattern (uniform or not) we must be able to + # associate a unique code: + # 1. Constant patterns patterns (with n_ones=0 and + # n_ones=P) and non uniform patterns are given fixed + # code values. + # 2. Other uniform patterns are indexed considering the + # value of n_ones, and an index called 'rot_index' + # reprenting the number of circular right shifts + # required to obtain the pattern starting from a + # reference position (corresponding to all zeros stacked + # on the right). This number of rotations (or circular + # right shifts) 'rot_index' is efficiently computed by + # considering the positions of the first 1 and the first + # 0 found in the pattern. - if changes <= 2: - for i in range(P): - lbp += signed_texture[i] - else: - lbp = P + 1 - - if method == 'V': - var = np.var(texture) - if var != 0: - lbp /= var + if changes <= 2: + # We have a uniform pattern + n_ones = 0 # determies the number of ones + first_one = -1 # position was the first one + first_zero = -1 # position of the first zero + for i in range(P): + if signed_texture[i]: + n_ones += 1 + if first_one == -1: + first_one = i + else: + if first_zero == -1: + first_zero = i + if n_ones == 0: + lbp = 0 + elif n_ones == P: + lbp = P * (P - 1) + 1 + else: + if first_one == 0: + rot_index = n_ones - first_zero + else: + rot_index = P - first_one + lbp = 1 + (n_ones - 1) * P + rot_index + else: # changes > 2 + lbp = P * (P - 1) + 2 + else: # method != 'N' + if changes <= 2: + for i in range(P): + lbp += signed_texture[i] else: - lbp = np.nan + lbp = P + 1 + + if method == 'V': + var = np.var(texture) + if var != 0: + lbp /= var + else: + lbp = np.nan else: # method == 'default' for i in range(P): @@ -181,4 +240,4 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, output[r, c] = lbp - return output + return np.asarray(output) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py new file mode 100644 index 00000000..4bb7fdda --- /dev/null +++ b/skimage/feature/censure.py @@ -0,0 +1,234 @@ +import numpy as np +from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve + +from skimage.transform import integral_image +from skimage.feature.corner import _compute_auto_correlation +from skimage.util import img_as_float +from skimage.morphology import octagon, star +from skimage.feature.util import _mask_border_keypoints + +from skimage.feature.censure_cy import _censure_dob_loop + + +# The paper(Reference [1]) mentions the sizes of the Octagon shaped filter +# kernel for the first seven scales only. The sizes of the later scales +# have been extrapolated based on the following statement in the paper. +# "These octagons scale linearly and were experimentally chosen to correspond +# to the seven DOBs described in the previous section." +OCTAGON_OUTER_SHAPE = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), + (15, 10), (15, 11), (15, 12), (17, 13), (17, 14)] +OCTAGON_INNER_SHAPE = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5), + (7, 5), (7, 6), (9, 6), (9, 7)] + +# The sizes for the STAR shaped filter kernel for different scales have been +# taken from the OpenCV implementation. +STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] +STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), + (9, 6), (11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] + + +def _filter_image(image, min_scale, max_scale, mode): + + response = np.zeros((image.shape[0], image.shape[1], + max_scale - min_scale + 1), dtype=np.double) + + if mode == 'dob': + + # make response[:, :, i] contiguous memory block + item_size = response.itemsize + response.strides = (item_size * response.shape[0], item_size, + item_size * response.shape[0] * response.shape[1]) + + integral_img = integral_image(image) + + for i in range(max_scale - min_scale + 1): + n = min_scale + i + + # Constant multipliers for the outer region and the inner region + # of the bi-level filters with the constraint of keeping the + # DC bias 0. + inner_weight = (1.0 / (2 * n + 1)**2) + outer_weight = (1.0 / (12 * n**2 + 4 * n)) + + _censure_dob_loop(n, integral_img, response[:, :, i], + inner_weight, outer_weight) + + # NOTE : For the Octagon shaped filter, we implemented and evaluated the + # slanted integral image based image filtering but the performance was + # more or less equal to image filtering using + # scipy.ndimage.filters.convolve(). Hence we have decided to use the + # later for a much cleaner implementation. + elif mode == 'octagon': + # TODO : Decide the shapes of Octagon filters for scales > 7 + + for i in range(max_scale - min_scale + 1): + mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1] + mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1] + response[:, :, i] = convolve(image, + _octagon_filter_kernel(mo, no, mi, ni)) + + elif mode == 'star': + + for i in range(max_scale - min_scale + 1): + m = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][0]] + n = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][1]] + response[:, :, i] = convolve(image, _star_filter_kernel(m, n)) + + return response + + +def _octagon_filter_kernel(mo, no, mi, ni): + outer = (mo + 2 * no)**2 - 2 * no * (no + 1) + inner = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) + outer_weight = 1.0 / (outer - inner) + inner_weight = 1.0 / inner + c = ((mo + 2 * no) - (mi + 2 * ni)) // 2 + outer_oct = octagon(mo, no) + inner_oct = np.zeros((mo + 2 * no, mo + 2 * no)) + inner_oct[c: -c, c: -c] = octagon(mi, ni) + bfilter = (outer_weight * outer_oct - + (outer_weight + inner_weight) * inner_oct) + return bfilter + + +def _star_filter_kernel(m, n): + c = m + m // 2 - n - n // 2 + outer_star = star(m) + inner_star = np.zeros_like(outer_star) + inner_star[c: -c, c: -c] = star(n) + outer_weight = 1.0 / (np.sum(outer_star - inner_star)) + inner_weight = 1.0 / np.sum(inner_star) + bfilter = (outer_weight * outer_star - + (outer_weight + inner_weight) * inner_star) + return bfilter + + +def _suppress_lines(feature_mask, image, sigma, line_threshold): + Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + feature_mask[(Axx + Ayy) * (Axx + Ayy) + > line_threshold * (Axx * Ayy - Axy * Axy)] = False + + +def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', + non_max_threshold=0.15, line_threshold=10): + """**Experimental function**. + + Extracts CenSurE keypoints along with the corresponding scale using + either Difference of Boxes, Octagon or STAR bi-level filter. + + Parameters + ---------- + image : 2D ndarray + Input image. + min_scale : int + Minimum scale to extract keypoints from. + max_scale : int + Maximum scale to extract keypoints from. The keypoints will be + extracted from all the scales except the first and the last i.e. + from the scales in the range [min_scale + 1, max_scale - 1]. + mode : {'DoB', 'Octagon', 'STAR'} + Type of bi-level filter used to get the scales of the input image. + Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes + represent the shape of the bi-level filters i.e. box(square), octagon + and star respectively. For instance, a bi-level octagon filter consists + of a smaller inner octagon and a larger outer octagon with the filter + weights being uniformly negative in both the inner octagon while + uniformly positive in the difference region. Use STAR and Octagon for + better features and DoB for better performance. + non_max_threshold : float + Threshold value used to suppress maximas and minimas with a weak + magnitude response obtained after Non-Maximal Suppression. + line_threshold : float + Threshold for rejecting interest points which have ratio of principal + curvatures greater than this value. + + Returns + ------- + keypoints : (N, 2) array + Location of the extracted keypoints in the ``(row, col)`` format. + scales : (N, 1) array + The corresponding scale of the N extracted keypoints. + + References + ---------- + .. [1] Motilal Agrawal, Kurt Konolige and Morten Rufus Blas + "CenSurE: Center Surround Extremas for Realtime Feature + Detection and Matching", + http://link.springer.com/content/pdf/10.1007%2F978-3-540-88693-8_8.pdf + + .. [2] Adam Schmidt, Marek Kraft, Michal Fularz and Zuzanna Domagala + "Comparative Assessment of Point Feature Detectors and + Descriptors in the Context of Robot Navigation" + http://www.jamris.org/01_2013/saveas.php?QUEST=JAMRIS_No01_2013_P_11-20.pdf + + """ + + # (1) First we generate the required scales on the input grayscale image + # using a bi-level filter and stack them up in `filter_response`. + # (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on the + # filter_response to suppress points that are neither minima or maxima in + # 3 x 3 x 3 neighbourhood. We obtain a boolean ndarray `feature_mask` + # containing all the minimas and maximas in `filter_response` as True. + # (3) Then we suppress all the points in the `feature_mask` for which the + # corresponding point in the image at a particular scale has the ratio of + # principal curvatures greater than `line_threshold`. + # (4) Finally, we remove the border keypoints and return the keypoints + # along with its corresponding scale. + + image = np.squeeze(image) + if image.ndim != 2: + raise ValueError("Only 2-D gray-scale images supported.") + + mode = mode.lower() + if mode not in ('dob', 'octagon', 'star'): + raise ValueError('Mode must be one of "DoB", "Octagon", "STAR".') + + if min_scale < 1 or max_scale < 1 or max_scale - min_scale < 2: + raise ValueError('The scales must be >= 1 and the number of scales ' + 'should be >= 3.') + + image = img_as_float(image) + image = np.ascontiguousarray(image) + + # Generating all the scales + filter_response = _filter_image(image, min_scale, max_scale, mode) + + # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 + # neighbourhood to zero + minimas = minimum_filter(filter_response, (3, 3, 3)) == filter_response + maximas = maximum_filter(filter_response, (3, 3, 3)) == filter_response + + feature_mask = minimas | maximas + feature_mask[filter_response < non_max_threshold] = False + + for i in range(1, max_scale - min_scale): + # sigma = (window_size - 1) / 6.0, so the window covers > 99% of the + # kernel's distribution + # window_size = 7 + 2 * (min_scale - 1 + i) + # Hence sigma = 1 + (min_scale - 1 + i)/ 3.0 + _suppress_lines(feature_mask[:, :, i], image, + (1 + (min_scale + i - 1) / 3.0), line_threshold) + + rows, cols, scales = np.nonzero(feature_mask[..., 1:max_scale - min_scale]) + keypoints = np.column_stack([rows, cols]) + scales = scales + min_scale + 1 + + if mode == 'dob': + return keypoints, scales + + cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool) + + if mode == 'octagon': + for i in range(min_scale + 1, max_scale): + c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 \ + + OCTAGON_OUTER_SHAPE[i - 1][1] + cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ + & (scales == i) + elif mode == 'star': + for i in range(min_scale + 1, max_scale): + c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] \ + + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 + cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ + & (scales == i) + + return keypoints[cumulative_mask], scales[cumulative_mask] diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx new file mode 100644 index 00000000..1c352fde --- /dev/null +++ b/skimage/feature/censure_cy.pyx @@ -0,0 +1,72 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + + +def _censure_dob_loop(Py_ssize_t n, + double[:, ::1] integral_img, + double[:, ::1] filtered_image, + double inner_weight, double outer_weight): + # This function calculates the value in the DoB filtered image using + # integral images. If r = right. l = left, u = up, d = down, the sum of + # pixel values in the rectangle formed by (u, l), (u, r), (d, r), (d, l) + # is calculated as I(d, r) + I(u - 1, l - 1) - I(u - 1, r) - I(d, l - 1). + + cdef Py_ssize_t i, j + cdef double inner, outer + cdef Py_ssize_t n2 = 2 * n + cdef double total_weight = inner_weight + outer_weight + + # top-left pixel + inner = (integral_img[n2 + n, n2 + n] + + integral_img[n2 - n - 1, n2 - n - 1] + - integral_img[n2 + n, n2 - n - 1] + - integral_img[n2 - n - 1, n2 + n]) + + outer = integral_img[2 * n2, 2 * n2] + + filtered_image[n2, n2] = (outer_weight * outer + - total_weight * inner) + + # left column + for i in range(n2 + 1, integral_img.shape[0] - n2): + inner = (integral_img[i + n, n2 + n] + + integral_img[i - n - 1, n2 - n - 1] + - integral_img[i + n, n2 - n - 1] + - integral_img[i - n - 1, n2 + n]) + + outer = (integral_img[i + n2, 2 * n2] + - integral_img[i - n2 - 1, 2 * n2]) + + filtered_image[i, n2] = (outer_weight * outer + - total_weight * inner) + + # top row + for j in range(n2 + 1, integral_img.shape[1] - n2): + inner = (integral_img[n2 + n, j + n] + + integral_img[n2 - n - 1, j - n - 1] + - integral_img[n2 + n, j - n - 1] + - integral_img[n2 - n - 1, j + n]) + + outer = (integral_img[2 * n2, j + n2] + - integral_img[2 * n2, j - n2 - 1]) + + filtered_image[n2, j] = (outer_weight * outer + - total_weight * inner) + + # remaining block + for i in range(n2 + 1, integral_img.shape[0] - n2): + for j in range(n2 + 1, integral_img.shape[1] - n2): + inner = (integral_img[i + n, j + n] + + integral_img[i - n - 1, j - n - 1] + - integral_img[i + n, j - n - 1] + - integral_img[i - n - 1, j + n]) + + outer = (integral_img[i + n2, j + n2] + + integral_img[i - n2 - 1, j - n2 - 1] + - integral_img[i + n2, j - n2 - 1] + - integral_img[i - n2 - 1, j + n2]) + + filtered_image[i, j] = (outer_weight * outer + - total_weight * inner) diff --git a/skimage/feature/corner.py b/skimage/feature/corner.py index 1e7968d9..53b2241e 100644 --- a/skimage/feature/corner.py +++ b/skimage/feature/corner.py @@ -277,8 +277,7 @@ def corner_foerstner(image, sigma=1): References ---------- - .. [1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/\ - foerstner87.fast.pdf + .. [1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/foerstner87.fast.pdf .. [2] http://en.wikipedia.org/wiki/Corner_detection Examples diff --git a/skimage/feature/corner_cy.pyx b/skimage/feature/corner_cy.pyx index d4bc5e5a..7d558e52 100644 --- a/skimage/feature/corner_cy.pyx +++ b/skimage/feature/corner_cy.pyx @@ -59,16 +59,8 @@ def corner_moravec(image, Py_ssize_t window_size=1): cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] - cdef cnp.ndarray[dtype=cnp.double_t, ndim=2, mode='c'] cimage, out - - if image.ndim == 3: - cimage = rgb2grey(image) - cimage = np.ascontiguousarray(img_as_float(image)) - - out = np.zeros(image.shape, dtype=np.double) - - cdef double* image_data = cimage.data - cdef double* out_data = out.data + cdef double[:, ::1] cimage = np.ascontiguousarray(img_as_float(image)) + cdef double[:, ::1] out = np.zeros(image.shape, dtype=np.double) cdef double msum, min_msum cdef Py_ssize_t r, c, br, bc, mr, mc, a, b @@ -81,11 +73,10 @@ def corner_moravec(image, Py_ssize_t window_size=1): msum = 0 for mr in range(- window_size, window_size + 1): for mc in range(- window_size, window_size + 1): - a = (r + mr) * cols + c + mc - b = (br + mr) * cols + bc + mc - msum += (image_data[a] - image_data[b]) ** 2 + msum += (cimage[r + mr, c + mc] + - cimage[br + mr, bc + mc]) ** 2 min_msum = min(msum, min_msum) - out_data[r * cols + c] = min_msum + out[r, c] = min_msum - return out + return np.asarray(out) diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index e769621d..7df64c32 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -13,11 +13,17 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['corner_cy.pyx'], working_path=base_path) + cython(['censure_cy.pyx'], working_path=base_path) + cython(['_brief_cy.pyx'], working_path=base_path) cython(['_texture.pyx'], working_path=base_path) cython(['_template.pyx'], working_path=base_path) config.add_extension('corner_cy', sources=['corner_cy.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('censure_cy', sources=['censure_cy.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_brief_cy', sources=['_brief_cy.c'], + include_dirs=[get_numpy_include_dirs()]) config.add_extension('_texture', sources=['_texture.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) config.add_extension('_template', sources=['_template.c'], diff --git a/skimage/feature/tests/_test_brief.py b/skimage/feature/tests/_test_brief.py new file mode 100644 index 00000000..1d26cbbd --- /dev/null +++ b/skimage/feature/tests/_test_brief.py @@ -0,0 +1,83 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage import data +from skimage import transform as tf +from skimage.color import rgb2gray +from skimage.feature import (brief, match_keypoints_brief, corner_peaks, + corner_harris) + + +def test_brief_color_image_unsupported_error(): + """Brief descriptors can be evaluated on gray-scale images only.""" + img = np.zeros((20, 20, 3)) + keypoints = [[7, 5], [11, 13]] + assert_raises(ValueError, brief, img, keypoints) + + +def test_match_keypoints_brief_lena_translation(): + """Test matched keypoints between lena image and its translated version.""" + img = data.lena() + img = rgb2gray(img) + img.shape + tform = tf.SimilarityTransform(scale=1, rotation=0, translation=(15, 20)) + translated_img = tf.warp(img, tform) + + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) + + keypoints2 = corner_peaks(corner_harris(translated_img), min_distance=5) + descriptors2, keypoints2 = brief(translated_img, keypoints2, + descriptor_size=512) + + matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, + keypoints2, descriptors2, + threshold=0.10) + + assert_array_equal(matched_keypoints[:, 0, :], matched_keypoints[:, 1, :] + + [20, 15]) + + +def test_match_keypoints_brief_lena_rotation(): + """Verify matched keypoints result between lena image and its rotated + version with the expected keypoint pairs.""" + img = data.lena() + img = rgb2gray(img) + img.shape + tform = tf.SimilarityTransform(scale=1, rotation=0.10, translation=(0, 0)) + rotated_img = tf.warp(img, tform) + + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) + + keypoints2 = corner_peaks(corner_harris(rotated_img), min_distance=5) + descriptors2, keypoints2 = brief(rotated_img, keypoints2, + descriptor_size=512) + + matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, + keypoints2, descriptors2, + threshold=0.07) + + expected = np.array([[[263, 272], + [234, 298]], + + [[271, 120], + [258, 146]], + + [[323, 164], + [305, 195]], + + [[414, 70], + [405, 111]], + + [[435, 181], + [415, 223]], + + [[454, 176], + [435, 221]]]) + + assert_array_equal(matched_keypoints, expected) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/_test_censure.py b/skimage/feature/tests/_test_censure.py new file mode 100644 index 00000000..4cd2ad68 --- /dev/null +++ b/skimage/feature/tests/_test_censure.py @@ -0,0 +1,89 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage.data import moon +from skimage.feature import keypoints_censure + + +def test_keypoints_censure_color_image_unsupported_error(): + """Censure keypoints can be extracted from gray-scale images only.""" + img = np.zeros((20, 20, 3)) + assert_raises(ValueError, keypoints_censure, img) + + +def test_keypoints_censure_mode_validity_error(): + """Mode argument in keypoints_censure can be either DoB, Octagon or + STAR.""" + img = np.zeros((20, 20)) + assert_raises(ValueError, keypoints_censure, img, mode='dummy') + + +def test_keypoints_censure_scale_range_error(): + """Difference between the the max_scale and min_scale parameters in + keypoints_censure should be greater than or equal to two.""" + img = np.zeros((20, 20)) + assert_raises(ValueError, keypoints_censure, img, min_scale=1, max_scale=2) + + +def test_keypoints_censure_moon_image_dob(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for DoB filter.""" + img = moon() + actual_kp_dob, actual_scale = keypoints_censure(img, 1, 7, 'DoB', 0.15) + expected_kp_dob = np.array([[ 21, 497], + [ 36, 46], + [119, 350], + [185, 177], + [287, 250], + [357, 239], + [463, 116], + [464, 132], + [467, 260]]) + expected_scale = np.array([3, 4, 4, 2, 2, 3, 2, 2, 2]) + + assert_array_equal(expected_kp_dob, actual_kp_dob) + assert_array_equal(expected_scale, actual_scale) + + +def test_keypoints_censure_moon_image_octagon(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for Octagon filter.""" + img = moon() + actual_kp_octagon, actual_scale = keypoints_censure(img, 1, 7, 'Octagon', + 0.15) + expected_kp_octagon = np.array([[ 21, 496], + [ 35, 46], + [287, 250], + [356, 239], + [463, 116]]) + + expected_scale = np.array([3, 4, 2, 2, 2]) + + assert_array_equal(expected_kp_octagon, actual_kp_octagon) + assert_array_equal(expected_scale, actual_scale) + + +def test_keypoints_censure_moon_image_star(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for STAR filter.""" + img = moon() + actual_kp_star, actual_scale = keypoints_censure(img, 1, 7, 'STAR', 0.15) + expected_kp_star = np.array([[ 21, 497], + [ 36, 46], + [117, 356], + [185, 177], + [260, 227], + [287, 250], + [357, 239], + [451, 281], + [463, 116], + [467, 260]]) + + expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) + + assert_array_equal(expected_kp_star, actual_kp_star) + assert_array_equal(expected_scale, actual_scale) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py index d48a14f7..e4fb6acb 100644 --- a/skimage/feature/tests/test_texture.py +++ b/skimage/feature/tests/test_texture.py @@ -199,5 +199,16 @@ class TestLBP(): np.testing.assert_array_almost_equal(lbp, ref) + def test_nri_uniform(self): + lbp = local_binary_pattern(self.image, 8, 1, 'nri_uniform') + ref = np.array([[ 0, 54, 0, 57, 12, 57], + [34, 0, 58, 58, 3, 22], + [58, 57, 15, 50, 0, 47], + [10, 3, 40, 42, 35, 0], + [57, 7, 57, 58, 0, 56], + [ 9, 58, 0, 57, 7, 14]]) + np.testing.assert_array_almost_equal(lbp, ref) + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/feature/tests/test_util.py b/skimage/feature/tests/test_util.py new file mode 100644 index 00000000..6e25f51a --- /dev/null +++ b/skimage/feature/tests/test_util.py @@ -0,0 +1,32 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.feature.util import pairwise_hamming_distance + + +def test_pairwise_hamming_distance_range(): + """Values of all the pairwise hamming distances should be in the range + [0, 1].""" + a = np.random.random_sample((10, 50)) > 0.5 + b = np.random.random_sample((20, 50)) > 0.5 + dist = pairwise_hamming_distance(a, b) + assert np.all((0 <= dist) & (dist <= 1)) + + +def test_pairwise_hamming_distance_value(): + """The result of pairwise_hamming_distance of two fixed sets of boolean + vectors should be same as expected.""" + np.random.seed(10) + a = np.random.random_sample((4, 100)) > 0.5 + np.random.seed(20) + b = np.random.random_sample((3, 100)) > 0.5 + result = pairwise_hamming_distance(a, b) + expected = np.array([[0.5 , 0.49, 0.44], + [0.44, 0.53, 0.52], + [0.4 , 0.55, 0.5 ], + [0.47, 0.48, 0.57]]) + assert_array_equal(result, expected) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 7655b82a..f9bf6c9f 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -248,6 +248,8 @@ def local_binary_pattern(image, P, R, method='default'): * 'uniform': improved rotation invariance with uniform patterns and finer quantization of the angular space which is gray scale and rotation invariant. + * 'nri_uniform': non rotation-invariant uniform patterns variant + which is only gray scale invariant [2]. * 'var': rotation invariant variance measures of the contrast of local image texture which is rotation but not gray scale invariant. @@ -263,14 +265,19 @@ def local_binary_pattern(image, P, R, method='default'): Timo Ojala, Matti Pietikainen, Topi Maenpaa. http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ Articoliriferimento/LBP.pdf, 2002. + .. [2] Face recognition with local binary patterns. + Timo Ahonen, Abdenour Hadid, Matti Pietikainen, + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.214.6851, + 2004. """ methods = { 'default': ord('D'), 'ror': ord('R'), 'uniform': ord('U'), + 'nri_uniform': ord('N'), 'var': ord('V') } - image = np.array(image, dtype='double', copy=True) + image = np.ascontiguousarray(image, dtype=np.double) output = _local_binary_pattern(image, P, R, methods[method.lower()]) return output diff --git a/skimage/feature/util.py b/skimage/feature/util.py new file mode 100644 index 00000000..a5267d44 --- /dev/null +++ b/skimage/feature/util.py @@ -0,0 +1,38 @@ + + +def _mask_border_keypoints(image, keypoints, dist): + """Removes keypoints that are within dist pixels from the image border.""" + width = image.shape[0] + height = image.shape[1] + + keypoints_filtering_mask = ((dist - 1 < keypoints[:, 0]) & + (keypoints[:, 0] < width - dist + 1) & + (dist - 1 < keypoints[:, 1]) & + (keypoints[:, 1] < height - dist + 1)) + + return keypoints_filtering_mask + + +def pairwise_hamming_distance(array1, array2): + """**Experimental function**. + + Calculate hamming dissimilarity measure between two sets of + vectors. + + Parameters + ---------- + array1 : (P1, D) array + P1 vectors of size D. + array2 : (P2, D) array + P2 vectors of size D. + + Returns + ------- + distance : (P1, P2) array of dtype float + 2D ndarray with value at an index (i, j) representing the hamming + distance in the range [0, 1] between ith vector in array1 and jth + vector in array2. + + """ + distance = (array1[:, None] != array2[None]).mean(axis=2) + return distance diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 1e13a87e..0d1c33bb 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,8 +1,9 @@ from .lpi_filter import inverse, wiener, LPIFilter2D from .ctmf import median_filter +from ._gaussian import gaussian_filter from ._canny import canny from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, - hprewitt, vprewitt, roberts , roberts_positive_diagonal, + hprewitt, vprewitt, roberts, roberts_positive_diagonal, roberts_negative_diagonal) from ._denoise import denoise_tv_chambolle, tv_denoise from ._denoise_cy import denoise_bilateral, denoise_tv_bregman @@ -16,6 +17,7 @@ __all__ = ['inverse', 'wiener', 'LPIFilter2D', 'median_filter', + 'gaussian_filter', 'canny', 'sobel', 'hsobel', diff --git a/skimage/filter/_canny.py b/skimage/filter/_canny.py index 904919be..185bfd40 100644 --- a/skimage/filter/_canny.py +++ b/skimage/filter/_canny.py @@ -1,4 +1,5 @@ -'''canny.py - Canny Edge detector +""" +canny.py - Canny Edge detector Reference: Canny, J., A Computational Approach To Edge Detection, IEEE Trans. Pattern Analysis and Machine Intelligence, 8:679-714, 1986 @@ -9,13 +10,13 @@ Copyright (c) 2003-2009 Massachusetts Institute of Technology Copyright (c) 2009-2011 Broad Institute All rights reserved. Original author: Lee Kamentsky - -''' +""" import numpy as np import scipy.ndimage as ndi from scipy.ndimage import (gaussian_filter, generate_binary_structure, binary_erosion, label) +from skimage import dtype_limits def smooth_with_function_and_mask(image, function, mask): @@ -24,13 +25,11 @@ def smooth_with_function_and_mask(image, function, mask): Parameters ---------- image : array - The image to smooth - + Image you want to smooth. function : callable - A function that takes an image and returns a smoothed image - + A function that does image smoothing. mask : array - Mask with 1's for significant pixels, 0 for masked pixels + Mask with 1's for significant pixels, 0's for masked pixels. Notes ------ @@ -50,31 +49,28 @@ def smooth_with_function_and_mask(image, function, mask): return output_image -def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): - '''Edge filter an image using the Canny algorithm. +def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): + """Edge filter an image using the Canny algorithm. Parameters ----------- - image : array_like, dtype=float - The greyscale input image to detect edges on; should be normalized to - 0.0 to 1.0. - + image : 2D array + Greyscale input image to detect edges on; can be of any dtype. sigma : float - The standard deviation of the Gaussian filter - + Standard deviation of the Gaussian filter. low_threshold : float - The lower bound for hysterisis thresholding (linking edges) - + Lower bound for hysteresis thresholding (linking edges). + If None, low_threshold is set to 10% of dtype's max. high_threshold : float - The upper bound for hysterisis thresholding (linking edges) - + Upper bound for hysteresis thresholding (linking edges). + If None, high_threshold is set to 20% of dtype's max. mask : array, dtype=bool, optional - An optional mask to limit the application of Canny to a certain area. + Mask to limit the application of Canny to a certain area. Returns ------- - output : array (image) - The binary edge map. + output : 2D array (image) + The binary edge map. See also -------- @@ -107,7 +103,7 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): Canny, J., A Computational Approach To Edge Detection, IEEE Trans. Pattern Analysis and Machine Intelligence, 8:679-714, 1986 - William Green' Canny tutorial + William Green's Canny tutorial http://dasl.mem.drexel.edu/alumni/bGreen/www.pages.drexel.edu/_weg22/can_tut.html Examples @@ -116,12 +112,12 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): >>> # Generate noisy image of a square >>> im = np.zeros((256, 256)) >>> im[64:-64, 64:-64] = 1 - >>> im += 0.2*np.random.random(im.shape) + >>> im += 0.2 * np.random.random(im.shape) >>> # First trial with the Canny filter, with the default smoothing >>> edges1 = filter.canny(im) >>> # Increase the smoothing for better results >>> edges2 = filter.canny(im, sigma=3) - ''' + """ # # The steps involved: @@ -154,7 +150,13 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): # if image.ndim != 2: - raise TypeError("The input 'image' must be a two dimensional array.") + raise TypeError("The input 'image' must be a two-dimensional array.") + + if low_threshold is None: + low_threshold = 0.1 * dtype_limits(image)[1] + + if high_threshold is None: + high_threshold = 0.2 * dtype_limits(image)[1] if mask is None: mask = np.ones(image.shape, dtype=bool) diff --git a/skimage/filter/_ctmf.pyx b/skimage/filter/_ctmf.pyx index e3e845e4..70a59aa3 100644 --- a/skimage/filter/_ctmf.pyx +++ b/skimage/filter/_ctmf.pyx @@ -731,13 +731,8 @@ cdef int c_median_filter(Py_ssize_t rows, return 0 -def median_filter(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] data, - cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] mask, - cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] output, - int radius, +def median_filter(cnp.uint8_t[:, ::1] data, cnp.uint8_t[:, ::1] mask, + cnp.uint8_t[:, ::1] output, int radius, cnp.int32_t percent): """Median filter with octagon shape and masking. @@ -773,12 +768,10 @@ def median_filter(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, raise ValueError('Data shape (%d, %d) is not output shape (%d, %d)' % (data.shape[0], data.shape[1], output.shape[0], output.shape[1])) - if c_median_filter(data.shape[0], - data.shape[1], - data.strides[0], - data.strides[1], + if c_median_filter(data.shape[0], data.shape[1], + data.strides[0], data.strides[1], radius, percent, - data.data, - mask.data, - output.data): + &data[0, 0], + &mask[0, 0], + &output[0, 0]): raise MemoryError('Failed to allocate scratchpad memory') diff --git a/skimage/filter/_denoise.py b/skimage/filter/_denoise.py index 87850759..5a810336 100644 --- a/skimage/filter/_denoise.py +++ b/skimage/filter/_denoise.py @@ -37,7 +37,7 @@ def _denoise_tv_chambolle_3d(im, weight=100, eps=2.e-4, n_iter_max=200): >>> mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 >>> mask = mask.astype(np.float) >>> mask += 0.2 * np.random.randn(*mask.shape) - >>> res = denoise_tv(mask, weight=100) + >>> res = denoise_tv_chambolle(mask, weight=100) """ @@ -127,7 +127,7 @@ def _denoise_tv_chambolle_2d(im, weight=50, eps=2.e-4, n_iter_max=200): >>> from skimage import color, data >>> lena = color.rgb2gray(data.lena()) >>> lena += 0.5 * lena.std() * np.random.randn(*lena.shape) - >>> denoised_lena = denoise_tv(lena, weight=60) + >>> denoised_lena = denoise_tv_chambolle(lena, weight=60) """ @@ -227,7 +227,7 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, >>> from skimage import color, data >>> lena = color.rgb2gray(data.lena()) >>> lena += 0.5 * lena.std() * np.random.randn(*lena.shape) - >>> denoised_lena = denoise_tv(lena, weight=60) + >>> denoised_lena = denoise_tv_chambolle(lena, weight=60) 3D example on synthetic data: @@ -235,7 +235,7 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, >>> mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 >>> mask = mask.astype(np.float) >>> mask += 0.2*np.random.randn(*mask.shape) - >>> res = denoise_tv(mask, weight=100) + >>> res = denoise_tv_chambolle(mask, weight=100) """ @@ -250,7 +250,7 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, out = np.zeros_like(im) for c in range(im.shape[2]): out[..., c] = _denoise_tv_chambolle_2d(im[..., c], weight, eps, - n_iter_max) + n_iter_max) else: out = _denoise_tv_chambolle_3d(im, weight, eps, n_iter_max) else: diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/filter/_denoise_cy.pyx index 6fd95f03..0c4f2539 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/filter/_denoise_cy.pyx @@ -98,7 +98,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, """ image = np.atleast_3d(img_as_float(image)) - + # if image.max() is 0, then dist_scale can have an unverified value # and color_lut[(dist * dist_scale)] may cause a segmentation fault # so we verify we have a positive image and that the max is not 0.0. @@ -112,12 +112,9 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, Py_ssize_t window_ext = (win_size - 1) / 2 double max_value - - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] cimage - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] out - double* image_data - double* out_data + double[:, :, ::1] cimage + double[:, :, ::1] out double* color_lut double* range_lut @@ -136,15 +133,13 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, csigma_range = sigma_range max_value = image.max() - + if max_value == 0.0: raise ValueError("The maximum value found in the image was 0.") cimage = np.ascontiguousarray(image) - + out = np.zeros((rows, cols, dims), dtype=np.double) - image_data = cimage.data - out_data = out.data color_lut = _compute_color_lut(bins, csigma_range, max_value) range_lut = _compute_range_lut(win_size, sigma_spatial) dist_scale = bins / dims / max_value @@ -159,11 +154,10 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, for r in range(rows): for c in range(cols): - pixel_addr = r * cols * dims + c * dims total_weight = 0 for d in range(dims): total_values[d] = 0 - centres[d] = image_data[pixel_addr + d] + centres[d] = cimage[r, c, d] for wr in range(-window_ext, window_ext + 1): rr = wr + r kr = wr + window_ext @@ -175,7 +169,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, # distance between centre stack and current position dist = 0 for d in range(dims): - value = get_pixel3d(image_data, rows, cols, dims, + value = get_pixel3d(&cimage[0, 0, 0], rows, cols, dims, rr, cc, d, cmode, cval) values[d] = value dist += (centres[d] - value)**2 @@ -189,7 +183,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, total_values[d] += values[d] * weight total_weight += weight for d in range(dims): - out_data[pixel_addr + d] = total_values[d] / total_weight + out[r, c, d] = total_values[d] / total_weight free(color_lut) free(range_lut) @@ -197,10 +191,11 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, free(centres) free(total_values) - return np.squeeze(out) + return np.squeeze(np.asarray(out)) -def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): +def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, + char isotropic=True): """Perform total-variation denoising using split-Bregman optimization. Total-variation denoising (also know as total-variation regularization) @@ -222,8 +217,10 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): SUM((u(n) - u(n-1))**2) < eps - max_iter: int, optional + max_iter : int, optional Maximal number of iterations used for the optimization. + isotropic : boolean, optional + Switch between isotropic and anisotropic TV denoising. Returns ------- @@ -239,6 +236,7 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): .. [3] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising using Split Bregman" in Image Processing On Line on 2012–05–19, http://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf + .. [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf """ @@ -254,21 +252,17 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): Py_ssize_t total = rows * cols * dims - shape_ext = (rows2, cols2, dims) + shape_ext = (rows2, cols2, dims) + u = np.zeros(shape_ext, dtype=np.double) - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] cimage = \ - np.ascontiguousarray(image) - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] u = \ - np.zeros(shape_ext, dtype=np.double) + cdef: + double[:, :, ::1] cimage = np.ascontiguousarray(image) + double[:, :, ::1] cu = u - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] dx = \ - np.zeros(shape_ext, dtype=np.double) - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] dy = \ - np.zeros(shape_ext, dtype=np.double) - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] bx = \ - np.zeros(shape_ext, dtype=np.double) - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] by = \ - np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] dx = np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] dy = np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] bx = np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] by = np.zeros(shape_ext, dtype=np.double) double ux, uy, uprev, unew, bxx, byy, dxx, dyy, s int i = 0 @@ -292,19 +286,19 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): for r in range(1, rows + 1): for c in range(1, cols + 1): - uprev = u[r, c, k] + uprev = cu[r, c, k] # forward derivatives - ux = u[r, c + 1, k] - uprev - uy = u[r + 1, c, k] - uprev + ux = cu[r, c + 1, k] - uprev + uy = cu[r + 1, c, k] - uprev # Gauss-Seidel method unew = ( lam * ( - + u[r + 1, c, k] - + u[r - 1, c, k] - + u[r, c + 1, k] - + u[r, c - 1, k] + + cu[r + 1, c, k] + + cu[r - 1, c, k] + + cu[r, c + 1, k] + + cu[r, c - 1, k] + dx[r, c - 1, k] - dx[r, c, k] @@ -317,7 +311,7 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): + by[r, c, k] ) + weight * cimage[r - 1, c - 1, k] ) / norm - u[r, c, k] = unew + cu[r, c, k] = unew # update root mean square error rmse += (unew - uprev)**2 @@ -325,9 +319,27 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): bxx = bx[r, c, k] byy = by[r, c, k] - s = sqrt((ux + bxx)**2 + (uy + byy)**2) - dxx = s * lam * (ux + bxx) / (s * lam + 1) - dyy = s * lam * (uy + byy) / (s * lam + 1) + # d_subproblem after reference [4] + if isotropic: + s = sqrt((ux + bxx)**2 + (uy + byy)**2) + dxx = s * lam * (ux + bxx) / (s * lam + 1) + dyy = s * lam * (uy + byy) / (s * lam + 1) + + else: + s = ux + bxx + if s > 1 / lam: + dxx = s - 1/lam + elif s < -1 / lam: + dxx = s + 1 / lam + else: + dxx = 0 + s = uy + byy + if s > 1 / lam: + dyy = s - 1 / lam + elif s < -1 / lam: + dyy = s + 1 / lam + else: + dyy = 0 dx[r, c, k] = dxx dy[r, c, k] = dyy @@ -338,4 +350,4 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): rmse = sqrt(rmse / total) i += 1 - return np.squeeze(u[1:-1, 1:-1]) + return np.squeeze(np.asarray(u[1:-1, 1:-1])) diff --git a/skimage/filter/_gaussian.py b/skimage/filter/_gaussian.py new file mode 100644 index 00000000..af63ba10 --- /dev/null +++ b/skimage/filter/_gaussian.py @@ -0,0 +1,105 @@ +import collections as coll +import numpy as np +from scipy import ndimage +import warnings + +from ..util import img_as_float +from ..color import guess_spatial_dimensions + +__all__ = ['gaussian_filter'] + + +def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, + multichannel=None): + """ + Multi-dimensional Gaussian filter + + Parameters + ---------- + + image : array-like + input image (grayscale or color) to filter. + sigma : scalar or sequence of scalars + standard deviation for Gaussian kernel. The standard + deviations of the Gaussian filter are given for each axis as a + sequence, or as a single number, in which case it is equal for + all axes. + output : array, optional + The ``output`` parameter passes an array in which to store the + filter output. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The `mode` parameter determines how the array borders are + handled, where `cval` is the value when mode is equal to + 'constant'. Default is 'nearest'. + cval : scalar, optional + Value to fill past edges of input if `mode` is 'constant'. Default + is 0.0 + multichannel : bool, optional (default: None) + Whether the last axis of the image is to be interpreted as multiple + channels. If True, each channel is filtered separately (channels are + not mixed together). Only 3 channels are supported. If `None`, + the function will attempt to guess this, and raise a warning if + ambiguous, when the array has shape (M, N, 3). + + Returns + ------- + + filtered_image : ndarray + the filtered array + + Notes + ----- + + This function is a wrapper around :func:`scipy.ndimage.gaussian_filter`. + + Integer arrays are converted to float. + + The multi-dimensional filter is implemented as a sequence of + one-dimensional convolution filters. The intermediate arrays are + stored in the same data type as the output. Therefore, for output + types with a limited precision, the results may be imprecise + because intermediate results may be stored with insufficient + precision. + + Examples + -------- + + >>> a = np.zeros((3, 3)) + >>> a[1, 1] = 1 + >>> a + array([[ 0., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 0.]]) + >>> gaussian_filter(a, sigma=0.4) # mild smoothing + array([[ 0.00163116, 0.03712502, 0.00163116], + [ 0.03712502, 0.84496158, 0.03712502], + [ 0.00163116, 0.03712502, 0.00163116]]) + >>> gaussian_filter(a, sigma=1) # more smooting + array([[ 0.05855018, 0.09653293, 0.05855018], + [ 0.09653293, 0.15915589, 0.09653293], + [ 0.05855018, 0.09653293, 0.05855018]]) + >>> # Several modes are possible for handling boundaries + >>> gaussian_filter(a, sigma=1, mode='reflect') + array([[ 0.08767308, 0.12075024, 0.08767308], + [ 0.12075024, 0.16630671, 0.12075024], + [ 0.08767308, 0.12075024, 0.08767308]]) + >>> # For RGB images, each is filtered separately + >>> from skimage.data import lena + >>> image = lena() + >>> filtered_lena = gaussian_filter(image, sigma=1, multichannel=True) + """ + spatial_dims = guess_spatial_dimensions(image) + if spatial_dims is None and multichannel is None: + msg = ("Images with dimensions (M, N, 3) are interpreted as 2D+RGB" + + " by default. Use `multichannel=False` to interpret as " + + " 3D image with last dimension of length 3.") + warnings.warn(RuntimeWarning(msg)) + multichannel = True + if multichannel: + # do not filter across channels + if not isinstance(sigma, coll.Iterable): + sigma = [sigma] * (image.ndim - 1) + if len(sigma) != image.ndim: + sigma = np.concatenate((np.asarray(sigma), [0])) + image = img_as_float(image) + return ndimage.gaussian_filter(image, sigma, mode=mode, cval=cval) diff --git a/skimage/filter/_rank_order.py b/skimage/filter/_rank_order.py index f878702f..cdd992ff 100644 --- a/skimage/filter/_rank_order.py +++ b/skimage/filter/_rank_order.py @@ -8,7 +8,7 @@ Copyright (c) 2009-2011 Broad Institute All rights reserved. Original author: Lee Kamentstky """ -import numpy +import numpy as np def rank_order(image): @@ -47,14 +47,14 @@ def rank_order(image): (array([0, 1, 2, 1], dtype=uint32), array([-1. , 2.5, 3.1])) """ flat_image = image.ravel() - sort_order = flat_image.argsort().astype(numpy.uint32) + sort_order = flat_image.argsort().astype(np.uint32) flat_image = flat_image[sort_order] - sort_rank = numpy.zeros_like(sort_order) + sort_rank = np.zeros_like(sort_order) is_different = flat_image[:-1] != flat_image[1:] - numpy.cumsum(is_different, out=sort_rank[1:]) - original_values = numpy.zeros((sort_rank[-1] + 1,), image.dtype) + np.cumsum(is_different, out=sort_rank[1:]) + original_values = np.zeros((sort_rank[-1] + 1,), image.dtype) original_values[0] = flat_image[0] original_values[1:] = flat_image[1:][is_different] - int_image = numpy.zeros_like(sort_order) + int_image = np.zeros_like(sort_order) int_image[sort_order] = sort_rank return (int_image.reshape(image.shape), original_values) diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index 7a70f00b..764c7d34 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -31,8 +31,8 @@ HPREWITT_WEIGHTS = np.array([[ 1, 1, 1], [-1,-1,-1]]) / 3.0 VPREWITT_WEIGHTS = HPREWITT_WEIGHTS.T -ROBERTS_PD_WEIGHTS = np.array([[ 1, 0], - [ 0, -1]], dtype=np.double) +ROBERTS_PD_WEIGHTS = np.array([[1, 0], + [0, -1]], dtype=np.double) ROBERTS_ND_WEIGHTS = np.array([[0, 1], [-1, 0]], dtype=np.double) @@ -346,7 +346,7 @@ def roberts(image, mask=None): """Find the edge magnitude using Roberts' cross operator. Parameters - ---------- + ---------- image : 2-D array Image to process. mask : 2-D array, optional diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index b3755a8b..5b5705f2 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -66,9 +66,10 @@ class LPIFilter2D(object): -------- Gaussian filter: - - >>> def filt_func(r, c): - ... return np.exp(-np.hypot(r, c)/1) + Use a 1-D gaussian in each direction without normalization + coefficients. + >>> def filt_func(r, c, sigma = 1): + ... return np.exp(-np.hypot(r, c)/sigma) >>> filter = LPIFilter2D(filt_func) """ diff --git a/skimage/filter/rank/README.rst b/skimage/filter/rank/README.rst index cdf8205c..e5c5a9ad 100644 --- a/skimage/filter/rank/README.rst +++ b/skimage/filter/rank/README.rst @@ -23,10 +23,10 @@ size. This implementation gives better results for large structuring elements. The local histogram is updated at each pixel as the structuring element window moves by, i.e. only those pixels entering and leaving the structuring element update the local histogram. The histogram size is 8-bit (256 bins) for 8-bit -images and 2 to 12-bit (up to 4096 bins) for 16-bit images depending on the -maximum value of the image. Pixel values higher than 4095 raise a ValueError. +images and 2 to 16-bit for 16-bit images depending on the maximum value of the +image. -The filter is applied up to the image border, the neighboorhood used is adjusted -accordingly. The user may provide a mask image (same size as input image) where -non zero values are the part of the image participating in the histogram -computation. By default the entire image is filtered. +The filter is applied up to the image border, the neighboorhood used is +adjusted accordingly. The user may provide a mask image (same size as input +image) where non zero values are the part of the image participating in the +histogram computation. By default the entire image is filtered. diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py index 906566b7..cfd034f1 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -1,36 +1,69 @@ -from .rank import (autolevel, bottomhat, equalize, gradient, maximum, mean, - meansubtraction, median, minimum, modal, morph_contr_enh, - pop, threshold, tophat, noise_filter, entropy, otsu) -from .percentile_rank import (percentile_autolevel, percentile_gradient, - percentile_mean, percentile_mean_subtraction, - percentile_morph_contr_enh, percentile, - percentile_pop, percentile_threshold) -from .bilateral_rank import bilateral_mean, bilateral_pop +from .generic import (autolevel, bottomhat, equalize, gradient, maximum, mean, + subtract_mean, median, minimum, modal, enhance_contrast, + pop, threshold, tophat, noise_filter, entropy, otsu) +from .percentile import (autolevel_percentile, gradient_percentile, + mean_percentile, subtract_mean_percentile, + enhance_contrast_percentile, percentile, + pop_percentile, threshold_percentile) +from .bilateral import mean_bilateral, pop_bilateral + +from skimage._shared.utils import deprecated + + +percentile_autolevel = deprecated('autolevel_percentile')(autolevel_percentile) + +percentile_gradient = deprecated('gradient_percentile')(gradient_percentile) + +percentile_mean = deprecated('mean_percentile')(mean_percentile) +bilateral_mean = deprecated('mean_bilateral')(mean_bilateral) + +meansubtraction = deprecated('subtract_mean')(subtract_mean) +percentile_mean_subtraction = deprecated('subtract_mean_percentile')\ + (subtract_mean_percentile) + +morph_contr_enh = deprecated('enhance_contrast')(enhance_contrast) +percentile_morph_contr_enh = deprecated('enhance_contrast_percentile')\ + (enhance_contrast_percentile) + +percentile_pop = deprecated('pop_percentile')(pop_percentile) +bilateral_pop = deprecated('pop_bilateral')(pop_bilateral) + +percentile_threshold = deprecated('threshold_percentile')(threshold_percentile) __all__ = ['autolevel', + 'autolevel_percentile', 'bottomhat', 'equalize', 'gradient', + 'gradient_percentile', 'maximum', 'mean', - 'meansubtraction', + 'mean_percentile', + 'mean_bilateral', + 'subtract_mean', + 'subtract_mean_percentile', 'median', 'minimum', 'modal', - 'morph_contr_enh', + 'enhance_contrast', + 'enhance_contrast_percentile', 'pop', + 'pop_percentile', + 'pop_bilateral', 'threshold', + 'threshold_percentile', 'tophat', 'noise_filter', 'entropy', - 'otsu', + 'otsu' + 'percentile', + # Deprecated 'percentile_autolevel', 'percentile_gradient', 'percentile_mean', 'percentile_mean_subtraction', 'percentile_morph_contr_enh', - 'percentile', 'percentile_pop', 'percentile_threshold', 'bilateral_mean', diff --git a/skimage/filter/rank/_core16.pxd b/skimage/filter/rank/_core16.pxd deleted file mode 100644 index 5586aea1..00000000 --- a/skimage/filter/rank/_core16.pxd +++ /dev/null @@ -1,20 +0,0 @@ -cimport numpy as cnp - - -ctypedef cnp.uint16_t dtype_t - - -cdef int int_max(int a, int b) -cdef int int_min(int a, int b) - - -# 16-bit core kernel receives extra information about data bitdepth -cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, - Py_ssize_t, Py_ssize_t, Py_ssize_t, float, - float, Py_ssize_t, Py_ssize_t), - cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask, - cnp.ndarray[dtype_t, ndim=2] out, - char shift_x, char shift_y, Py_ssize_t bitdepth, - float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except * diff --git a/skimage/filter/rank/_core16.pyx b/skimage/filter/rank/_core16.pyx deleted file mode 100644 index 0c7a7a82..00000000 --- a/skimage/filter/rank/_core16.pyx +++ /dev/null @@ -1,255 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -import numpy as np - -cimport numpy as cnp -from libc.stdlib cimport malloc, free -from _core8 cimport is_in_mask - - -cdef inline int int_max(int a, int b): - return a if a >= b else b - - -cdef inline int int_min(int a, int b): - return a if a <= b else b - - -cdef inline void histogram_increment(Py_ssize_t * histo, float * pop, - dtype_t value): - histo[value] += 1 - pop[0] += 1 - - -cdef inline void histogram_decrement(Py_ssize_t * histo, float * pop, - dtype_t value): - histo[value] -= 1 - pop[0] -= 1 - - -cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, - Py_ssize_t, Py_ssize_t, Py_ssize_t, float, - float, Py_ssize_t, Py_ssize_t), - cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask, - cnp.ndarray[dtype_t, ndim=2] out, - char shift_x, char shift_y, Py_ssize_t bitdepth, - float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except *: - """Compute histogram for each pixel neighborhood, apply kernel function and - use kernel function return value for output image. - """ - - cdef Py_ssize_t rows = image.shape[0] - cdef Py_ssize_t cols = image.shape[1] - cdef Py_ssize_t srows = selem.shape[0] - cdef Py_ssize_t scols = selem.shape[1] - - cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) + shift_y - cdef Py_ssize_t centre_c = int(selem.shape[1] / 2) + shift_x - - # check that structuring element center is inside the element bounding box - assert centre_r >= 0 - assert centre_c >= 0 - assert centre_r < srows - assert centre_c < scols - assert bitdepth in range(2, 13) - - maxbin_list = [0, 0, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096] - midbin_list = [0, 0, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048] - - # set maxbin and midbin - cdef Py_ssize_t maxbin = maxbin_list[bitdepth] - cdef Py_ssize_t midbin = midbin_list[bitdepth] - - assert (image < maxbin).all() - - # define pointers to the data - cdef dtype_t * out_data = out.data - cdef dtype_t * image_data = image.data - cdef cnp.uint8_t * mask_data = mask.data - - # define local variable types - cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row - # number of pixels actually inside the neighborhood (float) - cdef float pop - - # allocate memory with malloc - cdef Py_ssize_t max_se = srows * scols - - # number of element in each attack border - cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w - - # the current local histogram distribution - cdef Py_ssize_t * histo = malloc(maxbin * sizeof(Py_ssize_t)) - - # these lists contain the relative pixel row and column for each of the 4 - # attack borders east, west, north and south e.g. se_e_r lists the rows of - # the east structuring element border - cdef Py_ssize_t * se_e_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_e_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_w_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_w_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_n_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_n_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_s_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_s_c = malloc(max_se * sizeof(Py_ssize_t)) - - # build attack and release borders - # by using difference along axis - t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) - t_e = np.diff(t, axis=1) < 0 - - t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) - t_w = np.diff(t, axis=1) > 0 - - t = np.vstack((selem, np.zeros((1, selem.shape[1])))) - t_s = np.diff(t, axis=0) < 0 - - t = np.vstack((np.zeros((1, selem.shape[1])), selem)) - t_n = np.diff(t, axis=0) > 0 - - num_se_n = num_se_s = num_se_e = num_se_w = 0 - - for r in range(srows): - for c in range(scols): - if t_e[r, c]: - se_e_r[num_se_e] = r - centre_r - se_e_c[num_se_e] = c - centre_c - num_se_e += 1 - if t_w[r, c]: - se_w_r[num_se_w] = r - centre_r - se_w_c[num_se_w] = c - centre_c - num_se_w += 1 - if t_n[r, c]: - se_n_r[num_se_n] = r - centre_r - se_n_c[num_se_n] = c - centre_c - num_se_n += 1 - if t_s[r, c]: - se_s_r[num_se_s] = r - centre_r - se_s_c[num_se_s] = c - centre_c - num_se_s += 1 - - # initial population and histogram - for i in range(maxbin): - histo[i] = 0 - - pop = 0 - - for r in range(srows): - for c in range(scols): - rr = r - centre_r - cc = c - centre_c - if selem[r, c]: - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) - - r = 0 - c = 0 - # kernel ------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) - # kernel ------------------------------------------- - - # main loop - r = 0 - for even_row in range(0, rows, 2): - # ---> west to east - for c in range(1, cols): - for s in range(num_se_e): - rr = r + se_e_r[s] - cc = c + se_e_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) - - for s in range(num_se_w): - rr = r + se_w_r[s] - cc = c + se_w_c[s] - 1 - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) - - # kernel ------------------------------------------- - out_data[r * cols + c] = kernel( - histo, pop, image_data[r * cols + c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) - # kernel ------------------------------------------- - - r += 1 # pass to the next row - if r >= rows: - break - - # ---> north to south - for s in range(num_se_s): - rr = r + se_s_r[s] - cc = c + se_s_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) - - for s in range(num_se_n): - rr = r + se_n_r[s] - 1 - cc = c + se_n_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) - - # kernel ------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) - # kernel ------------------------------------------- - - # ---> east to west - for c in range(cols - 2, -1, -1): - for s in range(num_se_w): - rr = r + se_w_r[s] - cc = c + se_w_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) - - for s in range(num_se_e): - rr = r + se_e_r[s] - cc = c + se_e_c[s] + 1 - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) - - # kernel ------------------------------------------- - out_data[r * cols + c] = kernel( - histo, pop, image_data[r * cols + c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) - # kernel ------------------------------------------- - - r += 1 # pass to the next row - if r >= rows: - break - - # ---> north to south - for s in range(num_se_s): - rr = r + se_s_r[s] - cc = c + se_s_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) - - for s in range(num_se_n): - rr = r + se_n_r[s] - 1 - cc = c + se_n_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) - - # kernel ------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) - # kernel ------------------------------------------- - - # release memory allocated by malloc - - free(se_e_r) - free(se_e_c) - free(se_w_r) - free(se_w_c) - free(se_n_r) - free(se_n_c) - free(se_s_r) - free(se_s_c) - - free(histo) diff --git a/skimage/filter/rank/_core8.pxd b/skimage/filter/rank/_core8.pxd deleted file mode 100644 index d3b6d8c2..00000000 --- a/skimage/filter/rank/_core8.pxd +++ /dev/null @@ -1,25 +0,0 @@ -cimport numpy as cnp - - -ctypedef cnp.uint8_t dtype_t - - -cdef dtype_t uint8_max(dtype_t a, dtype_t b) -cdef dtype_t uint8_min(dtype_t a, dtype_t b) - - -cdef dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, - Py_ssize_t r, Py_ssize_t c, - dtype_t * mask) - - -# 8-bit core kernel receives extra information about data inferior and superior -# percentiles -cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, - float, Py_ssize_t, Py_ssize_t), - cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask, - cnp.ndarray[dtype_t, ndim=2] out, - char shift_x, char shift_y, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1) except * diff --git a/skimage/filter/rank/_crank16.pyx b/skimage/filter/rank/_crank16.pyx deleted file mode 100644 index e704afe0..00000000 --- a/skimage/filter/rank/_crank16.pyx +++ /dev/null @@ -1,422 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from libc.math cimport log -from skimage.filter.rank._core16 cimport _core16 - - -# ----------------------------------------------------------------- -# kernels uint16 take extra parameter for defining the bitdepth -# ----------------------------------------------------------------- - - -ctypedef cnp.uint16_t dtype_t - - -cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i, imin, imax, delta - - if pop: - for i in range(maxbin - 1, -1, -1): - if histo[i]: - imax = i - break - for i in range(maxbin): - if histo[i]: - imin = i - break - delta = imax - imin - if delta > 0: - return (1. * (maxbin - 1) * (g - imin) / delta) - else: - return (imax - imin) - - -cdef inline dtype_t kernel_bottomhat(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - - if pop: - for i in range(maxbin): - if histo[i]: - break - - return (g - i) - else: - return (0) - -cdef inline dtype_t kernel_equalize(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float sum = 0. - - if pop: - for i in range(maxbin): - sum += histo[i] - if i >= g: - break - - return (((maxbin - 1) * sum) / pop) - else: - return (0) - - -cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i, imin, imax - - if pop: - for i in range(maxbin - 1, -1, -1): - if histo[i]: - imax = i - break - for i in range(maxbin): - if histo[i]: - imin = i - break - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_maximum(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - - if pop: - for i in range(maxbin - 1, -1, -1): - if histo[i]: - return (i) - - return (0) - - -cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(maxbin): - mean += histo[i] * i - return (mean / pop) - else: - return (0) - - -cdef inline dtype_t kernel_meansubtraction(Py_ssize_t * histo, - float pop, - dtype_t g, - Py_ssize_t bitdepth, - Py_ssize_t maxbin, - Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(maxbin): - mean += histo[i] * i - return ((g - mean / pop) / 2. + (midbin - 1)) - else: - return (0) - - -cdef inline dtype_t kernel_median(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float sum = pop / 2.0 - - if pop: - for i in range(maxbin): - if histo[i]: - sum -= histo[i] - if sum < 0: - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_minimum(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - - if pop: - for i in range(maxbin): - if histo[i]: - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_modal(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t hmax = 0, imax = 0 - - if pop: - for i in range(maxbin): - if histo[i] > hmax: - hmax = histo[i] - imax = i - return (imax) - else: - return (0) - - -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, - float pop, - dtype_t g, - Py_ssize_t bitdepth, - Py_ssize_t maxbin, - Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i, imin, imax - - if pop: - for i in range(maxbin - 1, -1, -1): - if histo[i]: - imax = i - break - for i in range(maxbin): - if histo[i]: - imin = i - break - if imax - g < g - imin: - return (imax) - else: - return (imin) - else: - return (0) - - -cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - return (pop) - - -cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(maxbin): - mean += histo[i] * i - return (g > (mean / pop)) - else: - return (0) - - -cdef inline dtype_t kernel_tophat(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - - if pop: - for i in range(maxbin - 1, -1, -1): - if histo[i]: - break - - return (i - g) - else: - return (0) - -cdef inline dtype_t kernel_entropy(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float e, p - - if pop: - e = 0. - - for i in range(maxbin): - p = histo[i] / pop - if p > 0: - e -= p * log(p) / 0.6931471805599453 - - return e * 1000 - else: - return (0) - -# ----------------------------------------------------------------- -# python wrappers -# ----------------------------------------------------------------- - - -def autolevel(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def bottomhat(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_bottomhat, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def equalize(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_equalize, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def gradient(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_gradient, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def maximum(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_maximum, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def mean(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def meansubtraction(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_meansubtraction, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def median(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_median, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def minimum(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_minimum, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def modal(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_modal, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def pop(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def threshold(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_threshold, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def tophat(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_tophat, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def entropy(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_entropy, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) diff --git a/skimage/filter/rank/_crank16_bilateral.pyx b/skimage/filter/rank/_crank16_bilateral.pyx deleted file mode 100644 index e431e42b..00000000 --- a/skimage/filter/rank/_crank16_bilateral.pyx +++ /dev/null @@ -1,82 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from skimage.filter.rank._core16 cimport _core16 - - -# ----------------------------------------------------------------- -# kernels uint16 take extra parameter for defining the bitdepth -# ----------------------------------------------------------------- - - -ctypedef cnp.uint16_t dtype_t - - -cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, bilat_pop = 0 - cdef float mean = 0. - - if pop: - for i in range(maxbin): - if (g > (i - s0)) and (g < (i + s1)): - bilat_pop += histo[i] - mean += histo[i] * i - if bilat_pop: - return (mean / bilat_pop) - else: - return (0) - else: - return (0) - - -cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, bilat_pop = 0 - - if pop: - for i in range(maxbin): - if (g > (i - s0)) and (g < (i + s1)): - bilat_pop += histo[i] - return (bilat_pop) - else: - return (0) - - -# ----------------------------------------------------------------- -# python wrappers -# ----------------------------------------------------------------- - - -def mean(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, int s0=1, int s1=1): - """average greylevel (clipped on uint8) - """ - _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0., 0., s0, s1) - - -def pop(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, int s0=1, int s1=1): - """returns the number of actual pixels of the structuring element inside - the mask - """ - _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, - bitdepth, .0, .0, s0, s1) diff --git a/skimage/filter/rank/_crank16_percentiles.pyx b/skimage/filter/rank/_crank16_percentiles.pyx deleted file mode 100644 index f4a4c9b2..00000000 --- a/skimage/filter/rank/_crank16_percentiles.pyx +++ /dev/null @@ -1,330 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from skimage.filter.rank._core16 cimport _core16, int_min, int_max - - -# ----------------------------------------------------------------- -# kernels uint16 (SOFT version using percentiles) -# ----------------------------------------------------------------- - - -ctypedef cnp.uint16_t dtype_t - - -cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(maxbin): - sum += histo[i] - if sum > p0 * pop: - imin = i - break - sum = 0 - for i in range(maxbin - 1, -1, -1): - sum += histo[i] - if sum > p1 * pop: - imax = i - break - - delta = imax - imin - if delta > 0: - return (1.0 * (maxbin - 1) - * (int_min(int_max(imin, g), imax) - - imin) / delta) - else: - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(maxbin): - sum += histo[i] - if sum >= p0 * pop: - imin = i - break - sum = 0 - for i in range((maxbin - 1), -1, -1): - sum += histo[i] - if sum >= p1 * pop: - imax = i - break - - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, sum, mean, n - - if pop: - sum = 0 - mean = 0 - n = 0 - for i in range(maxbin): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - mean += histo[i] * i - - if n > 0: - return (1.0 * mean / n) - else: - return (0) - else: - return (0) - - -cdef inline dtype_t kernel_mean_subtraction(Py_ssize_t * histo, - float pop, - dtype_t g, - Py_ssize_t bitdepth, - Py_ssize_t maxbin, - Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, sum, mean, n - - if pop: - sum = 0 - mean = 0 - n = 0 - for i in range(maxbin): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - mean += histo[i] * i - if n > 0: - return ((g - (mean / n)) * .5 + midbin) - else: - return (0) - else: - return (0) - - -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, - float pop, - dtype_t g, - Py_ssize_t bitdepth, - Py_ssize_t maxbin, - Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(maxbin): - sum += histo[i] - if sum > p0 * pop: - imin = i - break - sum = 0 - for i in range((maxbin - 1), -1, -1): - sum += histo[i] - if sum > p1 * pop: - imax = i - break - if g > imax: - return imax - if g < imin: - return imin - if imax - g < g - imin: - return imax - else: - return imin - else: - return (0) - - -cdef inline dtype_t kernel_percentile(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i - cdef float sum = 0. - - if pop: - for i in range(maxbin): - sum += histo[i] - if sum >= p0 * pop: - break - - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, sum, n - - if pop: - sum = 0 - n = 0 - for i in range(maxbin): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - return (n) - else: - return (0) - - -cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i - cdef float sum = 0. - - if pop: - for i in range(maxbin): - sum += histo[i] - if sum >= p0 * pop: - break - - return ((maxbin - 1) * (g >= i)) - else: - return (0) - - -# ----------------------------------------------------------------- -# python wrappers -# ----------------------------------------------------------------- - - -def autolevel(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """bottom hat - """ - _core16(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def gradient(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """return p0,p1 percentile gradient - """ - _core16(kernel_gradient, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def mean(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """return mean between [p0 and p1] percentiles - """ - _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def mean_subtraction(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """return original - mean between [p0 and p1] percentiles *.5 +127 - """ - _core16( - kernel_mean_subtraction, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """reforce contrast using percentiles - """ - _core16(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def percentile(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0.): - """return p0 percentile - """ - _core16(kernel_percentile, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, .0, 0, 0) - - -def pop(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """return nb of pixels between [p0 and p1] - """ - _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def threshold(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0.): - """return (maxbin-1) if g > percentile p0 - """ - _core16(kernel_threshold, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, 0., 0, 0) diff --git a/skimage/filter/rank/_crank8.pyx b/skimage/filter/rank/_crank8.pyx deleted file mode 100644 index da511790..00000000 --- a/skimage/filter/rank/_crank8.pyx +++ /dev/null @@ -1,483 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from libc.math cimport log -from skimage.filter.rank._core8 cimport _core8 - - -# ----------------------------------------------------------------- -# kernels uint8 -# ----------------------------------------------------------------- - - -ctypedef cnp.uint8_t dtype_t - - -cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax, delta - - if pop: - for i in range(255, -1, -1): - if histo[i]: - imax = i - break - for i in range(256): - if histo[i]: - imin = i - break - delta = imax - imin - if delta > 0: - return (255. * (g - imin) / delta) - else: - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_bottomhat(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(256): - if histo[i]: - break - - return (g - i) - else: - return (0) - - -cdef inline dtype_t kernel_equalize(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef float sum = 0. - - if pop: - for i in range(256): - sum += histo[i] - if i >= g: - break - - return ((255 * sum) / pop) - else: - return (0) - - -cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax - - if pop: - for i in range(255, -1, -1): - if histo[i]: - imax = i - break - for i in range(256): - if histo[i]: - imin = i - break - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_maximum(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(255, -1, -1): - if histo[i]: - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(256): - mean += histo[i] * i - return (mean / pop) - else: - return (0) - - -cdef inline dtype_t kernel_meansubtraction(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(256): - mean += histo[i] * i - return ((g - mean / pop) / 2. + 127) - else: - return (0) - - -cdef inline dtype_t kernel_median(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef float sum = pop / 2.0 - - if pop: - for i in range(256): - if histo[i]: - sum -= histo[i] - if sum < 0: - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_minimum(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(256): - if histo[i]: - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_modal(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t hmax = 0, imax = 0 - - if pop: - for i in range(256): - if histo[i] > hmax: - hmax = histo[i] - imax = i - return (imax) - else: - return (0) - - -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax - - if pop: - for i in range(255, -1, -1): - if histo[i]: - imax = i - break - for i in range(256): - if histo[i]: - imin = i - break - if imax - g < g - imin: - return (imax) - else: - return (imin) - else: - return (0) - - -cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - return (pop) - - -cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(256): - mean += histo[i] * i - return (g > (mean / pop)) - else: - return (0) - - -cdef inline dtype_t kernel_tophat(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(255, -1, -1): - if histo[i]: - break - - return (i - g) - else: - return (0) - -cdef inline dtype_t kernel_noise_filter(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t min_i - - # early stop if at least one pixel of the neighborhood has the same g - if histo[g] > 0: - return 0 - - for i in range(g, -1, -1): - if histo[i]: - break - min_i = g - i - for i in range(g, 256): - if histo[i]: - break - if i - g < min_i: - return (i - g) - else: - return min_i - - -cdef inline dtype_t kernel_entropy(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float e, p - - if pop: - e = 0. - - for i in range(256): - p = histo[i] / pop - if p > 0: - e -= p * log(p) / 0.6931471805599453 - - return e * 10 - else: - return (0) - -cdef inline dtype_t kernel_otsu(Py_ssize_t * histo, float pop, dtype_t g, - float p0, float p1, Py_ssize_t s0, - Py_ssize_t s1): - cdef Py_ssize_t i - cdef Py_ssize_t max_i - cdef float P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b - cdef float mu = 0. - - # compute local mean - if pop: - for i in range(256): - mu += histo[i] * i - mu = (mu / pop) - else: - return (0) - - # maximizing the between class variance - max_i = 0 - q1 = histo[0] / pop - m1 = 0. - max_sigma_b = 0. - - for i in range(1, 256): - P = histo[i] / pop - new_q1 = q1 + P - if new_q1 > 0: - mu1 = (q1 * mu1 + i * P) / new_q1 - mu2 = (mu - new_q1 * mu1) / (1. - new_q1) - sigma_b = new_q1 * (1. - new_q1) * (mu1 - mu2) ** 2 - if sigma_b > max_sigma_b: - max_sigma_b = sigma_b - max_i = i - q1 = new_q1 - - return max_i - - -# ----------------------------------------------------------------- -# python wrappers -# used only internally -# ----------------------------------------------------------------- - - -def autolevel(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def bottomhat(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_bottomhat, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def equalize(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_equalize, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def gradient(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_gradient, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def maximum(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_maximum, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def mean(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_mean, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def meansubtraction(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_meansubtraction, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def median(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_median, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def minimum(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_minimum, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def modal(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_modal, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def pop(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_pop, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def threshold(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_threshold, image, selem, mask, out, shift_x, shift_y, 0, 0, - 0, 0) - - -def tophat(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_tophat, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def noise_filter(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_noise_filter, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def entropy(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_entropy, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def otsu(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_otsu, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) diff --git a/skimage/filter/rank/_crank8_percentiles.pyx b/skimage/filter/rank/_crank8_percentiles.pyx deleted file mode 100644 index 8e5cee9c..00000000 --- a/skimage/filter/rank/_crank8_percentiles.pyx +++ /dev/null @@ -1,294 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from skimage.filter.rank._core8 cimport _core8, uint8_max, uint8_min - - -# ----------------------------------------------------------------- -# kernels uint8 (SOFT version using percentiles) -# ----------------------------------------------------------------- - - -ctypedef cnp.uint8_t dtype_t - - -cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - imin = 0 - imax = 255 - - for i in range(256): - sum += histo[i] - if sum > (p0 * pop): - imin = i - break - sum = 0 - for i in range(255, -1, -1): - sum += histo[i] - if sum > (p1 * pop): - imax = i - break - delta = imax - imin - if delta > 0: - return (255 * (uint8_min(uint8_max(imin, g), imax) - - imin) / delta) - else: - return (imax - imin) - else: - return (128) - - -cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(256): - sum += histo[i] - if sum >= p0 * pop: - imin = i - break - sum = 0 - for i in range(255, -1, -1): - sum += histo[i] - if sum >= p1 * pop: - imax = i - break - - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, sum, mean, n - - if pop: - sum = 0 - mean = 0 - n = 0 - for i in range(256): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - mean += histo[i] * i - if n > 0: - return (1.0 * mean / n) - else: - return (0) - else: - return (0) - - -cdef inline dtype_t kernel_mean_subtraction(Py_ssize_t * histo, - float pop, - dtype_t g, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, sum, mean, n - - if pop: - sum = 0 - mean = 0 - n = 0 - for i in range(256): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - mean += histo[i] * i - if n > 0: - return ((g - (mean / n)) * .5 + 127) - else: - return (0) - else: - return (0) - - -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, - float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(256): - sum += histo[i] - if sum >= p0 * pop: - imin = i - break - sum = 0 - for i in range(255, -1, -1): - sum += histo[i] - if sum >= p1 * pop: - imax = i - break - if g > imax: - return imax - if g < imin: - return imin - if imax - g < g - imin: - return imax - else: - return imin - else: - return (0) - - -cdef inline dtype_t kernel_percentile(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i - cdef float sum = 0. - - if pop: - for i in range(256): - sum += histo[i] - if sum >= p0 * pop: - break - - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, sum, n - - if pop: - sum = 0 - n = 0 - for i in range(256): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - return (n) - else: - return (0) - - -cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i - cdef float sum = 0. - - if pop: - for i in range(256): - sum += histo[i] - if sum >= p0 * pop: - break - - return (255 * (g >= i)) - else: - return (0) - - -# ----------------------------------------------------------------- -# python wrappers -# ----------------------------------------------------------------- - - -def autolevel(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """autolevel - """ - _core8(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, p0, p1, - 0, 0) - - -def gradient(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """return p0,p1 percentile gradient - """ - _core8(kernel_gradient, image, selem, mask, out, shift_x, shift_y, p0, p1, - 0, 0) - - -def mean(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """return mean between [p0 and p1] percentiles - """ - _core8(kernel_mean, image, selem, mask, out, shift_x, shift_y, p0, p1, - 0, 0) - - -def mean_subtraction(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """return original - mean between [p0 and p1] percentiles *.5 +127 - """ - _core8(kernel_mean_subtraction, image, selem, mask, out, shift_x, shift_y, - p0, p1, 0, 0) - - -def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """reforce contrast using percentiles - """ - _core8(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, - p0, p1, 0, 0) - - -def percentile(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, float p0=0.): - """return p0 percentile - """ - _core8(kernel_percentile, image, selem, mask, out, shift_x, shift_y, - p0, 0., 0, 0) - - -def pop(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """return nb of pixels between [p0 and p1] - """ - _core8(kernel_pop, image, selem, mask, out, shift_x, shift_y, p0, p1, - 0, 0) - - -def threshold(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0, float p0=0.): - """return 255 if g > percentile p0 - """ - _core8(kernel_threshold, image, selem, mask, out, shift_x, shift_y, p0, 0., - 0, 0) diff --git a/skimage/filter/rank/bilateral_rank.pyx b/skimage/filter/rank/bilateral.py similarity index 53% rename from skimage/filter/rank/bilateral_rank.pyx rename to skimage/filter/rank/bilateral.py index e2a1fcf3..f1b10fec 100644 --- a/skimage/filter/rank/bilateral_rank.pyx +++ b/skimage/filter/rank/bilateral.py @@ -3,19 +3,16 @@ The local histogram is computed using a sliding window similar to the method described in [1]_. -Input image must be 16-bit with a value < 4096 (i.e. 12 bit), -the number of histogram bins is determined from the -maximum value present in the image. - The pixel neighborhood is defined by: * the given structuring element -* an interval [g-s0,g+s1] in greylevel around g the processed pixel greylevel +* an interval [g-s0, g+s1] in greylevel around g the processed pixel greylevel The kernel is flat (i.e. each pixel belonging to the neighborhood contributes equally). -Result image is 16-bit with respect to the input image. +Result image is 8-/16-bit or double with respect to the input image and the +rank filter operation. References ---------- @@ -28,50 +25,27 @@ References import numpy as np from skimage import img_as_ubyte -from skimage.filter.rank import _crank16_bilateral -from skimage.filter.rank.generic import find_bitdepth + +from . import bilateral_cy +from .generic import _handle_input -__all__ = ['bilateral_mean', 'bilateral_pop'] +__all__ = ['mean_bilateral', 'pop_bilateral'] -def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, s0, s1): - selem = img_as_ubyte(selem > 0) - image = np.ascontiguousarray(image) +def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1, + out_dtype=None): - if mask is None: - mask = np.ones(image.shape, dtype=np.uint8) - else: - mask = np.ascontiguousarray(mask) - mask = img_as_ubyte(mask) + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype) - if image is out: - raise NotImplementedError("Cannot perform rank operation in place.") - - if image.dtype == np.uint8: - if func8 is None: - raise TypeError("Not implemented for uint8 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint8) - func8(image, selem, shift_x=shift_x, shift_y=shift_y, - mask=mask, out=out, s0=s0, s1=s1) - elif image.dtype == np.uint16: - if func16 is None: - raise TypeError("Not implemented for uint16 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint16) - bitdepth = find_bitdepth(image) - if bitdepth > 11: - raise ValueError("Only uint16 <4096 image (12bit) supported.") - func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, - bitdepth=bitdepth + 1, out=out, s0=s0, s1=s1) - else: - raise TypeError("Only uint8 and uint16 image supported.") + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin, s0=s0, s1=s1) return out -def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, +def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, shift_y=False, s0=10, s1=10): """Apply a flat kernel bilateral filter. @@ -81,43 +55,38 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, Spatial closeness is measured by considering only the local pixel neighborhood given by a structuring element (selem). - Radiometric similarity is defined by the greylevel interval [g-s0,g+s1] + Radiometric similarity is defined by the greylevel interval [g-s0, g+s1] where g is the current pixel greylevel. Only pixels belonging to the structuring element AND having a greylevel inside this interval are averaged. Return greyscale local bilateral_mean of an image. Parameters ---------- - image : ndarray - Image array (uint16). As the algorithm uses max. 12bit histogram, - an exception will be raised if image has a value > 4095 + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). - shift_x, shift_y : (int) + shift_x, shift_y : int Offset added to the structuring element center point. Shift is bounded to the structuring element sizes (center must be inside the given structuring element). s0, s1 : int - define the [s0, s1] interval to be considered for computing the value. + Define the [s0, s1] interval around the greyvalue of the center pixel + to be considered for computing the value. Returns ------- - out : uint16 array - The result of the local bilateral mean. + out : ndarray (same dtype as input image) + Output image. See also -------- - skimage.filter.denoise_bilateral() for a gaussian bilateral filter. - - Notes - ----- - - * input image are 16-bit only + skimage.filter.denoise_bilateral for a gaussian bilateral filter. Examples -------- @@ -128,13 +97,14 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, >>> ima = data.camera().astype(np.uint16) >>> # bilateral filtering of cameraman image using a flat kernel >>> bilat_ima = bilateral_mean(ima, disk(20), s0=10,s1=10) + """ - return _apply(None, _crank16_bilateral.mean, image, selem, out=out, + return _apply(bilateral_cy._mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) -def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, +def pop_bilateral(image, selem, out=None, mask=None, shift_x=False, shift_y=False, s0=10, s1=10): """Return the number (population) of pixels actually inside the bilateral neighborhood, i.e. being inside the structuring element AND having a gray @@ -142,32 +112,27 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint16). As the algorithm uses max. 12bit histogram, - an exception will be raised if image has a value > 4095 + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). - shift_x, shift_y : (int) + shift_x, shift_y : int Offset added to the structuring element center point. Shift is bounded to the structuring element sizes (center must be inside the given structuring element). s0, s1 : int - define the [s0, s1] interval to be considered for computing the value. + Define the [s0, s1] interval around the greyvalue of the center pixel + to be considered for computing the value. Returns ------- - out : uint16 array - the local number of pixels inside the bilateral neighborhood - - Notes - ----- - - * input image are 16-bit only + out : ndarray (same dtype as input image) + Output image. Examples -------- @@ -175,10 +140,10 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, >>> from skimage.morphology import square >>> import skimage.filter.rank as rank >>> ima16 = 255 * np.array([[0, 0, 0, 0, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 0, 0, 0, 0]], dtype=np.uint16) + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint16) >>> rank.bilateral_pop(ima16, square(3), s0=10,s1=10) array([[3, 4, 3, 4, 3], [4, 4, 6, 4, 4], @@ -188,5 +153,5 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(None, _crank16_bilateral.pop, image, selem, out=out, + return _apply(bilateral_cy._pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) diff --git a/skimage/filter/rank/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx new file mode 100644 index 00000000..de2b3e53 --- /dev/null +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -0,0 +1,70 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log + +from .core_cy cimport dtype_t, dtype_t_out, _core + + +cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t bilat_pop = 0 + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + mean += histo[i] * i + if bilat_pop: + return mean / bilat_pop + else: + return 0 + else: + return 0 + + +cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t bilat_pop = 0 + + if pop: + for i in range(max_bin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + return bilat_pop + else: + return 0 + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): + + _core(_kernel_mean[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): + + _core(_kernel_pop[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) diff --git a/skimage/filter/rank/core_cy.pxd b/skimage/filter/rank/core_cy.pxd new file mode 100644 index 00000000..2e97e50a --- /dev/null +++ b/skimage/filter/rank/core_cy.pxd @@ -0,0 +1,28 @@ +from numpy cimport uint8_t, uint16_t, double_t + + +ctypedef fused dtype_t: + uint8_t + uint16_t + +ctypedef fused dtype_t_out: + uint8_t + uint16_t + double_t + + +cdef dtype_t _max(dtype_t a, dtype_t b) +cdef dtype_t _min(dtype_t a, dtype_t b) + + +cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, + Py_ssize_t, Py_ssize_t, double, + double, Py_ssize_t, Py_ssize_t), + dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin) except * diff --git a/skimage/filter/rank/_core8.pyx b/skimage/filter/rank/core_cy.pyx similarity index 51% rename from skimage/filter/rank/_core8.pyx rename to skimage/filter/rank/core_cy.pyx index 79ae9bbf..02c2c8d0 100644 --- a/skimage/filter/rank/_core8.pyx +++ b/skimage/filter/rank/core_cy.pyx @@ -9,29 +9,29 @@ cimport numpy as cnp from libc.stdlib cimport malloc, free -cdef inline dtype_t uint8_max(dtype_t a, dtype_t b): +cdef inline dtype_t _max(dtype_t a, dtype_t b): return a if a >= b else b -cdef inline dtype_t uint8_min(dtype_t a, dtype_t b): +cdef inline dtype_t _min(dtype_t a, dtype_t b): return a if a <= b else b -cdef inline void histogram_increment(Py_ssize_t * histo, float * pop, +cdef inline void histogram_increment(Py_ssize_t* histo, double* pop, dtype_t value): histo[value] += 1 pop[0] += 1 -cdef inline void histogram_decrement(Py_ssize_t * histo, float * pop, +cdef inline void histogram_decrement(Py_ssize_t* histo, double* pop, dtype_t value): histo[value] -= 1 pop[0] -= 1 -cdef inline dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, - Py_ssize_t r, Py_ssize_t c, - dtype_t * mask): +cdef inline char is_in_mask(Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t c, + char* mask): """Check whether given coordinate is within image and mask is true.""" if r < 0 or r > rows - 1 or c < 0 or c > cols - 1: return 0 @@ -42,14 +42,17 @@ cdef inline dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, return 0 -cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, - float, Py_ssize_t, Py_ssize_t), - cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask, - cnp.ndarray[dtype_t, ndim=2] out, - char shift_x, char shift_y, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1) except *: +cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, + Py_ssize_t, Py_ssize_t, double, + double, Py_ssize_t, Py_ssize_t), + dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin) except *: """Compute histogram for each pixel neighborhood, apply kernel function and use kernel function return value for output image. """ @@ -59,8 +62,8 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, cdef Py_ssize_t srows = selem.shape[0] cdef Py_ssize_t scols = selem.shape[1] - cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) + shift_y - cdef Py_ssize_t centre_c = int(selem.shape[1] / 2) + shift_x + cdef Py_ssize_t centre_r = (selem.shape[0] / 2) + shift_y + cdef Py_ssize_t centre_c = (selem.shape[1] / 2) + shift_x # check that structuring element center is inside the element bounding box assert centre_r >= 0 @@ -68,54 +71,56 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, assert centre_r < srows assert centre_c < scols - # define pointers to the data + # add 1 to ensure maximum value is included in histogram -> range(max_bin) + max_bin += 1 - cdef dtype_t * out_data = out.data - cdef dtype_t * image_data = image.data - cdef dtype_t * mask_data = mask.data + cdef Py_ssize_t mid_bin = max_bin / 2 + + # define pointers to the data + cdef char* mask_data = &mask[0, 0] # define local variable types cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row - # number of pixels actually inside the neighborhood (float) - cdef float pop - - # allocate memory with malloc - cdef Py_ssize_t max_se = srows * scols - - # number of element in each attack border - cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w + # number of pixels actually inside the neighborhood (double) + cdef double pop = 0 # the current local histogram distribution - cdef Py_ssize_t * histo = malloc(256 * sizeof(Py_ssize_t)) + cdef Py_ssize_t* histo = malloc(max_bin * sizeof(Py_ssize_t)) + for i in range(max_bin): + histo[i] = 0 # these lists contain the relative pixel row and column for each of the 4 # attack borders east, west, north and south e.g. se_e_r lists the rows of # the east structuring element border - cdef Py_ssize_t * se_e_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_e_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_w_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_w_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_n_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_n_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_s_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_s_c = malloc(max_se * sizeof(Py_ssize_t)) - # build attack and release borders - # by using difference along axis + cdef Py_ssize_t max_se = srows * scols + + # number of element in each attack border + cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w + num_se_n = num_se_s = num_se_e = num_se_w = 0 + + cdef Py_ssize_t* se_e_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_e_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_w_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_w_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_n_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_n_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_s_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_s_c = malloc(max_se * sizeof(Py_ssize_t)) + + # build attack and release borders by using difference along axis t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) - t_e = np.diff(t, axis=1) < 0 + cdef char[:, :] t_e = (np.diff(t, axis=1) < 0).view(np.uint8) t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) - t_w = np.diff(t, axis=1) > 0 + cdef char[:, :] t_w = (np.diff(t, axis=1) > 0).view(np.uint8) t = np.vstack((selem, np.zeros((1, selem.shape[1])))) - t_s = np.diff(t, axis=0) < 0 + cdef char[:, :] t_s = (np.diff(t, axis=0) < 0).view(np.uint8) t = np.vstack((np.zeros((1, selem.shape[1])), selem)) - t_n = np.diff(t, axis=0) > 0 - - num_se_n = num_se_s = num_se_e = num_se_w = 0 + cdef char[:, :] t_n = (np.diff(t, axis=0) > 0).view(np.uint8) for r in range(srows): for c in range(scols): @@ -136,92 +141,41 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, se_s_c[num_se_s] = c - centre_c num_se_s += 1 - # initial population and histogram (kernel is centered on the first row and - # column) - for i in range(256): - histo[i] = 0 - - pop = 0 - for r in range(srows): for c in range(scols): rr = r - centre_r cc = c - centre_c if selem[r, c]: if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) r = 0 c = 0 - # kernel ------------------------------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + out[r, c] = kernel(histo, pop, image[r, c], max_bin, mid_bin, p0, p1, s0, s1) - # kernel ------------------------------------------------------------------- # main loop r = 0 for even_row in range(0, rows, 2): + # ---> west to east for c in range(1, cols): for s in range(num_se_e): rr = r + se_e_r[s] cc = c + se_e_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) for s in range(num_se_w): rr = r + se_w_r[s] cc = c + se_w_c[s] - 1 if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + histogram_decrement(histo, &pop, image[rr, cc]) - # kernel ----------------------------------------------------------- - out_data[r * cols + c] = \ - kernel(histo, pop, image_data[r * cols + c], p0, p1, s0, s1) - # kernel ----------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) - r += 1 # pass to the next row - if r >= rows: - break - - # ---> north to south - for s in range(num_se_s): - rr = r + se_s_r[s] - cc = c + se_s_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) - - for s in range(num_se_n): - rr = r + se_n_r[s] - 1 - cc = c + se_n_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) - - # kernel --------------------------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], - p0, p1, s0, s1) - # kernel --------------------------------------------------------------- - - # ---> east to west - for c in range(cols - 2, -1, -1): - for s in range(num_se_w): - rr = r + se_w_r[s] - cc = c + se_w_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) - - for s in range(num_se_e): - rr = r + se_e_r[s] - cc = c + se_e_c[s] + 1 - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) - - # kernel ----------------------------------------------------------- - out_data[r * cols + c] = kernel( - histo, pop, image_data[r * cols + c], p0, p1, s0, s1) - # kernel ----------------------------------------------------------- - - r += 1 # pass to the next row + r += 1 # pass to the next row if r >= rows: break @@ -230,21 +184,55 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, rr = r + se_s_r[s] cc = c + se_s_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) for s in range(num_se_n): rr = r + se_n_r[s] - 1 cc = c + se_n_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + histogram_decrement(histo, &pop, image[rr, cc]) - # kernel --------------------------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], - p0, p1, s0, s1) - # kernel --------------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) + + # ---> east to west + for c in range(cols - 2, -1, -1): + for s in range(num_se_w): + rr = r + se_w_r[s] + cc = c + se_w_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image[rr, cc]) + + for s in range(num_se_e): + rr = r + se_e_r[s] + cc = c + se_e_c[s] + 1 + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image[rr, cc]) + + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) + + r += 1 # pass to the next row + if r >= rows: + break + + # ---> north to south + for s in range(num_se_s): + rr = r + se_s_r[s] + cc = c + se_s_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image[rr, cc]) + + for s in range(num_se_n): + rr = r + se_n_r[s] - 1 + cc = c + se_n_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image[rr, cc]) + + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) # release memory allocated by malloc - free(se_e_r) free(se_e_c) free(se_w_r) @@ -253,5 +241,4 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, free(se_n_c) free(se_s_r) free(se_s_c) - free(histo) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 94fc3130..50c9a370 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -1,11 +1,740 @@ +"""The local histogram is computed using a sliding window similar to the method +described in [1]_. + +Input image can be 8-bit or 16-bit, for 16-bit input images, the number of +histogram bins is determined from the maximum value present in the image. + +Result image is 8-/16-bit or double with respect to the input image and the +rank filter operation. + +References +---------- + +.. [1] Huang, T. ,Yang, G. ; Tang, G.. "A fast two-dimensional + median filtering algorithm", IEEE Transactions on Acoustics, Speech and + Signal Processing, Feb 1979. Volume: 27 , Issue: 1, Page(s): 13 - 18. + +""" + +import warnings import numpy as np +from skimage import img_as_ubyte + +from . import generic_cy -def find_bitdepth(image): - """returns the max bith depth of a uint16 image - """ - umax = np.max(image) - if umax > 2: - return int(np.log2(umax)) +__all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', + 'subtract_mean', 'median', 'minimum', 'modal', 'enhance_contrast', + 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] + + +def _handle_input(image, selem, out, mask, out_dtype=None): + + if image.dtype not in (np.uint8, np.uint16): + image = img_as_ubyte(image) + + selem = np.ascontiguousarray(img_as_ubyte(selem > 0)) + image = np.ascontiguousarray(image) + + if mask is None: + mask = np.ones(image.shape, dtype=np.uint8) else: - return 1 + mask = img_as_ubyte(mask) + mask = np.ascontiguousarray(mask) + + if out is None: + if out_dtype is None: + out_dtype = image.dtype + out = np.empty_like(image, dtype=out_dtype) + + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + + is_8bit = image.dtype in (np.uint8, np.int8) + + if is_8bit: + max_bin = 255 + else: + max_bin = max(4, image.max()) + + bitdepth = int(np.log2(max_bin)) + if bitdepth > 10: + warnings.warn("Bitdepth of %d may result in bad rank filter " + "performance due to large number of bins." % bitdepth) + + return image, selem, out, mask, max_bin + + +def _apply(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): + + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype) + + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin) + + return out + + +def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Autolevel image using local histogram. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import autolevel + >>> # Load test image + >>> ima = data.camera() + >>> # Stretch image contrast locally + >>> auto = autolevel(ima, disk(20)) + + """ + + return _apply(generic_cy._autolevel, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns greyscale local bottomhat of an image. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + bottomhat : ndarray (same dtype as input image) + The result of the local bottomhat. + + """ + + return _apply(generic_cy._bottomhat, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Equalize image using local histogram. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import equalize + >>> # Load test image + >>> ima = data.camera() + >>> # Local equalization + >>> equ = equalize(ima, disk(20)) + + """ + + return _apply(generic_cy._equalize, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local gradient of an image (i.e. local maximum - local + minimum). + + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + """ + + return _apply(generic_cy._gradient, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local maximum of an image. + + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + See also + -------- + skimage.morphology.dilation + + Note + ---- + * the lower algorithm complexity makes the rank.maximum() more efficient + for larger images and structuring elements + + """ + + return _apply(generic_cy._maximum, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local mean of an image. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import mean + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = mean(ima, disk(20)) + + """ + + return _apply(generic_cy._mean, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def subtract_mean(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Return image subtracted from its local mean. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + """ + + return _apply(generic_cy._subtract_mean, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local median of an image. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import median + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = median(ima, disk(20)) + + """ + + return _apply(generic_cy._median, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local minimum of an image. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + See also + -------- + skimage.morphology.erosion + + Note + ---- + * the lower algorithm complexity makes the rank.minimum() more efficient + for larger images and structuring elements + + """ + + return _apply(generic_cy._minimum, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local mode of an image. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + """ + + return _apply(generic_cy._modal, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Enhance an image replacing each pixel by the local maximum if pixel + greylevel is closest to maximimum than local minimum OR local minimum + otherwise. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + Output image. + out : ndarray (same dtype as input image) + The result of the local enhance_contrast. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import enhance_contrast + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = enhance_contrast(ima, disk(20)) + + """ + + return _apply(generic_cy._enhance_contrast, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return the number (population) of pixels actually inside the + neighborhood. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + Examples + -------- + >>> # Local mean + >>> from skimage.morphology import square + >>> import skimage.filter.rank as rank + >>> ima = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> rank.pop(ima, square(3)) + array([[4, 6, 6, 6, 4], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [4, 6, 6, 6, 4]], dtype=uint8) + + """ + + return _apply(generic_cy._pop, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local threshold of an image. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + Examples + -------- + >>> # Local threshold + >>> from skimage.morphology import square + >>> from skimage.filter.rank import threshold + >>> ima = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> threshold(ima, square(3)) + array([[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + """ + + return _apply(generic_cy._threshold, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local tophat of an image. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + """ + + return _apply(generic_cy._tophat, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def noise_filter(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Returns the noise feature as described in [Hashimoto12]_ + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + References + ---------- + .. [Hashimoto12] N. Hashimoto et al. Referenceless image quality evaluation + for whole slide imaging. J Pathol Inform 2012;3:9. + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + """ + + # ensure that the central pixel in the structuring element is empty + centre_r = int(selem.shape[0] / 2) + shift_y + centre_c = int(selem.shape[1] / 2) + shift_x + # make a local copy + selem_cpy = selem.copy() + selem_cpy[centre_r, centre_c] = 0 + + return _apply(generic_cy._noise_filter, image, selem_cpy, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns the entropy [1]_ computed locally. Entropy is computed + using base 2 logarithm i.e. the filter returns the minimum number of + bits needed to encode local greylevel distribution. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (double) + Output image. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Entropy_(information_theory)> + + Examples + -------- + >>> # Local entropy + >>> from skimage import data + >>> from skimage.filter.rank import entropy + >>> from skimage.morphology import disk + >>> a8 = data.camera() + >>> ent8 = entropy(a8, disk(5)) + + """ + + return _apply(generic_cy._entropy, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, + out_dtype=np.double) + + +def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns the Otsu's threshold value for each pixel. + + Parameters + ---------- + image : ndarray + Image array (uint8 array). + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + References + ---------- + .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method + + Examples + -------- + >>> # Local entropy + >>> from skimage import data + >>> from skimage.filter.rank import otsu + >>> from skimage.morphology import disk + >>> # defining a 8-bit test images + >>> a8 = data.camera() + >>> loc_otsu = otsu(a8, disk(5)) + >>> thresh_image = a8 >= loc_otsu + + """ + + return _apply(generic_cy._otsu, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx new file mode 100644 index 00000000..dcf6e361 --- /dev/null +++ b/skimage/filter/rank/generic_cy.pyx @@ -0,0 +1,506 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log + +from .core_cy cimport dtype_t, dtype_t_out, _core + + +cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, delta + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + delta = imax - imin + if delta > 0: + return (max_bin - 1) * (g - imin) / delta + else: + return 0 + else: + return 0 + + +cdef inline double _kernel_bottomhat(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin): + if histo[i]: + break + return g - i + else: + return 0 + + +cdef inline double _kernel_equalize(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + sum += histo[i] + if i >= g: + break + return ((max_bin - 1) * sum) / pop + else: + return 0 + + +cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + return imax - imin + else: + return 0 + + +cdef inline double _kernel_maximum(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + return i + else: + return 0 + + +cdef inline double _kernel_mean(Py_ssize_t* histo, double pop,dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return mean / pop + else: + return 0 + + +cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return (g - mean / pop) / 2. + 127 + else: + return 0 + + +cdef inline double _kernel_median(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef double sum = pop / 2.0 + + if pop: + for i in range(max_bin): + if histo[i]: + sum -= histo[i] + if sum < 0: + return i + else: + return 0 + + +cdef inline double _kernel_minimum(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin): + if histo[i]: + return i + else: + return 0 + + +cdef inline double _kernel_modal(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t hmax = 0, imax = 0 + + if pop: + for i in range(max_bin): + if histo[i] > hmax: + hmax = histo[i] + imax = i + return imax + else: + return 0 + + +cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + if imax - g < g - imin: + return imax + else: + return imin + else: + return 0 + + +cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + return pop + + +cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return g > (mean / pop) + else: + return 0 + + +cdef inline double _kernel_tophat(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + break + return i - g + else: + return 0 + + +cdef inline double _kernel_noise_filter(Py_ssize_t* histo, double pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t min_i + + # early stop if at least one pixel of the neighborhood has the same g + if histo[g] > 0: + return 0 + + for i in range(g, -1, -1): + if histo[i]: + break + min_i = g - i + for i in range(g, max_bin): + if histo[i]: + break + if i - g < min_i: + return i - g + else: + return min_i + + +cdef inline double _kernel_entropy(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef double e, p + + if pop: + e = 0. + for i in range(max_bin): + p = histo[i] / pop + if p > 0: + e -= p * log(p) / 0.6931471805599453 + return e + else: + return 0 + + +cdef inline double _kernel_otsu(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef Py_ssize_t max_i + cdef double P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b + cdef double mu = 0. + + # compute local mean + if pop: + for i in range(max_bin): + mu += histo[i] * i + mu = mu / pop + else: + return 0 + + # maximizing the between class variance + max_i = 0 + q1 = histo[0] / pop + mu1 = 0. + max_sigma_b = 0. + + for i in range(1, max_bin): + P = histo[i] / pop + new_q1 = q1 + P + if new_q1 > 0: + mu1 = (q1 * mu1 + i * P) / new_q1 + mu2 = (mu - new_q1 * mu1) / (1. - new_q1) + sigma_b = new_q1 * (1. - new_q1) * (mu1 - mu2) ** 2 + if sigma_b > max_sigma_b: + max_sigma_b = sigma_b + max_i = i + q1 = new_q1 + + return max_i + + +def _autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_autolevel[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _bottomhat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_bottomhat[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _equalize(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_equalize[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_gradient[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _maximum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_maximum[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_mean[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _subtract_mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_subtract_mean[dtype_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _median(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_median[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _minimum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_minimum[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _enhance_contrast(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _modal(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_modal[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_pop[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_threshold[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _tophat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_tophat[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _noise_filter(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_noise_filter[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _entropy(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_entropy[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _otsu(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_otsu[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) diff --git a/skimage/filter/rank/percentile_rank.pyx b/skimage/filter/rank/percentile.py similarity index 53% rename from skimage/filter/rank/percentile_rank.pyx rename to skimage/filter/rank/percentile.py index 704f53c2..ff3b1559 100644 --- a/skimage/filter/rank/percentile_rank.pyx +++ b/skimage/filter/rank/percentile.py @@ -1,17 +1,17 @@ """Inferior and superior ranks, provided by the user, are passed to the kernel function to provide a softer version of the rank filters. E.g. -percentile_autolevel will stretch image levels between percentile [p0, p1] +``autolevel_percentile`` will stretch image levels between percentile [p0, p1] instead of using [min, max]. It means that isolated bright or dark pixels will not produce halos. The local histogram is computed using a sliding window similar to the method described in [1]_. -Input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit), for 16-bit -input images, the number of histogram bins is determined from the maximum value -present in the image. +Input image can be 8-bit or 16-bit, for 16-bit input images, the number of +histogram bins is determined from the maximum value present in the image. -Result image is 8 or 16-bit with respect to the input image. +Result image is 8-/16-bit or double with respect to the input image and the +rank filter operation. References ---------- @@ -23,55 +23,31 @@ References """ import numpy as np -from skimage import img_as_ubyte -from skimage.filter.rank.generic import find_bitdepth -from skimage.filter.rank import _crank16_percentiles, _crank8_percentiles + +from . import percentile_cy +from .generic import _handle_input -__all__ = ['percentile_autolevel', 'percentile_gradient', - 'percentile_mean', 'percentile_mean_subtraction', - 'percentile_morph_contr_enh', 'percentile', 'percentile_pop', - 'percentile_threshold'] +__all__ = ['autolevel_percentile', 'gradient_percentile', + 'mean_percentile', 'subtract_mean_percentile', + 'enhance_contrast_percentile', 'percentile', 'pop_percentile', + 'threshold_percentile'] -def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, p0, p1): - selem = img_as_ubyte(selem > 0) - image = np.ascontiguousarray(image) +def _apply(func, image, selem, out, mask, shift_x, shift_y, p0, p1, + out_dtype=None): - if mask is None: - mask = np.ones(image.shape, dtype=np.uint8) - else: - mask = np.ascontiguousarray(mask) - mask = img_as_ubyte(mask) + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype) - if image is out: - raise NotImplementedError("Cannot perform rank operation in place.") - - if image.dtype == np.uint8: - if func8 is None: - raise TypeError("Not implemented for uint8 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint8) - func8(image, selem, shift_x=shift_x, shift_y=shift_y, - mask=mask, out=out, p0=p0, p1=p1) - elif image.dtype == np.uint16: - if func16 is None: - raise TypeError("Not implemented for uint16 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint16) - bitdepth = find_bitdepth(image) - if bitdepth > 11: - raise ValueError("Only uint16 <4096 image (12bit) supported.") - func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, - bitdepth=bitdepth + 1, out=out, p0=p0, p1=p1) - else: - raise TypeError("Only uint8 and uint16 image supported.") + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin, p0=p0, p1=p1) return out -def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): +def autolevel_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0, p1=1): """Return greyscale local autolevel of an image. Autolevel is computed on the given structuring element. Only levels between @@ -79,15 +55,13 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -100,59 +74,56 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local autolevel : uint8 array or uint16 - The result of the local autolevel. + out : ndarray (same dtype as input image) + Output image. """ - return _apply( - _crank8_percentiles.autolevel, _crank16_percentiles.autolevel, - image, selem, out=out, mask=mask, shift_x=shift_x, - shift_y=shift_y, p0=p0, p1=p1) - - -def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): - """Return greyscale local percentile_gradient of an image. - - percentile_gradient is computed on the given structuring element. Only - levels between percentiles [p0, p1] are used. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - p0, p1 : float in [0, ..., 1] - Define the [p0, p1] percentile interval to be considered for computing - the value. - - Returns - ------- - local percentile_gradient : uint8 array or uint16 - The result of the local percentile_gradient. - - """ - - return _apply(_crank8_percentiles.gradient, _crank16_percentiles.gradient, + return _apply(percentile_cy._autolevel, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) -def percentile_mean(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): +def gradient_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0, p1=1): + """Return greyscale local gradient of an image. + + gradient is computed on the given structuring element. Only + levels between percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray (uint8, uint16) + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray (same dtype as input) + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + out : ndarray (same dtype as input image) + Output image. + + """ + + return _apply(percentile_cy._gradient, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def mean_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0, p1=1): """Return greyscale local mean of an image. Mean is computed on the given structuring element. Only levels between @@ -160,15 +131,13 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -181,34 +150,32 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local mean : uint8 array or uint16 - The result of the local mean. + out : ndarray (same dtype as input image) + Output image. """ - return _apply(_crank8_percentiles.mean, _crank16_percentiles.mean, + return _apply(percentile_cy._mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) -def percentile_mean_subtraction(image, selem, out=None, mask=None, - shift_x=False, shift_y=False, p0=.0, p1=1.): - """Return greyscale local mean_subtraction of an image. +def subtract_mean_percentile(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, p0=0, p1=1): + """Return greyscale local subtract_mean of an image. - mean_subtraction is computed on the given structuring element. Only levels + subtract_mean is computed on the given structuring element. Only levels between percentiles [p0, p1] are used. Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -221,36 +188,32 @@ def percentile_mean_subtraction(image, selem, out=None, mask=None, Returns ------- - local mean_subtraction : uint8 array or uint16 - The result of the local mean_subtraction. + out : ndarray (same dtype as input image) + Output image. """ - return _apply(_crank8_percentiles.mean_subtraction, - _crank16_percentiles.mean_subtraction, + return _apply(percentile_cy._subtract_mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) -def percentile_morph_contr_enh( - image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): - """Return greyscale local morph_contr_enh of an image. +def enhance_contrast_percentile(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, p0=0, p1=1): + """Return greyscale local enhance_contrast of an image. - morph_contr_enh is computed on the given structuring element. Only levels + enhance_contrast is computed on the given structuring element. Only levels between percentiles [p0, p1] are used. Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -263,19 +226,18 @@ def percentile_morph_contr_enh( Returns ------- - local morph_contr_enh : uint8 array or uint16 - The result of the local morph_contr_enh. + out : ndarray (same dtype as input image) + Output image. """ - return _apply(_crank8_percentiles.morph_contr_enh, - _crank16_percentiles.morph_contr_enh, + return _apply(percentile_cy._enhance_contrast, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, - p0=.0): + p0=0): """Return greyscale local percentile of an image. percentile is computed on the given structuring element. Returns the value @@ -283,15 +245,13 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -303,19 +263,18 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, Returns ------- - local percentile : uint8 array or uint16 - The result of the local percentile. + out : ndarray (same dtype as input image) + Output image. """ - return _apply(_crank8_percentiles.percentile, - _crank16_percentiles.percentile, + return _apply(percentile_cy._percentile, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=0.) -def percentile_pop(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): +def pop_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0, p1=1): """Return greyscale local pop of an image. pop is computed on the given structuring element. Only levels between @@ -323,15 +282,13 @@ def percentile_pop(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -344,18 +301,18 @@ def percentile_pop(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local pop : uint8 array or uint16 - The result of the local pop. + out : ndarray (same dtype as input image) + Output image. """ - return _apply(_crank8_percentiles.pop, _crank16_percentiles.pop, + return _apply(percentile_cy._pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) -def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0): +def threshold_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0): """Return greyscale local threshold of an image. threshold is computed on the given structuring element. Returns @@ -365,15 +322,13 @@ def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -383,14 +338,13 @@ def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, p0 : float in [0, ..., 1] Set the percentile value. - Returns - ------- - local threshold : uint8 array or uint16 + out : ndarray (same dtype as input image) + Output image. + local threshold : ndarray (same dtype as input) The result of the local threshold. """ - return _apply( - _crank8_percentiles.threshold, _crank16_percentiles.threshold, - image, selem, out=out, mask=mask, shift_x=shift_x, - shift_y=shift_y, p0=p0, p1=0.) + return _apply(percentile_cy._threshold, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=0) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx new file mode 100644 index 00000000..e951a76e --- /dev/null +++ b/skimage/filter/rank/percentile_cy.pyx @@ -0,0 +1,301 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from .core_cy cimport dtype_t, dtype_t_out, _core, _min, _max + + +cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range(max_bin - 1, -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + + delta = imax - imin + if delta > 0: + return (max_bin - 1) * (_min(_max(imin, g), imax) + - imin) / delta + else: + return imax - imin + else: + return 0 + + +cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum >= p0 * pop: + imin = i + break + sum = 0 + for i in range(max_bin - 1, -1, -1): + sum += histo[i] + if sum >= p1 * pop: + imax = i + break + + return imax - imin + else: + return 0 + + +cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + + if n > 0: + return mean / n + else: + return 0 + else: + return 0 + + +cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + if n > 0: + return (g - (mean / n)) * .5 + mid_bin + else: + return 0 + else: + return 0 + + +cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range(max_bin - 1, -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + if g > imax: + return imax + if g < imin: + return imin + if imax - g < g - imin: + return imax + else: + return imin + else: + return 0 + + +cdef inline double _kernel_percentile(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t sum = 0 + + if pop: + if p0 == 1: # make sure p0 = 1 returns the maximum filter + for i in range(max_bin - 1, -1, -1): + if histo[i]: + break + else: + for i in range(max_bin): + sum += histo[i] + if sum > p0 * pop: + break + return i + else: + return 0 + + +cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, sum, n + + if pop: + sum = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + return n + else: + return 0 + + +cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + sum += histo[i] + if sum >= p0 * pop: + break + + return (max_bin - 1) * (g >= i) + else: + return 0 + + +def _autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_autolevel[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_gradient[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_mean[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _subtract_mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_subtract_mean[dtype_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _enhance_contrast(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _percentile(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_percentile[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_pop[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_threshold[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) diff --git a/skimage/filter/rank/rank.pyx b/skimage/filter/rank/rank.pyx deleted file mode 100644 index 559f1c76..00000000 --- a/skimage/filter/rank/rank.pyx +++ /dev/null @@ -1,769 +0,0 @@ -"""The local histogram is computed using a sliding window similar to the method -described in [1]_. - -Input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit), for 16-bit -input images, the number of histogram bins is determined from the maximum value -present in the image. - -Result image is 8 or 16-bit with respect to the input image. - -References ----------- - -.. [1] Huang, T. ,Yang, G. ; Tang, G.. "A fast two-dimensional - median filtering algorithm", IEEE Transactions on Acoustics, Speech and - Signal Processing, Feb 1979. Volume: 27 , Issue: 1, Page(s): 13 - 18. - -""" - -import numpy as np -from skimage import img_as_ubyte -from skimage.filter.rank import _crank8, _crank16 -from skimage.filter.rank.generic import find_bitdepth - - -__all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', - 'meansubtraction', 'median', 'minimum', 'modal', 'morph_contr_enh', - 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] - - -def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): - selem = img_as_ubyte(selem > 0) - image = np.ascontiguousarray(image) - - if mask is None: - mask = np.ones(image.shape, dtype=np.uint8) - else: - mask = np.ascontiguousarray(mask) - mask = img_as_ubyte(mask) - - if image is out: - raise NotImplementedError("Cannot perform rank operation in place.") - - if image.dtype == np.uint8: - if func8 is None: - raise TypeError("Not implemented for uint8 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint8) - func8(image, selem, shift_x=shift_x, shift_y=shift_y, - mask=mask, out=out) - elif image.dtype == np.uint16: - if func16 is None: - raise TypeError("Not implemented for uint16 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint16) - bitdepth = find_bitdepth(image) - if bitdepth > 11: - raise ValueError("Only uint16 <4096 image (12bit) supported.") - func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, - bitdepth=bitdepth + 1, out=out) - else: - raise TypeError("Only uint8 and uint16 image supported.") - - return out - - -def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Autolevel image using local histogram. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The result of the local autolevel. - - Examples - -------- - >>> from skimage import data - >>> from skimage.morphology import disk - >>> from skimage.filter.rank import autolevel - >>> # Load test image - >>> ima = data.camera() - >>> # Stretch image contrast locally - >>> auto = autolevel(ima, disk(20)) - - """ - - return _apply(_crank8.autolevel, _crank16.autolevel, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns greyscale local bottomhat of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - local bottomhat : uint8 array or uint16 array depending on input image - The result of the local bottomhat. - - """ - - return _apply(_crank8.bottomhat, _crank16.bottomhat, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Equalize image using local histogram. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The result of the local equalize. - - Examples - -------- - >>> from skimage import data - >>> from skimage.morphology import disk - >>> from skimage.filter.rank import equalize - >>> # Load test image - >>> ima = data.camera() - >>> # Local equalization - >>> equ = equalize(ima, disk(20)) - - """ - - return _apply(_crank8.equalize, _crank16.equalize, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local gradient of an image (i.e. local maximum - local - minimum). - - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local gradient. - - """ - - return _apply(_crank8.gradient, _crank16.gradient, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local maximum of an image. - - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local maximum. - - See also - -------- - skimage.morphology.dilation - - Note - ---- - * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) - * the lower algorithm complexity makes the rank.maximum() more efficient for - larger images and structuring elements - - """ - - return _apply(_crank8.maximum, _crank16.maximum, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local mean of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local mean. - - Examples - -------- - >>> from skimage import data - >>> from skimage.morphology import disk - >>> from skimage.filter.rank import mean - >>> # Load test image - >>> ima = data.camera() - >>> # Local mean - >>> avg = mean(ima, disk(20)) - - """ - - return _apply(_crank8.mean, _crank16.mean, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def meansubtraction(image, selem, out=None, mask=None, shift_x=False, - shift_y=False): - """Return image subtracted from its local mean. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The result of the local meansubtraction. - - """ - - return _apply(_crank8.meansubtraction, _crank16.meansubtraction, image, - selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local median of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local median. - - Examples - -------- - >>> from skimage import data - >>> from skimage.morphology import disk - >>> from skimage.filter.rank import median - >>> # Load test image - >>> ima = data.camera() - >>> # Local mean - >>> avg = median(ima, disk(20)) - - """ - - return _apply(_crank8.median, _crank16.median, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local minimum of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local minimum. - - See also - -------- - skimage.morphology.erosion - - Note - ---- - * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) - * the lower algorithm complexity makes the rank.minimum() more efficient - for larger images and structuring elements - - """ - - return _apply(_crank8.minimum, _crank16.minimum, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local mode of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local modal. - - """ - - return _apply(_crank8.modal, _crank16.modal, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, - shift_y=False): - """Enhance an image replacing each pixel by the local maximum if pixel - greylevel is closest to maximimum than local minimum OR local minimum - otherwise. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The result of the local morph_contr_enh. - - Examples - -------- - >>> from skimage import data - >>> from skimage.morphology import disk - >>> from skimage.filter.rank import morph_contr_enh - >>> # Load test image - >>> ima = data.camera() - >>> # Local mean - >>> avg = morph_contr_enh(ima, disk(20)) - - """ - - return _apply(_crank8.morph_contr_enh, _crank16.morph_contr_enh, image, - selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return the number (population) of pixels actually inside the - neighborhood. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The number of pixels belonging to the neighborhood. - - Examples - -------- - >>> # Local mean - >>> from skimage.morphology import square - >>> import skimage.filter.rank as rank - >>> ima = 255 * np.array([[0, 0, 0, 0, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 0, 0, 0, 0]], dtype=np.uint8) - >>> rank.pop(ima, square(3)) - array([[4, 6, 6, 6, 4], - [6, 9, 9, 9, 6], - [6, 9, 9, 9, 6], - [6, 9, 9, 9, 6], - [4, 6, 6, 6, 4]], dtype=uint8) - - """ - - return _apply(_crank8.pop, _crank16.pop, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local threshold of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The result of the local threshold. - - Examples - -------- - >>> # Local threshold - >>> from skimage.morphology import square - >>> from skimage.filter.rank import threshold - >>> ima = 255 * np.array([[0, 0, 0, 0, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 0, 0, 0, 0]], dtype=np.uint8) - >>> threshold(ima, square(3)) - array([[0, 0, 0, 0, 0], - [0, 1, 1, 1, 0], - [0, 1, 0, 1, 0], - [0, 1, 1, 1, 0], - [0, 0, 0, 0, 0]], dtype=uint8) - - """ - - return _apply(_crank8.threshold, _crank16.threshold, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local tophat of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The image tophat. - - """ - - return _apply(_crank8.tophat, _crank16.tophat, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def noise_filter(image, selem, out=None, mask=None, shift_x=False, - shift_y=False): - """Returns the noise feature as described in [Hashimoto12]_ - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - References - ---------- - .. [Hashimoto12] N. Hashimoto et al. Referenceless image quality evaluation - for whole slide imaging. J Pathol Inform 2012;3:9. - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The image noise. - - """ - - # ensure that the central pixel in the structuring element is empty - centre_r = int(selem.shape[0] / 2) + shift_y - centre_c = int(selem.shape[1] / 2) + shift_x - # make a local copy - selem_cpy = selem.copy() - selem_cpy[centre_r, centre_c] = 0 - - return _apply(_crank8.noise_filter, None, image, selem_cpy, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns the entropy [1]_ computed locally. Entropy is computed - using base 2 logarithm i.e. the filter returns the minimum number of - bits needed to encode local greylevel distribution. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - entropy x10 (uint8 images) and entropy x1000 (uint16 images) - - References - ---------- - .. [1] http://en.wikipedia.org/wiki/Entropy_(information_theory) - - Examples - -------- - >>> # Local entropy - >>> from skimage import data - >>> from skimage.filter.rank import entropy - >>> from skimage.morphology import disk - >>> # defining a 8- and a 16-bit test images - >>> a8 = data.camera() - >>> a16 = data.camera().astype(np.uint16) * 4 - >>> # pixel values contain 10x the local entropy - >>> ent8 = entropy(a8, disk(5)) - >>> # pixel values contain 1000x the local entropy - >>> ent16 = entropy(a16, disk(5)) - - """ - - return _apply(_crank8.entropy, _crank16.entropy, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns the Otsu's threshold value for each pixel. - - Parameters - ---------- - image : ndarray - Image array (uint8 array). - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array - Otsu's threshold values - - References - ---------- - .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method - - Notes - ----- - * input image are 8-bit only - - Examples - -------- - >>> # Local entropy - >>> from skimage import data - >>> from skimage.filter.rank import otsu - >>> from skimage.morphology import disk - >>> # defining a 8-bit test images - >>> a8 = data.camera() - >>> loc_otsu = otsu(a8, disk(5)) - >>> thresh_image = a8 >= loc_otsu - - """ - - return _apply(_crank8.otsu, None, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index 1cfb92fe..1d573b81 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -1,7 +1,8 @@ import numpy as np from numpy.testing import run_module_suite, assert_array_equal, assert_raises -from skimage import data +from skimage import img_as_ubyte, img_as_uint, img_as_float +from skimage import data, util from skimage.morphology import cmorph, disk from skimage.filter import rank @@ -32,10 +33,10 @@ def test_random_sizes(): shift_x=+1, shift_y=+1) assert_array_equal(image16.shape, out16.shape) - rank.percentile_mean(image=image16, mask=mask, out=out16, + rank.mean_percentile(image=image16, mask=mask, out=out16, selem=elem, shift_x=0, shift_y=0, p0=.1, p1=.9) assert_array_equal(image16.shape, out16.shape) - rank.percentile_mean(image=image16, mask=mask, out=out16, + rank.mean_percentile(image=image16, mask=mask, out=out16, selem=elem, shift_x=+1, shift_y=+1, p0=.1, p1=.9) assert_array_equal(image16.shape, out16.shape) @@ -50,7 +51,7 @@ def test_compare_with_cmorph_dilate(): for r in range(1, 20, 1): elem = np.ones((r, r), dtype=np.uint8) rank.maximum(image=image, selem=elem, out=out, mask=mask) - cm = cmorph.dilate(image=image, selem=elem) + cm = cmorph._dilate(image=image, selem=elem) assert_array_equal(out, cm) @@ -64,7 +65,7 @@ def test_compare_with_cmorph_erode(): for r in range(1, 20, 1): elem = np.ones((r, r), dtype=np.uint8) rank.minimum(image=image, selem=elem, out=out, mask=mask) - cm = cmorph.erode(image=image, selem=elem) + cm = cmorph._erode(image=image, selem=elem) assert_array_equal(out, cm) @@ -77,7 +78,7 @@ def test_bitdepth(): for i in range(5): image = np.ones((100, 100), dtype=np.uint16) * 255 * 2 ** i - r = rank.percentile_mean(image=image, selem=elem, mask=mask, + r = rank.mean_percentile(image=image, selem=elem, mask=mask, out=out, shift_x=0, shift_y=0, p0=.1, p1=.9) @@ -129,17 +130,6 @@ def test_structuring_element8(): assert_array_equal(r, out) -def test_fail_on_bitdepth(): - # should fail because data bitdepth is too high for the function - - image = np.ones((100, 100), dtype=np.uint16) * 2 ** 12 - elem = np.ones((3, 3), dtype=np.uint8) - out = np.empty_like(image) - mask = np.ones(image.shape, dtype=np.uint8) - assert_raises(ValueError, rank.percentile_mean, image=image, - selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - - def test_pass_on_bitdepth(): # should pass because data bitdepth is not too high for the function @@ -162,41 +152,80 @@ def test_compare_autolevels(): # compare autolevel and percentile autolevel with p0=0.0 and p1=1.0 # should returns the same arrays - image = data.camera() + image = util.img_as_ubyte(data.camera()) selem = disk(20) loc_autolevel = rank.autolevel(image, selem=selem) - loc_perc_autolevel = rank.percentile_autolevel(image, selem=selem, + loc_perc_autolevel = rank.autolevel_percentile(image, selem=selem, p0=.0, p1=1.) assert_array_equal(loc_autolevel, loc_perc_autolevel) def test_compare_autolevels_16bit(): - # compare autolevel(16-bit) and percentile autolevel(16-bit) with p0=0.0 and - # p1=1.0 should returns the same arrays + # compare autolevel(16-bit) and percentile autolevel(16-bit) with p0=0.0 + # and p1=1.0 should returns the same arrays image = data.camera().astype(np.uint16) * 4 selem = disk(20) loc_autolevel = rank.autolevel(image, selem=selem) - loc_perc_autolevel = rank.percentile_autolevel(image, selem=selem, + loc_perc_autolevel = rank.autolevel_percentile(image, selem=selem, p0=.0, p1=1.) assert_array_equal(loc_autolevel, loc_perc_autolevel) -def test_compare_8bit_vs_16bit(): - # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of - # dynamic) should be identical +def test_compare_ubyte_vs_float(): - image8 = data.camera() + # Create signed int8 image that and convert it to uint8 + image_uint = img_as_ubyte(data.camera()[:50, :50]) + image_float = img_as_float(image_uint) + + methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'threshold', + 'subtract_mean', 'enhance_contrast', 'pop', 'tophat'] + + for method in methods: + func = getattr(rank, method) + out_u = func(image_uint, disk(3)) + out_f = func(image_float, disk(3)) + assert_array_equal(out_u, out_f) + + +def test_compare_8bit_unsigned_vs_signed(): + # filters applied on 8-bit image ore 16-bit image (having only real 8-bit + # of dynamic) should be identical + + # Create signed int8 image that and convert it to uint8 + image = img_as_ubyte(data.camera()) + image[image > 127] = 0 + image_s = image.astype(np.int8) + image_u = img_as_ubyte(image_s) + + assert_array_equal(image_u, img_as_ubyte(image_s)) + + methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', + 'mean', 'subtract_mean', 'median', 'minimum', 'modal', + 'enhance_contrast', 'pop', 'threshold', 'tophat'] + + for method in methods: + func = getattr(rank, method) + out_u = func(image_u, disk(3)) + out_s = func(image_s, disk(3)) + assert_array_equal(out_u, out_s) + + +def test_compare_8bit_vs_16bit(): + # filters applied on 8-bit image ore 16-bit image (having only real 8-bit + # of dynamic) should be identical + + image8 = util.img_as_ubyte(data.camera()) image16 = image8.astype(np.uint16) assert_array_equal(image8, image16) methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', - 'mean', 'meansubtraction', 'median', 'minimum', 'modal', - 'morph_contr_enh', 'pop', 'threshold', 'tophat'] + 'mean', 'subtract_mean', 'median', 'minimum', 'modal', + 'enhance_contrast', 'pop', 'threshold', 'tophat'] for method in methods: func = getattr(rank, method) @@ -298,7 +327,8 @@ def test_smallest_selem16(): def test_empty_selem(): - # check that min, max and mean returns zeros if structuring element is empty + # check that min, max and mean returns zeros if structuring element is + # empty image = np.zeros((5, 5), dtype=np.uint16) out = np.zeros_like(image) @@ -325,13 +355,11 @@ def test_otsu(): # test the local Otsu segmentation on a synthetic image # (left to right ramp * sinus) - test = np.tile( - [128, 145, 103, 127, 165, 83, 127, 185, 63, 127, 205, 43, - 127, 225, 23, 127], - (16, 1)) + test = np.tile([128, 145, 103, 127, 165, 83, 127, 185, 63, 127, 205, 43, + 127, 225, 23, 127], + (16, 1)) test = test.astype(np.uint8) - res = np.tile([1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1], - (16, 1)) + res = np.tile([1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1], (16, 1)) selem = np.ones((6, 6), dtype=np.uint8) th = 1 * (test >= rank.otsu(test, selem)) assert_array_equal(th, res) @@ -343,37 +371,41 @@ def test_entropy(): selem = np.ones((16, 16), dtype=np.uint8) # 1 bit per pixel data = np.tile(np.asarray([0, 1]), (100, 100)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 10) + assert(np.max(rank.entropy(data, selem)) == 1) # 2 bit per pixel data = np.tile(np.asarray([[0, 1], [2, 3]]), (10, 10)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 20) + assert(np.max(rank.entropy(data, selem)) == 2) # 3 bit per pixel data = np.tile( np.asarray([[0, 1, 2, 3], [4, 5, 6, 7]]), (10, 10)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 30) + assert(np.max(rank.entropy(data, selem)) == 3) # 4 bit per pixel data = np.tile( np.reshape(np.arange(16), (4, 4)), (10, 10)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 40) + assert(np.max(rank.entropy(data, selem)) == 4) # 6 bit per pixel data = np.tile( np.reshape(np.arange(64), (8, 8)), (10, 10)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 60) + assert(np.max(rank.entropy(data, selem)) == 6) # 8-bit per pixel data = np.tile( np.reshape(np.arange(256), (16, 16)), (10, 10)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 80) + assert(np.max(rank.entropy(data, selem)) == 8) # 12 bit per pixel selem = np.ones((64, 64), dtype=np.uint8) data = np.tile( np.reshape(np.arange(4096), (64, 64)), (2, 2)).astype(np.uint16) - assert(np.max(rank.entropy(data, selem)) == 12000) + assert(np.max(rank.entropy(data, selem)) == 12) + + # make sure output is of dtype double + out = rank.entropy(data, np.ones((16, 16), dtype=np.uint8)) + assert out.dtype == np.double def test_selem_dtypes(): @@ -391,10 +423,81 @@ def test_selem_dtypes(): rank.mean(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) assert_array_equal(image, out) - rank.percentile_mean(image=image, selem=elem, out=out, mask=mask, + rank.mean_percentile(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) assert_array_equal(image, out) +def test_16bit(): + image = np.zeros((21, 21), dtype=np.uint16) + selem = np.ones((3, 3), dtype=np.uint8) + + for bitdepth in range(17): + value = 2 ** bitdepth - 1 + image[10, 10] = value + assert rank.minimum(image, selem)[10, 10] == 0 + assert rank.maximum(image, selem)[10, 10] == value + assert rank.mean(image, selem)[10, 10] == value / selem.size + + +def test_bilateral(): + image = np.zeros((21, 21), dtype=np.uint16) + selem = np.ones((3, 3), dtype=np.uint8) + + image[10, 10] = 1000 + image[10, 11] = 1010 + image[10, 9] = 900 + + assert rank.mean_bilateral(image, selem, s0=1, s1=1)[10, 10] == 1000 + assert rank.pop_bilateral(image, selem, s0=1, s1=1)[10, 10] == 1 + assert rank.mean_bilateral(image, selem, s0=11, s1=11)[10, 10] == 1005 + assert rank.pop_bilateral(image, selem, s0=11, s1=11)[10, 10] == 2 + + +def test_percentile_min(): + # check that percentile p0 = 0 is identical to local min + img = data.camera() + img16 = img.astype(np.uint16) + selem = disk(15) + # check for 8bit + img_p0 = rank.percentile(img, selem=selem, p0=0) + img_min = rank.minimum(img, selem=selem) + assert_array_equal(img_p0, img_min) + # check for 16bit + img_p0 = rank.percentile(img16, selem=selem, p0=0) + img_min = rank.minimum(img16, selem=selem) + assert_array_equal(img_p0, img_min) + + +def test_percentile_max(): + # check that percentile p0 = 1 is identical to local max + img = data.camera() + img16 = img.astype(np.uint16) + selem = disk(15) + # check for 8bit + img_p0 = rank.percentile(img, selem=selem, p0=1.) + img_max = rank.maximum(img, selem=selem) + assert_array_equal(img_p0, img_max) + # check for 16bit + img_p0 = rank.percentile(img16, selem=selem, p0=1.) + img_max = rank.maximum(img16, selem=selem) + assert_array_equal(img_p0, img_max) + + +def test_percentile_median(): + # check that percentile p0 = 0.5 is identical to local median + img = data.camera() + img16 = img.astype(np.uint16) + selem = disk(15) + # check for 8bit + img_p0 = rank.percentile(img, selem=selem, p0=.5) + img_max = rank.median(img, selem=selem) + assert_array_equal(img_p0, img_max) + # check for 16bit + img_p0 = rank.percentile(img16, selem=selem, p0=.5) + img_max = rank.median(img16, selem=selem) + assert_array_equal(img_p0, img_max) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index b1d070fc..33ad97df 100644 --- a/skimage/filter/setup.py +++ b/skimage/filter/setup.py @@ -14,50 +14,29 @@ def configuration(parent_package='', top_path=None): cython(['_ctmf.pyx'], working_path=base_path) cython(['_denoise_cy.pyx'], working_path=base_path) - cython(['rank/_core8.pyx'], working_path=base_path) - cython(['rank/_core16.pyx'], working_path=base_path) - cython(['rank/_crank8.pyx'], working_path=base_path) - cython(['rank/_crank8_percentiles.pyx'], working_path=base_path) - cython(['rank/_crank16.pyx'], working_path=base_path) - cython(['rank/_crank16_percentiles.pyx'], working_path=base_path) - cython(['rank/_crank16_bilateral.pyx'], working_path=base_path) - cython(['rank/rank.pyx'], working_path=base_path) - cython(['rank/percentile_rank.pyx'], working_path=base_path) - cython(['rank/bilateral_rank.pyx'], working_path=base_path) + cython(['rank/core_cy.pyx'], working_path=base_path) + cython(['rank/generic_cy.pyx'], working_path=base_path) + cython(['rank/percentile_cy.pyx'], working_path=base_path) + cython(['rank/bilateral_cy.pyx'], working_path=base_path) config.add_extension('_ctmf', sources=['_ctmf.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_denoise_cy', sources=['_denoise_cy.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) - config.add_extension('rank._core8', sources=['rank/_core8.c'], + config.add_extension('rank.core_cy', sources=['rank/core_cy.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('rank._core16', sources=['rank/_core16.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension('rank._crank8', sources=['rank/_crank8.c'], + config.add_extension('rank.generic_cy', sources=['rank/generic_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension( - 'rank._crank8_percentiles', sources=['rank/_crank8_percentiles.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension('rank._crank16', sources=['rank/_crank16.c'], + 'rank.percentile_cy', sources=['rank/percentile_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension( - 'rank._crank16_percentiles', sources=['rank/_crank16_percentiles.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension( - 'rank._crank16_bilateral', sources=['rank/_crank16_bilateral.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension( - 'rank.rank', sources=['rank/rank.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension( - 'rank.percentile_rank', sources=['rank/percentile_rank.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension( - 'rank.bilateral_rank', sources=['rank/bilateral_rank.c'], + 'rank.bilateral_cy', sources=['rank/bilateral_cy.c'], include_dirs=[get_numpy_include_dirs()]) return config + if __name__ == '__main__': from numpy.distutils.core import setup setup(maintainer='scikit-image Developers', diff --git a/skimage/filter/tests/test_gaussian.py b/skimage/filter/tests/test_gaussian.py new file mode 100644 index 00000000..9118bfae --- /dev/null +++ b/skimage/filter/tests/test_gaussian.py @@ -0,0 +1,42 @@ +import numpy as np +from skimage.filter._gaussian import gaussian_filter + + +def test_null_sigma(): + a = np.zeros((3, 3)) + a[1, 1] = 1. + assert np.all(gaussian_filter(a, 0) == a) + + +def test_energy_decrease(): + a = np.zeros((3, 3)) + a[1, 1] = 1. + gaussian_a = gaussian_filter(a, sigma=1, mode='reflect') + assert gaussian_a.std() < a.std() + + +def test_multichannel(): + a = np.zeros((5, 5, 3)) + a[1, 1] = np.arange(1, 4) + gaussian_rgb_a = gaussian_filter(a, sigma=1, mode='reflect', + multichannel=True) + # Check that the mean value is conserved in each channel + # (color channels are not mixed together) + assert np.allclose([a[..., i].mean() for i in range(3)], + [gaussian_rgb_a[..., i].mean() for i in range(3)]) + # Test multichannel = None + gaussian_rgb_a = gaussian_filter(a, sigma=1, mode='reflect') + # Check that the mean value is conserved in each channel + # (color channels are not mixed together) + assert np.allclose([a[..., i].mean() for i in range(3)], + [gaussian_rgb_a[..., i].mean() for i in range(3)]) + # Iterable sigma + gaussian_rgb_a = gaussian_filter(a, sigma=[1, 2], mode='reflect', + multichannel=True) + assert np.allclose([a[..., i].mean() for i in range(3)], + [gaussian_rgb_a[..., i].mean() for i in range(3)]) + + +if __name__ == "__main__": + from numpy import testing + testing.run_module_suite() diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 97d3d9e3..0edfe4e7 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -3,7 +3,9 @@ from numpy.testing import assert_array_equal import skimage from skimage import data -from skimage.filter.thresholding import threshold_otsu, threshold_adaptive +from skimage.filter.thresholding import (threshold_adaptive, + threshold_otsu, + threshold_yen) class TestSimpleImage(): @@ -25,6 +27,26 @@ class TestSimpleImage(): image = np.float64(self.image) assert 2 <= threshold_otsu(image) < 3 + def test_yen(self): + assert threshold_yen(self.image) == 2 + + def test_yen_negative_int(self): + image = self.image - 2 + assert threshold_yen(image) == 0 + + def test_yen_float_image(self): + image = np.float64(self.image) + assert 2 <= threshold_yen(image) < 3 + + def test_yen_arange(self): + image = np.arange(256) + assert threshold_yen(image) == 127 + + def test_yen_binary(self): + image = np.zeros([2,256], dtype='uint8') + image[0] = 255 + assert threshold_yen(image) < 1 + def test_threshold_adaptive_generic(self): def func(arr): return arr.sum() / arr.shape[0] @@ -92,5 +114,15 @@ def test_otsu_lena_image(): assert 140 < threshold_otsu(lena) < 142 +def test_yen_coins_image(): + coins = skimage.img_as_ubyte(data.coins()) + assert 109 < threshold_yen(coins) < 111 + + +def test_yen_coins_image_as_float(): + coins = skimage.img_as_float(data.coins()) + assert 0.43 < threshold_yen(coins) < 0.44 + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 40e62d12..7f980387 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -1,4 +1,4 @@ -__all__ = ['threshold_otsu', 'threshold_adaptive'] +__all__ = ['threshold_adaptive', 'threshold_otsu', 'threshold_yen'] import numpy as np import scipy.ndimage @@ -65,7 +65,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, thresh_image = np.zeros(image.shape, 'double') if method == 'generic': scipy.ndimage.generic_filter(image, param, block_size, - output=thresh_image, mode=mode) + output=thresh_image, mode=mode) elif method == 'gaussian': if param is None: # automatically determine sigma which covers > 99% of distribution @@ -73,17 +73,17 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, else: sigma = param scipy.ndimage.gaussian_filter(image, sigma, output=thresh_image, - mode=mode) + mode=mode) elif method == 'mean': mask = 1. / block_size * np.ones((block_size,)) # separation of filters to speedup convolution scipy.ndimage.convolve1d(image, mask, axis=0, output=thresh_image, - mode=mode) + mode=mode) scipy.ndimage.convolve1d(thresh_image, mask, axis=1, - output=thresh_image, mode=mode) + output=thresh_image, mode=mode) elif method == 'median': scipy.ndimage.median_filter(image, block_size, output=thresh_image, - mode=mode) + mode=mode) return image > (thresh_image - offset) @@ -95,14 +95,15 @@ def threshold_otsu(image, nbins=256): ---------- image : array Input image. - nbins : int + nbins : int, optional Number of bins used to calculate histogram. This value is ignored for integer arrays. Returns ------- threshold : float - Threshold value. + Upper threshold value. All pixels intensities that less or equal of + this value assumed as foreground. References ---------- @@ -113,7 +114,7 @@ def threshold_otsu(image, nbins=256): >>> from skimage.data import camera >>> image = camera() >>> thresh = threshold_otsu(image) - >>> binary = image > thresh + >>> binary = image <= thresh """ hist, bin_centers = histogram(image, nbins) hist = hist.astype(float) @@ -133,3 +134,53 @@ def threshold_otsu(image, nbins=256): idx = np.argmax(variance12) threshold = bin_centers[:-1][idx] return threshold + + +def threshold_yen(image, nbins=256): + """Return threshold value based on Yen's method. + + Parameters + ---------- + image : array + Input image. + nbins : int, optional + Number of bins used to calculate histogram. This value is ignored for + integer arrays. + + Returns + ------- + threshold : float + Upper threshold value. All pixels intensities that less or equal of + this value assumed as foreground. + + References + ---------- + .. [1] Yen J.C., Chang F.J., and Chang S. (1995) "A New Criterion + for Automatic Multilevel Thresholding" IEEE Trans. on Image + Processing, 4(3): 370-378 + .. [2] Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding + Techniques and Quantitative Performance Evaluation" Journal of + Electronic Imaging, 13(1): 146-165, + http://www.busim.ee.boun.edu.tr/~sankur/SankurFolder/Threshold_survey.pdf + .. [3] ImageJ AutoThresholder code, http://fiji.sc/wiki/index.php/Auto_Threshold + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera() + >>> thresh = threshold_yen(image) + >>> binary = image <= thresh + """ + hist, bin_centers = histogram(image, nbins) + norm_histo = hist.astype(float) / hist.sum() # Probability mass function + P1 = np.cumsum(norm_histo) # Cumulative normalized histogram + P1_sq = np.cumsum(norm_histo ** 2) + # Get cumsum calculated from end of squared array: + P2_sq = np.cumsum(norm_histo[::-1] ** 2)[::-1] + # P2_sq indexes is shifted +1. I assume, with P1[:-1] it's help avoid '-inf' + # in crit. ImageJ Yen implementation replaces those values by zero. + crit = np.log(((P1_sq[:-1] * P2_sq[1:]) ** -1) * \ + (P1[:-1] * (1.0 - P1[:-1])) ** 2) + max_crit = np.argmax(crit) + threshold = bin_centers[:-1][max_crit] + return threshold diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index a1021380..560f19d0 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -152,4 +152,4 @@ def _test_random(shape): if __name__ == "__main__": - run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/graph/tests/test_spath.py b/skimage/graph/tests/test_spath.py index 62f9f303..d018449a 100644 --- a/skimage/graph/tests/test_spath.py +++ b/skimage/graph/tests/test_spath.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import * +from numpy.testing import assert_equal, assert_array_equal import skimage.graph.spath as spath @@ -33,4 +33,4 @@ def test_non_square(): if __name__ == "__main__": - run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 7b21b2b8..a7df4694 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -15,6 +15,7 @@ import numpy as np from skimage.io._plugins import call as call_plugin from skimage.color import rgb2grey +from skimage._shared import six # Shared image queue @@ -25,7 +26,7 @@ URL_REGEX = re.compile(r'http://|https://|ftp://|file://|file:\\') def is_url(filename): """Return True if string is an http or ftp path.""" - return (isinstance(filename, basestring) and + return (isinstance(filename, six.string_types) and URL_REGEX.match(filename) is not None) @@ -34,28 +35,33 @@ class Image(np.ndarray): These objects have tags for image metadata and IPython display protocol methods for image display. - """ - tags = {'filename': '', - 'EXIF': {}, - 'info': {}} + Parameters + ---------- + arr : ndarray + Image data. + kwargs : Image tags as keywords + Specified in the form ``tag0=value``, ``tag1=value``. + + Attributes + ---------- + tags : dict + Meta-data. + + """ def __new__(cls, arr, **kwargs): """Set the image data and tags according to given parameters. - Parameters - ---------- - arr : ndarray - Image data. - kwargs : Image tags as keywords - Specified in the form ``tag0=value``, ``tag1=value``. - """ x = np.asarray(arr).view(cls) - for tag, value in Image.tags.items(): - setattr(x, tag, kwargs.get(tag, getattr(arr, tag, value))) + x.tags = kwargs + return x + def __array_finalize__(self, obj): + self.tags = getattr(obj, 'tags', {}) + def _repr_png_(self): return self._repr_image_format('png') @@ -146,7 +152,7 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, if as_grey and getattr(img, 'ndim', 0) >= 3: img = rgb2grey(img) - return Image(img) + return img def imread_collection(load_pattern, conserve_memory=True, @@ -220,7 +226,7 @@ def imshow(arr, plugin=None, **plugin_args): Passed to the given plugin. """ - if isinstance(arr, basestring): + if isinstance(arr, six.string_types): arr = call_plugin('imread', arr, plugin=plugin) return call_plugin('imshow', arr, plugin=plugin, **plugin_args) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 2247f3a1..2ddbfe26 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -11,6 +11,8 @@ except ImportError: from skimage.util import img_as_ubyte +from skimage._shared import six + def imread(fname, dtype=None): """Load an image from file. @@ -71,9 +73,8 @@ def imsave(fname, arr, format_str=None): values in [0, 255], whereas floating-point arrays must be in [0, 1]. format_str: str - Format to save as, this is required if using a file-like object; - this is optional if fname is a string and the format can be - derived from the extension. + Format to save as, this is defaulted to PNG if using a file-like + object; this will be derived from the extension if fname is a string Notes ----- @@ -104,6 +105,10 @@ def imsave(fname, arr, format_str=None): # Force all integers to bytes arr = arr.astype(np.uint8) + # default to PNG if file-like object + if not isinstance(fname, six.string_types) and format_str is None: + format_str = "PNG" + img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) img.save(fname, format=format_str) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 7c3f739b..96855dbd 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -10,6 +10,7 @@ from copy import copy import numpy as np from ._io import imread +from .._shared import six def concatenate_images(ic): @@ -296,7 +297,7 @@ class ImageCollection(object): """ def __init__(self, load_pattern, conserve_memory=True, load_func=None): """Load and manage a collection of images.""" - if isinstance(load_pattern, basestring): + if isinstance(load_pattern, six.string_types): load_pattern = load_pattern.split(':') self._files = [] for pattern in load_pattern: diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index f650daeb..f56d753b 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -12,6 +12,7 @@ from skimage import data_dir from skimage.io import ImageCollection, MultiImage from skimage.io.collection import alphanumeric_key from skimage.io import Image as ioImage +from skimage._shared import six try: @@ -21,8 +22,6 @@ except ImportError: else: PIL_available = True -if sys.version_info[0] > 2: - basestring = str class TestAlphanumericKey(): def setUp(self): @@ -58,7 +57,7 @@ class TestImageCollection(): def test_getitem(self): num = len(self.collection) for i in range(-num, num): - assert type(self.collection[i]) is ioImage + assert type(self.collection[i]) is np.ndarray assert_array_almost_equal(self.collection[0], self.collection[-num]) @@ -129,7 +128,7 @@ class TestMultiImage(): @skipif(not PIL_available) def test_files_property(self): - assert isinstance(self.img.filename, basestring) + assert isinstance(self.img.filename, six.string_types) def set_filename(f): self.img.filename = f diff --git a/skimage/io/tests/test_image.py b/skimage/io/tests/test_image.py new file mode 100644 index 00000000..6c54695b --- /dev/null +++ b/skimage/io/tests/test_image.py @@ -0,0 +1,17 @@ +from skimage.io import Image + +from numpy.testing import assert_equal, assert_array_equal + +def test_tags(): + f = Image([1, 2, 3], foo='bar', sigma='delta') + g = Image([3, 2, 1], sun='moon') + h = Image([1, 1, 1]) + + assert_equal(f.tags['foo'], 'bar') + assert_array_equal((g + 2).tags['sun'], 'moon') + assert_equal(h.tags, {}) + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() + diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index 484784ee..8049860a 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -1,6 +1,6 @@ import os -from numpy.testing import * +from numpy.testing import assert_array_equal, raises, run_module_suite import numpy as np import skimage.io as io diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 3e2f3dde..aa582ebc 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -6,7 +6,10 @@ from numpy.testing.decorators import skipif from tempfile import NamedTemporaryFile from skimage import data_dir -from skimage.io import imread, imsave, use_plugin, reset_plugins +from skimage.io import (imread, imsave, use_plugin, reset_plugins, + Image as ioImage) +from skimage._shared.six import BytesIO + try: from PIL import Image @@ -81,7 +84,7 @@ def test_imread_uint16(): @skipif(not PIL_available) def test_repr_png(): img_path = os.path.join(data_dir, 'camera.png') - original_img = imread(img_path) + original_img = ioImage(imread(img_path)) original_img_str = original_img._repr_png_() with NamedTemporaryFile(suffix='.png') as temp_png: @@ -124,5 +127,22 @@ class TestSave: x = (x * 255).astype(dtype) yield self.roundtrip, dtype, x + +@skipif(not PIL_available) +def test_imsave_filelike(): + shape = (2, 2) + image = np.zeros(shape) + s = BytesIO() + + # save to file-like object + imsave(s, image) + + # read from file-like object + s.seek(0) + out = imread(s) + assert out.shape == shape + assert_allclose(out, image) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/video.py b/skimage/io/video.py index 6cb1a8e9..003a1f53 100644 --- a/skimage/io/video.py +++ b/skimage/io/video.py @@ -256,7 +256,7 @@ class Video(object): Backend to use. """ def __init__(self, source=None, size=None, sync=False, backend=None): - if backend == None: + if backend is None: # select backend that is available if gstreamer_available: self.video = GstVideo(source, size, sync) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 89f707c1..108cd7d9 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -1,8 +1,11 @@ from .find_contours import find_contours +from ._marching_cubes import marching_cubes, mesh_surface_area from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon +from ._moments import moments, moments_central, moments_normalized, moments_hu from .fit import LineModel, CircleModel, EllipseModel, ransac +from .block import block_reduce __all__ = ['find_contours', @@ -14,4 +17,12 @@ __all__ = ['find_contours', 'LineModel', 'CircleModel', 'EllipseModel', - 'ransac'] + 'ransac', + 'block_reduce', + 'moments', + 'moments_central', + 'moments_normalized', + 'moments_hu', + 'sum_blocks', + 'marching_cubes', + 'mesh_surface_area'] diff --git a/skimage/measure/_find_contours.pyx b/skimage/measure/_find_contours.pyx index d05d9aa7..e2f5a49b 100644 --- a/skimage/measure/_find_contours.pyx +++ b/skimage/measure/_find_contours.pyx @@ -4,8 +4,6 @@ #cython: wraparound=False import numpy as np -cimport numpy as cnp - cdef inline double _get_fraction(double from_value, double to_value, double level): @@ -14,7 +12,7 @@ cdef inline double _get_fraction(double from_value, double to_value, return ((level - from_value) / (to_value - from_value)) -def iterate_and_store(cnp.ndarray[double, ndim=2] array, +def iterate_and_store(double[:, :] array, double level, Py_ssize_t vertex_connect_high): """Iterate across the given array in a marching-squares fashion, looking for segments that cross 'level'. If such a segment is @@ -46,7 +44,7 @@ def iterate_and_store(cnp.ndarray[double, ndim=2] array, # Calculate the number of iterations we'll need cdef Py_ssize_t num_square_steps = (array.shape[0] - 1) \ - * (array.shape[1] - 1) + * (array.shape[1] - 1) cdef unsigned char square_case = 0 cdef tuple top, bottom, left, right diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py new file mode 100644 index 00000000..41bafd90 --- /dev/null +++ b/skimage/measure/_marching_cubes.py @@ -0,0 +1,157 @@ +import numpy as np +from . import _marching_cubes_cy + + +def marching_cubes(volume, level, spacing=(1., 1., 1.)): + """ + Marching cubes algorithm to find iso-valued surfaces in 3d volumetric data + + Parameters + ---------- + volume : (M, N, P) array of doubles + Input data volume to find isosurfaces. Will be cast to `np.float64`. + level : float + Contour value to search for isosurfaces in `volume`. + spacing : length-3 tuple of floats + Voxel spacing in spatial dimensions corresponding to numpy array + indexing dimensions (M, N, P) as in `volume`. + + Returns + ------- + verts : (V, 3) array + Spatial coordinates for V unique mesh vertices. Coordinate order + matches input `volume` (M, N, P). + faces : (F, 3) array + Define triangular faces via referencing vertex indices from ``verts``. + This algorithm specifically outputs triangles, so each face has + exactly three indices. + + Notes + ----- + The marching cubes algorithm is implemented as described in [1]_. + A simple explanation is available here:: + + http://www.essi.fr/~lingrand/MarchingCubes/algo.html + + There are several known ambiguous cases in the marching cubes algorithm. + Using point labeling as in [1]_, Figure 4, as shown: + + v8 ------ v7 + / | / | y + / | / | ^ z + v4 ------ v3 | | / + | v5 ----|- v6 |/ (note: NOT right handed!) + | / | / ----> x + | / | / + v1 ------ v2 + + Most notably, if v4, v8, v2, and v6 are all >= `level` (or any + generalization of this case) two parallel planes are generated by this + algorithm, separating v4 and v8 from v2 and v6. An equally valid + interpretation would be a single connected thin surface enclosing all + four points. This is the best known ambiguity, though there are others. + + This algorithm does not attempt to resolve such ambiguities; it is a naive + implementation of marching cubes as in [1]_, but may be a good beginning + for work with more recent techniques (Dual Marching Cubes, Extended + Marching Cubes, Cubic Marching Squares, etc.). + + Because of interactions between neighboring cubes, the isosurface(s) + generated by this algorithm are NOT guaranteed to be closed, particularly + for complicated contours. Furthermore, this algorithm does not guarantee + a single contour will be returned. Indeed, ALL isosurfaces which cross + `level` will be found, regardless of connectivity. + + The output is a triangular mesh consisting of a set of unique vertices and + connecting triangles. The order of these vertices and triangles in the + output list is determined by the position of the smallest ``x,y,z`` (in + lexicographical order) coordinate in the contour. This is a side-effect + of how the input array is traversed, but can be relied upon. + + To quantify the area of an isosurface generated by this algorithm, pass + the outputs directly into `skimage.measure.mesh_surface_area`. + + Regarding visualization of algorithm output, the ``mayavi`` package + is recommended. To contour a volume named `myvolume` about the level 0.0: + + >>> from mayavi import mlab + >>> verts, tris = marching_cubes(myvolume, 0.0, (1., 1., 2.)) + >>> mlab.triangular_mesh([vert[0] for vert in verts], + ... [vert[1] for vert in verts], + ... [vert[2] for vert in verts], + ... tris) + >>> mlab.show() + + References + ---------- + .. [1] Lorensen, William and Harvey E. Cline. Marching Cubes: A High + Resolution 3D Surface Construction Algorithm. Computer Graphics + (SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170). + + See Also + -------- + skimage.measure.mesh_surface_area + + """ + # Check inputs and ensure `volume` is C-contiguous for memoryviews + if volume.ndim != 3: + raise ValueError("Input volume must have 3 dimensions.") + if level < volume.min() or level > volume.max(): + raise ValueError("Contour level must be within volume data range.") + volume = np.array(volume, dtype=np.float64, order="C") + + # Extract raw triangles using marching cubes in Cython + # Returns a list of length-3 lists, each sub-list containing three + # tuples. The tuples hold (x, y, z) coordinates for triangle vertices. + # Note: this algorithm is fast, but returns degenerate "triangles" which + # have repeated vertices - and equivalent vertices are redundantly + # placed in every triangle they connect with. + raw_tris = _marching_cubes_cy.iterate_and_store_3d(volume, float(level), + spacing) + + # Find and collect unique vertices, storing triangle verts as indices. + # Returns a true mesh with no degenerate faces. + verts, faces = _marching_cubes_cy.unpack_unique_verts(raw_tris) + + return np.asarray(verts), np.asarray(faces) + + +def mesh_surface_area(verts, tris): + """ + Compute surface area, given vertices & triangular faces + + Parameters + ---------- + verts : (V, 3) array of floats + Array containing (x, y, z) coordinates for V unique mesh vertices. + faces : (F, 3) array of ints + List of length-3 lists of integers, referencing vertex coordinates as + provided in `verts` + + Returns + ------- + area : float + Surface area of mesh. Units now [coordinate units] ** 2. + + Notes + ----- + The arguments expected by this function are the exact outputs from + `skimage.measure.marching_cubes`. For unit correct output, ensure correct + `spacing` was passed to `skimage.measure.marching_cubes`. + + This algorithm works properly only if the ``faces`` provided are all + triangles. + + See Also + -------- + skimage.measure.marching_cubes + + """ + # Fancy indexing to define two vector arrays from triangle vertices + actual_verts = verts[tris] + a = actual_verts[:, 0, :] - actual_verts[:, 1, :] + b = actual_verts[:, 0, :] - actual_verts[:, 2, :] + del actual_verts + + # Area of triangle in 3D = 1/2 * Euclidean norm of cross product + return ((np.cross(a, b) ** 2).sum(axis=1) ** 0.5).sum() / 2. diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx new file mode 100644 index 00000000..085108ab --- /dev/null +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -0,0 +1,987 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +cimport numpy as cnp + + +cdef inline double _get_fraction(double from_value, double to_value, + double level): + if (to_value == from_value): + return 0 + return ((level - from_value) / (to_value - from_value)) + + +def unpack_unique_verts(list trilist): + """ + Convert a list of lists of tuples corresponding to triangle vertices + into a unique vertex list, and a list of triangle faces w/indices + corresponding to entries of the vertex list. + + """ + cdef Py_ssize_t idx = 0 + cdef Py_ssize_t n_tris = len(trilist) + cdef Py_ssize_t i, j + cdef dict vert_index = {} + cdef list vert_list = [] + cdef list face_list = [] + cdef list templist + + # Iterate over triangles + for i in range(n_tris): + templist = [] + + # Only parse vertices from non-degenerate triangles + if not ((trilist[i][0] == trilist[i][1]) or + (trilist[i][0] == trilist[i][2]) or + (trilist[i][1] == trilist[i][2])): + + # Iterate over vertices within each triangle + for j in range(3): + vert = trilist[i][j] + + # Check if a new unique vertex found + if vert not in vert_index: + vert_index[vert] = idx + templist.append(idx) + vert_list.append(vert) + idx += 1 + else: + templist.append(vert_index[vert]) + + face_list.append(templist) + + return vert_list, face_list + + +def iterate_and_store_3d(double[:, :, ::1] arr, double level, + tuple spacing=(1., 1., 1.)): + """Iterate across the given array in a marching-cubes fashion, + looking for volumes with edges that cross 'level'. If such a volume is + found, appropriate triangulations are added to a growing list of + faces to be returned by this function. + + If `spacing` is not provided, vertices are returned in the indexing + coordinate system (assuming all 3 spatial dimensions sampled equally). + If `spacing` is provided, vertices will be returned in volume coordinates + relative to the origin, regularly spaced as specified in each dimension. + + """ + if arr.shape[0] < 2 or arr.shape[1] < 2 or arr.shape[2] < 2: + raise ValueError("Input array must be at least 2x2x2.") + if len(spacing) != 3: + raise ValueError("`spacing` must be (double, double, double)") + + cdef list face_list = [] + cdef list norm_list = [] + cdef Py_ssize_t n + cdef bint odd_spacing, plus_z + plus_z = False + if [float(i) for i in spacing] == [1.0, 1.0, 1.0]: + odd_spacing = False + else: + odd_spacing = True + + # The plan is to iterate a 2x2x2 cube across the input array. This means + # the upper-left corner of the cube needs to iterate across a sub-array + # of size one-less-large in each direction (so we can get away with no + # bounds checking in Cython). The cube is represented by eight vertices: + # v1, v2, ..., v8, oriented thus (see Lorensen, Figure 4): + # + # v8 ------ v7 + # / | / | y + # / | / | ^ z + # v4 ------ v3 | | / + # | v5 ----|- v6 |/ (note: NOT right handed!) + # | / | / ----> x + # | / | / + # v1 ------ v2 + # + # We also maintain the current 2D coordinates for v1, and ensure the array + # is of type 'double' and is C-contiguous (last index varies fastest). + + # Coords start at (0, 0, 0). + cdef Py_ssize_t[3] coords + coords[0] = 0 + coords[1] = 0 + coords[2] = 0 + + # Extract doubles from `spacing` for speed + cdef double[3] spacing2 + spacing2[0] = spacing[0] + spacing2[1] = spacing[1] + spacing2[2] = spacing[2] + + # Calculate the number of iterations we'll need + cdef Py_ssize_t num_cube_steps = ((arr.shape[0] - 1) * + (arr.shape[1] - 1) * + (arr.shape[2] - 1)) + + cdef unsigned char cube_case = 0 + cdef tuple e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12 + cdef double v1, v2, v3, v4, v5, v6, v7, v8, r0, r1, c0, c1, d0, d1 + cdef Py_ssize_t x0, y0, z0, x1, y1, z1 + e5, e6, e7, e8 = (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0) + + for n in range(num_cube_steps): + # There are 255 unique values for `cube_case`. This algorithm follows + # the Lorensen paper in vertex and edge labeling, however, it should + # be noted that Lorensen used a left-handed coordinate system while + # NumPy uses a proper right handed system. Transforming between these + # coordinate systems was handled in the definitions of the cube + # vertices v1, v2, ..., v8. + # + # Refer to the paper, figure 4, for cube edge designations e1, ... e12 + + # Standard Py_ssize_t coordinates for indexing + x0, y0, z0 = coords[0], coords[1], coords[2] + x1, y1, z1 = x0 + 1, y0 + 1, z0 + 1 + + if odd_spacing: + # These doubles are the modified world coordinates; they are only + # calculated if non-default `spacing` provided. + r0 = coords[0] * spacing2[0] + c0 = coords[1] * spacing2[1] + d0 = coords[2] * spacing2[2] + r1 = r0 + spacing2[0] + c1 = c0 + spacing2[1] + d1 = d0 + spacing2[2] + else: + r0, c0, d0, r1, c1, d1 = x0, y0, z0, x1, y1, z1 + + # We use a right-handed coordinate system, UNlike the paper, but want + # to index in agreement - the coordinate adjustment takes place here. + v1 = arr[x0, y0, z0] + v2 = arr[x1, y0, z0] + v3 = arr[x1, y1, z0] + v4 = arr[x0, y1, z0] + v5 = arr[x0, y0, z1] + v6 = arr[x1, y0, z1] + v7 = arr[x1, y1, z1] + v8 = arr[x0, y1, z1] + + # Unique triangulation cases + cube_case = 0 + if (v1 > level): cube_case += 1 + if (v2 > level): cube_case += 2 + if (v3 > level): cube_case += 4 + if (v4 > level): cube_case += 8 + if (v5 > level): cube_case += 16 + if (v6 > level): cube_case += 32 + if (v7 > level): cube_case += 64 + if (v8 > level): cube_case += 128 + + if (cube_case != 0 and cube_case != 255): + # Only do anything if there's a plane intersecting the cube. + # Cases 0 and 255 are entirely below/above the contour. + + if cube_case > 127: + if ((cube_case != 150) and + (cube_case != 170) and + (cube_case != 195)): + cube_case = 255 - cube_case + + # Calculate cube edges, to become triangulation vertices. + # If we moved in a convenient direction, save 1/3 of the effort by + # re-assigning prior results. + if plus_z: + # Reassign prior calculated edges + e1 = e5 + e2 = e6 + e3 = e7 + e4 = e8 + else: + # Calculate edges normally + if odd_spacing: + e1 = r0 + _get_fraction(v1, v2, level) * spacing2[0], c0, d0 + e2 = r1, c0 + _get_fraction(v2, v3, level) * spacing2[1], d0 + e3 = r0 + _get_fraction(v4, v3, level) * spacing2[0], c1, d0 + e4 = r0, c0 + _get_fraction(v1, v4, level) * spacing2[1], d0 + else: + e1 = r0 + _get_fraction(v1, v2, level), c0, d0 + e2 = r1, c0 + _get_fraction(v2, v3, level), d0 + e3 = r0 + _get_fraction(v4, v3, level), c1, d0 + e4 = r0, c0 + _get_fraction(v1, v4, level), d0 + + # These must be calculated at each point unless we implemented a + # large, growing lookup table for all adjacent values; could save + # ~30% in terms of runtime at the expense of memory usage and + # much greater complexity. + if odd_spacing: + e5 = r0 + _get_fraction(v5, v6, level) * spacing2[0], c0, d1 + e6 = r1, c0 + _get_fraction(v6, v7, level) * spacing2[1], d1 + e7 = r0 + _get_fraction(v8, v7, level) * spacing2[0], c1, d1 + e8 = r0, c0 + _get_fraction(v5, v8, level) * spacing2[1], d1 + e9 = r0, c0, d0 + _get_fraction(v1, v5, level) * spacing2[2] + e10 = r1, c0, d0 + _get_fraction(v2, v6, level) * spacing2[2] + e11 = r0, c1, d0 + _get_fraction(v4, v8, level) * spacing2[2] + e12 = r1, c1, d0 + _get_fraction(v3, v7, level) * spacing2[2] + else: + e5 = r0 + _get_fraction(v5, v6, level), c0, d1 + e6 = r1, c0 + _get_fraction(v6, v7, level), d1 + e7 = r0 + _get_fraction(v8, v7, level), c1, d1 + e8 = r0, c0 + _get_fraction(v5, v8, level), d1 + e9 = r0, c0, d0 + _get_fraction(v1, v5, level) + e10 = r1, c0, d0 + _get_fraction(v2, v6, level) + e11 = r0, c1, d0 + _get_fraction(v4, v8, level) + e12 = r1, c1, d0 + _get_fraction(v3, v7, level) + + + # Append appropriate triangles to the growing output `face_list` + _append_tris(face_list, cube_case, e1, e2, e3, e4, e5, + e6, e7, e8, e9, e10, e11, e12) + + # Advance the coords indices + if coords[2] < arr.shape[2] - 2: + coords[2] += 1 + plus_z = True + elif coords[1] < arr.shape[1] - 2: + coords[1] += 1 + coords[2] = 0 + plus_z = False + else: + coords[0] += 1 + coords[1] = 0 + coords[2] = 0 + plus_z = False + + return face_list + + +def _append_tris(list face_list, unsigned char case, tuple e1, tuple e2, + tuple e3, tuple e4, tuple e5, tuple e6, tuple e7, tuple e8, + tuple e9, tuple e10, tuple e11, tuple e12): + # Permits recursive use for duplicated planes to conserve code - it's + # quite long enough as-is. + + if (case == 1): + # front lower left corner + face_list.append([e1, e4, e9]) + elif (case == 2): + # front lower right corner + face_list.append([e10, e2, e1]) + elif (case == 3): + # front lower plane + face_list.append([e2, e4, e9]) + face_list.append([e2, e9, e10]) + elif (case == 4): + # front upper right corner + face_list.append([e12, e3, e2]) + elif (case == 5): + # lower left, upper right corners + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 6): + # front right plane + face_list.append([e12, e3, e1]) + face_list.append([e12, e1, e10]) + elif (case == 7): + # Shelf including v1, v2, v3 + face_list.append([e3, e4, e12]) + face_list.append([e4, e9, e12]) + face_list.append([e12, e9, e10]) + elif (case == 8): + # front upper left corner + face_list.append([e3, e11, e4]) + elif (case == 9): + # front left plane + face_list.append([e3, e11, e9]) + face_list.append([e3, e9, e1]) + elif (case == 10): + # upper left, lower right corners + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 11): + # Shelf including v4, v1, v2 + face_list.append([e3, e11, e2]) + face_list.append([e11, e10, e2]) + face_list.append([e11, e9, e10]) + elif (case == 12): + # front upper plane + face_list.append([e11, e4, e12]) + face_list.append([e2, e4, e12]) + elif (case == 13): + # Shelf including v1, v4, v3 + face_list.append([e11, e9, e12]) + face_list.append([e12, e9, e1]) + face_list.append([e12, e1, e2]) + elif (case == 14): + # Shelf including v2, v3, v4 + face_list.append([e11, e10, e12]) + face_list.append([e11, e4, e10]) + face_list.append([e4, e1, e10]) + elif (case == 15): + # Plane parallel to x-axis through middle + face_list.append([e11, e9, e12]) + face_list.append([e12, e9, e10]) + elif (case == 16): + # back lower left corner + face_list.append([e8, e9, e5]) + elif (case == 17): + # lower left plane + face_list.append([e4, e1, e8]) + face_list.append([e8, e1, e5]) + elif (case == 18): + # lower left back, lower right front corners + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 19): + # Shelf including v1, v2, v5 + face_list.append([e8, e4, e2]) + face_list.append([e8, e2, e10]) + face_list.append([e8, e10, e5]) + elif (case == 20): + # lower left back, upper right front corners + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 21): + # lower left plane + upper right front corner, v1, v3, v5 + _append_tris(face_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 22): + # front right plane + lower left back corner, v2, v3, v5 + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 23): + # Rotated case 14 in the paper + face_list.append([e3, e10, e8]) + face_list.append([e3, e10, e12]) + face_list.append([e8, e10, e5]) + face_list.append([e3, e4, e8]) + elif (case == 24): + # upper front left, lower back left corners + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 25): + # Shelf including v1, v4, v5 + face_list.append([e1, e5, e3]) + face_list.append([e3, e8, e11]) + face_list.append([e3, e5, e8]) + elif (case == 26): + # Three isolated corners + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 27): + # Full corner v1, case 9 in paper: (v1, v2, v4, v5) + face_list.append([e11, e3, e2]) + face_list.append([e11, e2, e10]) + face_list.append([e10, e11, e8]) + face_list.append([e8, e5, e10]) + elif (case == 28): + # upper front plane + corner v5 + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 29): + # special case of 11 in the paper: (v1, v3, v4, v5) + face_list.append([e11, e5, e2]) + face_list.append([e11, e12, e2]) + face_list.append([e11, e5, e8]) + face_list.append([e2, e1, e5]) + elif (case == 30): + # Shelf (v2, v3, v4) and lower left back corner + _append_tris(face_list, 14, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 31): + # Shelf: (v6, v7, v8) by inversion + face_list.append([e11, e12, e10]) + face_list.append([e11, e8, e10]) + face_list.append([e8, e10, e5]) + elif (case == 32): + # lower right back corner + face_list.append([e6, e5, e10]) + elif (case == 33): + # lower right back, lower left front corners + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 34): + # lower right plane + face_list.append([e1, e2, e5]) + face_list.append([e2, e6, e5]) + elif (case == 35): + # Shelf: v1, v2, v6 + face_list.append([e4, e2, e6]) + face_list.append([e4, e9, e6]) + face_list.append([e6, e9, e5]) + elif (case == 36): + # upper right front, lower right back corners + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 37): + # lower left front, upper right front, lower right back corners + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 38): + # Shelf: v2, v3, v6 + face_list.append([e3, e1, e5]) + face_list.append([e3, e5, e12]) + face_list.append([e12, e5, e6]) + elif (case == 39): + # Full corner v2: (v1, v2, v3, v6) + face_list.append([e3, e4, e5]) + face_list.append([e4, e9, e5]) + face_list.append([e3, e5, e6]) + face_list.append([e3, e12, e6]) + elif (case == 40): + # upper left front, lower right back corners + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 41): + # front left plane, lower right back corner + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 42): + # lower right plane, upper front left corner + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 43): + # Rotated case 11 in paper + face_list.append([e11, e3, e9]) + face_list.append([e3, e9, e6]) + face_list.append([e3, e2, e6]) + face_list.append([e9, e5, e6]) + elif (case == 44): + # upper front plane, lower right back corner + _append_tris(face_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 45): + # Shelf: (v1, v3, v4) + lower right back corner + _append_tris(face_list, 13, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 46): + # Rotated case 14 in paper + face_list.append([e4, e11, e12]) + face_list.append([e4, e12, e5]) + face_list.append([e12, e5, e6]) + face_list.append([e4, e5, e1]) + elif (case == 47): + # Shelf: (v5, v8, v7) by inversion + face_list.append([e11, e9, e12]) + face_list.append([e12, e9, e5]) + face_list.append([e12, e5, e6]) + elif (case == 48): + # Back lower plane + face_list.append([e9, e10, e6]) + face_list.append([e9, e6, e8]) + elif (case == 49): + # Shelf: (v1, v5, v6) + face_list.append([e4, e8, e6]) + face_list.append([e4, e6, e1]) + face_list.append([e6, e1, e10]) + elif (case == 50): + # Shelf: (v2, v5, v6) + face_list.append([e8, e6, e2]) + face_list.append([e8, e2, e1]) + face_list.append([e8, e9, e1]) + elif (case == 51): + # Plane through middle of cube, parallel to x-z axis + face_list.append([e4, e8, e2]) + face_list.append([e8, e2, e6]) + elif (case == 52): + # Back lower plane, and front upper right corner + _append_tris(face_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 53): + # Shelf (v1, v5, v6) and front upper right corner + _append_tris(face_list, 49, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 54): + # Rotated case 11 from paper (v2, v3, v5, v6) + face_list.append([e1, e9, e3]) + face_list.append([e9, e3, e6]) + face_list.append([e9, e8, e6]) + face_list.append([e12, e3, e6]) + elif (case == 55): + # Shelf: (v4, v8, v7) by inversion + face_list.append([e4, e8, e6]) + face_list.append([e4, e6, e3]) + face_list.append([e6, e3, e12]) + elif (case == 56): + # Back lower plane + upper left front corner + _append_tris(face_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 57): + # Rotated case 14 from paper (v4, v1, v5, v6) + face_list.append([e3, e11, e8]) + face_list.append([e3, e8, e10]) + face_list.append([e10, e6, e8]) + face_list.append([e3, e1, e10]) + elif (case == 58): + # Shelf: (v2, v6, v5) + upper left front corner + _append_tris(face_list, 50, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 59): + # Shelf: (v3, v7, v8) by inversion + face_list.append([e2, e6, e8]) + face_list.append([e8, e2, e3]) + face_list.append([e8, e3, e11]) + elif (case == 60): + # AMBIGUOUS CASE: parallel planes (front upper, back lower) + _append_tris(face_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 61): + # Upper back plane + lower right front corner by inversion + _append_tris(face_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 62): + # Upper back plane + lower left front corner by inversion + _append_tris(face_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 63): + # Upper back plane + face_list.append([e11, e12, e6]) + face_list.append([e11, e8, e6]) + elif (case == 64): + # Upper right back corner + face_list.append([e12, e7, e6]) + elif (case == 65): + # upper right back, lower left front corners + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 66): + # upper right back, lower right front corners + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 67): + # lower front plane + upper right back corner + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 68): + # upper right plane + face_list.append([e3, e2, e6]) + face_list.append([e3, e7, e6]) + elif (case == 69): + # Upper right plane, lower left front corner + _append_tris(face_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 70): + # Shelf: (v2, v3, v7) + face_list.append([e1, e3, e7]) + face_list.append([e1, e10, e7]) + face_list.append([e7, e10, e6]) + elif (case == 71): + # Rotated version of case 11 in paper (v1, v2, v3, v7) + face_list.append([e10, e7, e4]) + face_list.append([e4, e3, e7]) + face_list.append([e10, e4, e9]) + face_list.append([e7, e10, e6]) + elif (case == 72): + # upper left front, upper right back corners + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 73): + # front left plane, upper right back corner + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 74): + # Three isolated corners, exactly case 7 in paper + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 75): + # Shelf: (v1, v2, v4) + upper right back corner + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 11, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 76): + # Shelf: (v4, v3, v7) + face_list.append([e4, e2, e6]) + face_list.append([e4, e11, e7]) + face_list.append([e4, e7, e6]) + elif (case == 77): + # Rotated case 14 in paper (v1, v4, v3, v7) + face_list.append([e11, e9, e1]) + face_list.append([e11, e1, e6]) + face_list.append([e1, e6, e2]) + face_list.append([e11, e6, e7]) + elif (case == 78): + # Full corner v3: (v2, v3, v4, v7) + face_list.append([e1, e4, e7]) + face_list.append([e1, e7, e6]) + face_list.append([e4, e11, e7]) + face_list.append([e1, e10, e6]) + elif (case == 79): + # Shelf: (v6, v5, v8) by inversion + face_list.append([e9, e11, e10]) + face_list.append([e11, e7, e10]) + face_list.append([e7, e10, e6]) + elif (case == 80): + # lower left back, upper right back corners (v5, v7) + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 81): + # lower left plane, upper right back corner + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 82): + # isolated corners (v2, v5, v7) + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 83): + # Shelf: (v1, v2, v5) + upper right back corner + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 19, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 84): + # upper right plane, lower left back corner + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 85): + # AMBIGUOUS CASE: upper right and lower left parallel planes + _append_tris(face_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 86): + # Shelf: (v2, v3, v7) + lower left back corner + _append_tris(face_list, 70, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 87): + # Upper left plane + lower right back corner, by inversion + _append_tris(face_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 88): + # Isolated corners v4, v5, v7 + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 89): + # Shelf: (v1, v4, v5) + isolated corner v7 + _append_tris(face_list, 25, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 90): + # Four isolated corners v2, v4, v5, v7 + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 91): + # Three isolated corners, v3, v6, v8 by inversion + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 92): + # Shelf (v4, v3, v7) + isolated corner v5 + _append_tris(face_list, 76, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 93): + # Lower right plane + isolated corner v8 by inversion + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 94): + # Isolated corners v1, v6, v8 by inversion + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 95): + # Isolated corners v6, v8 by inversion + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 96): + # back right plane + face_list.append([e7, e12, e5]) + face_list.append([e5, e10, e12]) + elif (case == 97): + # back right plane + isolated corner v1 + _append_tris(face_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 98): + # Shelf: (v2, v6, v7) + face_list.append([e1, e7, e5]) + face_list.append([e7, e1, e12]) + face_list.append([e1, e12, e2]) + elif (case == 99): + # Rotated case 14 in paper: (v1, v2, v6, v7) + face_list.append([e9, e2, e7]) + face_list.append([e9, e2, e4]) + face_list.append([e2, e7, e12]) + face_list.append([e7, e9, e5]) + elif (case == 100): + # Shelf: (v3, v6, v7) + face_list.append([e3, e7, e5]) + face_list.append([e3, e5, e2]) + face_list.append([e2, e5, e10]) + elif (case == 101): + # Shelf: (v3, v6, v7) + isolated corner v1 + _append_tris(face_list, 100, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 102): + # Plane bisecting left-right halves of cube + face_list.append([e1, e3, e7]) + face_list.append([e1, e7, e5]) + elif (case == 103): + # Shelf: (v4, v5, v8) by inversion + face_list.append([e3, e7, e5]) + face_list.append([e3, e5, e4]) + face_list.append([e4, e5, e9]) + elif (case == 104): + # Back right plane + isolated corner v4 + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 105): + # AMBIGUOUS CASE: back right and front left planes + _append_tris(face_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 106): + # Shelf: (v2, v6, v7) + isolated corner v4 + _append_tris(face_list, 98, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 107): + # Back left plane + isolated corner v3 by inversion + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 108): + # Rotated case 11 from paper: (v4, v3, v7, v6) + face_list.append([e4, e10, e7]) + face_list.append([e4, e10, e2]) + face_list.append([e4, e11, e7]) + face_list.append([e7, e10, e5]) + elif (case == 109): + # Back left plane + isolated corner v2 by inversion + _append_tris(face_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 110): + # Shelf: (v1, v5, v8) by inversion + face_list.append([e1, e5, e7]) + face_list.append([e1, e7, e11]) + face_list.append([e1, e11, e4]) + elif (case == 111): + # Back left plane + face_list.append([e11, e9, e7]) + face_list.append([e9, e7, e5]) + elif (case == 112): + # Shelf: (v5, v6, v7) + face_list.append([e9, e10, e12]) + face_list.append([e9, e12, e7]) + face_list.append([e9, e7, e8]) + elif (case == 113): + # Exactly case 11 from paper: (v1, v5, v6, v7) + face_list.append([e1, e8, e12]) + face_list.append([e1, e8, e4]) + face_list.append([e8, e7, e12]) + face_list.append([e12, e1, e10]) + elif (case == 114): + # Full corner v6: (v2, v6, v7, v5) + face_list.append([e1, e9, e7]) + face_list.append([e1, e7, e12]) + face_list.append([e1, e12, e2]) + face_list.append([e9, e8, e7]) + elif (case == 115): + # Shelf: (v3, v4, v8) + face_list.append([e2, e4, e8]) + face_list.append([e2, e12, e7]) + face_list.append([e2, e8, e7]) + elif (case == 116): + # Rotated case 14 in paper: (v5, v6, v7, v3) + face_list.append([e9, e2, e7]) + face_list.append([e9, e2, e10]) + face_list.append([e9, e8, e7]) + face_list.append([e2, e3, e7]) + elif (case == 117): + # upper left plane + isolated corner v2 by inversion + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 118): + # Shelf: (v1, v4, v8) + face_list.append([e1, e3, e7]) + face_list.append([e7, e1, e8]) + face_list.append([e1, e8, e9]) + elif (case == 119): + # Upper left plane + face_list.append([e4, e3, e7]) + face_list.append([e4, e8, e7]) + elif (case == 120): + # Shelf: (v5, v6, v7) + isolated corner v4 + _append_tris(face_list, 112, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 121): + # Front right plane + isolated corner v8 + _append_tris(face_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 122): + # Isolated corners v1, v3, v8 + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 123): + # Isolated corners v3, v8 + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 124): + # Front lower plane + isolated corner v8 + _append_tris(face_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 125): + # Isolated corners v2, v8 + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 126): + # Isolated corners v1, v8 + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 127): + # Isolated corner v8 + face_list.append([e11, e7, e8]) + elif (case == 150): + # AMBIGUOUS CASE: back right and front left planes + # In these cube_case > 127 cases, the vertices are identical BUT + # they are connected in the opposite fashion. + _append_tris(face_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 170): + # AMBIGUOUS CASE: upper left and lower right planes + # In these cube_case > 127 cases, the vertices are identical BUT + # they are connected in the opposite fashion. + _append_tris(face_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 195): + # AMBIGUOUS CASE: back upper and front lower planes + # In these cube_case > 127 cases, the vertices are identical BUT + # they are connected in the opposite fashion. + _append_tris(face_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(face_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + + return diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index 145f6052..6b7197d2 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -4,53 +4,178 @@ #cython: wraparound=False import numpy as np -cimport numpy as cnp + +def moments(double[:, :] image, Py_ssize_t order=3): + """Calculate all raw image moments up to a certain order. + + The following properties can be calculated from raw image moments: + * Area as ``m[0, 0]``. + * Centroid as {``m[0, 1] / m[0, 0]``, ``m[1, 0] / m[0, 0]``}. + + Note that raw moments are whether translation, scale nor rotation + invariant. + + Parameters + ---------- + image : 2D double array + Rasterized shape as image. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + m : (``order + 1``, ``order + 1``) array + Raw image moments. + + References + ---------- + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] http://en.wikipedia.org/wiki/Image_moment + + """ + return moments_central(image, 0, 0, order) -def central_moments(cnp.ndarray[cnp.double_t, ndim=2] array, double cr, - double cc, int order): +def moments_central(double[:, :] image, double cr, double cc, + Py_ssize_t order=3): + """Calculate all central image moments up to a certain order. + + Note that central moments are translation invariant but not scale and + rotation invariant. + + Parameters + ---------- + image : 2D double array + Rasterized shape as image. + cr : double + Center row coordinate. + cc : double + Center column coordinate. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + mu : (``order + 1``, ``order + 1``) array + Central image moments. + + References + ---------- + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] http://en.wikipedia.org/wiki/Image_moment + + """ cdef Py_ssize_t p, q, r, c - cdef cnp.ndarray[cnp.double_t, ndim=2] mu - mu = np.zeros((order + 1, order + 1), 'double') + cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) for p in range(order + 1): for q in range(order + 1): - for r in range(array.shape[0]): - for c in range(array.shape[1]): - mu[p,q] += array[r,c] * (r - cr) ** q * (c - cc) ** p - return mu + for r in range(image.shape[0]): + for c in range(image.shape[1]): + mu[p, q] += image[r, c] * (r - cr) ** q * (c - cc) ** p + return np.asarray(mu) -def normalized_moments(cnp.ndarray[cnp.double_t, ndim=2] mu, int order): +def moments_normalized(double[:, :] mu, Py_ssize_t order=3): + """Calculate all normalized central image moments up to a certain order. + + Note that normalized central moments are translation and scale invariant + but not rotation invariant. + + Parameters + ---------- + mu : (M, M) array + Central image moments, where M must be > ``order``. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + nu : (``order + 1``, ``order + 1``) array + Normalized central image moments. + + References + ---------- + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] http://en.wikipedia.org/wiki/Image_moment + + """ cdef Py_ssize_t p, q - cdef cnp.ndarray[cnp.double_t, ndim=2] nu - nu = np.zeros((order + 1, order + 1), 'double') + cdef double[:, ::1] nu = np.zeros((order + 1, order + 1), dtype=np.double) for p in range(order + 1): for q in range(order + 1): if p + q >= 2: - nu[p,q] = mu[p,q] / mu[0,0]**((p + q) / 2 + 1) + nu[p, q] = mu[p, q] / mu[0, 0] ** ((p + q) / 2 + 1) else: - nu[p,q] = np.nan - return nu + nu[p, q] = np.nan + return np.asarray(nu) -def hu_moments(cnp.ndarray[cnp.double_t, ndim=2] nu): - cdef cnp.ndarray[cnp.double_t, ndim=1] hu = np.zeros((7,), 'double') - cdef double t0 = nu[3,0] + nu[1,2] - cdef double t1 = nu[2,1] + nu[0,3] +def moments_hu(double[:, :] nu): + """Calculate Hu's set of image moments. + + Note that this set of moments is proofed to be translation, scale and + rotation invariant. + + Parameters + ---------- + nu : (M, M) array + Normalized central image moments, where M must be > 4. + + Returns + ------- + nu : (7, 1) array + Hu's set of image moments. + + References + ---------- + .. [1] M. K. Hu, "Visual Pattern Recognition by Moment Invariants", + IRE Trans. Info. Theory, vol. IT-8, pp. 179-187, 1962 + .. [2] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [3] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [4] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [5] http://en.wikipedia.org/wiki/Image_moment + + + """ + cdef double[::1] hu = np.zeros((7, ), dtype=np.double) + cdef double t0 = nu[3, 0] + nu[1, 2] + cdef double t1 = nu[2, 1] + nu[0, 3] cdef double q0 = t0 * t0 cdef double q1 = t1 * t1 - cdef double n4 = 4 * nu[1,1] - cdef double s = nu[2,0] + nu[0,2] - cdef double d = nu[2,0] - nu[0,2] + cdef double n4 = 4 * nu[1, 1] + cdef double s = nu[2, 0] + nu[0, 2] + cdef double d = nu[2, 0] - nu[0, 2] hu[0] = s - hu[1] = d * d + n4 * nu[1,1] + hu[1] = d * d + n4 * nu[1, 1] hu[3] = q0 + q1 hu[5] = d * (q0 - q1) + n4 * t0 * t1 t0 *= q0 - 3 * q1 t1 *= 3 * q0 - q1 - q0 = nu[3,0]- 3 * nu[1,2] - q1 = 3 * nu[2,1] - nu[0,3] + q0 = nu[3, 0]- 3 * nu[1, 2] + q1 = 3 * nu[2, 1] - nu[0, 3] hu[2] = q0 * q0 + q1 * q1 hu[4] = q0 * t0 + q1 * t1 hu[6] = q1 * t0 - q0 * t1 - return hu + return np.asarray(hu) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 72d1e2f4..10ac785d 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -1,60 +1,318 @@ # coding: utf-8 +import warnings from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage -from skimage.morphology import convex_hull_image -from . import _moments +from skimage.morphology import convex_hull_image, label +from skimage.measure import _moments -__all__ = ['regionprops'] +__all__ = ['regionprops', 'perimeter'] STREL_4 = np.array([[0, 1, 0], [1, 1, 1], - [0, 1, 0]]) -STREL_8 = np.ones((3, 3), 'int8') -PROPS = ( - 'Area', - 'BoundingBox', - 'CentralMoments', - 'Centroid', - 'ConvexArea', + [0, 1, 0]], dtype=np.uint8) +STREL_8 = np.ones((3, 3), dtype=np.uint8) +PROPS = { + 'Area': 'area', + 'BoundingBox': 'bbox', + 'CentralMoments': 'moments_central', + 'Centroid': 'centroid', + 'ConvexArea': 'convex_area', # 'ConvexHull', - 'ConvexImage', - 'Coordinates', - 'Eccentricity', - 'EquivDiameter', - 'EulerNumber', - 'Extent', + 'ConvexImage': 'convex_image', + 'Coordinates': 'coords', + 'Eccentricity': 'eccentricity', + 'EquivDiameter': 'equivalent_diameter', + 'EulerNumber': 'euler_number', + 'Extent': 'extent', # 'Extrema', - 'FilledArea', - 'FilledImage', - 'HuMoments', - 'Image', - 'MajorAxisLength', - 'MaxIntensity', - 'MeanIntensity', - 'MinIntensity', - 'MinorAxisLength', - 'Moments', - 'NormalizedMoments', - 'Orientation', - 'Perimeter', + 'FilledArea': 'filled_area', + 'FilledImage': 'filled_image', + 'HuMoments': 'moments_hu', + 'Image': 'image', + 'Label': 'label', + 'MajorAxisLength': 'major_axis_length', + 'MaxIntensity': 'max_intensity', + 'MeanIntensity': 'mean_intensity', + 'MinIntensity': 'min_intensity', + 'MinorAxisLength': 'minor_axis_length', + 'Moments': 'moments', + 'NormalizedMoments': 'moments_normalized', + 'Orientation': 'orientation', + 'Perimeter': 'perimeter', # 'PixelIdxList', # 'PixelList', - 'Solidity', + 'Solidity': 'solidity', # 'SubarrayIdx' - 'WeightedCentralMoments', - 'WeightedCentroid', - 'WeightedHuMoments', - 'WeightedMoments', - 'WeightedNormalizedMoments' -) + 'WeightedCentralMoments': 'weighted_central_moments', + 'WeightedCentroid': 'weighted_centroid', + 'WeightedHuMoments': 'weighted_moments_hu', + 'WeightedMoments': 'weighted_moments', + 'WeightedNormalizedMoments': 'weighted_moments_normalized' +} -def regionprops(label_image, properties=['Area', 'Centroid'], - intensity_image=None): +class _cached_property(object): + """Decorator to use a function as a cached property. + + The function is only called the first time and each successive call returns + the cached result of the first call. + + class Foo(object): + + @_cached_property + def foo(self): + return "Cached" + + class Foo(object): + + def __init__(self): + self._cache_active = False + + @_cached_property + def foo(self): + return "Not cached" + + Adapted from . + + """ + + def __init__(self, func, name=None, doc=None): + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func + + def __get__(self, obj, type=None): + if obj is None: + return self + + # call every time, if cache is not active + if not obj.__dict__.get('_cache_active', True): + return self.func(obj) + + # try to retrieve from cache or call and store result in cache + try: + value = obj.__dict__[self.__name__] + except KeyError: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + +class _RegionProperties(object): + + def __init__(self, slice, label, label_image, intensity_image, + cache_active): + self.label = label + self._slice = slice + self._label_image = label_image + self._intensity_image = intensity_image + self._cache_active = cache_active + + @_cached_property + def area(self): + return self.moments[0, 0] + + @_cached_property + def bbox(self): + return (self._slice[0].start, self._slice[1].start, + self._slice[0].stop, self._slice[1].stop) + + @_cached_property + def centroid(self): + row, col = self.local_centroid + return row + self._slice[0].start, col + self._slice[1].start + + @_cached_property + def convex_area(self): + return np.sum(self.convex_image) + + @_cached_property + def convex_image(self): + return convex_hull_image(self.image) + + @_cached_property + def coords(self): + rr, cc = np.nonzero(self.image) + return np.vstack((rr + self._slice[0].start, + cc + self._slice[1].start)).T + + @_cached_property + def eccentricity(self): + l1, l2 = self.inertia_tensor_eigvals + if l1 == 0: + return 0 + return sqrt(1 - l2 / l1) + + @_cached_property + def equivalent_diameter(self): + return sqrt(4 * self.moments[0, 0] / PI) + + @_cached_property + def euler_number(self): + euler_array = self.filled_image != self.image + _, num = label(euler_array, neighbors=8, return_num=True) + return -num + 1 + + @_cached_property + def extent(self): + rows, cols = self.image.shape + return self.moments[0, 0] / (rows * cols) + + @_cached_property + def filled_area(self): + return np.sum(self.filled_image) + + @_cached_property + def filled_image(self): + return ndimage.binary_fill_holes(self.image, STREL_8) + + @_cached_property + def image(self): + return self._label_image[self._slice] == self.label + + @_cached_property + def _image_double(self): + return self.image.astype(np.double) + + @_cached_property + def inertia_tensor(self): + mu = self.moments_central + a = mu[2, 0] / mu[0, 0] + b = -mu[1, 1] / mu[0, 0] + c = mu[0, 2] / mu[0, 0] + return np.array([[a, b], [b, c]]) + + @_cached_property + def inertia_tensor_eigvals(self): + a, b, b, c = self.inertia_tensor.flat + # eigen values of inertia tensor + l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 + l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 + return l1, l2 + + @_cached_property + def intensity_image(self): + if self._intensity_image is None: + raise AttributeError('No intensity image specified.') + return self._intensity_image[self._slice] * self.image + + @_cached_property + def _intensity_image_double(self): + return self.intensity_image.astype(np.double) + + @_cached_property + def local_centroid(self): + m = self.moments + row = m[0, 1] / m[0, 0] + col = m[1, 0] / m[0, 0] + return row, col + + @_cached_property + def max_intensity(self): + return np.max(self.intensity_image[self.image]) + + @_cached_property + def mean_intensity(self): + return np.mean(self.intensity_image[self.image]) + + @_cached_property + def min_intensity(self): + return np.min(self.intensity_image[self.image]) + + @_cached_property + def major_axis_length(self): + l1, _ = self.inertia_tensor_eigvals + return 4 * sqrt(l1) + + @_cached_property + def minor_axis_length(self): + _, l2 = self.inertia_tensor_eigvals + return 4 * sqrt(l2) + + @_cached_property + def moments(self): + return _moments.moments(self._image_double, 3) + + @_cached_property + def moments_central(self): + row, col = self.local_centroid + return _moments.moments_central(self._image_double, row, col, 3) + + @_cached_property + def moments_hu(self): + return _moments.moments_hu(self.moments_normalized) + + @_cached_property + def moments_normalized(self): + return _moments.moments_normalized(self.moments_central, 3) + + @_cached_property + def orientation(self): + a, b, b, c = self.inertia_tensor.flat + b = -b + if a - c == 0: + if b > 0: + return -PI / 4. + else: + return PI / 4. + else: + return - 0.5 * atan2(2 * b, (a - c)) + + @_cached_property + def perimeter(self): + return perimeter(self.image, 4) + + @_cached_property + def solidity(self): + return self.moments[0, 0] / np.sum(self.convex_image) + + @_cached_property + def weighted_centroid(self): + row, col = self.weighted_local_centroid + return row + self._slice[0].start, col + self._slice[1].start + + @_cached_property + def weighted_local_centroid(self): + m = self.weighted_moments + row = m[0, 1] / m[0, 0] + col = m[1, 0] / m[0, 0] + return row, col + + @_cached_property + def weighted_moments(self): + return _moments.moments_central(self._intensity_image_double, 0, 0, 3) + + @_cached_property + def weighted_central_moments(self): + row, col = self.weighted_local_centroid + return _moments.moments_central(self._intensity_image_double, + row, col, 3) + + @_cached_property + def weighted_moments_hu(self): + return _moments.moments_hu(self.weighted_moments_normalized) + + @_cached_property + def weighted_moments_normalized(self): + return _moments.moments_normalized(self.weighted_central_moments, 3) + + def __getitem__(self, key): + value = getattr(self, key, None) + if value is not None: + return value + else: # backwards compatability + warnings.warn('Usage of deprecated property name.', + category=DeprecationWarning) + return getattr(self, PROPS[key]) + + +def regionprops(label_image, properties=None, + intensity_image=None, cache=True): """Measure properties of labelled image regions. Parameters @@ -62,150 +320,134 @@ def regionprops(label_image, properties=['Area', 'Centroid'], label_image : (N, M) ndarray Labelled input image. properties : {'all', list} - Shape measurements to be determined for each labelled image region. - Default is `['Area', 'Centroid']`. The following properties can be - determined: + **Deprecated parameter** - * Area : int - Number of pixels of region. - - * BoundingBox : tuple - Bounding box `(min_row, min_col, max_row, max_col)` - - * CentralMoments : (3, 3) ndarray - Central moments (translation invariant) up to 3rd order. - - mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } - - where the sum is over the `x`, `y` coordinates of the region, - and `x_c` and `y_c` are the coordinates of the region's centroid. - - * Centroid : array - Centroid coordinate tuple `(row, col)`. - - * ConvexArea : int - Number of pixels of convex hull image. - - * ConvexImage : (H, J) ndarray - Binary convex hull image which has the same size as bounding box. - - * Coordinates : (N, 2) ndarray - Coordinate list `(row, col)` of the region. - - * Eccentricity : float - Eccentricity of the ellipse that has the same second-moments as the - region. The eccentricity is the ratio of the distance between its - minor and major axis length. The value is between 0 and 1. - - * EquivDiameter : float - The diameter of a circle with the same area as the region. - - * EulerNumber : int - Euler number of region. Computed as number of objects (= 1) - subtracted by number of holes (8-connectivity). - - * Extent : float - Ratio of pixels in the region to pixels in the total bounding box. - Computed as `Area / (rows*cols)` - - * FilledArea : int - Number of pixels of filled region. - - * FilledImage : (H, J) ndarray - Binary region image with filled holes which has the same size as - bounding box. - - * HuMoments : tuple - Hu moments (translation, scale and rotation invariant). - - * Image : (H, J) ndarray - Sliced binary region image which has the same size as bounding box. - - * MajorAxisLength : float - The length of the major axis of the ellipse that has the same - normalized second central moments as the region. - - * MaxIntensity: float - Value with the greatest intensity in the region. - - * MeanIntensity: float - Value with the mean intensity in the region. - - * MinIntensity: float - Value with the least intensity in the region. - - * MinorAxisLength : float - The length of the minor axis of the ellipse that has the same - normalized second central moments as the region. - - * Moments : (3, 3) ndarray - Spatial moments up to 3rd order. - - m_ji = sum{ array(x, y) * x^j * y^i } - - where the sum is over the `x`, `y` coordinates of the region. - - * NormalizedMoments : (3, 3) ndarray - Normalized moments (translation and scale invariant) up to 3rd - order. - - nu_ji = mu_ji / m_00^[(i+j)/2 + 1] - - where `m_00` is the zeroth spatial moment. - - * Orientation : float - Angle between the X-axis and the major axis of the ellipse that has - the same second-moments as the region. Ranging from `-pi/2` to - `pi/2` in counter-clockwise direction. - - * Perimeter : float - Perimeter of object which approximates the contour as a line - through the centers of border pixels using a 4-connectivity. - - * Solidity : float - Ratio of pixels in the region to pixels of the convex hull image. - - * WeightedCentralMoments : (3, 3) ndarray - Central moments (translation invariant) of intensity image up to - 3rd order. - - wmu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } - - where the sum is over the `x`, `y` coordinates of the region, - and `x_c` and `y_c` are the coordinates of the region's centroid. - - * WeightedCentroid : array - Centroid coordinate tuple `(row, col)` weighted with intensity - image. - - * WeightedHuMoments : tuple - Hu moments (translation, scale and rotation invariant) of intensity - image. - - * WeightedMoments : (3, 3) ndarray - Spatial moments of intensity image up to 3rd order. - - wm_ji = sum{ array(x, y) * x^j * y^i } - - where the sum is over the `x`, `y` coordinates of the region. - - * WeightedNormalizedMoments : (3, 3) ndarray - Normalized moments (translation and scale invariant) of intensity - image up to 3rd order. - - wnu_ji = wmu_ji / wm_00^[(i+j)/2 + 1] - - where `wm_00` is the zeroth spatial moment (intensity-weighted - area). + This parameter is not needed any more since all properties are + determined dynamically. intensity_image : (N, M) ndarray, optional Intensity image with same size as labelled image. Default is None. + cache : bool, optional + Determine whether to cache calculated properties. The computation is + much faster for cached properties, whereas the memory consumption + increases. Returns ------- - properties : list of dicts - List containing a property dict for each region. The property dicts - contain all the specified properties plus a 'Label' field. + properties : list + List containing a properties for each region. The properties of each + region can be accessed as attributes and keys. + + Notes + ----- + The following properties can be accessed as attributes or keys: + + **area** : int + Number of pixels of region. + **bbox** : tuple + Bounding box ``(min_row, min_col, max_row, max_col)`` + **centroid** : array + Centroid coordinate tuple ``(row, col)``. + **convex_area** : int + Number of pixels of convex hull image. + **convex_image** : (H, J) ndarray + Binary convex hull image which has the same size as bounding box. + **coords** : (N, 2) ndarray + Coordinate list ``(row, col)`` of the region. + **eccentricity** : float + Eccentricity of the ellipse that has the same second-moments as the + region. The eccentricity is the ratio of the distance between its + minor and major axis length. The value is between 0 and 1. + **equivalent_diameter** : float + The diameter of a circle with the same area as the region. + **euler_number** : int + Euler number of region. Computed as number of objects (= 1) + subtracted by number of holes (8-connectivity). + **extent** : float + Ratio of pixels in the region to pixels in the total bounding box. + Computed as ``area / (rows * cols)`` + **filled_area** : int + Number of pixels of filled region. + **filled_image** : (H, J) ndarray + Binary region image with filled holes which has the same size as + bounding box. + **image** : (H, J) ndarray + Sliced binary region image which has the same size as bounding box. + **inertia_tensor** : (2, 2) ndarray + Inertia tensor of the region for the rotation around its mass. + **inertia_tensor_eigvals** : tuple + The two eigen values of the inertia tensor in decreasing order. + **label** : int + The label in the labeled input image. + **major_axis_length** : float + The length of the major axis of the ellipse that has the same + normalized second central moments as the region. + **min_intensity** : float + Value with the greatest intensity in the region. + **mean_intensity** : float + Value with the mean intensity in the region. + **min_intensity** : float + Value with the least intensity in the region. + **minor_axis_length** : float + The length of the minor axis of the ellipse that has the same + normalized second central moments as the region. + **moments** : (3, 3) ndarray + Spatial moments up to 3rd order:: + + m_ji = sum{ array(x, y) * x^j * y^i } + + where the sum is over the `x`, `y` coordinates of the region. + **moments_central** : (3, 3) ndarray + Central moments (translation invariant) up to 3rd order:: + + mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + + where the sum is over the `x`, `y` coordinates of the region, + and `x_c` and `y_c` are the coordinates of the region's centroid. + **moments_hu** : tuple + Hu moments (translation, scale and rotation invariant). + **moments_normalized** : (3, 3) ndarray + Normalized moments (translation and scale invariant) up to 3rd order:: + + nu_ji = mu_ji / m_00^[(i+j)/2 + 1] + + where `m_00` is the zeroth spatial moment. + **orientation** : float + Angle between the X-axis and the major axis of the ellipse that has + the same second-moments as the region. Ranging from `-pi/2` to + `pi/2` in counter-clockwise direction. + **perimeter** : float + Perimeter of object which approximates the contour as a line + through the centers of border pixels using a 4-connectivity. + **solidity** : float + Ratio of pixels in the region to pixels of the convex hull image. + **weighted_centroid** : array + Centroid coordinate tuple ``(row, col)`` weighted with intensity + image. + **weighted_moments** : (3, 3) ndarray + Spatial moments of intensity image up to 3rd order:: + + wm_ji = sum{ array(x, y) * x^j * y^i } + + where the sum is over the `x`, `y` coordinates of the region. + **weighted_central_moments** : (3, 3) ndarray + Central moments (translation invariant) of intensity image up to + 3rd order:: + + wmu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + + where the sum is over the `x`, `y` coordinates of the region, + and `x_c` and `y_c` are the coordinates of the region's centroid. + **weighted_moments_hu** : tuple + Hu moments (translation, scale and rotation invariant) of intensity + image. + **weighted_moments_normalized** : (3, 3) ndarray + Normalized moments (translation and scale invariant) of intensity + image up to 3rd order:: + + wnu_ji = wmu_ji / wm_00^[(i+j)/2 + 1] + + where `wm_00` is the zeroth spatial moment (intensity-weighted area). References ---------- @@ -225,194 +467,32 @@ def regionprops(label_image, properties=['Area', 'Centroid'], >>> img = coins() > 110 >>> label_img = label(img) >>> props = regionprops(label_img) - >>> props[0]['Centroid'] # centroid of first labelled object + >>> props[0].centroid # centroid of first labelled object + >>> props[0]['centroid'] # centroid of first labelled object """ - if not np.issubdtype(label_image.dtype, 'int'): - raise TypeError('labelled image must be of integer dtype') - # determine all properties if nothing specified - if properties == 'all': - properties = PROPS + label_image = np.squeeze(label_image) - props = [] + if label_image.ndim != 2: + raise TypeError('Only 2-D images supported.') + + if properties is not None: + warnings.warn('The ``properties`` argument is deprecated and is ' + 'not needed any more as properties are ' + 'determined dynamically.', + category=DeprecationWarning) + + regions = [] objects = ndimage.find_objects(label_image) for i, sl in enumerate(objects): label = i + 1 - # create property dict for current label - obj_props = {} - props.append(obj_props) + props = _RegionProperties(sl, label, label_image, + intensity_image, cache) + regions.append(props) - obj_props['Label'] = label - - array = (label_image[sl] == label).astype('double') - - # upper left corner of object bbox - r0 = sl[0].start - c0 = sl[1].start - - m = _moments.central_moments(array, 0, 0, 3) - # centroid - cr = m[0, 1] / m[0, 0] - cc = m[1, 0] / m[0, 0] - mu = _moments.central_moments(array, cr, cc, 3) - - # elements of the inertia tensor [a b; b c] - a = mu[2, 0] / mu[0, 0] - b = mu[1, 1] / mu[0, 0] - c = mu[0, 2] / mu[0, 0] - # eigen values of inertia tensor - l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 - l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 - - # cached results which are used by several properties - _filled_image = None - _convex_image = None - _nu = None - - if 'Area' in properties: - obj_props['Area'] = m[0, 0] - - if 'BoundingBox' in properties: - obj_props['BoundingBox'] = (r0, c0, sl[0].stop, sl[1].stop) - - if 'Centroid' in properties: - obj_props['Centroid'] = cr + r0, cc + c0 - - if 'CentralMoments' in properties: - obj_props['CentralMoments'] = mu - - if 'ConvexArea' in properties: - if _convex_image is None: - _convex_image = convex_hull_image(array) - obj_props['ConvexArea'] = np.sum(_convex_image) - - if 'ConvexImage' in properties: - if _convex_image is None: - _convex_image = convex_hull_image(array) - obj_props['ConvexImage'] = _convex_image - - if 'Coordinates' in properties: - rr, cc = np.nonzero(array) - obj_props['Coordinates'] = np.vstack((rr + r0, cc + c0)).T - - if 'Eccentricity' in properties: - if l1 == 0: - obj_props['Eccentricity'] = 0 - else: - obj_props['Eccentricity'] = sqrt(1 - l2 / l1) - - if 'EquivDiameter' in properties: - obj_props['EquivDiameter'] = sqrt(4 * m[0, 0] / PI) - - if 'EulerNumber' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - euler_array = _filled_image != array - _, num = ndimage.label(euler_array, STREL_8) - obj_props['EulerNumber'] = - num - - if 'Extent' in properties: - obj_props['Extent'] = m[0, 0] / (array.shape[0] * array.shape[1]) - - if 'HuMoments' in properties: - if _nu is None: - _nu = _moments.normalized_moments(mu, 3) - obj_props['HuMoments'] = _moments.hu_moments(_nu) - - if 'Image' in properties: - obj_props['Image'] = array - - if 'FilledArea' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - obj_props['FilledArea'] = np.sum(_filled_image) - - if 'FilledImage' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - obj_props['FilledImage'] = _filled_image - - if 'MajorAxisLength' in properties: - obj_props['MajorAxisLength'] = 4 * sqrt(l1) - - if 'MinorAxisLength' in properties: - obj_props['MinorAxisLength'] = 4 * sqrt(l2) - - if 'Moments' in properties: - obj_props['Moments'] = m - - if 'NormalizedMoments' in properties: - if _nu is None: - _nu = _moments.normalized_moments(mu, 3) - obj_props['NormalizedMoments'] = _nu - - if 'Orientation' in properties: - if a - c == 0: - if b > 0: - obj_props['Orientation'] = -PI / 4. - else: - obj_props['Orientation'] = PI / 4. - else: - obj_props['Orientation'] = - 0.5 * atan2(2 * b, (a - c)) - - if 'Perimeter' in properties: - obj_props['Perimeter'] = perimeter(array, 4) - - if 'Solidity' in properties: - if _convex_image is None: - _convex_image = convex_hull_image(array) - obj_props['Solidity'] = m[0, 0] / np.sum(_convex_image) - - if intensity_image is not None: - weighted_array = array * intensity_image[sl] - - wm = _moments.central_moments(weighted_array, 0, 0, 3) - # weighted centroid - wcr = wm[0, 1] / wm[0, 0] - wcc = wm[1, 0] / wm[0, 0] - wmu = _moments.central_moments(weighted_array, wcr, wcc, 3) - - # cached results which are used by several properties - _wnu = None - _vals = None - - if 'MaxIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MaxIntensity'] = np.max(_vals) - - if 'MeanIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MeanIntensity'] = np.mean(_vals) - - if 'MinIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MinIntensity'] = np.min(_vals) - - if 'WeightedCentralMoments' in properties: - obj_props['WeightedCentralMoments'] = wmu - - if 'WeightedCentroid' in properties: - obj_props['WeightedCentroid'] = wcr + r0, wcc + c0 - - if 'WeightedHuMoments' in properties: - if _wnu is None: - _wnu = _moments.normalized_moments(wmu, 3) - obj_props['WeightedHuMoments'] = _moments.hu_moments(_wnu) - - if 'WeightedMoments' in properties: - obj_props['WeightedMoments'] = wm - - if 'WeightedNormalizedMoments' in properties: - if _wnu is None: - _wnu = _moments.normalized_moments(wmu, 3) - obj_props['WeightedNormalizedMoments'] = _wnu - - return props + return regions def perimeter(image, neighbourhood=4): @@ -421,14 +501,14 @@ def perimeter(image, neighbourhood=4): Parameters ---------- image : array - binary image + Binary image. neighbourhood : 4 or 8, optional - neighbourhood connectivity for border pixel determination, default 4 + Neighborhood connectivity for border pixel determination. Returns ------- perimeter : float - total perimeter of all objects in binary image + Total perimeter of all objects in binary image. References ---------- @@ -440,24 +520,25 @@ def perimeter(image, neighbourhood=4): strel = STREL_4 else: strel = STREL_8 + image = image.astype(np.uint8) eroded_image = ndimage.binary_erosion(image, strel, border_value=0) border_image = image - eroded_image - # perimeter contribution: corresponding values in convolved image - perimeter_weights = { - 1: (5, 7, 15, 17, 25, 27), - sqrt(2): (21, 33), - (1 + sqrt(2)) / 2: (13, 23) - } + perimeter_weights = np.zeros(50, dtype=np.double) + perimeter_weights[[5, 7, 15, 17, 25, 27]] = 1 + perimeter_weights[[21, 33]] = sqrt(2) + perimeter_weights[[13, 23]] = (1 + sqrt(2)) / 2 + + perimeter_image = ndimage.convolve(border_image, np.array([[10, 2, 10], [ 2, 1, 2], [10, 2, 10]]), mode='constant', cval=0) - total_perimeter = 0 - for weight, values in perimeter_weights.items(): - num_values = 0 - for value in values: - num_values += np.sum(perimeter_image == value) - total_perimeter += num_values * weight + # You can also write + # return perimeter_weights[perimeter_image].sum() + # but that was measured as taking much longer than bincount + np.dot (5x + # as much time) + perimeter_histogram = np.bincount(perimeter_image.ravel(), minlength=50) + total_perimeter = np.dot(perimeter_histogram, perimeter_weights) return total_perimeter diff --git a/skimage/measure/block.py b/skimage/measure/block.py new file mode 100644 index 00000000..fad5668c --- /dev/null +++ b/skimage/measure/block.py @@ -0,0 +1,77 @@ +import numpy as np +from skimage.util import view_as_blocks, pad + + +def block_reduce(image, block_size, func=np.sum, cval=0): + """Down-sample image by applying function to local blocks. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + block_size : array_like + Array containing down-sampling integer factor along each axis. + func : callable + Function object which is used to calculate the return value for each + local block. This function must implement an ``axis`` parameter such as + ``numpy.sum`` or ``numpy.min``. + cval : float + Constant padding value if image is not perfectly divisible by the + block size. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Examples + -------- + >>> from skimage.measure import block_reduce + >>> image = np.arange(3*3*4).reshape(3, 3, 4) + >>> image + array([[[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]], + + [[12, 13, 14, 15], + [16, 17, 18, 19], + [20, 21, 22, 23]], + + [[24, 25, 26, 27], + [28, 29, 30, 31], + [32, 33, 34, 35]]]) + >>> block_reduce(image, block_size=(3, 3, 1), func=np.mean) + array([[[ 16., 17., 18., 19.]]]) + >>> block_reduce(image, block_size=(1, 3, 4), func=np.max) + array([[[11]], + + [[23]], + + [[35]]]) + >>> block_reduce(image, block_size=(3, 1, 4), func=np.max) + array([[[27], + [31], + [35]]]) + """ + + if len(block_size) != image.ndim: + raise ValueError("`block_size` must have the same length " + "as `image.shape`.") + + pad_width = [] + for i in range(len(block_size)): + if image.shape[i] % block_size[i] != 0: + after_width = block_size[i] - (image.shape[i] % block_size[i]) + else: + after_width = 0 + pad_width.append((0, after_width)) + + image = pad(image, pad_width=pad_width, mode='constant', + constant_values=cval) + + out = view_as_blocks(image, block_size) + + for i in range(len(out.shape) // 2): + out = func(out, axis=-1) + + return out diff --git a/skimage/measure/find_contours.py b/skimage/measure/find_contours.py index 68e471ac..d36c2110 100755 --- a/skimage/measure/find_contours.py +++ b/skimage/measure/find_contours.py @@ -116,7 +116,7 @@ def find_contours(array, level, raise ValueError('Parameters "fully_connected" and' ' "positive_orientation" must be either "high" or "low".') point_list = _find_contours.iterate_and_store(array, level, - fully_connected == 'high') + fully_connected == 'high') contours = _assemble_contours(_take_2(point_list)) if positive_orientation == 'high': contours = [c[::-1] for c in contours] @@ -126,8 +126,8 @@ def find_contours(array, level, def _take_2(seq): iterator = iter(seq) while(True): - n1 = iterator.next() - n2 = iterator.next() + n1 = next(iterator) + n2 = next(iterator) yield (n1, n2) diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index 21d9964e..be57ca7b 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -14,11 +14,15 @@ def configuration(parent_package='', top_path=None): cython(['_find_contours.pyx'], working_path=base_path) cython(['_moments.pyx'], working_path=base_path) + cython(['_marching_cubes_cy.pyx'], working_path=base_path) config.add_extension('_find_contours', sources=['_find_contours.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_moments', sources=['_moments.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_marching_cubes_cy', + sources=['_marching_cubes_cy.c'], + include_dirs=[get_numpy_include_dirs()]) return config diff --git a/skimage/measure/tests/test_block.py b/skimage/measure/tests/test_block.py new file mode 100644 index 00000000..a8bc62a9 --- /dev/null +++ b/skimage/measure/tests/test_block.py @@ -0,0 +1,81 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.measure import block_reduce + + +def test_block_reduce_sum(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_reduce(image1, (2, 3)) + expected1 = np.array([[ 24, 42], + [ 96, 114]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_reduce(image2, (3, 3)) + expected2 = np.array([[ 81, 108, 87], + [174, 192, 138]]) + assert_array_equal(expected2, out2) + + +def test_block_reduce_mean(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_reduce(image1, (2, 3), func=np.mean) + expected1 = np.array([[ 4., 7.], + [ 16., 19.]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_reduce(image2, (4, 5), func=np.mean) + expected2 = np.array([[14. , 10.8], + [ 8.5, 5.7]]) + assert_array_equal(expected2, out2) + + +def test_block_reduce_median(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_reduce(image1, (2, 3), func=np.median) + expected1 = np.array([[ 4., 7.], + [ 16., 19.]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_reduce(image2, (4, 5), func=np.median) + expected2 = np.array([[ 14., 17.], + [ 0., 0.]]) + assert_array_equal(expected2, out2) + + image3 = np.array([[1, 5, 5, 5], [5, 5, 5, 1000]]) + out3 = block_reduce(image3, (2, 4), func=np.median) + assert_array_equal(5, out3) + + +def test_block_reduce_min(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_reduce(image1, (2, 3), func=np.min) + expected1 = np.array([[ 0, 3], + [12, 15]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_reduce(image2, (4, 5), func=np.min) + expected2 = np.array([[0, 0], + [0, 0]]) + assert_array_equal(expected2, out2) + + +def test_block_reduce_max(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_reduce(image1, (2, 3), func=np.max) + expected1 = np.array([[ 8, 11], + [20, 23]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_reduce(image2, (4, 5), func=np.max) + expected2 = np.array([[28, 31], + [36, 39]]) + assert_array_equal(expected2, out2) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_marching_cubes.py b/skimage/measure/tests/test_marching_cubes.py new file mode 100644 index 00000000..b3c2ddc1 --- /dev/null +++ b/skimage/measure/tests/test_marching_cubes.py @@ -0,0 +1,40 @@ +import numpy as np +from numpy.testing import assert_raises + +from skimage.draw import ellipsoid, ellipsoid_stats +from skimage.measure import marching_cubes, mesh_surface_area + + +def test_marching_cubes_isotropic(): + ellipsoid_isotropic = ellipsoid(6, 10, 16, levelset=True) + _, surf = ellipsoid_stats(6, 10, 16) + verts, faces = marching_cubes(ellipsoid_isotropic, 0.) + surf_calc = mesh_surface_area(verts, faces) + + # Test within 1% tolerance for isotropic. Will always underestimate. + assert surf > surf_calc and surf_calc > surf * 0.99 + + +def test_marching_cubes_anisotropic(): + spacing = (1., 10 / 6., 16 / 6.) + ellipsoid_anisotropic = ellipsoid(6, 10, 16, spacing=spacing, + levelset=True) + _, surf = ellipsoid_stats(6, 10, 16) + verts, faces = marching_cubes(ellipsoid_anisotropic, 0., + spacing=spacing) + surf_calc = mesh_surface_area(verts, faces) + + # Test within 1.5% tolerance for anisotropic. Will always underestimate. + assert surf > surf_calc and surf_calc > surf * 0.985 + + +def test_invalid_input(): + assert_raises(ValueError, marching_cubes, np.zeros((2, 2, 1)), 0) + assert_raises(ValueError, marching_cubes, np.zeros((2, 2, 1)), 1) + assert_raises(ValueError, marching_cubes, np.ones((3, 3, 3)), 1, + spacing=(1, 2)) + assert_raises(ValueError, marching_cubes, np.zeros((20, 20)), 0) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_moments.py b/skimage/measure/tests/test_moments.py new file mode 100644 index 00000000..0667f688 --- /dev/null +++ b/skimage/measure/tests/test_moments.py @@ -0,0 +1,72 @@ +from numpy.testing import assert_equal, assert_almost_equal +import numpy as np + +from skimage.measure import (moments, moments_central, moments_normalized, + moments_hu) + + +def test_moments(): + image = np.zeros((20, 20), dtype=np.double) + image[14, 14] = 1 + image[15, 15] = 1 + image[14, 15] = 0.5 + image[15, 14] = 0.5 + m = moments(image) + assert_equal(m[0, 0], 3) + assert_almost_equal(m[0, 1] / m[0, 0], 14.5) + assert_almost_equal(m[1, 0] / m[0, 0], 14.5) + + +def test_moments_central(): + image = np.zeros((20, 20), dtype=np.double) + image[14, 14] = 1 + image[15, 15] = 1 + image[14, 15] = 0.5 + image[15, 14] = 0.5 + mu = moments_central(image, 14.5, 14.5) + + # shift image by dx=2, dy=2 + image2 = np.zeros((20, 20), dtype=np.double) + image2[16, 16] = 1 + image2[17, 17] = 1 + image2[16, 17] = 0.5 + image2[17, 16] = 0.5 + mu2 = moments_central(image2, 14.5 + 2, 14.5 + 2) + # central moments must be translation invariant + assert_equal(mu, mu2) + + +def test_moments_normalized(): + image = np.zeros((20, 20), dtype=np.double) + image[13:17, 13:17] = 1 + mu = moments_central(image, 14.5, 14.5) + nu = moments_normalized(mu) + # shift image by dx=-3, dy=-3 and scale by 0.5 + image2 = np.zeros((20, 20), dtype=np.double) + image2[11:13, 11:13] = 1 + mu2 = moments_central(image2, 11.5, 11.5) + nu2 = moments_normalized(mu2) + # central moments must be translation and scale invariant + assert_almost_equal(nu, nu2, decimal=1) + + +def test_moments_hu(): + image = np.zeros((20, 20), dtype=np.double) + image[13:15, 13:17] = 1 + mu = moments_central(image, 13.5, 14.5) + nu = moments_normalized(mu) + hu = moments_hu(nu) + # shift image by dx=2, dy=3, scale by 0.5 and rotate by 90deg + image2 = np.zeros((20, 20), dtype=np.double) + image2[11, 11:13] = 1 + image2 = image2.T + mu2 = moments_central(image2, 11.5, 11) + nu2 = moments_normalized(mu2) + hu2 = moments_hu(nu2) + # central moments must be translation and scale invariant + assert_almost_equal(hu, hu2, decimal=1) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index c1396670..647c09a7 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -22,33 +22,42 @@ INTENSITY_SAMPLE = SAMPLE.copy() INTENSITY_SAMPLE[1, 9:11] = 2 -def test_unsupported_dtype(): +def test_all_props(): + regions = regionprops(SAMPLE, 'all', INTENSITY_SAMPLE)[0] + for prop in PROPS: + regions[prop] + + +def test_dtype(): + regionprops(np.zeros((10, 10), dtype=np.int)) + regionprops(np.zeros((10, 10), dtype=np.uint)) assert_raises(TypeError, regionprops, np.zeros((10, 10), dtype=np.double)) -def test_all_props(): - props = regionprops(SAMPLE, 'all', INTENSITY_SAMPLE)[0] - for prop in PROPS: - assert prop in props +def test_ndim(): + regionprops(np.zeros((10, 10), dtype=np.int)) + regionprops(np.zeros((10, 10, 1), dtype=np.int)) + regionprops(np.zeros((10, 10, 1, 1), dtype=np.int)) + assert_raises(TypeError, regionprops, np.zeros((10, 10, 2), dtype=np.int)) def test_area(): - area = regionprops(SAMPLE, ['Area'])[0]['Area'] + area = regionprops(SAMPLE)[0].area assert area == np.sum(SAMPLE) def test_bbox(): - bbox = regionprops(SAMPLE, ['BoundingBox'])[0]['BoundingBox'] + bbox = regionprops(SAMPLE)[0].bbox assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1])) SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[:, -1] = 0 - bbox = regionprops(SAMPLE_mod, ['BoundingBox'])[0]['BoundingBox'] + bbox = regionprops(SAMPLE_mod)[0].bbox assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]-1)) -def test_central_moments(): - mu = regionprops(SAMPLE, ['CentralMoments'])[0]['CentralMoments'] +def test_moments_central(): + mu = regionprops(SAMPLE)[0].moments_central # determined with OpenCV assert_almost_equal(mu[0,2], 436.00000000000045) # different from OpenCV results, bug in OpenCV @@ -61,19 +70,19 @@ def test_central_moments(): def test_centroid(): - centroid = regionprops(SAMPLE, ['Centroid'])[0]['Centroid'] + centroid = regionprops(SAMPLE)[0].centroid # determined with MATLAB assert_array_almost_equal(centroid, (5.66666666666666, 9.444444444444444)) def test_convex_area(): - area = regionprops(SAMPLE, ['ConvexArea'])[0]['ConvexArea'] + area = regionprops(SAMPLE)[0].convex_area # determined with MATLAB assert area == 124 def test_convex_image(): - img = regionprops(SAMPLE, ['ConvexImage'])[0]['ConvexImage'] + img = regionprops(SAMPLE)[0].convex_image # determined with MATLAB ref = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0], @@ -94,43 +103,43 @@ def test_coordinates(): sample = np.zeros((10, 10), dtype=np.int8) coords = np.array([[3, 2], [3, 3], [3, 4]]) sample[coords[:, 0], coords[:, 1]] = 1 - prop_coords = regionprops(sample, ['Coordinates'])[0]['Coordinates'] + prop_coords = regionprops(sample)[0].coords assert_array_equal(prop_coords, coords) def test_eccentricity(): - eps = regionprops(SAMPLE, ['Eccentricity'])[0]['Eccentricity'] + eps = regionprops(SAMPLE)[0].eccentricity assert_almost_equal(eps, 0.814629313427) img = np.zeros((5, 5), dtype=np.int) img[2, 2] = 1 - eps = regionprops(img, ['Eccentricity'])[0]['Eccentricity'] + eps = regionprops(img)[0].eccentricity assert_almost_equal(eps, 0) def test_equiv_diameter(): - diameter = regionprops(SAMPLE, ['EquivDiameter'])[0]['EquivDiameter'] + diameter = regionprops(SAMPLE)[0].equivalent_diameter # determined with MATLAB assert_almost_equal(diameter, 9.57461472963) def test_euler_number(): - en = regionprops(SAMPLE, ['EulerNumber'])[0]['EulerNumber'] + en = regionprops(SAMPLE)[0].euler_number assert en == 0 SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[7, -3] = 0 - en = regionprops(SAMPLE_mod, ['EulerNumber'])[0]['EulerNumber'] + en = regionprops(SAMPLE_mod)[0].euler_number assert en == -1 def test_extent(): - extent = regionprops(SAMPLE, ['Extent'])[0]['Extent'] + extent = regionprops(SAMPLE)[0].extent assert_almost_equal(extent, 0.4) -def test_hu_moments(): - hu = regionprops(SAMPLE, ['HuMoments'])[0]['HuMoments'] +def test_moments_hu(): + hu = regionprops(SAMPLE)[0].moments_hu ref = np.array([ 3.27117627e-01, 2.63869194e-02, @@ -145,59 +154,64 @@ def test_hu_moments(): def test_image(): - img = regionprops(SAMPLE, ['Image'])[0]['Image'] + img = regionprops(SAMPLE)[0].image assert_array_equal(img, SAMPLE) +def test_label(): + label = regionprops(SAMPLE)[0].label + assert_array_equal(label, 1) + + def test_filled_area(): - area = regionprops(SAMPLE, ['FilledArea'])[0]['FilledArea'] + area = regionprops(SAMPLE)[0].filled_area assert area == np.sum(SAMPLE) SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[7, -3] = 0 - area = regionprops(SAMPLE_mod, ['FilledArea'])[0]['FilledArea'] + area = regionprops(SAMPLE_mod)[0].filled_area assert area == np.sum(SAMPLE) def test_filled_image(): - img = regionprops(SAMPLE, ['FilledImage'])[0]['FilledImage'] + img = regionprops(SAMPLE)[0].filled_image assert_array_equal(img, SAMPLE) def test_major_axis_length(): - length = regionprops(SAMPLE, ['MajorAxisLength'])[0]['MajorAxisLength'] + length = regionprops(SAMPLE)[0].major_axis_length # MATLAB has different interpretation of ellipse than found in literature, # here implemented as found in literature assert_almost_equal(length, 16.7924234999) def test_max_intensity(): - intensity = regionprops(SAMPLE, ['MaxIntensity'], INTENSITY_SAMPLE - )[0]['MaxIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].max_intensity assert_almost_equal(intensity, 2) def test_mean_intensity(): - intensity = regionprops(SAMPLE, ['MeanIntensity'], INTENSITY_SAMPLE - )[0]['MeanIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].mean_intensity assert_almost_equal(intensity, 1.02777777777777) def test_min_intensity(): - intensity = regionprops(SAMPLE, ['MinIntensity'], INTENSITY_SAMPLE - )[0]['MinIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].min_intensity assert_almost_equal(intensity, 1) def test_minor_axis_length(): - length = regionprops(SAMPLE, ['MinorAxisLength'])[0]['MinorAxisLength'] + length = regionprops(SAMPLE)[0].minor_axis_length # MATLAB has different interpretation of ellipse than found in literature, # here implemented as found in literature assert_almost_equal(length, 9.739302807263) def test_moments(): - m = regionprops(SAMPLE, ['Moments'])[0]['Moments'] + m = regionprops(SAMPLE)[0].moments # determined with OpenCV assert_almost_equal(m[0,0], 72.0) assert_almost_equal(m[0,1], 408.0) @@ -211,8 +225,8 @@ def test_moments(): assert_almost_equal(m[3,0], 95588.0) -def test_normalized_moments(): - nu = regionprops(SAMPLE, ['NormalizedMoments'])[0]['NormalizedMoments'] +def test_moments_normalized(): + nu = regionprops(SAMPLE)[0].moments_normalized # determined with OpenCV assert_almost_equal(nu[0,2], 0.08410493827160502) assert_almost_equal(nu[1,1], -0.016846707818929982) @@ -223,29 +237,26 @@ def test_normalized_moments(): def test_orientation(): - orientation = regionprops(SAMPLE, ['Orientation'])[0]['Orientation'] + orientation = regionprops(SAMPLE)[0].orientation # determined with MATLAB assert_almost_equal(orientation, 0.10446844651921) # test correct quadrant determination - orientation2 = regionprops(SAMPLE.T, ['Orientation'])[0]['Orientation'] + orientation2 = regionprops(SAMPLE.T)[0].orientation assert_almost_equal(orientation2, math.pi / 2 - orientation) # test diagonal regions diag = np.eye(10, dtype=int) - orientation_diag = regionprops(diag, ['Orientation'])[0]['Orientation'] + orientation_diag = regionprops(diag)[0].orientation assert_almost_equal(orientation_diag, -math.pi / 4) - orientation_diag = regionprops(np.flipud(diag), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.flipud(diag))[0].orientation assert_almost_equal(orientation_diag, math.pi / 4) - orientation_diag = regionprops(np.fliplr(diag), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.fliplr(diag))[0].orientation assert_almost_equal(orientation_diag, math.pi / 4) - orientation_diag = regionprops(np.fliplr(np.flipud(diag)), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.fliplr(np.flipud(diag)))[0].orientation assert_almost_equal(orientation_diag, -math.pi / 4) def test_perimeter(): - per = regionprops(SAMPLE, ['Perimeter'])[0]['Perimeter'] + per = regionprops(SAMPLE)[0].perimeter assert_almost_equal(per, 55.2487373415) per = perimeter(SAMPLE.astype('double'), neighbourhood=8) @@ -253,14 +264,14 @@ def test_perimeter(): def test_solidity(): - solidity = regionprops(SAMPLE, ['Solidity'])[0]['Solidity'] + solidity = regionprops(SAMPLE)[0].solidity # determined with MATLAB assert_almost_equal(solidity, 0.580645161290323) -def test_weighted_central_moments(): - wmu = regionprops(SAMPLE, ['WeightedCentralMoments'], INTENSITY_SAMPLE - )[0]['WeightedCentralMoments'] +def test_weighted_moments(): + wmu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_moments_central ref = np.array( [[ 7.4000000000e+01, -2.1316282073e-13, 4.7837837838e+02, -7.5943608473e+02], @@ -276,14 +287,14 @@ def test_weighted_central_moments(): def test_weighted_centroid(): - centroid = regionprops(SAMPLE, ['WeightedCentroid'], INTENSITY_SAMPLE - )[0]['WeightedCentroid'] + centroid = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_centroid assert_array_almost_equal(centroid, (5.540540540540, 9.445945945945)) -def test_weighted_hu_moments(): - whu = regionprops(SAMPLE, ['WeightedHuMoments'], INTENSITY_SAMPLE - )[0]['WeightedHuMoments'] +def test_weighted_moments_hu(): + whu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_moments_hu ref = np.array([ 3.1750587329e-01, 2.1417517159e-02, @@ -297,8 +308,8 @@ def test_weighted_hu_moments(): def test_weighted_moments(): - wm = regionprops(SAMPLE, ['WeightedMoments'], INTENSITY_SAMPLE - )[0]['WeightedMoments'] + wm = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_moments ref = np.array( [[ 7.4000000000e+01, 4.1000000000e+02, 2.7500000000e+03, 1.9778000000e+04], @@ -312,9 +323,9 @@ def test_weighted_moments(): assert_array_almost_equal(wm, ref) -def test_weighted_normalized_moments(): - wnu = regionprops(SAMPLE, ['WeightedNormalizedMoments'], INTENSITY_SAMPLE - )[0]['WeightedNormalizedMoments'] +def test_weighted_moments_normalized(): + wnu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_moments_normalized ref = np.array( [[ np.nan, np.nan, 0.0873590903, -0.0161217406], [ np.nan, -0.0160405109, -0.0031421072, -0.0031376984], diff --git a/skimage/measure/tests/test_structural_similarity.py b/skimage/measure/tests/test_structural_similarity.py index ec5ce7ef..e08f2c31 100644 --- a/skimage/measure/tests/test_structural_similarity.py +++ b/skimage/measure/tests/test_structural_similarity.py @@ -68,5 +68,6 @@ def test_invalid_input(): assert_raises(ValueError, ssim, X, X, win_size=8) + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index c8fafe6f..f01fc3ed 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -4,7 +4,8 @@ from .grey import (erosion, dilation, opening, closing, white_tophat, black_tophat, greyscale_erode, greyscale_dilate, greyscale_open, greyscale_close, greyscale_white_top_hat, greyscale_black_top_hat) -from .selem import square, rectangle, diamond, disk, cube, octahedron, ball +from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball, + octagon, star) from .ccomp import label from .watershed import watershed, is_local_maximum from ._skeletonize import skeletonize, medial_axis @@ -36,6 +37,7 @@ __all__ = ['binary_erosion', 'cube', 'octahedron', 'ball', + 'octagon', 'label', 'watershed', 'is_local_maximum', diff --git a/skimage/morphology/_convex_hull.pyx b/skimage/morphology/_convex_hull.pyx index 7298e1ed..cd9270cc 100644 --- a/skimage/morphology/_convex_hull.pyx +++ b/skimage/morphology/_convex_hull.pyx @@ -7,7 +7,7 @@ import numpy as np cimport numpy as cnp -def possible_hull(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] img): +def possible_hull(cnp.uint8_t[:, ::1] img): """Return positions of pixels that possibly belong to the convex hull. Parameters @@ -30,31 +30,43 @@ def possible_hull(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] img): # cols storage slots for top boundary pixels # rows storage slots for right boundary pixels # cols storage slots for bottom boundary pixels - cdef cnp.ndarray[dtype=cnp.intp_t, ndim=2] nonzero = \ - np.ones((2 * (rows + cols), 2), dtype=np.intp) - nonzero *= -1 + coords = np.ones((2 * (rows + cols), 2), dtype=np.intp) + coords *= -1 + + cdef Py_ssize_t[:, ::1] nonzero = coords + cdef Py_ssize_t rows_cols = rows + cols + cdef Py_ssize_t rows_2_cols = 2 * rows + cols + cdef Py_ssize_t rows_cols_r, rows_c for r in range(rows): + + rows_cols_r = rows_cols + r + for c in range(cols): + if img[r, c] != 0: + + rows_c = rows + c + rows_2_cols_c = rows_2_cols + c + # Left check if nonzero[r, 1] == -1: nonzero[r, 0] = r nonzero[r, 1] = c # Right check - elif nonzero[rows + cols + r, 1] < c: - nonzero[rows + cols + r, 0] = r - nonzero[rows + cols + r, 1] = c + elif nonzero[rows_cols_r, 1] < c: + nonzero[rows_cols_r, 0] = r + nonzero[rows_cols_r, 1] = c # Top check - if nonzero[rows + c, 1] == -1: - nonzero[rows + c, 0] = r - nonzero[rows + c, 1] = c + if nonzero[rows_c, 1] == -1: + nonzero[rows_c, 0] = r + nonzero[rows_c, 1] = c # Bottom check - elif nonzero[2 * rows + cols + c, 0] < r: - nonzero[2 * rows + cols + c, 0] = r - nonzero[2 * rows + cols + c, 1] = c + elif nonzero[rows_2_cols_c, 0] < r: + nonzero[rows_2_cols_c, 0] = r + nonzero[rows_2_cols_c, 1] = c - return nonzero[nonzero[:, 0] != -1] + return coords[coords[:, 0] != -1] diff --git a/skimage/morphology/_greyreconstruct.pyx b/skimage/morphology/_greyreconstruct.pyx index e8a84f3b..fa92ecec 100644 --- a/skimage/morphology/_greyreconstruct.pyx +++ b/skimage/morphology/_greyreconstruct.pyx @@ -21,8 +21,8 @@ def reconstruction_loop(cnp.ndarray[dtype=cnp.uint32_t, ndim=1, negative_indices=False, mode='c'] anext, cnp.ndarray[dtype=cnp.int32_t, ndim=1, negative_indices=False, mode='c'] astrides, - int current_idx, - int image_stride): + Py_ssize_t current_idx, + Py_ssize_t image_stride): """The inner loop for reconstruction. This algorithm uses the rank-order of pixels. If low intensity pixels have diff --git a/skimage/morphology/_pnpoly.pyx b/skimage/morphology/_pnpoly.pyx index f32778cc..12b48e5d 100644 --- a/skimage/morphology/_pnpoly.pyx +++ b/skimage/morphology/_pnpoly.pyx @@ -29,7 +29,7 @@ def grid_points_inside_poly(shape, verts): True where the grid falls inside the polygon. """ - cdef cnp.ndarray[cnp.double_t, ndim=1, mode="c"] vx, vy + cdef double[:] vx, vy verts = np.asarray(verts) vx = verts[:, 0].astype(np.double) @@ -45,8 +45,7 @@ def grid_points_inside_poly(shape, verts): for m in range(M): for n in range(N): - out[m, n] = point_in_polygon(V, vx.data, vy.data, - m, n) + out[m, n] = point_in_polygon(V, &vx[0], &vy[0], m, n) return out.view(bool) @@ -57,18 +56,18 @@ def points_inside_poly(points, verts): Parameters ---------- points : (N, 2) array - Input points, ``(x, y)``. + Input points, ``(x, y)``. verts : (M, 2) array - Vertices of the polygon, sorted either clockwise or anti-clockwise. - The first point may (but does not need to be) duplicated. + Vertices of the polygon, sorted either clockwise or anti-clockwise. + The first point may (but does not need to be) duplicated. Returns ------- mask : (N,) array of bool - True if corresponding point is inside the polygon. + True if corresponding point is inside the polygon. """ - cdef cnp.ndarray[cnp.double_t, ndim=1, mode="c"] x, y, vx, vy + cdef double[:] x, y, vx, vy points = np.asarray(points) verts = np.asarray(verts) @@ -82,8 +81,8 @@ def points_inside_poly(points, verts): cdef cnp.ndarray[cnp.uint8_t, ndim=1] out = \ np.zeros(x.shape[0], dtype=np.uint8) - points_in_polygon(vx.shape[0], vx.data, vy.data, - x.shape[0], x.data, y.data, + points_in_polygon(vx.shape[0], &vx[0], &vy[0], + x.shape[0], &x[0], &y[0], out.data) return out.astype(bool) diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py index bd9f225c..8872aada 100644 --- a/skimage/morphology/_skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -212,6 +212,7 @@ def medial_axis(image, mask=None, return_distance=False): Examples -------- + >>> from skimage import morphology >>> square = np.zeros((7, 7), dtype=np.uint8) >>> square[1:-1, 2:-2] = 1 >>> square diff --git a/skimage/morphology/_skeletonize_cy.pyx b/skimage/morphology/_skeletonize_cy.pyx index 13e303d4..10e70d98 100644 --- a/skimage/morphology/_skeletonize_cy.pyx +++ b/skimage/morphology/_skeletonize_cy.pyx @@ -19,16 +19,9 @@ import numpy as np cimport numpy as cnp -def _skeletonize_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] result, - cnp.ndarray[dtype=cnp.intp_t, ndim=1, - negative_indices=False, mode='c'] i, - cnp.ndarray[dtype=cnp.intp_t, ndim=1, - negative_indices=False, mode='c'] j, - cnp.ndarray[dtype=cnp.int32_t, ndim=1, - negative_indices=False, mode='c'] order, - cnp.ndarray[dtype=cnp.uint8_t, ndim=1, - negative_indices=False, mode='c'] table): +def _skeletonize_loop(cnp.uint8_t[:, ::1] result, + Py_ssize_t[:] i, Py_ssize_t[:] j, + cnp.int32_t[:] order, cnp.uint8_t[:] table): """ Inner loop of skeletonize function @@ -65,9 +58,11 @@ def _skeletonize_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, pixels. """ cdef: - cnp.int32_t accumulator + Py_ssize_t accumulator Py_ssize_t index, order_index Py_ssize_t ii, jj + Py_ssize_t rows = result.shape[0] + Py_ssize_t cols = result.shape[1] for index in range(order.shape[0]): accumulator = 16 @@ -80,26 +75,25 @@ def _skeletonize_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, accumulator += 1 if result[ii - 1, jj]: accumulator += 2 - if jj < result.shape[1] - 1 and result[ii - 1, jj + 1]: + if jj < cols - 1 and result[ii - 1, jj + 1]: accumulator += 4 if jj > 0 and result[ii, jj - 1]: accumulator += 8 - if jj < result.shape[1] - 1 and result[ii, jj + 1]: + if jj < cols - 1 and result[ii, jj + 1]: accumulator += 32 - if ii < result.shape[0]-1: + if ii < rows - 1: if jj > 0 and result[ii + 1, jj - 1]: accumulator += 64 if result[ii + 1, jj]: accumulator += 128 - if jj < result.shape[1] - 1 and result[ii + 1, jj + 1]: + if jj < cols - 1 and result[ii + 1, jj + 1]: accumulator += 256 # Assign the value of table corresponding to the configuration result[ii, jj] = table[accumulator] -def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] image): +def _table_lookup_index(cnp.uint8_t[:, ::1] image): """ Return an index into a table per pixel of a binary image @@ -120,9 +114,8 @@ def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, hardwired kernel. """ cdef: - cnp.ndarray[dtype=cnp.int32_t, ndim=2, - negative_indices=False, mode='c'] indexer - cnp.int32_t *p_indexer + Py_ssize_t[:, ::1] indexer + Py_ssize_t *p_indexer cnp.uint8_t *p_image Py_ssize_t i_stride Py_ssize_t i_shape @@ -133,9 +126,9 @@ def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, i_shape = image.shape[0] j_shape = image.shape[1] - indexer = np.zeros((i_shape, j_shape), np.int32) - p_indexer = indexer.data - p_image = image.data + indexer = np.zeros((i_shape, j_shape), dtype=np.intp) + p_indexer = &indexer[0, 0] + p_image = &image[0, 0] i_stride = image.strides[0] assert i_shape >= 3 and j_shape >= 3, \ "Please use the slow method for arrays < 3x3" @@ -214,4 +207,4 @@ def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, indexer[i - 1, j_shape - 1] += 128 indexer[i, j_shape - 1] += 16 indexer[i + 1, j_shape - 1] += 2 - return indexer + return np.asarray(indexer) diff --git a/skimage/morphology/_watershed.pyx b/skimage/morphology/_watershed.pyx index 122f0262..beaf874b 100644 --- a/skimage/morphology/_watershed.pyx +++ b/skimage/morphology/_watershed.pyx @@ -23,17 +23,12 @@ include "heap_watershed.pxi" @cython.boundscheck(False) -def watershed(np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, - mode='c'] image, - np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, - mode='c'] pq, +def watershed(DTYPE_INT32_t[:] image, + DTYPE_INT32_t[:, ::1] pq, Py_ssize_t age, - np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, - mode='c'] structure, - np.ndarray[DTYPE_BOOL_t, ndim=1, negative_indices=False, - mode='c'] mask, - np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, - mode='c'] output): + DTYPE_INT32_t[:, ::1] structure, + DTYPE_BOOL_t[:] mask, + DTYPE_INT32_t[:] output): """Do heavy lifting of watershed algorithm Parameters diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index e2e0f20b..89ae8057 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -1,3 +1,4 @@ +import warnings import numpy as np from scipy import ndimage @@ -8,32 +9,40 @@ def binary_erosion(image, selem, out=None): This function returns the same result as greyscale erosion but performs faster for binary images. - Morphological erosion sets a pixel at (i,j) to the minimum over all pixels - in the neighborhood centered at (i,j). Erosion shrinks bright regions and - enlarges dark regions. + Morphological erosion sets a pixel at ``(i,j)`` to the minimum over all + pixels in the neighborhood centered at ``(i,j)``. Erosion shrinks bright + regions and enlarges dark regions. Parameters ---------- image : ndarray - Image array. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool The array to store the result of the morphology. If None is passed, a new array will be allocated. Returns ------- - eroded : bool array - The result of the morphological erosion. + eroded : ndarray of bool or intp + The result of the morphological erosion with values in ``[0, 1]``. """ + selem = (selem != 0) + selem_sum = np.sum(selem) - conv = ndimage.convolve(image > 0, selem, output=out, - mode='constant', cval=1) - if conv is not None: + if selem_sum <= 255: + conv = np.empty_like(image, dtype=np.uint8) + else: + conv = np.empty_like(image, dtype=np.intp) + + binary = (image > 0).view(np.uint8) + ndimage.convolve(binary, selem, mode='constant', cval=1, output=conv) + + if out is None: out = conv - return np.equal(out, np.sum(selem), out=out) + return np.equal(conv, selem_sum, out=out) def binary_dilation(image, selem, out=None): @@ -42,33 +51,40 @@ def binary_dilation(image, selem, out=None): This function returns the same result as greyscale dilation but performs faster for binary images. - Morphological dilation sets a pixel at (i,j) to the maximum over all pixels - in the neighborhood centered at (i,j). Dilation enlarges bright regions - and shrinks dark regions. + Morphological dilation sets a pixel at ``(i,j)`` to the maximum over all + pixels in the neighborhood centered at ``(i,j)``. Dilation enlarges bright + regions and shrinks dark regions. Parameters ---------- image : ndarray - Image array. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool The array to store the result of the morphology. If None, is passed, a new array will be allocated. Returns ------- - dilated : bool array - The result of the morphological dilation. + dilated : ndarray of bool or intp + The result of the morphological dilation with values in ``[0, 1]``. """ + selem = (selem != 0) - conv = ndimage.convolve(image > 0, selem, output=out, - mode='constant', cval=0) - if conv is not None: + if np.sum(selem) <= 255: + conv = np.empty_like(image, dtype=np.uint8) + else: + conv = np.empty_like(image, dtype=np.intp) + + binary = (image > 0).view(np.uint8) + ndimage.convolve(binary, selem, mode='constant', cval=0, output=conv) + + if out is None: out = conv - return np.not_equal(out, 0, out=out) + return np.not_equal(conv, 0, out=out) def binary_opening(image, selem, out=None): @@ -85,20 +101,19 @@ def binary_opening(image, selem, out=None): Parameters ---------- image : ndarray - Image array. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool The array to store the result of the morphology. If None is passed, a new array will be allocated. Returns ------- - opening : bool array + opening : ndarray of bool The result of the morphological opening. """ - eroded = binary_erosion(image, selem) out = binary_dilation(eroded, selem, out=out) return out @@ -118,16 +133,16 @@ def binary_closing(image, selem, out=None): Parameters ---------- image : ndarray - Image array. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool The array to store the result of the morphology. If None, is passed, a new array will be allocated. Returns ------- - closing : bool array + closing : ndarray of bool The result of the morphological closing. """ diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index a09a39a3..4d491c3b 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -8,10 +8,35 @@ cimport numpy as np from libc.stdlib cimport malloc, free -def dilate(np.ndarray[np.uint8_t, ndim=2] image, - np.ndarray[np.uint8_t, ndim=2] selem, - np.ndarray[np.uint8_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): +def _dilate(np.uint8_t[:, :] image, + np.uint8_t[:, :] selem, + np.uint8_t[:, :] out=None, + char shift_x=0, char shift_y=0): + """Return greyscale morphological dilation of an image. + + Morphological dilation sets a pixel at (i,j) to the maximum over all pixels + in the neighborhood centered at (i,j). Dilation enlarges bright regions + and shrinks dark regions. + + Parameters + ---------- + + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None, is + passed, a new array will be allocated. + shift_x, shift_y : bool + shift structuring element about center point. This only affects + eccentric structuring elements (i.e. selem with even numbered sides). + + Returns + ------- + dilated : uint8 array + The result of the morphological dilation. + """ cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] @@ -27,12 +52,9 @@ def dilate(np.ndarray[np.uint8_t, ndim=2] image, else: out = np.ascontiguousarray(out) - cdef np.uint8_t* out_data = out.data - cdef np.uint8_t* image_data = image.data - cdef Py_ssize_t r, c, rr, cc, s, value, local_max - cdef Py_ssize_t selem_num = np.sum(selem != 0) + cdef Py_ssize_t selem_num = np.sum(np.asarray(selem) != 0) cdef Py_ssize_t* sr = malloc(selem_num * sizeof(Py_ssize_t)) cdef Py_ssize_t* sc = malloc(selem_num * sizeof(Py_ssize_t)) @@ -51,22 +73,46 @@ def dilate(np.ndarray[np.uint8_t, ndim=2] image, rr = r + sr[s] cc = c + sc[s] if 0 <= rr < rows and 0 <= cc < cols: - value = image_data[rr * cols + cc] + value = image[rr, cc] if value > local_max: local_max = value - out_data[r * cols + c] = local_max + out[r, c] = local_max free(sr) free(sc) - return out + return np.asarray(out) -def erode(np.ndarray[np.uint8_t, ndim=2] image, - np.ndarray[np.uint8_t, ndim=2] selem, - np.ndarray[np.uint8_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): +def _erode(np.uint8_t[:, :] image, + np.uint8_t[:, :] selem, + np.uint8_t[:, :] out=None, + char shift_x=0, char shift_y=0): + """Return greyscale morphological erosion of an image. + + Morphological erosion sets a pixel at (i,j) to the minimum over all pixels + in the neighborhood centered at (i,j). Erosion shrinks bright regions and + enlarges dark regions. + + Parameters + ---------- + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None is + passed, a new array will be allocated. + shift_x, shift_y : bool + shift structuring element about center point. This only affects + eccentric structuring elements (i.e. selem with even numbered sides). + + Returns + ------- + eroded : uint8 array + The result of the morphological erosion. + """ cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] @@ -82,12 +128,9 @@ def erode(np.ndarray[np.uint8_t, ndim=2] image, else: out = np.ascontiguousarray(out) - cdef np.uint8_t* out_data = out.data - cdef np.uint8_t* image_data = image.data - cdef int r, c, rr, cc, s, value, local_min - cdef Py_ssize_t selem_num = np.sum(selem != 0) + cdef Py_ssize_t selem_num = np.sum(np.asarray(selem) != 0) cdef Py_ssize_t* sr = malloc(selem_num * sizeof(Py_ssize_t)) cdef Py_ssize_t* sc = malloc(selem_num * sizeof(Py_ssize_t)) @@ -106,13 +149,13 @@ def erode(np.ndarray[np.uint8_t, ndim=2] image, rr = r + sr[s] cc = c + sc[s] if 0 <= rr < rows and 0 <= cc < cols: - value = image_data[rr * cols + cc] + value = image[rr, cc] if value < local_min: local_min = value - out_data[r * cols + c] = local_min + out[r, c] = local_min free(sr) free(sc) - return out + return np.asarray(out) diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index 28ff204b..adc63607 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -4,6 +4,7 @@ import numpy as np from ._pnpoly import grid_points_inside_poly from ._convex_hull import possible_hull from skimage.morphology import label +from skimage.util import unique_rows def convex_hull_image(image): @@ -43,7 +44,9 @@ def convex_hull_image(image): (-0.5, 0.5, 0, 0))): coords_corners[i * N:(i + 1) * N] = coords + [x_offset, y_offset] - coords = coords_corners + # repeated coordinates can *sometimes* cause problems in + # scipy.spatial.Delaunay, so we remove them. + coords = unique_rows(coords_corners) try: from scipy.spatial import Delaunay diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 2cca69b6..ef7158b2 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -58,8 +58,8 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): raise NotImplementedError("In-place erosion not supported!") image = img_as_ubyte(image) selem = img_as_ubyte(selem) - return cmorph.erode(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) + return cmorph._erode(image, selem, out=out, + shift_x=shift_x, shift_y=shift_y) def dilation(image, selem, out=None, shift_x=False, shift_y=False): @@ -111,8 +111,8 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): raise NotImplementedError("In-place dilation not supported!") image = img_as_ubyte(image) selem = img_as_ubyte(selem) - return cmorph.dilate(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) + return cmorph._dilate(image, selem, out=out, + shift_x=shift_x, shift_y=shift_y) def opening(image, selem, out=None): diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 049488f8..3fffd28e 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -133,7 +133,7 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): else: selem = selem.copy() - if offset == None: + if offset is None: if not all([d % 2 == 1 for d in selem.shape]): ValueError("Footprint dimensions must all be odd") offset = np.array([d // 2 for d in selem.shape]) @@ -159,7 +159,7 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): # Create a list of strides across the array to get the neighbors within # a flattened array value_stride = np.array(images.strides[1:]) / images.dtype.itemsize - image_stride = images.strides[0] / images.dtype.itemsize + image_stride = images.strides[0] // images.dtype.itemsize selem_mgrid = np.mgrid[[slice(-o, d - o) for d, o in zip(selem.shape, offset)]] selem_offsets = selem_mgrid[:, selem].transpose() diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 820ce4a1..5c157e7f 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -52,7 +52,7 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): True """ # Should use `issubdtype` for bool below, but there's a bug in numpy 1.7 - if not (ar.dtype == bool or np.issubdtype(ar.dtype, int)): + if not (ar.dtype == bool or np.issubdtype(ar.dtype, np.integer)): raise TypeError("Only bool or integer image types are supported. " "Got %s." % ar.dtype) @@ -66,7 +66,8 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): if out.dtype == bool: selem = nd.generate_binary_structure(ar.ndim, connectivity) - ccs = nd.label(ar, selem)[0] + ccs = np.zeros_like(ar, dtype=np.int32) + nd.label(ar, selem, output=ccs) else: ccs = out diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 2cccae22..7e566773 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -15,18 +15,18 @@ def square(width, dtype=np.uint8): Parameters ---------- width : int - The width and height of the square + The width and height of the square Other Parameters ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - A structuring element consisting only of ones, i.e. every - pixel belongs to the neighborhood. + A structuring element consisting only of ones, i.e. every + pixel belongs to the neighborhood. """ return np.ones((width, width), dtype=dtype) @@ -41,21 +41,20 @@ def rectangle(width, height, dtype=np.uint8): Parameters ---------- width : int - The width of the rectangle - + The width of the rectangle height : int - The height of the rectangle + The height of the rectangle Other Parameters ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - A structuring element consisting only of ones, i.e. every - pixel belongs to the neighborhood. + A structuring element consisting only of ones, i.e. every + pixel belongs to the neighborhood. """ return np.ones((width, height), dtype=dtype) @@ -71,17 +70,19 @@ def diamond(radius, dtype=np.uint8): Parameters ---------- radius : int - The radius of the diamond-shaped structuring element. + The radius of the diamond-shaped structuring element. + Other Parameters + ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. """ half = radius (I, J) = np.meshgrid(range(0, radius * 2 + 1), range(0, radius * 2 + 1)) @@ -98,16 +99,18 @@ def disk(radius, dtype=np.uint8): Parameters ---------- radius : int - The radius of the disk-shaped structuring element. + The radius of the disk-shaped structuring element. + Other Parameters + ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. """ L = np.linspace(-radius, radius, 2 * radius + 1) (X, Y) = np.meshgrid(L, L) @@ -125,18 +128,18 @@ def cube(width, dtype=np.uint8): Parameters ---------- width : int - The width, height and depth of the cube + The width, height and depth of the cube Other Parameters ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - A structuring element consisting only of ones, i.e. every - pixel belongs to the neighborhood. + A structuring element consisting only of ones, i.e. every + pixel belongs to the neighborhood. """ return np.ones((width, width, width), dtype=dtype) @@ -153,24 +156,26 @@ def octahedron(radius, dtype=np.uint8): Parameters ---------- radius : int - The radius of the octahedron-shaped structuring element. + The radius of the octahedron-shaped structuring element. + Other Parameters + ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. """ # note that in contrast to diamond(), this method allows non-integer radii n = 2 * radius + 1 - Z, Y, X = np.mgrid[ -radius:radius:n*1j, - -radius:radius:n*1j, - -radius:radius:n*1j] - s = np.abs(X) + np.abs(Y) + np.abs(Z) + 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) @@ -184,20 +189,104 @@ def ball(radius, dtype=np.uint8): Parameters ---------- radius : int - The radius of the ball-shaped structuring element. + The radius of the ball-shaped structuring element. + Other Parameters + ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. """ 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 = X**2 + Y**2 + Z**2 return np.array(s <= radius * radius, dtype=dtype) + + +def octagon(m, n, dtype=np.uint8): + """ + Generates an octagon shaped structuring element with a given size of + horizontal and vertical sides and a given height or width of slanted + sides. The slanted sides are 45 or 135 degrees to the horizontal axis + and hence the widths and heights are equal. + + Parameters + ---------- + m : int + The size of the horizontal and vertical sides. + n : int + The height or width of the slanted sides. + + Other Parameters + ---------------- + dtype : data-type + The data type of the structuring element. + + Returns + ------- + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + + """ + from . import convex_hull_image + selem = np.zeros((m + 2*n, m + 2*n)) + selem[0, n] = 1 + selem[n, 0] = 1 + selem[0, m + n - 1] = 1 + selem[m + n - 1, 0] = 1 + selem[-1, n] = 1 + selem[n, -1] = 1 + selem[-1, m + n - 1] = 1 + selem[m + n - 1, -1] = 1 + selem = convex_hull_image(selem).astype(dtype) + return selem + + +def star(a, dtype=np.uint8): + """ + Generates a star shaped structuring element that has 8 vertices and is an + overlap of square of size `2*a + 1` with its 45 degree rotated version. + The slanted sides are 45 or 135 degrees to the horizontal axis. + + Parameters + ---------- + a : int + Parameter deciding the size of the star structural element. The side + of the square array returned is `2*a + 1 + 2*floor(a / 2)`. + + Other Parameters + ---------------- + dtype : data-type + The data type of the structuring element. + + Returns + ------- + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + + """ + from . import convex_hull_image + if a == 1: + bfilter = np.zeros((3, 3), dtype) + bfilter[:] = 1 + return bfilter + m = 2 * a + 1 + n = a // 2 + selem_square = np.zeros((m + 2 * n, m + 2 * n)) + selem_square[n: m + n, n: m + n] = 1 + c = (m + 2 * n - 1) // 2 + selem_rotated = np.zeros((m + 2 * n, m + 2 * n)) + selem_rotated[0, c] = selem_rotated[-1, c] = selem_rotated[c, 0] = selem_rotated[c, -1] = 1 + selem_rotated = convex_hull_image(selem_rotated).astype(int) + selem = selem_square + selem_rotated + selem[selem > 0] = 1 + return selem.astype(dtype) diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py new file mode 100644 index 00000000..deab3d82 --- /dev/null +++ b/skimage/morphology/tests/test_binary.py @@ -0,0 +1,68 @@ +import numpy as np +from numpy import testing + +from skimage import data, color +from skimage.util import img_as_bool +from skimage.morphology import binary, grey, selem + + +lena = color.rgb2gray(data.lena()) +bw_lena = lena > 100 + + +def test_non_square_image(): + strel = selem.square(3) + binary_res = binary.binary_erosion(bw_lena[:100, :200], strel) + grey_res = img_as_bool(grey.erosion(bw_lena[:100, :200], strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_erosion(): + strel = selem.square(3) + binary_res = binary.binary_erosion(bw_lena, strel) + grey_res = img_as_bool(grey.erosion(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_dilation(): + strel = selem.square(3) + binary_res = binary.binary_dilation(bw_lena, strel) + grey_res = img_as_bool(grey.dilation(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_closing(): + strel = selem.square(3) + binary_res = binary.binary_closing(bw_lena, strel) + grey_res = img_as_bool(grey.closing(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_opening(): + strel = selem.square(3) + binary_res = binary.binary_opening(bw_lena, strel) + grey_res = img_as_bool(grey.opening(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_selem_overflow(): + strel = np.ones((17, 17), dtype=np.uint8) + img = np.zeros((20, 20)) + img[2:19, 2:19] = 1 + binary_res = binary.binary_erosion(img, strel) + grey_res = img_as_bool(grey.erosion(img, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_out_argument(): + for func in (binary.binary_erosion, binary.binary_dilation): + strel = np.ones((3, 3), dtype=np.uint8) + img = np.ones((10, 10)) + out = np.zeros_like(img) + out_saved = out.copy() + func(img, strel, out=out) + testing.assert_(np.any(out != out_saved)) + testing.assert_array_equal(out, func(img, strel)) + +if __name__ == '__main__': + testing.run_module_suite() diff --git a/skimage/morphology/tests/test_convex_hull.py b/skimage/morphology/tests/test_convex_hull.py index 075762ed..ee3b6bfa 100644 --- a/skimage/morphology/tests/test_convex_hull.py +++ b/skimage/morphology/tests/test_convex_hull.py @@ -32,6 +32,19 @@ def test_basic(): assert_array_equal(convex_hull_image(image), expected) +@skipif(not scipy_spatial) +def test_pathological_qhull_example(): + image = np.array( + [[0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1], + [1, 1, 1, 0, 0, 0, 0]], dtype=bool) + expected = np.array( + [[0, 0, 0, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 0, 0, 0]], dtype=bool) + assert_array_equal(convex_hull_image(image), expected) + + @skipif(not scipy_spatial) def test_possible_hull(): image = np.array( diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 244ec566..e2a3928d 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -6,7 +6,7 @@ from numpy import testing import skimage from skimage import data_dir from skimage.util import img_as_bool -from skimage.morphology import binary, grey, selem +from skimage.morphology import grey, selem lena = np.load(os.path.join(data_dir, 'lena_GRAY_U8.npy')) @@ -155,40 +155,5 @@ class TestDTypes(): self._test_image(image) -def test_non_square_image(): - strel = selem.square(3) - binary_res = binary.binary_erosion(bw_lena[:100, :200], strel) - grey_res = img_as_bool(grey.erosion(bw_lena[:100, :200], strel)) - testing.assert_array_equal(binary_res, grey_res) - - -def test_binary_erosion(): - strel = selem.square(3) - binary_res = binary.binary_erosion(bw_lena, strel) - grey_res = img_as_bool(grey.erosion(bw_lena, strel)) - testing.assert_array_equal(binary_res, grey_res) - - -def test_binary_dilation(): - strel = selem.square(3) - binary_res = binary.binary_dilation(bw_lena, strel) - grey_res = img_as_bool(grey.dilation(bw_lena, strel)) - testing.assert_array_equal(binary_res, grey_res) - - -def test_binary_closing(): - strel = selem.square(3) - binary_res = binary.binary_closing(bw_lena, strel) - grey_res = img_as_bool(grey.closing(bw_lena, strel)) - testing.assert_array_equal(binary_res, grey_res) - - -def test_binary_opening(): - strel = selem.square(3) - binary_res = binary.binary_opening(bw_lena, strel) - grey_res = img_as_bool(grey.opening(bw_lena, strel)) - testing.assert_array_equal(binary_res, grey_res) - - if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/morphology/tests/test_misc.py b/skimage/morphology/tests/test_misc.py index a5ab7ba9..94d27f64 100644 --- a/skimage/morphology/tests/test_misc.py +++ b/skimage/morphology/tests/test_misc.py @@ -42,6 +42,19 @@ def test_labeled_image(): assert_array_equal(observed, expected) +def test_uint_image(): + labeled_image = np.array([[2, 2, 2, 0, 1], + [2, 2, 2, 0, 1], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=np.uint8) + expected = np.array([[2, 2, 2, 0, 0], + [2, 2, 2, 0, 0], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=np.uint8) + observed = remove_small_objects(labeled_image, min_size=3) + assert_array_equal(observed, expected) + + def test_float_input(): float_test = np.random.rand(5, 5) assert_raises(TypeError, remove_small_objects, float_test) diff --git a/skimage/morphology/tests/test_selem.py b/skimage/morphology/tests/test_selem.py index 9a8ca895..7895fa58 100644 --- a/skimage/morphology/tests/test_selem.py +++ b/skimage/morphology/tests/test_selem.py @@ -66,6 +66,48 @@ class TestSElem(): def test_selem_octahedron(self): self.strel_worker_3d("diamond-matlab-output.npz", selem.octahedron) + def test_selem_octagon(self): + expected_mask1 = np.array([[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0]], dtype=np.uint8) + actual_mask1 = selem.octagon(5, 3) + expected_mask2 = np.array([[0, 1, 0], + [1, 1, 1], + [0, 1, 0]], dtype=np.uint8) + actual_mask2 = selem.octagon(1, 1) + assert_equal(expected_mask1, actual_mask1) + assert_equal(expected_mask2, actual_mask2) + + def test_selem_star(self): + expected_mask1 = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]], dtype=np.uint8) + actual_mask1 = selem.star(4) + expected_mask2 = np.array([[1, 1, 1], + [1, 1, 1], + [1, 1, 1]], dtype=np.uint8) + actual_mask2 = selem.star(1) + assert_equal(expected_mask1, actual_mask1) + assert_equal(expected_mask2, actual_mask2) + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py index fbd63281..097e3129 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -124,13 +124,13 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): separate overlapping spheres. """ - if connectivity == None: + if connectivity is None: c_connectivity = scipy.ndimage.generate_binary_structure(image.ndim, 1) else: c_connectivity = np.array(connectivity, bool) if c_connectivity.ndim != image.ndim: raise ValueError("Connectivity dimension must be same as image") - if offset == None: + if offset is None: if any([x % 2 == 0 for x in c_connectivity.shape]): raise ValueError("Connectivity array must have an unambiguous " "center") @@ -162,7 +162,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): "as image (ndim=%d)" % (c_markers.ndim, c_image.ndim)) if c_markers.shape != c_image.shape: raise ValueError("image and markers must have the same shape") - if mask != None: + if mask is not None: c_mask = np.ascontiguousarray(mask, dtype=bool) if c_mask.ndim != c_markers.ndim: raise ValueError("mask must have same # of dimensions as image") @@ -398,7 +398,7 @@ def _slow_watershed(image, markers, connectivity=8, mask=None): continue if labels[x, y]: continue - if mask != None and not mask[x, y]: + if mask is not None and not mask[x, y]: continue # label the pixel labels[x, y] = pix_label diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index c3aa1afc..aea6c70f 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,10 +1,10 @@ from .random_walker_segmentation import random_walker from ._felzenszwalb import felzenszwalb -from ._slic import slic +from .slic_superpixels import slic from ._quickshift import quickshift from .boundaries import find_boundaries, visualize_boundaries, mark_boundaries from ._clear_border import clear_border -from ._join import join_segmentations, relabel_from_one +from ._join import join_segmentations, relabel_from_one, relabel_sequential __all__ = ['random_walker', @@ -16,4 +16,5 @@ __all__ = ['random_walker', 'mark_boundaries', 'clear_border', 'join_segmentations', - 'relabel_from_one'] + 'relabel_from_one', + 'relabel_sequential'] diff --git a/skimage/segmentation/_felzenszwalb_cy.pyx b/skimage/segmentation/_felzenszwalb_cy.pyx index 3babb86e..8590e17d 100644 --- a/skimage/segmentation/_felzenszwalb_cy.pyx +++ b/skimage/segmentation/_felzenszwalb_cy.pyx @@ -12,7 +12,8 @@ from skimage.morphology.ccomp cimport find_root, join_trees from ..util import img_as_float -def _felzenszwalb_grey(image, double scale=1, sigma=0.8, Py_ssize_t min_size=20): +def _felzenszwalb_grey(image, double scale=1, sigma=0.8, + Py_ssize_t min_size=20): """Felzenszwalb's efficient graph based segmentation for a single channel. Produces an oversegmentation of a 2d image using a fast, minimum spanning diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index 454da71e..6095382d 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -1,11 +1,13 @@ import numpy as np +from skimage._shared.utils import deprecated + def join_segmentations(s1, s2): """Return the join of the two input segmentations. - The join J of S1 and S2 is defined as the segmentation in which two voxels - are in the same segment in J if and only if they are in the same segment - in *both* S1 and S2. + The join J of S1 and S2 is defined as the segmentation in which two + voxels are in the same segment if and only if they are in the same + segment in *both* S1 and S2. Parameters ---------- @@ -19,7 +21,6 @@ def join_segmentations(s1, s2): Examples -------- - >>> import numpy as np >>> from skimage.segmentation import join_segmentations >>> s1 = np.array([[0, 0, 1, 1], ... [0, 2, 1, 1], @@ -35,36 +36,66 @@ def join_segmentations(s1, s2): if s1.shape != s2.shape: raise ValueError("Cannot join segmentations of different shape. " + "s1.shape: %s, s2.shape: %s" % (s1.shape, s2.shape)) - s1 = relabel_from_one(s1)[0] - s2 = relabel_from_one(s2)[0] + s1 = relabel_sequential(s1)[0] + s2 = relabel_sequential(s2)[0] j = (s2.max() + 1) * s1 + s2 - j = relabel_from_one(j)[0] + j = relabel_sequential(j)[0] return j + +@deprecated('relabel_sequential') def relabel_from_one(label_field): """Convert labels in an arbitrary label field to {1, ... number_of_labels}. + This function is deprecated, see ``relabel_sequential`` for more. + """ + return relabel_sequential(label_field, offset=1) + + +def relabel_sequential(label_field, offset=1): + """Relabel arbitrary labels to {`offset`, ... `offset` + number_of_labels}. + This function also returns the forward map (mapping the original labels to the reduced labels) and the inverse map (mapping the reduced labels back to the original ones). Parameters ---------- - label_field : numpy ndarray (integer type) + label_field : numpy array of int, arbitrary shape + An array of labels. + offset : int, optional + The return labels will start at `offset`, which should be + strictly positive. Returns ------- - relabeled : numpy array of same shape as ar - forward_map : 1d numpy array of length np.unique(ar) + 1 - inverse_map : 1d numpy array of length len(np.unique(ar)) - The length is len(np.unique(ar)) + 1 if 0 is not in np.unique(ar) + relabeled : numpy array of int, same shape as `label_field` + The input label field with labels mapped to + {1, ..., number_of_labels}. + forward_map : numpy array of int, shape ``(label_field.max() + 1,)`` + The map from the original label space to the returned label + space. Can be used to re-apply the same mapping. See examples + for usage. + inverse_map : 1D numpy array of int, of length offset + number of labels + The map from the new label space to the original space. This + can be used to reconstruct the original label field from the + relabeled one. + + Notes + ----- + The label 0 is assumed to denote the background and is never remapped. + + The forward map can be extremely big for some inputs, since its + length is given by the maximum of the label field. However, in most + situations, ``label_field.max()`` is much smaller than + ``label_field.size``, and in these cases the forward map is + guaranteed to be smaller than either the input or output images. Examples -------- - >>> import numpy as np - >>> from skimage.segmentation import relabel_from_one - >>> label_field = array([1, 1, 5, 5, 8, 99, 42]) - >>> relab, fw, inv = relabel_from_one(label_field) + >>> from skimage.segmentation import relabel_sequential + >>> label_field = np.array([1, 1, 5, 5, 8, 99, 42]) + >>> relab, fw, inv = relabel_sequential(label_field) >>> relab array([1, 1, 2, 2, 3, 5, 4]) >>> fw @@ -79,15 +110,20 @@ def relabel_from_one(label_field): True >>> (inv[relab] == label_field).all() True + >>> relab, fw, inv = relabel_sequential(label_field, offset=5) + >>> relab + array([5, 5, 6, 6, 7, 9, 8]) """ labels = np.unique(label_field) labels0 = labels[labels != 0] m = labels.max() - if m == len(labels0): # nothing to do, already 1...n labels + if m == len(labels0): # nothing to do, already 1...n labels return label_field, labels, labels forward_map = np.zeros(m+1, int) - forward_map[labels0] = np.arange(1, len(labels0) + 1) + forward_map[labels0] = np.arange(offset, offset + len(labels0) + 1) if not (labels == 0).any(): labels = np.concatenate(([0], labels)) - inverse_map = labels - return forward_map[label_field], forward_map, inverse_map + inverse_map = np.zeros(offset - 1 + len(labels), dtype=np.intp) + inverse_map[(offset - 1):] = labels + relabeled = forward_map[label_field] + return relabeled, forward_map, inverse_map diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 9a5374d6..c3d95ee0 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -2,140 +2,147 @@ #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False -import numpy as np -from time import time -from scipy import ndimage +from libc.float cimport DBL_MAX +import numpy as np cimport numpy as cnp -from ..util import img_as_float -from ..color import rgb2lab, gray2rgb +from skimage.util import regular_grid -def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, - convert2lab=True): - """Segments image using k-means clustering in Color-(x,y) space. +def _slic_cython(double[:, :, :, ::1] image_zyx, + double[:, ::1] segments, + Py_ssize_t max_iter, + double[::1] spacing): + """Helper function for SLIC segmentation. Parameters ---------- - image : (width, height [, 3]) ndarray - Input image. - n_segments : int, optional (default 100) - The (approximate) number of labels in the segmented output image. - ratio: float, optional (default 10) - Balances color-space proximity and image-space proximity. - Higher values give more weight to color-space. - max_iter : int, optional (default 10) - Maximum number of iterations of k-means. - sigma : float, optional (default 1) - Width of Gaussian smoothing kernel for preprocessing. Zero means no - smoothing. - convert2lab : bool, optional (default True) - Whether the input should be converted to Lab colorspace prior to - segmentation. For this purpose, the input is assumed to be RGB. Highly - recommended. + image_zyx : 4D array of double, shape (Z, Y, X, C) + The input image. + segments : 2D array of double, shape (N, 3 + C) + The initial centroids obtained by SLIC as [Z, Y, X, C...]. + max_iter : int + The maximum number of k-means iterations. + spacing : 1D array of double, shape (3,) + The voxel spacing along each image dimension. This parameter + controls the weights of the distances along z, y, and x during + k-means clustering. Returns ------- - segment_mask : (width, height) ndarray - Integer mask indicating segment labels. + nearest_segments : 3D array of int, shape (Z, Y, X) + The label field/superpixels found by SLIC. Notes ----- - The image is smoothed using a Gaussian kernel prior to segmentation. + The image is considered to be in (z, y, x) order, which can be + surprising. More commonly, the order (x, y, z) is used. However, + in 3D image analysis, 'z' is usually the "special" dimension, with, + for example, a different effective resolution than the other two + axes. Therefore, x and y are often processed together, or viewed as + a cut-plane through the volume. So, if the order was (x, y, z) and + we wanted to look at the 5th cut plane, we would write:: - References - ---------- - .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, - Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to - State-of-the-art Superpixel Methods, TPAMI, May 2012. + my_z_plane = img3d[:, :, 5] - Examples - -------- - >>> from skimage.segmentation import slic - >>> from skimage.data import lena - >>> img = lena() - >>> segments = slic(img, n_segments=100, ratio=10) - >>> # Increasing the ratio parameter yields more square regions - >>> segments = slic(img, n_segments=100, ratio=20) + but, assuming a C-contiguous array, this would grab a discontiguous + slice of memory, which is bad for performance. In contrast, if we + see the image as (z, y, x) ordered, we would do:: + + my_z_plane = img3d[5] + + and get back a contiguous block of memory. This is better both for + performance and for readability. """ - if image.ndim == 2: - image = gray2rgb(image) - if image.ndim != 3 or image.shape[2] != 3: - ValueError("Only 1- or 3-channel 2D images are supported.") - image = ndimage.gaussian_filter(img_as_float(image), [sigma, sigma, 0]) - if convert2lab: - image = rgb2lab(image) - # initialize on grid: - cdef Py_ssize_t height, width - height, width = image.shape[:2] + # initialize on grid + cdef Py_ssize_t depth, height, width + depth = image_zyx.shape[0] + height = image_zyx.shape[1] + width = image_zyx.shape[2] + + cdef Py_ssize_t n_segments = segments.shape[0] + # number of features [X, Y, Z, ...] + cdef Py_ssize_t n_features = segments.shape[1] + # approximate grid size for desired n_segments - cdef Py_ssize_t step = int(np.ceil(np.sqrt(height * width / n_segments))) - grid_y, grid_x = np.mgrid[:height, :width] - means_y = grid_y[::step, ::step] - means_x = grid_x[::step, ::step] + cdef Py_ssize_t step_z, step_y, step_x + slices = regular_grid((depth, height, width), n_segments) + step_z, step_y, step_x = [int(s.step) for s in slices] - means_color = np.zeros((means_y.shape[0], means_y.shape[1], 3)) - cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] means \ - = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) - cdef cnp.float_t* current_mean - cdef cnp.float_t* mean_entry - n_means = means.shape[0] - # we do the scaling of ratio in the same way as in the SLIC paper - # so the values have the same meaning - ratio = (ratio / float(step)) ** 2 - cdef cnp.ndarray[dtype=cnp.float_t, ndim=3] image_yx \ - = np.dstack([grid_y, grid_x, image / ratio]).copy("C") - cdef Py_ssize_t i, k, x, y, x_min, x_max, y_min, y_max, changes - cdef double dist_mean + cdef Py_ssize_t[:, :, ::1] nearest_segments \ + = np.empty((depth, height, width), dtype=np.intp) + cdef double[:, :, ::1] distance \ + = np.empty((depth, height, width), dtype=np.double) + cdef Py_ssize_t[::1] n_segment_elems = np.zeros(n_segments, dtype=np.intp) + + cdef Py_ssize_t i, c, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max + cdef char change + cdef double dist_center, cx, cy, cz, dy, dz + + cdef double sz, sy, sx + sz = spacing[0] + sy = spacing[1] + sx = spacing[2] - cdef cnp.ndarray[dtype=cnp.intp_t, ndim=2] nearest_mean \ - = np.zeros((height, width), dtype=np.intp) - cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] distance \ - = np.empty((height, width)) - cdef cnp.float_t* image_p = image_yx.data - cdef cnp.float_t* distance_p = distance.data - cdef cnp.float_t* current_distance - cdef cnp.float_t* current_pixel - cdef double tmp for i in range(max_iter): - distance.fill(np.inf) - changes = 0 - current_mean = means.data - # assign pixels to means - for k in range(n_means): - # compute windows: - y_min = int(max(current_mean[0] - 2 * step, 0)) - y_max = int(min(current_mean[0] + 2 * step, height)) - x_min = int(max(current_mean[1] - 2 * step, 0)) - x_max = int(min(current_mean[1] + 2 * step, width)) - for y in range(y_min, y_max): - current_pixel = &image_p[5 * (y * width + x_min)] - current_distance = &distance_p[y * width + x_min] - for x in range(x_min, x_max): - mean_entry = current_mean - dist_mean = 0 - for c in range(5): - # you would think the compiler can optimize the squaring - # itself. mine can't (with O2) - tmp = current_pixel[0] - mean_entry[0] - dist_mean += tmp * tmp - current_pixel += 1 - mean_entry += 1 - # some precision issue here. Doesnt work if testing ">" - if current_distance[0] - dist_mean > 1e-10: - nearest_mean[y, x] = k - current_distance[0] = dist_mean - changes += 1 - current_distance += 1 - current_mean += 5 - if changes == 0: + change = 0 + distance[:, :, :] = DBL_MAX + + # assign pixels to segments + for k in range(n_segments): + + # segment coordinate centers + cz = segments[k, 0] + cy = segments[k, 1] + cx = segments[k, 2] + + # compute windows + z_min = max(cz - 2 * step_z, 0) + z_max = min(cz + 2 * step_z + 1, depth) + y_min = max(cy - 2 * step_y, 0) + y_max = min(cy + 2 * step_y + 1, height) + x_min = max(cx - 2 * step_x, 0) + x_max = min(cx + 2 * step_x + 1, width) + + for z in range(z_min, z_max): + dz = (sz * (cz - z)) ** 2 + for y in range(y_min, y_max): + dy = (sy * (cy - y)) ** 2 + for x in range(x_min, x_max): + dist_center = dz + dy + (sx * (cx - x)) ** 2 + for c in range(3, n_features): + dist_center += (image_zyx[z, y, x, c - 3] + - segments[k, c]) ** 2 + if distance[z, y, x] > dist_center: + nearest_segments[z, y, x] = k + distance[z, y, x] = dist_center + change = 1 + + # stop if no pixel changed its segment + if change == 0: break - # recompute means: - means_list = [np.bincount(nearest_mean.ravel(), - image_yx[:, :, j].ravel()) for j in range(5)] - in_mean = np.bincount(nearest_mean.ravel()) - in_mean[in_mean == 0] = 1 - means = (np.vstack(means_list) / in_mean).T.copy("C") - return nearest_mean + + # recompute segment centers + + # sum features for all segments + n_segment_elems[:] = 0 + segments[:, :] = 0 + for z in range(depth): + for y in range(height): + for x in range(width): + k = nearest_segments[z, y, x] + n_segment_elems[k] += 1 + segments[k, 0] += z + segments[k, 1] += y + segments[k, 2] += x + for c in range(3, n_features): + segments[k, c] += image_zyx[z, y, x, c - 3] + + # divide by number of elements per segment to obtain mean + for k in range(n_segments): + for c in range(n_features): + segments[k, c] /= n_segment_elems[k] + + return np.asarray(nearest_segments) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index bd143ed6..17c4c98b 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -12,11 +12,26 @@ import warnings import numpy as np from scipy import sparse, ndimage + +# executive summary for next code block: try to import umfpack from +# scipy, but make sure not to raise a fuss if it fails since it's only +# needed to speed up a few cases. +# See discussions at: +# https://groups.google.com/d/msg/scikit-image/FrM5IGP6wh4/1hp-FtVZmfcJ +# http://stackoverflow.com/questions/13977970/ignore-exceptions-printed-to-stderr-in-del/13977992?noredirect=1#comment28386412_13977992 try: from scipy.sparse.linalg.dsolve import umfpack + old_del = umfpack.UmfpackContext.__del__ + def new_del(self): + try: + old_del(self) + except AttributeError: + pass + umfpack.UmfpackContext.__del__ = new_del UmfpackContext = umfpack.UmfpackContext() except: UmfpackContext = None + try: from pyamg import ruge_stuben_solver amg_loaded = True @@ -62,14 +77,14 @@ def _make_graph_edges_3d(n_x, n_y, n_z): return edges -def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1., +def _compute_weights_3d(data, spacing, beta=130, eps=1.e-6, multichannel=False): # Weight calculation is main difference in multispectral version # Original gradient**2 replaced with sum of gradients ** 2 gradients = 0 for channel in range(0, data.shape[-1]): gradients += _compute_gradients_3d(data[..., channel], - depth=depth) ** 2 + spacing) ** 2 # All channels considered together in this standard deviation beta /= 10 * data.std() if multichannel: @@ -82,10 +97,10 @@ def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1., return weights -def _compute_gradients_3d(data, depth=1.): - gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel() / depth - gr_right = np.abs(data[:, :-1] - data[:, 1:]).ravel() - gr_down = np.abs(data[:-1] - data[1:]).ravel() +def _compute_gradients_3d(data, spacing): + gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel() / spacing[2] + gr_right = np.abs(data[:, :-1] - data[:, 1:]).ravel() / spacing[1] + gr_down = np.abs(data[:-1] - data[1:]).ravel() / spacing[0] return np.r_[gr_deep, gr_right, gr_down] @@ -101,9 +116,10 @@ def _make_laplacian_sparse(edges, weights): lap = sparse.coo_matrix((data, (i_indices, j_indices)), shape=(pixel_nb, pixel_nb)) connect = - np.ravel(lap.sum(axis=1)) - lap = sparse.coo_matrix((np.hstack((data, connect)), - (np.hstack((i_indices, diag)), np.hstack((j_indices, diag)))), - shape=(pixel_nb, pixel_nb)) + lap = sparse.coo_matrix( + (np.hstack((data, connect)), (np.hstack((i_indices, diag)), + np.hstack((j_indices, diag)))), + shape=(pixel_nb, pixel_nb)) return lap.tocsr() @@ -153,14 +169,15 @@ def _mask_edges_weights(edges, weights, mask): # Reassign edges labels to 0, 1, ... edges_number - 1 order = np.searchsorted(np.unique(edges.ravel()), np.arange(max_node_index + 1)) - edges = order[edges] + edges = order[edges.astype(np.int64)] return edges, weights -def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): - l_x, l_y, l_z = data.shape[:3] +def _build_laplacian(data, spacing, mask=None, beta=50, + multichannel=False): + l_x, l_y, l_z = tuple(data.shape[i] for i in range(3)) edges = _make_graph_edges_3d(l_x, l_y, l_z) - weights = _compute_weights_3d(data, beta=beta, eps=1.e-10, depth=depth, + weights = _compute_weights_3d(data, spacing, beta=beta, eps=1.e-10, multichannel=multichannel) if mask is not None: edges, weights = _mask_edges_weights(edges, weights, mask) @@ -173,7 +190,8 @@ def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, - multichannel=False, return_full_prob=False, depth=1.): + multichannel=False, return_full_prob=False, depth=1., + spacing=None): """Random walker algorithm for segmentation from markers. Random walker algorithm is implemented for gray-level or multichannel @@ -214,7 +232,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, - 'cg_mg' (conjugate gradient with multigrid preconditioner): a preconditioner is computed using a multigrid solver, then the solution is computed with the Conjugate Gradient method. This mode - requires that the pyamg module (http://code.google.com/p/pyamg/) is + requires that the pyamg module (http://pyamg.org/) is installed. For images of size > 512x512, this is the recommended (fastest) mode. @@ -231,12 +249,16 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, return_full_prob : bool, default False If True, the probability that a pixel belongs to each of the labels will be returned, instead of only the most likely label. - depth : float, default 1. + depth : float, default 1. [DEPRECATED] Correction for non-isotropic voxel depths in 3D volumes. Default (1.) implies isotropy. This factor is derived as follows: depth = (out-of-plane voxel spacing) / (in-plane voxel spacing), where in-plane voxel spacing represents the first two spatial dimensions and out-of-plane voxel spacing represents the third spatial dimension. + `depth` is deprecated as of 0.9, in favor of `spacing`. + spacing : iterable of floats + Spacing between voxels in each spatial dimension. If `None`, then + the spacing between pixels/voxels in each dimension is assumed 1. Returns ------- @@ -259,12 +281,9 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, Multichannel inputs are scaled with all channel data combined. Ensure all channels are separately normalized prior to running this algorithm. - The `depth` argument is specifically for certain types of 3-dimensional - volumes which, due to how they were acquired, have different spacing - along in-plane and out-of-plane dimensions. This is commonly encountered - in medical imaging. The `depth` argument corrects gradients calculated - along the third spatial dimension for the otherwise inherent assumption - that all points are equally spaced. + The `spacing` argument is specifically for anisotropic datasets, where + data points are spaced differently in one or more spatial dimensions. + Anisotropic data is commonly encountered in medical imaging. The algorithm was first proposed in *Random walks for image segmentation*, Leo Grady, IEEE Trans Pattern Anal Mach Intell. @@ -324,12 +343,31 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, """ - if UmfpackContext is None: + if mode is None: + mode = 'bf' + warnings.warn("Default mode will change in the next release from 'bf' " + "to 'cg_mg' if pyamg is installed, else to 'cg' if " + "SciPy was built with UMFPACK, or to 'bf' otherwise.") + + if UmfpackContext is None and mode == 'cg': warnings.warn('SciPy was built without UMFPACK. Consider rebuilding ' 'SciPy with UMFPACK, this will greatly speed up the ' 'random walker functions. You may also install pyamg ' 'and run the random walker function in cg_mg mode ' '(see the docstrings)') + if depth != 1.: + warnings.warn('`depth` kwarg is deprecated, and will be removed in the' + ' next major release. Use `spacing` instead.') + + # Spacing kwarg checks + if spacing is None: + spacing = (1., 1.) + (depth, ) + elif len(spacing) == 2: + spacing = tuple(spacing) + (depth, ) + elif len(spacing) == 3: + pass + else: + raise ValueError('Input argument `spacing` incorrect, see docstring.') # Parse input data if not multichannel: @@ -363,10 +401,10 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, del filled labels = np.atleast_3d(labels) if np.any(labels < 0): - lap_sparse = _build_laplacian(data, mask=labels >= 0, beta=beta, - depth=depth, multichannel=multichannel) + lap_sparse = _build_laplacian(data, spacing, mask=labels >= 0, + beta=beta, multichannel=multichannel) else: - lap_sparse = _build_laplacian(data, beta=beta, depth=depth, + lap_sparse = _build_laplacian(data, spacing, beta=beta, multichannel=multichannel) lap_sparse, B = _buildAB(lap_sparse, labels) # We solve the linear system @@ -379,7 +417,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, if mode == 'cg_mg': if not amg_loaded: warnings.warn( - """pyamg (http://code.google.com/p/pyamg/)) is needed to use + """pyamg (http://pyamg.org/)) is needed to use this mode, but is not installed. The 'cg' mode will be used instead.""") X = _solve_cg(lap_sparse, B, tol=tol, diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py new file mode 100644 index 00000000..8276b1a8 --- /dev/null +++ b/skimage/segmentation/slic_superpixels.py @@ -0,0 +1,166 @@ +# coding=utf-8 + +import collections as coll +import numpy as np +from scipy import ndimage +import warnings + +from skimage.util import img_as_float, regular_grid +from skimage.segmentation._slic import _slic_cython +from skimage.color import rgb2lab + + +def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, + spacing=None, multichannel=True, convert2lab=True, ratio=None): + """Segments image using k-means clustering in Color-(x,y,z) space. + + Parameters + ---------- + image : 2D, 3D or 4D ndarray + Input image, which can be 2D or 3D, and grayscale or multichannel + (see `multichannel` parameter). + n_segments : int, optional + The (approximate) number of labels in the segmented output image. + compactness : float, optional + Balances color-space proximity and image-space proximity. Higher + values give more weight to image-space. As `compactness` tends to + infinity, superpixel shapes become square/cubic. + max_iter : int, optional + Maximum number of iterations of k-means. + sigma : float or (3,) array-like of floats, optional + Width of Gaussian smoothing kernel for pre-processing for each + dimension of the image. The same sigma is applied to each dimension in + case of a scalar value. Zero means no smoothing. + Note, that `sigma` is automatically scaled if it is scalar and a + manual voxel spacing is provided (see Notes section). + spacing : (3,) array-like of floats, optional + The voxel spacing along each image dimension. By default, `slic` + assumes uniform spacing (same voxel resolution along z, y and x). + This parameter controls the weights of the distances along z, y, + and x during k-means clustering. + multichannel : bool, optional + Whether the last axis of the image is to be interpreted as multiple + channels or another spatial dimension. + convert2lab : bool, optional + Whether the input should be converted to Lab colorspace prior to + segmentation. For this purpose, the input is assumed to be RGB. Highly + recommended. + ratio : float, optional + Synonym for `compactness`. This keyword is deprecated. + + Returns + ------- + labels : 2D or 3D array + Integer mask indicating segment labels. + + Raises + ------ + ValueError + If: + - the image dimension is not 2 or 3 and `multichannel == False`, OR + - the image dimension is not 3 or 4 and `multichannel == True` + + Notes + ----- + * If `sigma > 0`, the image is smoothed using a Gaussian kernel prior to + segmentation. + + * If `sigma` is scalar and `spacing` is provided, the kernel width is + divided along each dimension by the spacing. For example, if ``sigma=1`` + and ``spacing=[5, 1, 1]``, the effective `sigma` is ``[0.2, 1, 1]``. This + ensures sensible smoothing for anisotropic images. + + * The image is rescaled to be in [0, 1] prior to processing. + + * Images of shape (M, N, 3) are interpreted as 2D RGB images by default. To + interpret them as 3D with the last dimension having length 3, use + `multichannel=False`. + + References + ---------- + .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, + Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to + State-of-the-art Superpixel Methods, TPAMI, May 2012. + + Examples + -------- + >>> from skimage.segmentation import slic + >>> from skimage.data import lena + >>> img = lena() + >>> segments = slic(img, n_segments=100, compactness=10) + >>> # Increasing the compactness parameter yields more square regions + >>> segments = slic(img, n_segments=100, compactness=20) + """ + + if sigma is None: + warnings.warn('Default value of keyword `sigma` changed from ``1`` ' + 'to ``0``.') + sigma = 0 + if ratio is not None: + warnings.warn('Keyword `ratio` is deprecated. Use `compactness` ' + 'instead.') + compactness = ratio + + image = img_as_float(image) + is_2d = False + if image.ndim == 2: + # 2D grayscale image + image = image[np.newaxis, ..., np.newaxis] + is_2d = True + elif image.ndim == 3 and multichannel: + # Make 2D multichannel image 3D with depth = 1 + image = image[np.newaxis, ...] + is_2d = True + elif image.ndim == 3 and not multichannel: + # Add channel as single last dimension + image = image[..., np.newaxis] + + if spacing is None: + spacing = np.ones(3) + elif isinstance(spacing, (list, tuple)): + spacing = np.array(spacing, dtype=np.double) + + if not isinstance(sigma, coll.Iterable): + sigma = np.array([sigma, sigma, sigma], dtype=np.double) + sigma /= spacing.astype(np.double) + elif isinstance(sigma, (list, tuple)): + sigma = np.array(sigma, dtype=np.double) + if (sigma > 0).any(): + # add zero smoothing for multichannel dimension + sigma = list(sigma) + [0] + image = ndimage.gaussian_filter(image, sigma) + + if convert2lab and multichannel: + if image.shape[3] != 3: + raise ValueError("Lab colorspace conversion requires a RGB image.") + image = rgb2lab(image) + + depth, height, width = image.shape[:3] + + # initialize cluster centroids for desired number of segments + grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] + slices = regular_grid(image.shape[:3], n_segments) + step_z, step_y, step_x = [int(s.step) for s in slices] + segments_z = grid_z[slices] + segments_y = grid_y[slices] + segments_x = grid_x[slices] + + segments_color = np.zeros(segments_z.shape + (image.shape[3],)) + segments = np.concatenate([segments_z[..., np.newaxis], + segments_y[..., np.newaxis], + segments_x[..., np.newaxis], + segments_color + ], axis=-1).reshape(-1, 3 + image.shape[3]) + segments = np.ascontiguousarray(segments) + + # we do the scaling of ratio in the same way as in the SLIC paper + # so the values have the same meaning + ratio = float(max((step_z, step_y, step_x))) / compactness + image = np.ascontiguousarray(image * ratio) + + labels = _slic_cython(image, segments, max_iter, spacing) + + if is_2d: + labels = labels[0] + + return labels diff --git a/skimage/segmentation/tests/test_join.py b/skimage/segmentation/tests/test_join.py index f03244e9..548fcc8d 100644 --- a/skimage/segmentation/tests/test_join.py +++ b/skimage/segmentation/tests/test_join.py @@ -1,6 +1,6 @@ import numpy as np from numpy.testing import assert_array_equal, assert_raises -from skimage.segmentation import join_segmentations, relabel_from_one +from skimage.segmentation import join_segmentations, relabel_sequential def test_join_segmentations(): s1 = np.array([[0, 0, 1, 1], @@ -24,9 +24,10 @@ def test_join_segmentations(): s3 = np.array([[0, 0, 1, 1], [0, 2, 2, 1]]) assert_raises(ValueError, join_segmentations, s1, s3) -def test_relabel_from_one(): + +def test_relabel_sequential_offset1(): ar = np.array([1, 1, 5, 5, 8, 99, 42]) - ar_relab, fw, inv = relabel_from_one(ar) + ar_relab, fw, inv = relabel_sequential(ar) ar_relab_ref = np.array([1, 1, 2, 2, 3, 5, 4]) assert_array_equal(ar_relab, ar_relab_ref) fw_ref = np.zeros(100, int) @@ -36,5 +37,29 @@ def test_relabel_from_one(): assert_array_equal(inv, inv_ref) +def test_relabel_sequential_offset5(): + ar = np.array([1, 1, 5, 5, 8, 99, 42]) + ar_relab, fw, inv = relabel_sequential(ar, offset=5) + ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 5; fw_ref[5] = 6; fw_ref[8] = 7; fw_ref[42] = 8; fw_ref[99] = 9 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + +def test_relabel_sequential_offset5_with0(): + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0]) + ar_relab, fw, inv = relabel_sequential(ar, offset=5) + ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8, 0]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 5; fw_ref[5] = 6; fw_ref[8] = 7; fw_ref[42] = 8; fw_ref[99] = 9 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index 1cc0a1ee..46a82d8e 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -1,5 +1,6 @@ import numpy as np from skimage.segmentation import random_walker +from skimage.transform import resize def make_2d_syntheticdata(lx, ly=None): @@ -7,16 +8,16 @@ def make_2d_syntheticdata(lx, ly=None): ly = lx np.random.seed(1234) data = np.zeros((lx, ly)) + 0.1 * np.random.randn(lx, ly) - small_l = int(lx / 5) - data[lx / 2 - small_l:lx / 2 + small_l, - ly / 2 - small_l:ly / 2 + small_l] = 1 - data[lx / 2 - small_l + 1:lx / 2 + small_l - 1, - ly / 2 - small_l + 1:ly / 2 + small_l - 1] = \ - 0.1 * np.random.randn(2 * small_l - 2, 2 * small_l - 2) - data[lx / 2 - small_l, ly / 2 - small_l / 8:ly / 2 + small_l / 8] = 0 + small_l = int(lx // 5) + data[lx // 2 - small_l:lx // 2 + small_l, + ly // 2 - small_l:ly // 2 + small_l] = 1 + data[lx // 2 - small_l + 1:lx // 2 + small_l - 1, + ly // 2 - small_l + 1:ly // 2 + small_l - 1] = ( + 0.1 * np.random.randn(2 * small_l - 2, 2 * small_l - 2)) + data[lx // 2 - small_l, ly // 2 - small_l // 8:ly // 2 + small_l // 8] = 0 seeds = np.zeros_like(data) - seeds[lx / 5, ly / 5] = 1 - seeds[lx / 2 + small_l / 4, ly / 2 - small_l / 4] = 2 + seeds[lx // 5, ly // 5] = 1 + seeds[lx // 2 + small_l // 4, ly // 2 - small_l // 4] = 2 return data, seeds @@ -27,21 +28,23 @@ def make_3d_syntheticdata(lx, ly=None, lz=None): lz = lx np.random.seed(1234) data = np.zeros((lx, ly, lz)) + 0.1 * np.random.randn(lx, ly, lz) - small_l = int(lx / 5) - data[lx / 2 - small_l:lx / 2 + small_l, - ly / 2 - small_l:ly / 2 + small_l, - lz / 2 - small_l:lz / 2 + small_l] = 1 - data[lx / 2 - small_l + 1:lx / 2 + small_l - 1, - ly / 2 - small_l + 1:ly / 2 + small_l - 1, - lz / 2 - small_l + 1:lz / 2 + small_l - 1] = 0 + small_l = int(lx // 5) + data[lx // 2 - small_l:lx // 2 + small_l, + ly // 2 - small_l:ly // 2 + small_l, + lz // 2 - small_l:lz // 2 + small_l] = 1 + data[lx // 2 - small_l + 1:lx // 2 + small_l - 1, + ly // 2 - small_l + 1:ly // 2 + small_l - 1, + lz // 2 - small_l + 1:lz // 2 + small_l - 1] = 0 # make a hole - hole_size = np.max([1, small_l / 8]) - data[lx / 2 - small_l, - ly / 2 - hole_size:ly / 2 + hole_size, - lz / 2 - hole_size:lz / 2 + hole_size] = 0 + hole_size = np.max([1, small_l // 8]) + data[lx // 2 - small_l, + ly // 2 - hole_size:ly // 2 + hole_size, + lz // 2 - hole_size:lz // 2 + hole_size] = 0 seeds = np.zeros_like(data) - seeds[lx / 5, ly / 5, lz / 5] = 1 - seeds[lx / 2 + small_l / 4, ly / 2 - small_l / 4, lz / 2 - small_l / 4] = 2 + seeds[lx // 5, ly // 5, lz // 5] = 1 + seeds[lx // 2 + small_l // 4, + ly // 2 - small_l // 4, + lz // 2 - small_l // 4] = 2 return data, seeds @@ -101,7 +104,7 @@ def test_types(): lx = 70 ly = 100 data, labels = make_2d_syntheticdata(lx, ly) - data = 255 * (data - data.min()) / (data.max() - data.min()) + data = 255 * (data - data.min()) // (data.max() - data.min()) data = data.astype(np.uint8) labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') assert (labels_cg_mg[25:45, 40:60] == 2).all() @@ -181,6 +184,77 @@ def test_multispectral_3d(): return data, multi_labels, single_labels, labels +def test_depth(): + n = 30 + lx, ly, lz = n, n, n + data, _ = make_3d_syntheticdata(lx, ly, lz) + + # Rescale `data` along Z axis + data_aniso = np.zeros((n, n, n // 2)) + for i, yz in enumerate(data): + data_aniso[i, :, :] = resize(yz, (n, n // 2)) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso = np.zeros_like(data_aniso) + labels_aniso[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso[lx // 2 + small_l // 4, + ly // 2 - small_l // 4, + lz // 4 - small_l // 8] = 2 + + # Test with `depth` kwarg + labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', + depth=0.5) + + assert (labels_aniso[13:17, 13:17, 7:9] == 2).all() + + +def test_spacing(): + n = 30 + lx, ly, lz = n, n, n + data, _ = make_3d_syntheticdata(lx, ly, lz) + + # Rescale `data` along Y axis + # `resize` is not yet 3D capable, so this must be done by looping in 2D. + data_aniso = np.zeros((n, n * 2, n)) + for i, yz in enumerate(data): + data_aniso[i, :, :] = resize(yz, (n * 2, n)) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso = np.zeros_like(data_aniso) + labels_aniso[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso[lx // 2 + small_l // 4, + ly - small_l // 2, + lz // 2 - small_l // 4] = 2 + + # Test with `spacing` kwarg + # First, anisotropic along Y + labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', + spacing=(1., 2., 1.)) + assert (labels_aniso[13:17, 26:34, 13:17] == 2).all() + + # Rescale `data` along X axis + # `resize` is not yet 3D capable, so this must be done by looping in 2D. + data_aniso = np.zeros((n, n * 2, n)) + for i in range(data.shape[1]): + data_aniso[i, :, :] = resize(data[:, 1, :], (n * 2, n)) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso2 = np.zeros_like(data_aniso) + labels_aniso2[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso2[lx - small_l // 2, + ly // 2 + small_l // 4, + lz // 2 - small_l // 4] = 2 + + # Anisotropic along X + labels_aniso2 = random_walker(data_aniso, + labels_aniso2, + mode='cg', spacing=(2., 1., 1.)) + assert (labels_aniso2[26:34, 13:17, 13:17] == 2).all() + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 89dee59b..a4657785 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -1,9 +1,11 @@ +import itertools as it +import warnings import numpy as np from numpy.testing import assert_equal, assert_array_equal from skimage.segmentation import slic -def test_color(): +def test_color_2d(): rnd = np.random.RandomState(0) img = np.zeros((20, 21, 3)) img[:10, :10, 0] = 1 @@ -12,16 +14,20 @@ def test_color(): img += 0.01 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + seg = slic(img, n_segments=4, sigma=0) # we expect 4 segments assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape[:-1]) assert_array_equal(seg[:10, :10], 0) assert_array_equal(seg[10:, :10], 2) assert_array_equal(seg[:10, 10:], 1) assert_array_equal(seg[10:, 10:], 3) -def test_gray(): + +def test_gray_2d(): rnd = np.random.RandomState(0) img = np.zeros((20, 21)) img[:10, :10] = 0.33 @@ -30,14 +36,89 @@ def test_gray(): img += 0.0033 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4, ratio=50.0) + seg = slic(img, sigma=0, n_segments=4, compactness=1, + multichannel=False, convert2lab=False) assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape) assert_array_equal(seg[:10, :10], 0) assert_array_equal(seg[10:, :10], 2) assert_array_equal(seg[:10, 10:], 1) assert_array_equal(seg[10:, 10:], 3) + +def test_color_3d(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 22, 3)) + slices = [] + for dim_size in img.shape[:-1]: + midpoint = dim_size // 2 + slices.append((slice(None, midpoint), slice(midpoint, None))) + slices = list(it.product(*slices)) + colors = list(it.product(*(([0, 1],) * 3))) + for s, c in zip(slices, colors): + img[s] = c + img += 0.01 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, sigma=0, n_segments=8) + + assert_equal(len(np.unique(seg)), 8) + for s, c in zip(slices, range(8)): + assert_array_equal(seg[s], c) + + +def test_gray_3d(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 22)) + slices = [] + for dim_size in img.shape: + midpoint = dim_size // 2 + slices.append((slice(None, midpoint), slice(midpoint, None))) + slices = list(it.product(*slices)) + shades = np.arange(0, 1.000001, 1.0/7) + for s, sh in zip(slices, shades): + img[s] = sh + img += 0.001 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, sigma=0, n_segments=8, compactness=1, + multichannel=False, convert2lab=False) + + assert_equal(len(np.unique(seg)), 8) + for s, c in zip(slices, range(8)): + assert_array_equal(seg[s], c) + + +def test_list_sigma(): + rnd = np.random.RandomState(0) + img = np.array([[1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1]], np.float) + img += 0.1 * rnd.normal(size=img.shape) + result_sigma = np.array([[0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1]], np.int) + seg_sigma = slic(img, n_segments=2, sigma=[1, 50, 1], multichannel=False) + assert_equal(seg_sigma, result_sigma) + + +def test_spacing(): + rnd = np.random.RandomState(0) + img = np.array([[1, 1, 1, 0, 0], + [1, 1, 0, 0, 0]], np.float) + result_non_spaced = np.array([[0, 0, 0, 1, 1], + [0, 0, 1, 1, 1]], np.int) + result_spaced = np.array([[0, 0, 0, 0, 0], + [1, 1, 1, 1, 1]], np.int) + img += 0.1 * rnd.normal(size=img.shape) + seg_non_spaced = slic(img, n_segments=2, sigma=0, multichannel=False, + compactness=1.0) + seg_spaced = slic(img, n_segments=2, sigma=0, spacing=[1, 500, 1], + compactness=1.0, multichannel=False) + assert_equal(seg_non_spaced, result_non_spaced) + assert_equal(seg_spaced, result_spaced) + + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 5aab2700..8fa2cfdb 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -2,14 +2,14 @@ from ._hough_transform import (hough_circle, hough_ellipse, hough_line, probabilistic_hough_line) from .hough_transform import (hough, probabilistic_hough, hough_peaks, hough_line_peaks) -from .radon_transform import radon, iradon +from .radon_transform import radon, iradon, iradon_sart from .finite_radon_transform import frt2, ifrt2 from .integral import integral_image, integrate from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) -from ._warps import swirl, resize, rotate, rescale +from ._warps import swirl, resize, rotate, rescale, downscale_local_mean from .pyramids import (pyramid_reduce, pyramid_expand, pyramid_gaussian, pyramid_laplacian) @@ -24,6 +24,7 @@ __all__ = ['hough_circle', 'hough_line_peaks', 'radon', 'iradon', + 'iradon_sart', 'frt2', 'ifrt2', 'integral_image', @@ -40,6 +41,7 @@ __all__ = ['hough_circle', 'resize', 'rotate', 'rescale', + 'downscale_local_mean', 'pyramid_reduce', 'pyramid_expand', 'pyramid_gaussian', diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 58109224..25a13765 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -4,6 +4,9 @@ from scipy import ndimage, spatial from skimage.util import img_as_float from ._warps_cy import _warp_fast +from skimage._shared.utils import get_bound_method_class +from skimage._shared import six + class GeometricTransform(object): """Perform geometric transformations on a set of coordinates. @@ -512,7 +515,7 @@ class SimilarityTransform(ProjectiveTransform): [math.sin(rotation), math.cos(rotation), 0], [ 0, 0, 1] ]) - self._matrix *= scale + self._matrix[0:2, 0:2] *= scale self._matrix[0:2, 2] = translation else: # default to an identity transform @@ -948,19 +951,24 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, ---------- image : 2-D or 3-D array Input image. - inverse_map : transformation object, callable ``xy = f(xy, **kwargs)`` + inverse_map : transformation object, callable ``xy = f(xy, **kwargs)``, (3, 3) array Inverse coordinate map. A function that transforms a (N, 2) array of ``(x, y)`` coordinates in the *output image* into their corresponding coordinates in the *source image* (e.g. a transformation object or its - inverse). + inverse). See example section for usage. map_args : dict, optional Keyword arguments passed to `inverse_map`. output_shape : tuple (rows, cols), optional Shape of the output image generated. By default the shape of the input image is preserved. order : int, optional - The order of the spline interpolation, default is 3. The order has to - be in the range 0-5. + The order of interpolation. The order has to be in the range 0-5: + * 0: Nearest-neighbor + * 1: Bi-linear (default) + * 2: Bi-quadratic + * 3: Bi-cubic + * 4: Bi-quartic + * 5: Bi-quintic mode : string, optional Points outside the boundaries of the input are filled according to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). @@ -968,26 +976,46 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Used in conjunction with mode 'constant', the value outside the image boundaries. + Notes + ----- + In case of a `SimilarityTransform`, `AffineTransform` and + `ProjectiveTransform` and `order` in [0, 3] this function uses the + underlying transformation matrix to warp the image with a much faster + routine. + Examples -------- - Shift an image to the right: - >>> from skimage.transform import warp >>> from skimage import data >>> image = data.camera() - >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 - ... return xy - >>> - >>> warp(image, shift_right) - Use a geometric transform to warp an image: + The following image warps are all equal but differ substantially in + execution time. + + Use a geometric transform to warp an image (fast): >>> from skimage.transform import SimilarityTransform - >>> tform = SimilarityTransform(scale=0.1, rotation=0.1) + >>> tform = SimilarityTransform(translation=(0, -10)) >>> warp(image, tform) + Shift an image to the right with a callable (slow): + + >>> def shift(xy): + ... xy[:, 1] -= 10 + ... return xy + >>> warp(image, shift_right) + + Use a transformation matrix to warp an image (fast): + + >>> matrix = np.array([[1, 0, 0], [0, 1, -10], [0, 0, 1]]) + >>> warp(image, matrix) + >>> from skimage.transform import ProjectiveTransform + >>> warp(image, ProjectiveTransform(matrix=matrix)) + + You can also use the inverse of a geometric transformation (fast): + + >>> warp(image, tform.inverse) + """ # Backward API compatibility if reverse_map is not None: @@ -1006,13 +1034,22 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # use fast Cython version for specific interpolation orders if order in range(4) and not map_args: matrix = None - if inverse_map in HOMOGRAPHY_TRANSFORMS: + + if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + matrix = inverse_map + + elif inverse_map in HOMOGRAPHY_TRANSFORMS: matrix = inverse_map._matrix - elif hasattr(inverse_map, '__name__') \ - and inverse_map.__name__ == 'inverse' \ - and inverse_map.im_class in HOMOGRAPHY_TRANSFORMS: - matrix = np.linalg.inv(inverse_map.im_self._matrix) + + elif (hasattr(inverse_map, '__name__') + and inverse_map.__name__ == 'inverse' + and get_bound_method_class(inverse_map) + in HOMOGRAPHY_TRANSFORMS): + + matrix = np.linalg.inv(six.get_method_self(inverse_map)._matrix) + if matrix is not None: + matrix = matrix.astype(np.double) # transform all bands dims = [] for dim in range(image.shape[2]): @@ -1030,25 +1067,30 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, rows, cols = output_shape[:2] + if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + inverse_map = ProjectiveTransform(matrix=inverse_map) + def coord_map(*args): return inverse_map(*args, **map_args) coords = warp_coords(coord_map, (rows, cols, bands)) - # Prefilter not necessary for order 1 interpolation + # Prefilter not necessary for order 0, 1 interpolation prefilter = order > 1 out = ndimage.map_coordinates(image, coords, prefilter=prefilter, mode=mode, order=order, cval=cval) - # The spline filters sometimes return results outside [0, 1], - # so clip to ensure valid data - clipped = np.clip(out, 0, 1) + # The spline filters sometimes return results outside [0, 1], + # so clip to ensure valid data + clipped = np.clip(out, 0, 1) - if mode == 'constant' and not (0 <= cval <= 1): - clipped[out == cval] = cval + if mode == 'constant' and not (0 <= cval <= 1): + clipped[out == cval] = cval - if clipped.ndim == 3 and orig_ndim == 2: - # remove singleton dim introduced by atleast_3d - return clipped[..., 0] + out = clipped + + if out.ndim == 3 and orig_ndim == 2: + # remove singleton dimension introduced by atleast_3d + return out[..., 0] else: - return clipped + return out diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index d158a84d..29344fa8 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -7,7 +7,7 @@ import numpy as np cimport numpy as cnp cimport cython -from libc.math cimport abs, fabs, sqrt, ceil +from libc.math cimport abs, fabs, sqrt, ceil, atan2, M_PI from libc.stdlib cimport rand from skimage.draw import circle_perimeter @@ -122,17 +122,18 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, Returns ------- - res : list of tuples [(x0, y0, a, b, angle, accumulator)] - Where (x0, y0) is the center, (a, b) major and minor axis. - The angle value follows `draw.ellipse_perimeter()` convention. + result : ndarray with fields [(accumulator, y0, x0, a, b, orientation)] + Where ``(yc, xc)`` is the center, ``(a, b)`` the major and minor + axes, respectively. The `orientation` value follows + `skimage.draw.ellipse_perimeter` convention. Examples -------- - >>> img = np.zeros((25, 25), dtype=int) - >>> rr, cc = draw.ellipse_perimeter(10, 10, 6, 8) - >>> img[rr, cc] = 1 - >>> result = hough_ellipse(img, threshold=6) - [(10.0, 10.0, 8.0, 6.0474292058692187, 0.0, 8)] + >>> img = np.zeros((25, 25), dtype=np.uint8) + >>> rr, cc = ellipse_perimeter(10, 10, 6, 8) + >>> img[cc, rr] = 1 + >>> result = hough_ellipse(img, threshold=8) + [(10, 10.0, 8.0, 6.0, 0.0, 10.0)] Notes ----- @@ -149,47 +150,47 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, if img.ndim != 2: raise ValueError('The input image must be 2D.') - cdef long[:, :] pixels = np.transpose(np.nonzero(img)) - cdef Py_ssize_t num_pixels = pixels.shape[0] + cdef Py_ssize_t[:, ::1] pixels = np.row_stack(np.nonzero(img)) + cdef Py_ssize_t num_pixels = pixels.shape[1] cdef list acc = list() cdef list results = list() - cdef bin_size = accuracy**2 + cdef double bin_size = accuracy ** 2 cdef int max_b_squared if max_size is None: if img.shape[0] < img.shape[1]: - max_b_squared = np.round(0.5 * img.shape[0])**2 + max_b_squared = np.round(0.5 * img.shape[0]) ** 2 else: - max_b_squared = np.round(0.5 * img.shape[1])**2 + max_b_squared = np.round(0.5 * img.shape[1]) ** 2 else: max_b_squared = max_size**2 cdef Py_ssize_t p1, p2, p3, p1x, p1y, p2x, p2y, p3x, p3y - cdef double x0, y0, a, b, d, k - cdef double cos_tau_squared, b_squared, f_squared, angle + cdef double xc, yc, a, b, d, k + cdef double cos_tau_squared, b_squared, f_squared, orientation for p1 in range(num_pixels): - p1x = pixels[p1, 1] - p1y = pixels[p1, 0] + p1x = pixels[1, p1] + p1y = pixels[0, p1] for p2 in range(p1): - p2x = pixels[p2, 1] - p2y = pixels[p2, 0] + p2x = pixels[1, p2] + p2y = pixels[0, p2] - # Candidate: center (x0, y0) and main axis a + # Candidate: center (xc, yc) and main axis a a = 0.5 * sqrt((p1x - p2x)**2 + (p1y - p2y)**2) if a > 0.5 * min_size: - x0 = 0.5 * (p1x + p2x) - y0 = 0.5 * (p1y + p2y) + xc = 0.5 * (p1x + p2x) + yc = 0.5 * (p1y + p2y) for p3 in range(num_pixels): - p3x = pixels[p3, 1] - p3y = pixels[p3, 0] + p3x = pixels[1, p3] + p3y = pixels[0, p3] - d = sqrt((p3x - x0)**2 + (p3y - y0)**2) + d = sqrt((p3x - xc)**2 + (p3y - yc)**2) if d > min_size: f_squared = (p3x - p1x)**2 + (p3y - p1y)**2 - cos_tau_squared = ((a**2 + d**2 - f_squared) \ + cos_tau_squared = ((a**2 + d**2 - f_squared) / (2 * a * d))**2 # Consider b2 > 0 and avoid division by zero k = a**2 - d**2 * cos_tau_squared @@ -205,21 +206,29 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, hist, bin_edges = np.histogram(acc, bins=bins) hist_max = np.max(hist) if hist_max > threshold: - angle = np.arctan2(p1x - p2x, p1y - p2y) - # pi - angle to keep ellipse_perimeter() convention - if angle != 0: - angle = np.pi - angle + orientation = atan2(p1x - p2x, p1y - p2y) b = sqrt(bin_edges[hist.argmax()]) - results.append((x0, - y0, - a, - b, - angle, - hist_max, # Accumulator - )) + # to keep ellipse_perimeter() convention + if orientation != 0: + orientation = M_PI - orientation + # When orientation is not in [-pi:pi] + # it would mean in ellipse_perimeter() + # that a < b. But we keep a > b. + if orientation > M_PI: + orientation = orientation - M_PI / 2. + a, b = b, a + results.append((hist_max, # Accumulator + yc, xc, + a, b, + orientation)) acc = [] - return results + return np.array(results, dtype=[('accumulator', np.intp), + ('yc', np.double), + ('xc', np.double), + ('a', np.double), + ('b', np.double), + ('orientation', np.double)]) def hough_line(cnp.ndarray img, diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx new file mode 100644 index 00000000..91b943b9 --- /dev/null +++ b/skimage/transform/_radon_transform.pyx @@ -0,0 +1,202 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np + +cimport numpy as cnp +cimport cython +from libc.math cimport cos, sin, floor, ceil, sqrt, abs, M_PI + + +cpdef bilinear_ray_sum(cnp.double_t[:, :] image, cnp.double_t theta, + cnp.double_t ray_position): + """ + Compute the projection of an image along a ray. + + Parameters + ---------- + image : 2D array, dtype=float + Image to project. + theta : float + Angle of the projection + ray_position : float + Position of the ray within the projection + + Returns + ------- + projected_value : float + Ray sum along the projection + norm_of_weights : + A measure of how long the ray's path through the reconstruction + circle was + """ + theta = theta / 180. * M_PI + cdef cnp.double_t radius = image.shape[0] // 2 - 1 + cdef cnp.double_t projection_center = image.shape[0] // 2 + cdef cnp.double_t rotation_center = image.shape[0] // 2 + # (s, t) is the (x, y) system rotated by theta + cdef cnp.double_t t = ray_position - projection_center + # s0 is the half-length of the ray's path in the reconstruction circle + cdef cnp.double_t s0 + s0 = sqrt(radius**2 - t**2) if radius**2 >= t**2 else 0. + cdef Py_ssize_t Ns = 2 * ( ceil(2 * s0)) # number of steps + # along the ray + cdef cnp.double_t ray_sum = 0. + cdef cnp.double_t weight_norm = 0. + cdef cnp.double_t ds, dx, dy, x0, y0, x, y, di, dj, + cdef cnp.double_t index_i, index_j, weight + cdef Py_ssize_t k, i, j + if Ns > 0: + # step length between samples + ds = 2 * s0 / Ns + dx = -ds * cos(theta) + dy = -ds * sin(theta) + # point of entry of the ray into the reconstruction circle + x0 = s0 * cos(theta) - t * sin(theta) + y0 = s0 * sin(theta) + t * cos(theta) + for k in range(Ns+1): + x = x0 + k * dx + y = y0 + k * dy + index_i = x + rotation_center + index_j = y + rotation_center + i = floor(index_i) + j = floor(index_j) + di = index_i - floor(index_i) + dj = index_j - floor(index_j) + # Use linear interpolation between values + # Where values fall outside the array, assume zero + if i > 0 and j > 0: + weight = (1. - di) * (1. - dj) * ds + ray_sum += weight * image[i, j] + weight_norm += weight**2 + if i > 0 and j < image.shape[1] - 1: + weight = (1. - di) * dj * ds + ray_sum += weight * image[i, j+1] + weight_norm += weight**2 + if i < image.shape[0] - 1 and j > 0: + weight = di * (1 - dj) * ds + ray_sum += weight * image[i+1, j] + weight_norm += weight**2 + if i < image.shape[0] - 1 and j < image.shape[1] - 1: + weight = di * dj * ds + ray_sum += weight * image[i+1, j+1] + weight_norm += weight**2 + return ray_sum, weight_norm + + +cpdef bilinear_ray_update(cnp.double_t[:, :] image, + cnp.double_t[:, :] image_update, + cnp.double_t theta, cnp.double_t ray_position, + cnp.double_t projected_value): + """ + Compute the update along a ray using bilinear interpolation. + + Parameters + ---------- + image : 2D array, dtype=float + Current reconstruction estimate + image_update : 2D array, dtype=float + Array of same shape as ``image``. Updates will be added to this array. + theta : float + Angle of the projection + ray_position : float + Position of the ray within the projection + projected_value : float + Projected value (from the sinogram) + + Returns + ------- + deviation : + Deviation before updating the image + """ + cdef cnp.double_t ray_sum, weight_norm, deviation + ray_sum, weight_norm = bilinear_ray_sum(image, theta, ray_position) + if weight_norm > 0.: + deviation = -(ray_sum - projected_value) / weight_norm + else: + deviation = 0. + theta = theta / 180. * M_PI + cdef cnp.double_t radius = image.shape[0] // 2 - 1 + cdef cnp.double_t projection_center = image.shape[0] // 2 + cdef cnp.double_t rotation_center = image.shape[0] // 2 + # (s, t) is the (x, y) system rotated by theta + cdef cnp.double_t t = ray_position - projection_center + # s0 is the half-length of the ray's path in the reconstruction circle + cdef cnp.double_t s0 + s0 = sqrt(radius*radius - t*t) if radius**2 >= t**2 else 0. + cdef Py_ssize_t Ns = 2 * ( ceil(2 * s0)) + cdef cnp.double_t hamming_beta = 0.46164 # beta for equiripple Hamming window + + cdef cnp.double_t ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j + cdef cnp.double_t hamming_window + cdef Py_ssize_t k, i, j + if Ns > 0: + # Step length between samples + ds = 2 * s0 / Ns + dx = -ds * cos(theta) + dy = -ds * sin(theta) + # Point of entry of the ray into the reconstruction circle + x0 = s0 * cos(theta) - t * sin(theta) + y0 = s0 * sin(theta) + t * cos(theta) + for k in range(Ns+1): + x = x0 + k * dx + y = y0 + k * dy + index_i = x + rotation_center + index_j = y + rotation_center + i = floor(index_i) + j = floor(index_j) + di = index_i - floor(index_i) + dj = index_j - floor(index_j) + hamming_window = ((1 - hamming_beta) + - hamming_beta * cos(2 * M_PI * k / (Ns - 1))) + if i > 0 and j > 0: + image_update[i, j] += (deviation * (1. - di) * (1. - dj) + * ds * hamming_window) + if i > 0 and j < image.shape[1] - 1: + image_update[i, j+1] += (deviation * (1. - di) * dj + * ds * hamming_window) + if i < image.shape[0] - 1 and j > 0: + image_update[i+1, j] += (deviation * di * (1 - dj) + * ds * hamming_window) + if i < image.shape[0] - 1 and j < image.shape[1] - 1: + image_update[i+1, j+1] += (deviation * di * dj + * ds * hamming_window) + return deviation + + +@cython.boundscheck(True) +def sart_projection_update(cnp.double_t[:, :] image not None, + cnp.double_t theta, + cnp.double_t[:] projection not None, + cnp.double_t projection_shift=0.): + """ + Compute update to a reconstruction estimate from a single projection + using bilinear interpolation. + + Parameters + ---------- + image : 2D array, dtype=float + Current reconstruction estimate + theta : float + Angle of the projection + projection : 1D array, dtype=float + Projected values, taken from the sinogram + projection_shift : float + Shift the position of the projection by this many pixels before + using it to compute an update to the reconstruction estimate + + Returns + ------- + image_update : 2D array, dtype=float + Array of same shape as ``image`` containing updates that should be + added to ``image`` to improve the reconstruction estimate + """ + cdef cnp.ndarray[cnp.double_t, ndim=2] image_update = np.zeros_like(image) + cdef cnp.double_t ray_position + cdef Py_ssize_t i + for i in range(projection.shape[0]): + ray_position = i + projection_shift + bilinear_ray_update(image, image_update, theta, ray_position, + projection[i]) + return image_update diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index caf2baaf..64b129dd 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,11 +1,19 @@ import numpy as np from scipy import ndimage -from ._geometric import warp, SimilarityTransform, AffineTransform + +from skimage.transform._geometric import (warp, SimilarityTransform, + AffineTransform) +from skimage.measure import block_reduce def resize(image, output_shape, order=1, mode='constant', cval=0.): """Resize image to match a certain size. + Performs interpolation to up-size or down-size images. For down-sampling + N-dimensional images by applying the arithmetic sum or mean, see + `skimage.measure.local_sum` and `skimage.transform.downscale_local_mean`, + respectively. + Parameters ---------- image : ndarray @@ -24,8 +32,8 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): Other parameters ---------------- order : int, optional - The order of the spline interpolation, default is 3. The order has to - be in the range 0-5. + 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'). @@ -87,6 +95,11 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): def rescale(image, scale, order=1, mode='constant', cval=0.): """Scale image by a certain factor. + Performs interpolation to upscale or down-scale images. For down-sampling + N-dimensional images with integer factors by applying the arithmetic sum or + mean, see `skimage.measure.local_sum` and + `skimage.transform.downscale_local_mean`, respectively. + Parameters ---------- image : ndarray @@ -103,8 +116,8 @@ def rescale(image, scale, order=1, mode='constant', cval=0.): Other parameters ---------------- order : int, optional - The order of the spline interpolation, default is 3. The order has to - be in the range 0-5. + 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'). @@ -159,8 +172,8 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): Other parameters ---------------- order : int, optional - The order of the spline interpolation, default is 3. The order has to - be in the range 0-5. + 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'). @@ -213,6 +226,47 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): mode=mode, cval=cval) +def downscale_local_mean(image, factors, cval=0): + """Down-sample N-dimensional image by local averaging. + + The image is padded with `cval` if it is not perfectly divisible by the + integer factors. + + In contrast to the 2-D interpolation in `skimage.transform.resize` and + `skimage.transform.rescale` this function may be applied to N-dimensional + images and calculates the local mean of elements in each block of size + `factors` in the input image. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + factors : array_like + Array containing down-sampling integer factor along each axis. + cval : float, optional + Constant padding value if image is not perfectly divisible by the + integer factors. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + array([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> downscale_local_mean(a, (2, 3)) + array([[3.5, 4.], + [5.5, 4.5]]) + + """ + return block_reduce(image, factors, np.mean, cval) + + def _swirl_mapping(xy, center, rotation, strength, radius): x, y = xy.T x0, y0 = center @@ -261,8 +315,8 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, Shape of the output image generated. By default the shape of the input image is preserved. order : int, optional - The order of the spline interpolation, default is 3. The order has to - be in the range 0-5. + 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'). diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index f51eb8d3..b3136c22 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -40,22 +40,18 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, """Projective transformation (homography). Perform a projective transformation (homography) of a - floating point image, using bi-linear interpolation. + floating point image, using interpolation. For each pixel, given its homogeneous coordinate :math:`\mathbf{x} = [x, y, 1]^T`, its target position is calculated by multiplying with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. - E.g., to rotate by theta degrees clockwise, the matrix should be - - :: + E.g., to rotate by theta degrees clockwise, the matrix should be:: [[cos(theta) -sin(theta) 0] [sin(theta) cos(theta) 0] [0 0 1]] - or, to translate x by 10 and y by 20, - - :: + or, to translate x by 10 and y by 20:: [[1 0 10] [0 1 20] @@ -69,12 +65,12 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, Transformation matrix H that defines the homography. output_shape : tuple (rows, cols), optional Shape of the output image generated (default None). - order : {0, 1}, optional + order : {0, 1, 2, 3}, optional Order of interpolation:: - * 0: Nearest-neighbour interpolation. - * 1: Bilinear interpolation (default). - * 2: Biquadratic interpolation. - * 3: Bicubic interpolation. + * 0: Nearest-neighbor + * 1: Bi-linear (default) + * 2: Bi-quadratic + * 3: Bi-cubic mode : {'constant', 'reflect', 'wrap', 'nearest'}, optional How to handle values outside the image borders (default is constant). cval : string, optional (default 0) @@ -83,10 +79,8 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, """ - cdef cnp.ndarray[dtype=cnp.double_t, ndim=2, mode="c"] img = \ - np.ascontiguousarray(image, dtype=np.double) - cdef cnp.ndarray[dtype=cnp.double_t, ndim=2, mode="c"] M = \ - np.ascontiguousarray(H) + cdef double[:, ::1] img = np.ascontiguousarray(image, dtype=np.double) + cdef double[:, ::1] M = np.ascontiguousarray(H) if mode not in ('constant', 'wrap', 'reflect', 'nearest'): raise ValueError("Invalid mode specified. Please use " @@ -101,8 +95,7 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, out_r = output_shape[0] out_c = output_shape[1] - cdef cnp.ndarray[dtype=cnp.double_t, ndim=2] out = \ - np.zeros((out_r, out_c), dtype=np.double) + cdef double[:, ::1] out = np.zeros((out_r, out_c), dtype=np.double) cdef Py_ssize_t tfr, tfc cdef double r, c @@ -122,8 +115,8 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, for tfr in range(out_r): for tfc in range(out_c): - _matrix_transform(tfc, tfr, M.data, &c, &r) - out[tfr, tfc] = interp_func(img.data, rows, cols, r, c, + _matrix_transform(tfc, tfr, &M[0, 0], &c, &r) + out[tfr, tfc] = interp_func(&img[0, 0], rows, cols, r, c, mode_c, cval) - return out + return np.asarray(out) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 19001de8..a68b7a70 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -6,11 +6,11 @@ from skimage.util import img_as_float def _smooth(image, sigma, mode, cval): - """Return image with each channel smoothed by the gaussian filter.""" + """Return image with each channel smoothed by the Gaussian filter.""" smoothed = np.empty(image.shape, dtype=np.double) - if image.ndim == 3: # apply gaussian filter to all dimensions independently + if image.ndim == 3: # apply Gaussian filter to all dimensions independently for dim in range(image.shape[2]): ndimage.gaussian_filter(image[..., dim], sigma, output=smoothed[..., dim], @@ -38,13 +38,13 @@ def pyramid_reduce(image, downscale=2, sigma=None, order=1, downscale : float, optional Downscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + Sigma for Gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that - covers more than 99% of the gaussian distribution. + covers more than 99% of the Gaussian distribution. order : int, optional Order of splines used in interpolation of downsampling. See - `scipy.ndimage.map_coordinates` for detail. - mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. cval : float, optional @@ -92,13 +92,13 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, upscale : float, optional Upscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * upscale / 6.0` which + Sigma for Gaussian filter. Default is `2 * upscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that - covers more than 99% of the gaussian distribution. + covers more than 99% of the Gaussian distribution. order : int, optional Order of splines used in interpolation of upsampling. See - `scipy.ndimage.map_coordinates` for detail. - mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. cval : float, optional @@ -137,7 +137,7 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, def pyramid_gaussian(image, max_layer=-1, downscale=2, sigma=None, order=1, mode='reflect', cval=0): - """Yield images of the gaussian pyramid formed by the input image. + """Yield images of the Gaussian pyramid formed by the input image. Recursively applies the `pyramid_reduce` function to the image, and yields the downscaled images. @@ -157,13 +157,13 @@ def pyramid_gaussian(image, max_layer=-1, downscale=2, sigma=None, order=1, downscale : float, optional Downscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + Sigma for Gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that - covers more than 99% of the gaussian distribution. + covers more than 99% of the Gaussian distribution. order : int, optional Order of splines used in interpolation of downsampling. See - `scipy.ndimage.map_coordinates` for detail. - mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. cval : float, optional @@ -238,13 +238,13 @@ def pyramid_laplacian(image, max_layer=-1, downscale=2, sigma=None, order=1, downscale : float, optional Downscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + Sigma for Gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that - covers more than 99% of the gaussian distribution. + covers more than 99% of the Gaussian distribution. order : int, optional Order of splines used in interpolation of downsampling. See - `scipy.ndimage.map_coordinates` for detail. - mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. cval : float, optional diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 59e6c338..101d09c6 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ radon.py - Radon and inverse radon transforms @@ -14,10 +15,14 @@ References: """ from __future__ import division import numpy as np -from scipy.fftpack import fftshift, fft, ifft +from scipy.fftpack import fft, ifft, fftfreq +from scipy.interpolate import interp1d from ._warps_cy import _warp_fast +from ._radon_transform import sart_projection_update +from .. import util -__all__ = ["radon", "iradon"] + +__all__ = ["radon", "iradon", "iradon_sart"] def radon(image, theta=None, circle=False): @@ -28,7 +33,8 @@ def radon(image, theta=None, circle=False): Parameters ---------- image : array_like, dtype=float - Input image. + Input image. The rotation axis will be located in the pixel with + indices ``(image.shape[0] // 2, image.shape[1] // 2)``. theta : array_like, dtype=float, optional (default np.arange(180)) Projection angles (in degrees). circle : boolean, optional @@ -38,8 +44,10 @@ def radon(image, theta=None, circle=False): Returns ------- - output : ndarray - Radon transform (sinogram). + radon_image : ndarray + Radon transform (sinogram). The tomography rotation axis will lie + at the pixel index ``radon_image.shape[0] // 2`` along the 0th + dimension of ``radon_image``. Raises ------ @@ -56,10 +64,11 @@ def radon(image, theta=None, circle=False): 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 + + (c1 - image.shape[1] // 2)**2) <= radius**2 if not np.all(reconstruction_circle | (image == 0)): raise ValueError('Image must be zero outside the reconstruction' ' circle') + # Crop image to make it square slices = [] for d in (0, 1): if image.shape[d] > min(image.shape): @@ -71,48 +80,48 @@ def radon(image, theta=None, circle=False): slices.append(slice(None)) slices = tuple(slices) padded_image = image[slices] - out = np.zeros((min(padded_image.shape), len(theta))) else: - height, width = image.shape - diagonal = np.sqrt(height**2 + width**2) - heightpad = np.ceil(diagonal - height) - widthpad = np.ceil(diagonal - width) - padded_image = np.zeros((int(height + heightpad), - int(width + widthpad))) - y0 = int(np.ceil(heightpad / 2)) - y1 = int((np.ceil(heightpad / 2) + height)) - x0 = int((np.ceil(widthpad / 2))) - x1 = int((np.ceil(widthpad / 2) + width)) + diagonal = np.sqrt(2) * max(image.shape) + pad = [int(np.ceil(diagonal - s)) for s in image.shape] + new_center = [(s + p) // 2 for s, p in zip(image.shape, pad)] + old_center = [s // 2 for s in image.shape] + pad_before = [nc - oc for oc, nc in zip(old_center, new_center)] + pad_width = [(pb, p - pb) for pb, p in zip(pad_before, pad)] + padded_image = util.pad(image, pad_width, mode='constant', + constant_values=0) + # padded_image is always square + assert padded_image.shape[0] == padded_image.shape[1] + radon_image = np.zeros((padded_image.shape[0], len(theta))) + center = padded_image.shape[0] // 2 - padded_image[y0:y1, x0:x1] = image - out = np.zeros((max(padded_image.shape), len(theta))) - - h, w = padded_image.shape - dh, dw = h // 2, w // 2 - shift0 = np.array([[1, 0, -dw], - [0, 1, -dh], + shift0 = np.array([[1, 0, -center], + [0, 1, -center], [0, 0, 1]]) - - shift1 = np.array([[1, 0, dw], - [0, 1, dh], + shift1 = np.array([[1, 0, center], + [0, 1, center], [0, 0, 1]]) def build_rotation(theta): - T = -np.deg2rad(theta) - - R = np.array([[np.cos(T), -np.sin(T), 0], - [np.sin(T), np.cos(T), 0], + T = np.deg2rad(theta) + R = np.array([[np.cos(T), np.sin(T), 0], + [-np.sin(T), np.cos(T), 0], [0, 0, 1]]) - return shift1.dot(R).dot(shift0) for i in range(len(theta)): - rotated = _warp_fast(padded_image, - np.linalg.inv(build_rotation(-theta[i]))) + rotated = _warp_fast(padded_image, build_rotation(theta[i])) + radon_image[:, i] = rotated.sum(0) + return radon_image - out[:, i] = rotated.sum(0)[::-1] - return out +def _sinogram_circle_to_square(sinogram): + diagonal = int(np.ceil(np.sqrt(2) * sinogram.shape[0])) + pad = diagonal - sinogram.shape[0] + old_center = sinogram.shape[0] // 2 + new_center = diagonal // 2 + pad_before = new_center - old_center + pad_width = ((pad_before, pad - pad_before), (0, 0)) + return util.pad(sinogram, pad_width, mode='constant', constant_values=0) def iradon(radon_image, theta=None, output_size=None, @@ -127,7 +136,10 @@ def iradon(radon_image, theta=None, output_size=None, ---------- radon_image : array_like, dtype=float Image containing radon transform (sinogram). Each column of - the image corresponds to a projection along a different angle. + the image corresponds to a projection along a different angle. The + tomography rotation axis should lie at the pixel index + ``radon_image.shape[0] // 2`` along the 0th dimension of + ``radon_image``. theta : array_like, dtype=float, optional Reconstruction angles (in degrees). Default: m angles evenly spaced between 0 and 180 (if the shape of `radon_image` is (N, M)). @@ -135,11 +147,11 @@ def iradon(radon_image, theta=None, output_size=None, Number of rows and columns in the reconstruction. filter : str, optional (default ramp) Filter used in frequency domain filtering. Ramp filter used by default. - Filters available: ramp, shepp-logan, cosine, hamming, hann + Filters available: ramp, shepp-logan, cosine, hamming, hann. Assign None to use no filter. - interpolation : str, optional (default linear) - Interpolation method used in reconstruction. - Methods available: nearest, linear. + interpolation : str, optional (default 'linear') + Interpolation method used in reconstruction. Methods available: + 'linear', 'nearest', and 'cubic' ('cubic' is slow). circle : boolean, optional Assume the reconstructed image is zero outside the inscribed circle. Also changes the default output_size to match the behaviour of @@ -147,117 +159,265 @@ def iradon(radon_image, theta=None, output_size=None, Returns ------- - output : ndarray - Reconstructed image. + reconstructed : ndarray + Reconstructed image. The rotation axis will be located in the pixel + with indices + ``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``. Notes ----- - It applies the fourier slice theorem to reconstruct an image by + It applies the Fourier slice theorem to reconstruct an image by multiplying the frequency domain of the filter with the FFT of the projection data. This algorithm is called filtered back projection. """ if radon_image.ndim != 2: raise ValueError('The input image must be 2-D') - if theta is None: m, n = radon_image.shape theta = np.linspace(0, 180, n, endpoint=False) else: theta = np.asarray(theta) - if len(theta) != radon_image.shape[1]: raise ValueError("The given ``theta`` does not match the number of " "projections in ``radon_image``.") - - th = (np.pi / 180.0) * theta - # if output size not specified, estimate from input radon image + interpolation_types = ('linear', 'nearest', 'cubic') + if not interpolation in interpolation_types: + raise ValueError("Unknown interpolation: %s" % interpolation) if not output_size: + # If output size not specified, estimate from input radon image if circle: output_size = radon_image.shape[0] else: output_size = int(np.floor(np.sqrt((radon_image.shape[0])**2 / 2.0))) if circle: - radon_size = int(np.ceil(np.sqrt(2) * radon_image.shape[0])) - radon_image_padded = np.zeros((radon_size, radon_image.shape[1])) - radon_pad = (radon_size - radon_image.shape[0]) // 2 - radon_image_padded[radon_pad:radon_pad + radon_image.shape[0], :] \ - = radon_image - radon_image = radon_image_padded + radon_image = _sinogram_circle_to_square(radon_image) - n = radon_image.shape[0] + th = (np.pi / 180.0) * theta + # resize image to next power of two (but no less than 64) for + # Fourier analysis; speeds up Fourier and lessens artifacts + projection_size_padded = \ + max(64, int(2**np.ceil(np.log2(2 * radon_image.shape[0])))) + pad_width = ((0, projection_size_padded - radon_image.shape[0]), (0, 0)) + img = util.pad(radon_image, pad_width, mode='constant', constant_values=0) - img = radon_image.copy() - # resize image to next power of two for fourier analysis - # speeds up fourier and lessens artifacts - order = max(64., 2**np.ceil(np.log(2 * n) / np.log(2))) - # zero pad input image - img.resize((order, img.shape[1])) - # construct the fourier filter - - f = fftshift(abs(np.mgrid[-1:1:2 / order])).reshape(-1, 1) - w = 2 * np.pi * f - # start from first element to avoid divide by zero + # Construct the Fourier filter + f = fftfreq(projection_size_padded).reshape(-1, 1) # digital frequency + omega = 2 * np.pi * f # angular frequency + fourier_filter = 2 * np.abs(f) # ramp filter if filter == "ramp": pass elif filter == "shepp-logan": - f[1:] = f[1:] * np.sin(w[1:] / 2) / (w[1:] / 2) + # Start from first element to avoid divide by zero + fourier_filter[1:] = fourier_filter[1:] * np.sin(omega[1:]) / omega[1:] elif filter == "cosine": - f[1:] = f[1:] * np.cos(w[1:] / 2) + fourier_filter *= np.cos(omega) elif filter == "hamming": - f[1:] = f[1:] * (0.54 + 0.46 * np.cos(w[1:])) + fourier_filter *= (0.54 + 0.46 * np.cos(omega / 2)) elif filter == "hann": - f[1:] = f[1:] * (1 + np.cos(w[1:])) / 2 + fourier_filter *= (1 + np.cos(omega / 2)) / 2 elif filter is None: - f[1:] = 1 + fourier_filter[:] = 1 else: raise ValueError("Unknown filter: %s" % filter) - - filter_ft = np.tile(f, (1, len(theta))) - - # apply filter in fourier domain - projection = fft(img, axis=0) * filter_ft + # Apply filter in Fourier domain + projection = fft(img, axis=0) * fourier_filter radon_filtered = np.real(ifft(projection, axis=0)) - # resize filtered image back to original size + # Resize filtered image back to original size radon_filtered = radon_filtered[:radon_image.shape[0], :] reconstructed = np.zeros((output_size, output_size)) - mid_index = np.ceil(n / 2.0) + # Determine the center of the projections (= center of sinogram) + mid_index = radon_image.shape[0] // 2 - x = output_size - y = output_size - [X, Y] = np.mgrid[0.0:x, 0.0:y] + [X, Y] = np.mgrid[0:output_size, 0:output_size] xpr = X - int(output_size) // 2 ypr = Y - int(output_size) // 2 + # Reconstruct image by interpolation + for i in range(len(theta)): + t = ypr * np.cos(th[i]) - xpr * np.sin(th[i]) + x = np.arange(radon_filtered.shape[0]) - mid_index + if interpolation == 'linear': + backprojected = np.interp(t, x, radon_filtered[:, i], + left=0, right=0) + else: + interpolant = interp1d(x, radon_filtered[:, i], kind=interpolation, + bounds_error=False, fill_value=0) + backprojected = interpolant(t) + reconstructed += backprojected if circle: - radius = (output_size - 1) // 2 - reconstruction_circle = (xpr**2 + ypr**2) < radius**2 - - # reconstruct image by interpolation - if interpolation == "nearest": - for i in range(len(theta)): - k = np.round(mid_index + xpr * np.sin(th[i]) - ypr * np.cos(th[i])) - backprojected = radon_filtered[ - ((((k > 0) & (k < n)) * k) - 1).astype(np.int), i] - if circle: - backprojected[~reconstruction_circle] = 0. - reconstructed += backprojected - - elif interpolation == "linear": - for i in range(len(theta)): - t = xpr * np.sin(th[i]) - ypr * np.cos(th[i]) - a = np.floor(t) - b = mid_index + a - b0 = ((((b + 1 > 0) & (b + 1 < n)) * (b + 1)) - 1).astype(np.int) - b1 = ((((b > 0) & (b < n)) * b) - 1).astype(np.int) - backprojected = (t - a) * radon_filtered[b0, i] + \ - (a - t + 1) * radon_filtered[b1, i] - if circle: - backprojected[~reconstruction_circle] = 0. - reconstructed += backprojected - else: - raise ValueError("Unknown interpolation: %s" % interpolation) + radius = output_size // 2 + reconstruction_circle = (xpr**2 + ypr**2) <= radius**2 + reconstructed[~reconstruction_circle] = 0. return reconstructed * np.pi / (2 * len(th)) + + +def order_angles_golden_ratio(theta): + """ + Order angles to reduce the amount of correlated information + in subsequent projections. + + Parameters + ---------- + theta : 1D array of floats + Projection angles in degrees. Duplicate angles are not allowed. + + Returns + ------- + indices_generator : generator yielding unsigned integers + The returned generator yields indices into ``theta`` such that + ``theta[indices]`` gives the approximate golden ratio ordering + of the projections. In total, ``len(theta)`` indices are yielded. + All non-negative integers < ``len(theta)`` are yielded exactly once. + + Notes + ----- + The method used here is that of the golden ratio introduced + by T. Kohler. + + References + ---------- + .. [1] Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + .. [2] Winkelmann, Stefanie, et al. "An optimal radial profile order + based on the Golden Ratio for time-resolved MRI." + Medical Imaging, IEEE Transactions on 26.1 (2007): 68-76. + """ + interval = 180 + + def angle_distance(a, b): + difference = a - b + return min(abs(difference % interval), abs(difference % -interval)) + + remaining = list(np.argsort(theta)) # indices into theta + # yield an arbitrary angle to start things off + index = remaining.pop(0) + angle = theta[index] + yield index + # determine subsequent angles using the golden ratio method + angle_increment = interval * (1 - (np.sqrt(5) - 1) / 2) + while remaining: + angle = (angle + angle_increment) % interval + insert_point = np.searchsorted(theta[remaining], angle) + index_below = insert_point - 1 + index_above = 0 if insert_point == len(remaining) else insert_point + distance_below = angle_distance(angle, theta[remaining[index_below]]) + distance_above = angle_distance(angle, theta[remaining[index_above]]) + if distance_below < distance_above: + yield remaining.pop(index_below) + else: + yield remaining.pop(index_above) + + +def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, + clip=None, relaxation=0.15): + """ + Inverse radon transform + + Reconstruct an image from the radon transform, using a single iteration of + the Simultaneous Algebraic Reconstruction Technique (SART) algorithm. + + Parameters + ---------- + radon_image : 2D array, dtype=float + Image containing radon transform (sinogram). Each column of + the image corresponds to a projection along a different angle. The + tomography rotation axis should lie at the pixel index + ``radon_image.shape[0] // 2`` along the 0th dimension of + ``radon_image``. + theta : 1D array, dtype=float, optional + Reconstruction angles (in degrees). Default: m angles evenly spaced + between 0 and 180 (if the shape of `radon_image` is (N, M)). + image : 2D array, dtype=float, optional + Image containing an initial reconstruction estimate. Shape of this + array should be ``(radon_image.shape[0], radon_image.shape[0])``. The + default is an array of zeros. + projection_shifts : 1D array, dtype=float + Shift the projections contained in ``radon_image`` (the sinogram) by + this many pixels before reconstructing the image. The i'th value + defines the shift of the i'th column of ``radon_image``. + clip : length-2 sequence of floats + Force all values in the reconstructed tomogram to lie in the range + ``[clip[0], clip[1]]`` + relaxation : float + Relaxation parameter for the update step. A higher value can + improve the convergence rate, but one runs the risk of instabilities. + Values close to or higher than 1 are not recommended. + + Returns + ------- + reconstructed : ndarray + Reconstructed image. The rotation axis will be located in the pixel + with indices + ``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``. + + Notes + ----- + Algebraic Reconstruction Techniques are based on formulating the tomography + reconstruction problem as a set of linear equations. Along each ray, + the projected value is the sum of all the values of the cross section along + the ray. A typical feature of SART (and a few other variants of algebraic + techniques) is that it samples the cross section at equidistant points + along the ray, using linear interpolation between the pixel values of the + cross section. The resulting set of linear equations are then solved using + a slightly modified Kaczmarz method. + + When using SART, a single iteration is usually sufficient to obtain a good + reconstruction. Further iterations will tend to enhance high-frequency + information, but will also often increase the noise. + + References + ---------- + .. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic + Imaging", IEEE Press 1988. + .. [2] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction + technique (SART): a superior implementation of the ART algorithm", + Ultrasonic Imaging 6 pp 81--94 (1984) + .. [3] S Kaczmarz, "Angenäherte auflösung von systemen linearer + gleichungen", Bulletin International de l’Academie Polonaise des + Sciences et des Lettres 35 pp 355--357 (1937) + .. [4] Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + .. [5] Kaczmarz' method, Wikipedia, + http://en.wikipedia.org/wiki/Kaczmarz_method + """ + if radon_image.ndim != 2: + raise ValueError('radon_image must be two dimensional') + reconstructed_shape = (radon_image.shape[0], radon_image.shape[0]) + if theta is None: + theta = np.linspace(0, 180, radon_image.shape[1], endpoint=False) + elif theta.shape != (radon_image.shape[1],): + raise ValueError('Shape of theta (%s) does not match the ' + 'number of projections (%d)' + % (projection_shifts.shape, radon_image.shape[1])) + if image is None: + image = np.zeros(reconstructed_shape, dtype=np.float) + elif image.shape != reconstructed_shape: + raise ValueError('Shape of image (%s) does not match first dimension ' + 'of radon_image (%s)' + % (image.shape, reconstructed_shape)) + if projection_shifts is None: + projection_shifts = np.zeros((radon_image.shape[1],), dtype=np.float) + elif projection_shifts.shape != (radon_image.shape[1],): + raise ValueError('Shape of projection_shifts (%s) does not match the ' + 'number of projections (%d)' + % (projection_shifts.shape, radon_image.shape[1])) + if not clip is None: + if len(clip) != 2: + raise ValueError('clip must be a length-2 sequence') + clip = (float(clip[0]), float(clip[1])) + relaxation = float(relaxation) + + for angle_index in order_angles_golden_ratio(theta): + image_update = sart_projection_update(image, theta[angle_index], + radon_image[:, angle_index], + projection_shifts[angle_index]) + image += relaxation * image_update + if not clip is None: + image = np.clip(image, clip[0], clip[1]) + return image diff --git a/skimage/transform/setup.py b/skimage/transform/setup.py index b0093d87..22f31696 100644 --- a/skimage/transform/setup.py +++ b/skimage/transform/setup.py @@ -15,6 +15,7 @@ def configuration(parent_package='', top_path=None): cython(['_hough_transform.pyx'], working_path=base_path) cython(['_warps_cy.pyx'], working_path=base_path) + cython(['_radon_transform.pyx'], working_path=base_path) config.add_extension('_hough_transform', sources=['_hough_transform.c'], include_dirs=[get_numpy_include_dirs()]) @@ -22,6 +23,10 @@ def configuration(parent_package='', top_path=None): config.add_extension('_warps_cy', sources=['_warps_cy.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) + config.add_extension('_radon_transform', + sources=['_radon_transform.c'], + include_dirs=[get_numpy_include_dirs()]) + return config if __name__ == '__main__': diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 344dbea0..fb19d8c1 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import * +from numpy.testing import assert_almost_equal, assert_equal import skimage.transform as tf from skimage.draw import line, circle_perimeter, ellipse_perimeter @@ -79,8 +79,10 @@ 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 + 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(): @@ -89,18 +91,24 @@ def test_hough_line_peaks_angle(): img[0, :] = True hspace, angles, dists = tf.hough_line(img) - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=45)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=90)[0]) == 1 theta = np.linspace(0, np.pi, 100) hspace, angles, dists = tf.hough_line(img, theta) - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=45)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=90)[0]) == 1 theta = np.linspace(np.pi / 3, 4. / 3 * np.pi, 100) hspace, angles, dists = tf.hough_line(img, theta) - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=45)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=90)[0]) == 1 def test_hough_line_peaks_num(): @@ -120,7 +128,7 @@ def test_hough_circle(): y, x = circle_perimeter(y_0, x_0, radius) img[x, y] = 1 - out = tf.hough_circle(img, np.array([radius])) + out = tf.hough_circle(img, np.array([radius], dtype=np.intp)) x, y = np.where(out[0] == out[0].max()) assert_equal(x[0], x_0) @@ -136,7 +144,8 @@ def test_hough_circle_extended(): y, x = circle_perimeter(y_0, x_0, radius) img[x[np.where(x > 0)], y[np.where(x > 0)]] = 1 - out = tf.hough_circle(img, np.array([radius]), full_output=True) + out = tf.hough_circle(img, np.array([radius], dtype=np.intp), + full_output=True) x, y = np.where(out[0] == out[0].max()) # Offset for x_0, y_0 @@ -146,37 +155,205 @@ def test_hough_circle_extended(): def test_hough_ellipse_zero_angle(): img = np.zeros((25, 25), dtype=int) - a = 6 - b = 8 + rx = 6 + ry = 8 x0 = 12 - y0 = 12 + y0 = 15 angle = 0 - rr, cc = ellipse_perimeter(x0, x0, b, a) + rr, cc = ellipse_perimeter(y0, x0, ry, rx) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=9) - assert_equal(result[0][0], x0) - assert_equal(result[0][1], y0) - assert_almost_equal(result[0][2], b, decimal=1) - assert_almost_equal(result[0][3], a, decimal=1) - assert_equal(result[0][4], angle) + best = result[-1] + assert_equal(best[1], y0) + assert_equal(best[2], x0) + assert_almost_equal(best[3], ry, decimal=1) + assert_almost_equal(best[4], rx, decimal=1) + assert_equal(best[5], angle) + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) -def test_hough_ellipse_non_zero_angle(): - img = np.zeros((20, 20), dtype=int) - a = 6 - b = 9 +def test_hough_ellipse_non_zero_posangle1(): + # ry > rx, angle in [0:pi/2] + img = np.zeros((30, 24), dtype=int) + rx = 6 + ry = 12 x0 = 10 - y0 = 10 - angle = np.pi/1.35 - rr, cc = ellipse_perimeter(x0, x0, b, a, orientation=angle) + y0 = 15 + angle = np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - print(result) - assert_almost_equal(result[0][0]/100., x0/100., decimal=1) - assert_almost_equal(result[0][1]/100., y0/100., decimal=1) - assert_almost_equal(result[0][2]/100., b/100., decimal=1) - assert_almost_equal(result[0][3]/100., a/100., decimal=1) - assert_almost_equal(result[0][4], angle, decimal=1) + result.sort(order='accumulator') + best = result[-1] + assert_almost_equal(best[1] / 100., y0 / 100., decimal=1) + assert_almost_equal(best[2] / 100., x0 / 100., decimal=1) + assert_almost_equal(best[3] / 10., ry / 10., decimal=1) + assert_almost_equal(best[4] / 100., rx / 100., decimal=1) + assert_almost_equal(best[5], angle, decimal=1) + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_posangle2(): + # ry < rx, angle in [0:pi/2] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + assert_almost_equal(best[1] / 100., y0 / 100., decimal=1) + assert_almost_equal(best[2] / 100., x0 / 100., decimal=1) + assert_almost_equal(best[3] / 10., ry / 10., decimal=1) + assert_almost_equal(best[4] / 100., rx / 100., decimal=1) + assert_almost_equal(best[5], angle, decimal=1) + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_posangle3(): + # ry < rx, angle in [pi/2:pi] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = np.pi / 1.35 + np.pi / 2. + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_posangle4(): + # ry < rx, angle in [pi:3pi/4] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = np.pi / 1.35 + np.pi + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle1(): + # ry > rx, angle in [0:-pi/2] + img = np.zeros((30, 24), dtype=int) + rx = 6 + ry = 12 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle2(): + # ry < rx, angle in [0:-pi/2] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle3(): + # ry < rx, angle in [-pi/2:-pi] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 - np.pi / 2. + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle4(): + # ry < rx, angle in [-pi:-3pi/4] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 - np.pi + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + result.sort(order='accumulator') + best = result[-1] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + if __name__ == "__main__": - run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 79cf80c5..6fbc3f62 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -1,10 +1,42 @@ -from __future__ import print_function -from __future__ import division +from __future__ import print_function, division import numpy as np -from numpy.testing import * +from numpy.testing import assert_raises import itertools -from skimage.transform import * +import os.path + +from skimage.transform import radon, iradon +from skimage.io import imread +from skimage import data_dir + + +__PHANTOM = imread(os.path.join(data_dir, "phantom.png"), + as_grey=True)[::2, ::2] + + +def _get_phantom(): + return __PHANTOM + + +def _debug_plot(original, result, sinogram=None): + from matplotlib import pyplot as plt + imkwargs = dict(cmap='gray', interpolation='nearest') + if sinogram is None: + plt.figure(figsize=(15, 6)) + sp = 130 + else: + plt.figure(figsize=(11, 11)) + sp = 221 + plt.subplot(sp + 0) + plt.imshow(sinogram, aspect='auto', **imkwargs) + plt.subplot(sp + 1) + plt.imshow(original, **imkwargs) + plt.subplot(sp + 2) + plt.imshow(result, vmin=original.min(), vmax=original.max(), **imkwargs) + plt.subplot(sp + 3) + plt.imshow(result - original, **imkwargs) + plt.colorbar() + plt.show() def rescale(x): @@ -14,35 +46,102 @@ def rescale(x): return x -def test_radon_iradon(): - size = 100 +def check_radon_center(shape, circle): + # Create a test image with only a single non-zero pixel at the origin + image = np.zeros(shape, dtype=np.float) + image[(shape[0] // 2, shape[1] // 2)] = 1. + # Calculate the sinogram + theta = np.linspace(0., 180., max(shape), endpoint=False) + sinogram = radon(image, theta=theta, circle=circle) + # The sinogram should be a straight, horizontal line + sinogram_max = np.argmax(sinogram, axis=0) + print(sinogram_max) + assert np.std(sinogram_max) < 1e-6 + + +def test_radon_center(): + shapes = [(16, 16), (17, 17)] + circles = [False, True] + for shape, circle in itertools.product(shapes, circles): + yield check_radon_center, shape, circle + rectangular_shapes = [(32, 16), (33, 17)] + for shape in rectangular_shapes: + yield check_radon_center, shape, False + + +def check_iradon_center(size, theta, circle): debug = False - image = np.tri(size) + np.tri(size)[::-1] - for filter_type in ["ramp", "shepp-logan", "cosine", "hamming", "hann"]: - reconstructed = iradon(radon(image), filter=filter_type) + # Create a test sinogram corresponding to a single projection + # with a single non-zero pixel at the rotation center + if circle: + sinogram = np.zeros((size, 1), dtype=np.float) + sinogram[size // 2, 0] = 1. + else: + diagonal = int(np.ceil(np.sqrt(2) * size)) + sinogram = np.zeros((diagonal, 1), dtype=np.float) + sinogram[sinogram.shape[0] // 2, 0] = 1. + maxpoint = np.unravel_index(np.argmax(sinogram), sinogram.shape) + print('shape of generated sinogram', sinogram.shape) + print('maximum in generated sinogram', maxpoint) + # Compare reconstructions for theta=angle and theta=angle + 180; + # these should be exactly equal + reconstruction = iradon(sinogram, theta=[theta], circle=circle) + reconstruction_opposite = iradon(sinogram, theta=[theta + 180], + circle=circle) + print('rms deviance:', + np.sqrt(np.mean((reconstruction_opposite - reconstruction)**2))) + if debug: + import matplotlib.pyplot as plt + imkwargs = dict(cmap='gray', interpolation='nearest') + plt.figure() + plt.subplot(221) + plt.imshow(sinogram, **imkwargs) + plt.subplot(222) + plt.imshow(reconstruction_opposite - reconstruction, **imkwargs) + plt.subplot(223) + plt.imshow(reconstruction, **imkwargs) + plt.subplot(224) + plt.imshow(reconstruction_opposite, **imkwargs) + plt.show() - image = rescale(image) - reconstructed = rescale(reconstructed) - delta = np.mean(np.abs(image - reconstructed)) + assert np.allclose(reconstruction, reconstruction_opposite) - if debug: - print(delta) - import matplotlib.pyplot as plt - f, (ax1, ax2) = plt.subplots(1, 2) - ax1.imshow(image, cmap=plt.cm.gray) - ax2.imshow(reconstructed, cmap=plt.cm.gray) - plt.show() - assert delta < 0.05 +def test_iradon_center(): + sizes = [16, 17] + thetas = [0, 90] + circles = [False, True] + for size, theta, circle in itertools.product(sizes, thetas, circles): + yield check_iradon_center, size, theta, circle - reconstructed = iradon(radon(image), filter="ramp", - interpolation="nearest") - delta = np.mean(abs(image - reconstructed)) - assert delta < 0.05 - size = 20 - image = np.tri(size) + np.tri(size)[::-1] - reconstructed = iradon(radon(image), filter="ramp", - interpolation="nearest") + +def check_radon_iradon(interpolation_type, filter_type): + debug = False + image = _get_phantom() + reconstructed = iradon(radon(image), filter=filter_type, + interpolation=interpolation_type) + delta = np.mean(np.abs(image - reconstructed)) + print('\n\tmean error:', delta) + if debug: + _debug_plot(image, reconstructed) + if filter_type in ('ramp', 'shepp-logan'): + if interpolation_type == 'nearest': + allowed_delta = 0.03 + else: + allowed_delta = 0.02 + else: + allowed_delta = 0.05 + assert delta < allowed_delta + + +def test_radon_iradon(): + filter_types = ["ramp", "shepp-logan", "cosine", "hamming", "hann"] + interpolation_types = ['linear', 'nearest'] + for interpolation_type, filter_type in \ + itertools.product(interpolation_types, filter_types): + yield check_radon_iradon, interpolation_type, filter_type + # cubic interpolation is slow; only run one test for it + yield check_radon_iradon, 'cubic', 'shepp-logan' def test_iradon_angles(): @@ -73,32 +172,29 @@ def test_iradon_angles(): assert delta_80 > delta_200 -def test_radon_minimal(): - """ - Test for small images for various angles - """ - thetas = [np.arange(180)] - for theta in thetas: - a = np.zeros((3, 3)) - a[1, 1] = 1 - p = radon(a, theta) - reconstructed = iradon(p, theta) - reconstructed /= np.max(reconstructed) - assert np.all(abs(a - reconstructed) < 0.4) +def check_radon_iradon_minimal(shape, slices): + debug = False + theta = np.arange(180) + image = np.zeros(shape, dtype=np.float) + image[slices] = 1. + sinogram = radon(image, theta) + reconstructed = iradon(sinogram, theta) + print('\n\tMaximum deviation:', np.max(np.abs(image - reconstructed))) + if debug: + _debug_plot(image, reconstructed, sinogram) + if image.sum() == 1: + assert (np.unravel_index(np.argmax(reconstructed), image.shape) + == np.unravel_index(np.argmax(image), image.shape)) - b = np.zeros((4, 4)) - b[1:3, 1:3] = 1 - p = radon(b, theta) - reconstructed = iradon(p, theta) - reconstructed /= np.max(reconstructed) - assert np.all(abs(b - reconstructed) < 0.4) - c = np.zeros((5, 5)) - c[1:3, 1:3] = 1 - p = radon(c, theta) - reconstructed = iradon(p, theta) - reconstructed /= np.max(reconstructed) - assert np.all(abs(c - reconstructed) < 0.4) +def test_radon_iradon_minimal(): + shapes = [(3, 3), (4, 4), (5, 5)] + for shape in shapes: + c0, c1 = shape[0] // 2, shape[1] // 2 + coordinates = itertools.product((c0 - 1, c0, c0 + 1), + (c1 - 1, c1, c1 + 1)) + for coordinate in coordinates: + yield check_radon_iradon_minimal, shape, coordinate def test_reconstruct_with_wrong_angles(): @@ -115,7 +211,7 @@ def _random_circle(shape): c0, c1 = np.ogrid[0:shape[0], 0:shape[1]] r = np.sqrt((c0 - shape[0] // 2)**2 + (c1 - shape[1] // 2)**2) radius = min(shape) // 2 - image[r >= radius] = 0. + image[r > radius] = 0. return image @@ -141,45 +237,148 @@ def test_radon_circle(): average_mass = mass.mean() relative_error = np.abs(mass - average_mass) / average_mass print(relative_error.max(), relative_error.mean()) - assert np.all(relative_error < 3e-3) + assert np.all(relative_error < 3.2e-3) + + +def check_sinogram_circle_to_square(size): + from skimage.transform.radon_transform import _sinogram_circle_to_square + image = _random_circle((size, size)) + theta = np.linspace(0., 180., size, False) + sinogram_circle = radon(image, theta, circle=True) + argmax_shape = lambda a: np.unravel_index(np.argmax(a), a.shape) + print('\n\targmax of circle:', argmax_shape(sinogram_circle)) + sinogram_square = radon(image, theta, circle=False) + print('\targmax of square:', argmax_shape(sinogram_square)) + sinogram_circle_to_square = _sinogram_circle_to_square(sinogram_circle) + print('\targmax of circle to square:', + argmax_shape(sinogram_circle_to_square)) + error = abs(sinogram_square - sinogram_circle_to_square) + print(np.mean(error), np.max(error)) + assert (argmax_shape(sinogram_square) + == argmax_shape(sinogram_circle_to_square)) + + +def test_sinogram_circle_to_square(): + for size in (50, 51): + yield check_sinogram_circle_to_square, size + + +def check_radon_iradon_circle(interpolation, shape, output_size): + # Forward and inverse radon on synthetic data + image = _random_circle(shape) + radius = min(shape) // 2 + sinogram_rectangle = radon(image, circle=False) + reconstruction_rectangle = iradon(sinogram_rectangle, + output_size=output_size, + interpolation=interpolation, + circle=False) + sinogram_circle = radon(image, circle=True) + reconstruction_circle = iradon(sinogram_circle, + output_size=output_size, + interpolation=interpolation, + circle=True) + # Crop rectangular reconstruction to match circle=True reconstruction + width = reconstruction_circle.shape[0] + excess = int(np.ceil((reconstruction_rectangle.shape[0] - width) / 2)) + s = np.s_[excess:width + excess, excess:width + excess] + reconstruction_rectangle = reconstruction_rectangle[s] + # Find the reconstruction circle, set reconstruction to zero outside + c0, c1 = np.ogrid[0:width, 0:width] + r = np.sqrt((c0 - width // 2)**2 + (c1 - width // 2)**2) + reconstruction_rectangle[r > radius] = 0. + print(reconstruction_circle.shape) + print(reconstruction_rectangle.shape) + np.allclose(reconstruction_rectangle, reconstruction_circle) def test_radon_iradon_circle(): shape = (61, 79) - radius = min(shape) // 2 - image = _random_circle(shape) interpolations = ('nearest', 'linear') output_sizes = (None, min(shape), max(shape), 97) - for interpolation, output_size in itertools.product(interpolations, output_sizes): - print('interpolation =', interpolation) - print('output_size =', output_size) - # Forward and inverse radon on synthetic data - sinogram_rectangle = radon(image, circle=False) - reconstruction_rectangle = iradon(sinogram_rectangle, - output_size=output_size, - interpolation=interpolation, - circle=False) - sinogram_circle = radon(image, circle=True) - reconstruction_circle = iradon(sinogram_circle, - output_size=output_size, - interpolation=interpolation, - circle=True) - # Crop rectangular reconstruction to match circle=True reconstruction - width = reconstruction_circle.shape[0] - excess = int(np.ceil((reconstruction_rectangle.shape[0] - width) / 2)) - s = np.s_[excess:width + excess, excess:width + excess] - reconstruction_rectangle = reconstruction_rectangle[s] - # Find the reconstruction circle, set reconstruction to zero outside - c0, c1 = np.ogrid[0:width, 0:width] - r = np.sqrt((c0 - width // 2)**2 + (c1 - width // 2)**2) - reconstruction_rectangle[r >= radius] = 0. - print(reconstruction_circle.shape) - print(reconstruction_rectangle.shape) - np.allclose(reconstruction_rectangle, reconstruction_circle) + yield check_radon_iradon_circle, interpolation, shape, output_size +def test_order_angles_golden_ratio(): + from skimage.transform.radon_transform import order_angles_golden_ratio + np.random.seed(1231) + lengths = [1, 4, 10, 180] + for l in lengths: + theta_ordered = np.linspace(0, 180, l, endpoint=False) + theta_random = np.random.uniform(0, 180, l) + for theta in (theta_random, theta_ordered): + indices = [x for x in order_angles_golden_ratio(theta)] + # no duplicate indices allowed + assert len(indices) == len(set(indices)) + + +def test_iradon_sart(): + from skimage.io import imread + from skimage import data_dir + from skimage.transform import rescale, radon, iradon_sart + + debug = False + + shepp_logan = imread(os.path.join(data_dir, "phantom.png"), as_grey=True) + image = rescale(shepp_logan, scale=0.4) + theta_ordered = np.linspace(0., 180., image.shape[0], endpoint=False) + theta_missing_wedge = np.linspace(0., 150., image.shape[0], endpoint=True) + for theta, error_factor in ((theta_ordered, 1.), + (theta_missing_wedge, 2.)): + sinogram = radon(image, theta, circle=True) + reconstructed = iradon_sart(sinogram, theta) + + if debug: + from matplotlib import pyplot as plt + plt.figure() + plt.subplot(221) + plt.imshow(image, interpolation='nearest') + plt.subplot(222) + plt.imshow(sinogram, interpolation='nearest') + plt.subplot(223) + plt.imshow(reconstructed, interpolation='nearest') + plt.subplot(224) + plt.imshow(reconstructed - image, interpolation='nearest') + plt.show() + + delta = np.mean(np.abs(reconstructed - image)) + print('delta (1 iteration) =', delta) + assert delta < 0.016 * error_factor + reconstructed = iradon_sart(sinogram, theta, reconstructed) + delta = np.mean(np.abs(reconstructed - image)) + print('delta (2 iterations) =', delta) + assert delta < 0.013 * error_factor + reconstructed = iradon_sart(sinogram, theta, clip=(0, 1)) + delta = np.mean(np.abs(reconstructed - image)) + print('delta (1 iteration, clip) =', delta) + assert delta < 0.015 * error_factor + + np.random.seed(1239867) + shifts = np.random.uniform(-3, 3, sinogram.shape[1]) + x = np.arange(sinogram.shape[0]) + sinogram_shifted = np.vstack(np.interp(x + shifts[i], x, + sinogram[:, i]) + for i in range(sinogram.shape[1])).T + reconstructed = iradon_sart(sinogram_shifted, theta, + projection_shifts=shifts) + if debug: + from matplotlib import pyplot as plt + plt.figure() + plt.subplot(221) + plt.imshow(image, interpolation='nearest') + plt.subplot(222) + plt.imshow(sinogram_shifted, interpolation='nearest') + plt.subplot(223) + plt.imshow(reconstructed, interpolation='nearest') + plt.subplot(224) + plt.imshow(reconstructed - image, interpolation='nearest') + plt.show() + + delta = np.mean(np.abs(reconstructed - image)) + print('delta (1 iteration, shifted sinogram) =', delta) + assert delta < 0.018 * error_factor + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 93f87320..7f7ef47d 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -1,19 +1,19 @@ -from numpy.testing import assert_array_almost_equal, run_module_suite +from numpy.testing import assert_array_almost_equal, run_module_suite, assert_array_equal import numpy as np from scipy.ndimage import map_coordinates from skimage.transform import (warp, warp_coords, rotate, resize, rescale, AffineTransform, ProjectiveTransform, - SimilarityTransform) + SimilarityTransform, + downscale_local_mean) from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray -def test_warp(): - x = np.zeros((5, 5), dtype=np.uint8) - x[2, 2] = 255 - x = img_as_float(x) +def test_warp_tform(): + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 theta = - np.pi / 2 tform = SimilarityTransform(scale=1, rotation=theta, translation=(0, 4)) @@ -24,10 +24,36 @@ def test_warp(): assert_array_almost_equal(x90, np.rot90(x)) +def test_warp_callable(): + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 + refx = np.zeros((5, 5), dtype=np.double) + refx[1, 1] = 1 + + shift = lambda xy: xy + 1 + + outx = warp(x, shift, order=1) + assert_array_almost_equal(outx, refx) + + +def test_warp_matrix(): + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 + refx = np.zeros((5, 5), dtype=np.double) + refx[1, 1] = 1 + + matrix = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 1]]) + + # _warp_fast + outx = warp(x, matrix, order=1) + assert_array_almost_equal(outx, refx) + # check for ndimage.map_coordinates + outx = warp(x, matrix, order=5) + + def test_homography(): - x = np.zeros((5, 5), dtype=np.uint8) - x[1, 1] = 255 - x = img_as_float(x) + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 theta = -np.pi / 2 M = np.array([[np.cos(theta), - np.sin(theta), 0], [np.sin(theta), np.cos(theta), 4], @@ -194,5 +220,19 @@ def test_warp_coords_example(): map_coordinates(image[:, :, 0], coords[:2]) +def test_downscale_local_mean(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = downscale_local_mean(image1, (2, 3)) + expected1 = np.array([[ 4., 7.], + [ 16., 19.]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = downscale_local_mean(image2, (4, 5)) + expected2 = np.array([[ 14. , 10.8], + [ 8.5, 5.7]]) + assert_array_equal(expected2, out2) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index 5433a14b..9cd2bc50 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -1,6 +1,7 @@ from .dtype import (img_as_float, img_as_int, img_as_uint, img_as_ubyte, img_as_bool, dtype_limits) from .shape import view_as_blocks, view_as_windows +from .noise import random_noise import numpy ver = numpy.__version__.split('.') @@ -10,6 +11,8 @@ if chk < 18: # Use internal version for numpy versions < 1.8.x else: from numpy import pad del numpy, ver, chk +from ._regular_grid import regular_grid +from .unique import unique_rows __all__ = ['img_as_float', @@ -20,4 +23,7 @@ __all__ = ['img_as_float', 'dtype_limits', 'view_as_blocks', 'view_as_windows', - 'pad'] + 'pad', + 'random_noise', + 'regular_grid', + 'unique_rows'] diff --git a/skimage/util/_regular_grid.py b/skimage/util/_regular_grid.py new file mode 100644 index 00000000..898a4aed --- /dev/null +++ b/skimage/util/_regular_grid.py @@ -0,0 +1,72 @@ +import numpy as np + + +def regular_grid(ar_shape, n_points): + """Find `n_points` regularly spaced along `ar_shape`. + + The returned points (as slices) should be as close to cubically-spaced as + possible. Essentially, the points are spaced by the Nth root of the input + array size, where N is the number of dimensions. However, if an array + dimension cannot fit a full step size, it is "discarded", and the + computation is done for only the remaining dimensions. + + Parameters + ---------- + ar_shape : array-like of ints + The shape of the space embedding the grid. ``len(ar_shape)`` is the + number of dimensions. + n_points : int + The (approximate) number of points to embed in the space. + + Returns + ------- + slices : list of slice objects + A slice along each dimension of `ar_shape`, such that the intersection + of all the slices give the coordinates of regularly spaced points. + + Examples + -------- + >>> ar = np.zeros((20, 40)) + >>> g = regular_grid(ar.shape, 8) + >>> g + [slice(5.0, None, 10.0), slice(5.0, None, 10.0)] + >>> ar[g] = 1 + >>> ar.sum() + 8.0 + >>> ar = np.zeros((20, 40)) + >>> g = regular_grid(ar.shape, 32) + >>> g + [slice(2.0, None, 5.0), slice(2.0, None, 5.0)] + >>> ar[g] = 1 + >>> ar.sum() + 32.0 + >>> ar = np.zeros((3, 20, 40)) + >>> g = regular_grid(ar.shape, 8) + >>> g + [slice(1.0, None, 3.0), slice(5.0, None, 10.0), slice(5.0, None, 10.0)] + >>> ar[g] = 1 + >>> ar.sum() + 8.0 + """ + ar_shape = np.asanyarray(ar_shape) + ndim = len(ar_shape) + unsort_dim_idxs = np.argsort(np.argsort(ar_shape)) + sorted_dims = np.sort(ar_shape) + space_size = float(np.prod(ar_shape)) + if space_size <= n_points: + return [slice(None)] * ndim + stepsizes = (space_size / n_points) ** (1.0 / ndim) * np.ones(ndim) + if (sorted_dims < stepsizes).any(): + for dim in range(ndim): + stepsizes[dim] = sorted_dims[dim] + space_size = float(np.prod(sorted_dims[dim+1:])) + stepsizes[dim+1:] = ((space_size / n_points) ** + (1.0 / (ndim - dim - 1))) + if (sorted_dims >= stepsizes).all(): + break + starts = stepsizes // 2 + stepsizes = np.round(stepsizes) + slices = [slice(start, None, step) for + start, step in zip(starts, stepsizes)] + slices = [slices[i] for i in unsort_dim_idxs] + return slices diff --git a/skimage/util/arraypad.py b/skimage/util/arraypad.py index 52a2e3e5..93f66bb8 100644 --- a/skimage/util/arraypad.py +++ b/skimage/util/arraypad.py @@ -4,8 +4,10 @@ of an n-dimensional array. """ from __future__ import division, absolute_import, print_function +from skimage._shared.six import integer_types import numpy as np + try: # Available on 2.x at base, Py3 requires this compatibility import. # Later versions of NumPy have this for 2.x as well. @@ -1036,11 +1038,11 @@ def _normalize_shape(narray, shape): fmt = "Unable to create correctly shaped tuple from %s" raise ValueError(fmt % (normshp,)) elif (isinstance(shape, (tuple, list)) - and isinstance(shape[0], (int, float, long)) + 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], (int, float, long)) + and isinstance(shape[0], integer_types + (float,)) and len(shape) == 2): normshp = (shape, ) * shapelen if normshp is None: @@ -1083,7 +1085,7 @@ def _validate_lengths(narray, number_elements): normshp = _normalize_shape(narray, number_elements) for i in normshp: chk = [1 if x is None else x for x in i] - chk = [1 if x > 0 else -1 for x in chk] + chk = [1 if x >= 0 else -1 for x in chk] if (chk[0] < 0) or (chk[1] < 0): fmt = "%s cannot contain negative values." raise ValueError(fmt % (number_elements,)) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 8daea421..3f85cc2b 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -1,12 +1,10 @@ from __future__ import division import numpy as np +from warnings import warn __all__ = ['img_as_float', 'img_as_int', 'img_as_uint', 'img_as_ubyte', 'img_as_bool', 'dtype_limits'] -from .. import get_log -log = get_log('dtype_converter') - dtype_range = {np.bool_: (False, True), np.bool8: (False, True), np.uint8: (0, 255), @@ -101,12 +99,12 @@ def convert(image, dtype, force_copy=False, uniform=False): raise ValueError("can not convert %s to %s." % (dtypeobj_in, dtypeobj)) def sign_loss(): - log.warn("Possible sign loss when converting negative image of type " - "%s to positive image of type %s." % (dtypeobj_in, dtypeobj)) + warn("Possible sign loss when converting negative image of type " + "%s to positive image of type %s." % (dtypeobj_in, dtypeobj)) def prec_loss(): - log.warn("Possible precision loss when converting from " - "%s to %s" % (dtypeobj_in, dtypeobj)) + warn("Possible precision loss when converting from " + "%s to %s" % (dtypeobj_in, dtypeobj)) def _dtype(itemsize, *dtypes): # Return first of `dtypes` with itemsize greater than `itemsize` diff --git a/skimage/util/montage.py b/skimage/util/montage.py index df3ad12e..805b78a8 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -6,11 +6,11 @@ from .. import exposure EPSILON = 1e-6 -def montage2d(arr_in, fill='mean', rescale_intensity=False): +def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=None): """Create a 2-dimensional 'montage' from a 3-dimensional input array representing an ensemble of equally shaped 2-dimensional images. - For example, montage2d(arr_in, fill) with the following `arr_in` + For example, ``montage2d(arr_in, fill)`` with the following `arr_in` +---+---+---+ | 1 | 2 | 3 | @@ -31,13 +31,14 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): arr_in: ndarray, shape=[n_images, height, width] 3-dimensional input array representing an ensemble of n_images of equal shape (i.e. [height, width]). - fill: float or 'mean', optional How to fill the 2-dimensional output array when sqrt(n_images) is not an integer. If 'mean' is chosen, then fill = arr_in.mean(). - rescale_intensity: bool, optional Whether to rescale the intensity of each image to [0, 1]. + grid_shape: tuple, optional + The desired grid shape for the montage (tiles_y, tiles_x). + The default aspect ratio is square. Returns ------- @@ -67,6 +68,13 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): [ 10. 11. 5.5 5.5]] >>> print(arr_in.mean()) 5.5 + >>> arr_out_nonsquare = montage2d(arr_in, grid_shape=(3, 4)) + >>> print(arr_out_nonsquare) + [[ 0. 1. 4. 5. ] + [ 2. 3. 6. 7. ] + [ 8. 9. 10. 11. ]] + >>> print(arr_out_nonsquare.shape) + (3, 4) """ assert arr_in.ndim == 3 @@ -80,19 +88,22 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): arr_in[i] = exposure.rescale_intensity(arr_in[i]) # -- determine alpha - alpha = int(np.ceil(np.sqrt(n_images))) + if grid_shape: + alpha_y, alpha_x = grid_shape + else: + alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) # -- fill missing patches if fill == 'mean': fill = arr_in.mean() - n_missing = int((alpha**2.) - n_images) + n_missing = int((alpha_y * alpha_x) - n_images) missing = np.ones((n_missing, height, width), dtype=arr_in.dtype) * fill arr_out = np.vstack((arr_in, missing)) # -- reshape to 2d montage, step by step - arr_out = arr_out.reshape(alpha, alpha, height, width) + arr_out = arr_out.reshape(alpha_y, alpha_x, height, width) arr_out = arr_out.swapaxes(1, 2) - arr_out = arr_out.reshape(alpha * height, alpha * width) + arr_out = arr_out.reshape(alpha_y * height, alpha_x * width) return arr_out diff --git a/skimage/util/noise.py b/skimage/util/noise.py new file mode 100644 index 00000000..9283f537 --- /dev/null +++ b/skimage/util/noise.py @@ -0,0 +1,196 @@ +import numpy as np +from .dtype import img_as_float + + +__all__ = ['random_noise'] + + +def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): + """ + Function to add random noise of various types to a floating-point image. + + Parameters + ---------- + image : ndarray + Input image data. Will be converted to float. + mode : str + One of the following strings, selecting the type of noise to add: + + 'gaussian' Gaussian-distributed additive noise. + 'localvar' Gaussian-distributed additive noise, with specified + local variance at each point of `image` + 'poisson' Poisson-distributed noise generated from the data. + 'salt' Replaces random pixels with 1. + 'pepper' Replaces random pixels with 0. + 's&p' Replaces random pixels with 0 or 1. + 'speckle' Multiplicative noise using out = image + n*image, where + n is uniform noise with specified mean & variance. + seed : int + If provided, this will set the random seed before generating noise, + for valid pseudo-random comparisons. + clip : bool + If True (default), the output will be clipped after noise applied + for modes `'speckle'`, `'poisson'`, and `'gaussian'`. This is + needed to maintain the proper image data range. If False, clipping + is not applied, and the output may extend beyond the range [-1, 1]. + mean : float + Mean of random distribution. Used in 'gaussian' and 'speckle'. + Default : 0. + var : float + Variance of random distribution. Used in 'gaussian' and 'speckle'. + Note: variance = (standard deviation) ** 2. Default : 0.01 + local_vars : ndarray + Array of positive floats, same shape as `image`, defining the local + variance at every image point. Used in 'localvar'. + amount : float + Proportion of image pixels to replace with noise on range [0, 1]. + Used in 'salt', 'pepper', and 'salt & pepper'. Default : 0.05 + salt_vs_pepper : float + Proportion of salt vs. pepper noise for 's&p' on range [0, 1]. + Higher values represent more salt. Default : 0.5 (equal amounts) + + Returns + ------- + out : ndarray + Output floating-point image data on range [0, 1] or [-1, 1] if the + input `image` was unsigned or signed, respectively. + + Notes + ----- + Speckle, Poisson, Localvar, and Gaussian noise may generate noise outside + the valid image range. The default is to clip (not alias) these values, + but they may be preserved by setting `clip=False`. Note that in this case + the output may contain values outside the ranges [0, 1] or [-1, 1]. + Use this option with care. + + Because of the prevalence of exclusively positive floating-point images in + intermediate calculations, it is not possible to intuit if an input is + signed based on dtype alone. Instead, negative values are explicity + searched for. Only if found does this function assume signed input. + Unexpected results only occur in rare, poorly exposes cases (e.g. if all + values are above 50 percent gray in a signed `image`). In this event, + manually scaling the input to the positive domain will solve the problem. + + The Poisson distribution is only defined for positive integers. To apply + this noise type, the number of unique values in the image is found and + the next round power of two is used to scale up the floating-point result, + after which it is scaled back down to the floating-point image range. + + To generate Poisson noise against a signed image, the signed image is + temporarily converted to an unsigned image in the floating point domain, + Poisson noise is generated, then it is returned to the original range. + + """ + mode = mode.lower() + + # Detect if a signed image was input + if image.min() < 0: + low_clip = -1. + else: + low_clip = 0. + + image = img_as_float(image) + if seed is not None: + np.random.seed(seed=seed) + + allowedtypes = { + 'gaussian': 'gaussian_values', + 'localvar': 'localvar_values', + 'poisson': 'poisson_values', + 'salt': 'sp_values', + 'pepper': 'sp_values', + 's&p': 's&p_values', + 'speckle': 'gaussian_values'} + + kwdefaults = { + 'mean': 0., + 'var': 0.01, + 'amount': 0.05, + 'salt_vs_pepper': 0.5, + 'local_vars': np.zeros_like(image) + 0.01} + + allowedkwargs = { + 'gaussian_values': ['mean', 'var'], + 'localvar_values': ['local_vars'], + 'sp_values': ['amount'], + 's&p_values': ['amount', 'salt_vs_pepper'], + 'poisson_values': []} + + for key in kwargs: + if key not in allowedkwargs[allowedtypes[mode]]: + raise ValueError('%s keyword not in allowed keywords %s' % + (key, allowedkwargs[allowedtypes[mode]])) + + # Set kwarg defaults + for kw in allowedkwargs[allowedtypes[mode]]: + kwargs.setdefault(kw, kwdefaults[kw]) + + if mode == 'gaussian': + noise = np.random.normal(kwargs['mean'], kwargs['var'] ** 0.5, + image.shape) + out = image + noise + + elif mode == 'localvar': + # Ensure local variance input is correct + if (kwargs['local_vars'] <= 0).any(): + raise ValueError('All values of `local_vars` must be > 0.') + + # Safe shortcut usage broadcasts kwargs['local_vars'] as a ufunc + out = image + np.random.normal(0, kwargs['local_vars'] ** 0.5) + + elif mode == 'poisson': + # Determine unique values in image & calculate the next power of two + vals = len(np.unique(image)) + vals = 2 ** np.ceil(np.log2(vals)) + + # Ensure image is exclusively positive + if low_clip == -1.: + old_max = image.max() + image = (image + 1.) / (old_max + 1.) + + # Generating noise for each unique value in image. + out = np.random.poisson(image * vals) / float(vals) + + # Return image to original range if input was signed + if low_clip == -1.: + out = out * (old_max + 1.) - 1. + + elif mode == 'salt': + # Re-call function with mode='s&p' and p=1 (all salt noise) + out = random_noise(image, mode='s&p', seed=seed, + amount=kwargs['amount'], salt_vs_pepper=1.) + + elif mode == 'pepper': + # Re-call function with mode='s&p' and p=1 (all pepper noise) + out = random_noise(image, mode='s&p', seed=seed, + amount=kwargs['amount'], salt_vs_pepper=0.) + + elif mode == 's&p': + # This mode makes no effort to avoid repeat sampling. Thus, the + # exact number of replaced pixels is only approximate. + out = image.copy() + + # Salt mode + num_salt = np.ceil( + kwargs['amount'] * image.size * kwargs['salt_vs_pepper']) + coords = [np.random.randint(0, i - 1, int(num_salt)) + for i in image.shape] + out[coords] = 1 + + # Pepper mode + num_pepper = np.ceil( + kwargs['amount'] * image.size * (1. - kwargs['salt_vs_pepper'])) + coords = [np.random.randint(0, i - 1, int(num_pepper)) + for i in image.shape] + out[coords] = low_clip + + elif mode == 'speckle': + noise = np.random.normal(kwargs['mean'], kwargs['var'] ** 0.5, + image.shape) + out = image + image * noise + + # Clip back to original range, if necessary + if clip: + out = np.clip(out, low_clip, 1.0) + + return out diff --git a/skimage/util/shape.py b/skimage/util/shape.py index 0126d2e3..f91286c3 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -98,7 +98,7 @@ def view_as_blocks(arr_in, block_shape): return arr_out -def view_as_windows(arr_in, window_shape): +def view_as_windows(arr_in, window_shape, step=1): """Rolling window view of the input n-dimensional array. Windows are overlapping views of the input array, with adjacent windows @@ -108,10 +108,12 @@ def view_as_windows(arr_in, window_shape): ---------- arr_in: ndarray The n-dimensional input array. - window_shape: tuple Defines the shape of the elementary n-dimensional orthotope (better know as hyperrectangle [1]_) of the rolling window view. + step : int + Number of elements to skip when moving the window forward (by + default, move forward by one). Returns ------- @@ -206,26 +208,32 @@ def view_as_windows(arr_in, window_shape): # -- basic checks on arguments if not isinstance(arr_in, np.ndarray): - raise TypeError("'arr_in' must be a numpy ndarray") + raise TypeError("`arr_in` must be a numpy ndarray") if not isinstance(window_shape, tuple): - raise TypeError("'window_shape' must be a tuple") + raise TypeError("`window_shape` must be a tuple") if not (len(window_shape) == arr_in.ndim): - raise ValueError("'window_shape' is incompatible with 'arr_in.shape'") + raise ValueError("`window_shape` is incompatible with `arr_in.shape`") + + if step < 1: + raise ValueError("`step` must be >= 1") arr_shape = np.array(arr_in.shape) window_shape = np.array(window_shape, dtype=arr_shape.dtype) if ((arr_shape - window_shape) < 0).any(): - raise ValueError("'window_shape' is too large") + raise ValueError("`window_shape` is too large") if ((window_shape - 1) < 0).any(): - raise ValueError("'window_shape' is too small") + raise ValueError("`window_shape` is too small") # -- build rolling window view arr_in = np.ascontiguousarray(arr_in) - new_shape = tuple(arr_shape - window_shape + 1) + tuple(window_shape) - new_strides = arr_in.strides + arr_in.strides + new_shape = tuple((arr_shape - window_shape) // step + 1) + \ + tuple(window_shape) + + arr_strides = np.array(arr_in.strides) + new_strides = np.concatenate((arr_strides * step, arr_strides)) arr_out = as_strided(arr_in, shape=new_shape, strides=new_strides) diff --git a/skimage/util/tests/test_arraypad.py b/skimage/util/tests/test_arraypad.py index 2c0e5ba7..008c3516 100644 --- a/skimage/util/tests/test_arraypad.py +++ b/skimage/util/tests/test_arraypad.py @@ -518,5 +518,12 @@ def test_pad_one_axis_three_ways(): **kwargs) +def test_zero_pad_width(): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + for pad_width in (0, (0, 0), ((0, 0), (0, 0))): + assert np.all(arr == pad(arr, pad_width, mode='constant')) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/util/tests/test_montage.py b/skimage/util/tests/test_montage.py index 47e5426d..cb7f92dc 100644 --- a/skimage/util/tests/test_montage.py +++ b/skimage/util/tests/test_montage.py @@ -51,6 +51,23 @@ def test_shape(): arr_out = montage2d(arr_in) assert_equal(arr_out.shape, (alpha * height, alpha * width)) + + +def test_grid_shape(): + n_images = 6 + height, width = 2, 2 + arr_in = np.arange(n_images * height * width, dtype=np.float32) + arr_in = arr_in.reshape(n_images, height, width) + arr_out = montage2d(arr_in, grid_shape=(3,2)) + correct_arr_out = np.array( + [[ 0., 1., 4., 5.], + [ 2., 3., 6., 7.], + [ 8., 9., 12., 13.], + [ 10., 11., 14., 15.], + [ 16., 17., 20., 21.], + [ 18., 19., 22., 23.]] + ) + assert_array_equal(arr_out, correct_arr_out) def test_rescale_intensity(): @@ -79,3 +96,7 @@ def test_rescale_intensity(): def test_error_ndim(): arr_error = np.random.randn(1, 2, 3, 4) montage2d(arr_error) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py new file mode 100644 index 00000000..39477cb0 --- /dev/null +++ b/skimage/util/tests/test_random_noise.py @@ -0,0 +1,201 @@ +from numpy.testing import assert_array_equal, assert_allclose, assert_raises + +import numpy as np +from skimage.data import camera +from skimage.util import random_noise, img_as_float + + +def test_set_seed(): + seed = 42 + cam = camera() + test = random_noise(cam, seed=seed) + assert_array_equal(test, random_noise(cam, seed=seed)) + + +def test_salt(): + seed = 42 + cam = img_as_float(camera()) + cam_noisy = random_noise(cam, seed=seed, mode='salt', amount=0.15) + saltmask = cam != cam_noisy + + # Ensure all changes are to 1.0 + assert_allclose(cam_noisy[saltmask], np.ones(saltmask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float(saltmask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.15 + + +def test_pepper(): + seed = 42 + cam = img_as_float(camera()) + data_signed = cam * 2. - 1. # Same image, on range [-1, 1] + + cam_noisy = random_noise(cam, seed=seed, mode='pepper', amount=0.15) + peppermask = cam != cam_noisy + + # Ensure all changes are to 1.0 + assert_allclose(cam_noisy[peppermask], np.zeros(peppermask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float(peppermask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.15 + + # Check to make sure pepper gets added properly to signed images + orig_zeros = (data_signed == -1).sum() + cam_noisy_signed = random_noise(data_signed, seed=seed, mode='pepper', + amount=.15) + + proportion = (float((cam_noisy_signed == -1).sum() - orig_zeros) / + (cam.shape[0] * cam.shape[1])) + assert 0.11 < proportion <= 0.15 + + +def test_salt_and_pepper(): + seed = 42 + cam = img_as_float(camera()) + cam_noisy = random_noise(cam, seed=seed, mode='s&p', amount=0.15, + salt_vs_pepper=0.25) + saltmask = np.logical_and(cam != cam_noisy, cam_noisy == 1.) + peppermask = np.logical_and(cam != cam_noisy, cam_noisy == 0.) + + # Ensure all changes are to 0. or 1. + assert_allclose(cam_noisy[saltmask], np.ones(saltmask.sum())) + assert_allclose(cam_noisy[peppermask], np.zeros(peppermask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float( + saltmask.sum() + peppermask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.18 + + # Verify the relative amount of salt vs. pepper is close to expected + assert 0.18 < saltmask.sum() / float(peppermask.sum()) < 0.32 + + +def test_gaussian(): + seed = 42 + data = np.zeros((128, 128)) + 0.5 + data_gaussian = random_noise(data, seed=seed, var=0.01) + assert 0.008 < data_gaussian.var() < 0.012 + + data_gaussian = random_noise(data, seed=seed, mean=0.3, var=0.015) + assert 0.28 < data_gaussian.mean() - 0.5 < 0.32 + assert 0.012 < data_gaussian.var() < 0.018 + + +def test_localvar(): + seed = 42 + data = np.zeros((128, 128)) + 0.5 + local_vars = np.zeros((128, 128)) + 0.001 + local_vars[:64, 64:] = 0.1 + local_vars[64:, :64] = 0.25 + local_vars[64:, 64:] = 0.45 + + data_gaussian = random_noise(data, mode='localvar', seed=seed, + local_vars=local_vars, clip=False) + assert 0. < data_gaussian[:64, :64].var() < 0.002 + assert 0.095 < data_gaussian[:64, 64:].var() < 0.105 + assert 0.245 < data_gaussian[64:, :64].var() < 0.255 + assert 0.445 < data_gaussian[64:, 64:].var() < 0.455 + + # Ensure local variance bounds checking works properly + bad_local_vars = np.zeros_like(data) + assert_raises(ValueError, random_noise, data, mode='localvar', seed=seed, + local_vars=bad_local_vars) + bad_local_vars += 0.1 + bad_local_vars[0, 0] = -1 + assert_raises(ValueError, random_noise, data, mode='localvar', seed=seed, + local_vars=bad_local_vars) + + +def test_speckle(): + seed = 42 + data = np.zeros((128, 128)) + 0.1 + np.random.seed(seed=seed) + noise = np.random.normal(0.1, 0.02 ** 0.5, (128, 128)) + expected = np.clip(data + data * noise, 0, 1) + + data_speckle = random_noise(data, mode='speckle', seed=seed, mean=0.1, + var=0.02) + assert_allclose(expected, data_speckle) + + +def test_poisson(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + cam_noisy = random_noise(data, mode='poisson', seed=seed) + cam_noisy2 = random_noise(data, mode='poisson', seed=seed, clip=False) + + np.random.seed(seed=seed) + expected = np.random.poisson(img_as_float(data) * 256) / 256. + assert_allclose(cam_noisy, np.clip(expected, 0., 1.)) + assert_allclose(cam_noisy2, expected) + + +def test_clip_poisson(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + data_signed = img_as_float(data) * 2. - 1. # Same image, on range [-1, 1] + + # Signed and unsigned, clipped + cam_poisson = random_noise(data, mode='poisson', seed=seed, clip=True) + cam_poisson2 = random_noise(data_signed, mode='poisson', seed=seed, + clip=True) + assert (cam_poisson.max() == 1.) and (cam_poisson.min() == 0.) + assert (cam_poisson2.max() == 1.) and (cam_poisson2.min() == -1.) + + # Signed and unsigned, unclipped + cam_poisson = random_noise(data, mode='poisson', seed=seed, clip=False) + cam_poisson2 = random_noise(data_signed, mode='poisson', seed=seed, + clip=False) + assert (cam_poisson.max() > 1.15) and (cam_poisson.min() == 0.) + assert (cam_poisson2.max() > 1.3) and (cam_poisson2.min() == -1.) + + +def test_clip_gaussian(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + data_signed = img_as_float(data) * 2. - 1. # Same image, on range [-1, 1] + + # Signed and unsigned, clipped + cam_gauss = random_noise(data, mode='gaussian', seed=seed, clip=True) + cam_gauss2 = random_noise(data_signed, mode='gaussian', seed=seed, + clip=True) + assert (cam_gauss.max() == 1.) and (cam_gauss.min() == 0.) + assert (cam_gauss2.max() == 1.) and (cam_gauss2.min() == -1.) + + # Signed and unsigned, unclipped + cam_gauss = random_noise(data, mode='gaussian', seed=seed, clip=False) + cam_gauss2 = random_noise(data_signed, mode='gaussian', seed=seed, + clip=False) + assert (cam_gauss.max() > 1.22) and (cam_gauss.min() < -0.36) + assert (cam_gauss2.max() > 1.219) and (cam_gauss2.min() < -1.337) + + +def test_clip_speckle(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + data_signed = img_as_float(data) * 2. - 1. # Same image, on range [-1, 1] + + # Signed and unsigned, clipped + cam_speckle = random_noise(data, mode='speckle', seed=seed, clip=True) + cam_speckle2 = random_noise(data_signed, mode='speckle', seed=seed, + clip=True) + assert (cam_speckle.max() == 1.) and (cam_speckle.min() == 0.) + assert (cam_speckle2.max() == 1.) and (cam_speckle2.min() == -1.) + + # Signed and unsigned, unclipped + cam_speckle = random_noise(data, mode='speckle', seed=seed, clip=False) + cam_speckle2 = random_noise(data_signed, mode='speckle', seed=seed, + clip=False) + assert (cam_speckle.max() > 1.219) and (cam_speckle.min() == 0.) + assert (cam_speckle2.max() > 1.219) and (cam_speckle2.min() < -1.306) + + +def test_bad_mode(): + data = np.zeros((64, 64)) + assert_raises(KeyError, random_noise, data, 'perlin') + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/tests/test_regular_grid.py b/skimage/util/tests/test_regular_grid.py new file mode 100644 index 00000000..61736a76 --- /dev/null +++ b/skimage/util/tests/test_regular_grid.py @@ -0,0 +1,40 @@ +import numpy as np +from numpy.testing import assert_equal +from skimage.util import regular_grid + + +def test_regular_grid_full(): + ar = np.zeros((2, 2)) + g = regular_grid(ar, 25) + assert_equal(g, [slice(None, None, None), slice(None, None, None)]) + ar[g] = 1 + assert_equal(ar.size, ar.sum()) + + +def test_regular_grid_2d_8(): + ar = np.zeros((20, 40)) + g = regular_grid(ar.shape, 8) + assert_equal(g, [slice(5.0, None, 10.0), slice(5.0, None, 10.0)]) + ar[g] = 1 + assert_equal(ar.sum(), 8) + + +def test_regular_grid_2d_32(): + ar = np.zeros((20, 40)) + g = regular_grid(ar.shape, 32) + assert_equal(g, [slice(2.0, None, 5.0), slice(2.0, None, 5.0)]) + ar[g] = 1 + assert_equal(ar.sum(), 32) + + +def test_regular_grid_3d_8(): + ar = np.zeros((3, 20, 40)) + g = regular_grid(ar.shape, 8) + assert_equal(g, [slice(1.0, None, 3.0), slice(5.0, None, 10.0), + slice(5.0, None, 10.0)]) + ar[g] = 1 + assert_equal(ar.sum(), 8) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/tests/test_shape.py b/skimage/util/tests/test_shape.py index 3b5f8d41..b6975d0f 100644 --- a/skimage/util/tests/test_shape.py +++ b/skimage/util/tests/test_shape.py @@ -139,3 +139,23 @@ def test_view_as_windows_2D(): [9, 10, 11], [13, 14, 15], [17, 18, 19]]]])) + + +def test_view_as_windows_With_skip(): + A = np.arange(20).reshape((5, 4)) + B = view_as_windows(A, (2, 2), step=2) + assert_equal(B, [[[[0, 1], + [4, 5]], + [[2, 3], + [6, 7]]], + [[[8, 9], + [12, 13]], + [[10, 11], + [14, 15]]]]) + + C = view_as_windows(A, (2, 2), step=4) + assert_equal(C.shape, (1, 1, 2, 2)) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/tests/test_unique_rows.py b/skimage/util/tests/test_unique_rows.py new file mode 100644 index 00000000..ad033b69 --- /dev/null +++ b/skimage/util/tests/test_unique_rows.py @@ -0,0 +1,40 @@ +import numpy as np +from numpy.testing import assert_equal, assert_raises +from skimage.util import unique_rows + + +def test_discontiguous_array(): + ar = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], np.uint8) + ar = ar[::2] + ar_out = unique_rows(ar) + desired_ar_out = np.array([[1, 0, 1]], np.uint8) + assert_equal(ar_out, desired_ar_out) + + +def test_uint8_array(): + ar = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], np.uint8) + ar_out = unique_rows(ar) + desired_ar_out = np.array([[0, 1, 0], [1, 0, 1]], np.uint8) + assert_equal(ar_out, desired_ar_out) + + +def test_float_array(): + ar = np.array([[1.1, 0.0, 1.1], [0.0, 1.1, 0.0], [1.1, 0.0, 1.1]], + np.float) + ar_out = unique_rows(ar) + desired_ar_out = np.array([[0.0, 1.1, 0.0], [1.1, 0.0, 1.1]], np.float) + assert_equal(ar_out, desired_ar_out) + + +def test_1d_array(): + ar = np.array([1, 0, 1, 1], np.uint8) + assert_raises(ValueError, unique_rows, ar) + + +def test_3d_array(): + ar = np.arange(8).reshape((2, 2, 2)) + assert_raises(ValueError, unique_rows, ar) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/unique.py b/skimage/util/unique.py new file mode 100644 index 00000000..635f6e89 --- /dev/null +++ b/skimage/util/unique.py @@ -0,0 +1,50 @@ +import numpy as np + + +def unique_rows(ar): + """Remove repeated rows from a 2D array. + + In particular, if given an array of coordinates of shape + (Npoints, Ndim), it will remove repeated points. + + Parameters + ---------- + ar : 2-D ndarray + The input array. + + Returns + ------- + ar_out : 2-D ndarray + A copy of the input array with repeated rows removed. + + Raises + ------ + ValueError : if `ar` is not two-dimensional. + + Notes + ----- + The function will generate a copy of `ar` if it is not + C-contiguous, which will negatively affect performance for large + input arrays. + + Examples + -------- + >>> ar = np.array([[1, 0, 1], + ... [0, 1, 0], + ... [1, 0, 1]], np.uint8) + >>> unique_rows(ar) + array([[0, 1, 0], + [1, 0, 1]], dtype=uint8) + """ + if ar.ndim != 2: + raise ValueError("unique_rows() only makes sense for 2D arrays, " + "got %dd" % ar.ndim) + # the view in the next line only works if the array is C-contiguous + ar = np.ascontiguousarray(ar) + # np.unique() finds identical items in a raveled array. To make it + # see each row as a single item, we create a view of each row as a + # byte string of length itemsize times number of columns in `ar` + ar_row_view = ar.view('|S%d' % (ar.itemsize * ar.shape[1])) + _, unique_row_indices = np.unique(ar_row_view, return_index=True) + ar_out = ar[unique_row_indices] + return ar_out diff --git a/skimage/viewer/canvastools/__init__.py b/skimage/viewer/canvastools/__init__.py index 22a2d205..f5b89e2c 100644 --- a/skimage/viewer/canvastools/__init__.py +++ b/skimage/viewer/canvastools/__init__.py @@ -1,3 +1,3 @@ -from linetool import LineTool, ThickLineTool -from recttool import RectangleTool -from painttool import PaintTool +from .linetool import LineTool, ThickLineTool +from .recttool import RectangleTool +from .painttool import PaintTool diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index 7a459401..c32d0ea7 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -5,7 +5,7 @@ try: except ImportError: print("Could not import matplotlib -- skimage.viewer not available.") -from base import CanvasToolBase, ToolHandles +from skimage.viewer.canvastools.base import CanvasToolBase, ToolHandles __all__ = ['LineTool', 'ThickLineTool'] diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index 3fbab153..3b4132f0 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -1,14 +1,10 @@ import numpy as np - -try: - import matplotlib.pyplot as plt - import matplotlib.colors as mcolors - LABELS_CMAP = mcolors.ListedColormap(['white', 'red', 'dodgerblue', 'gold', +import matplotlib.pyplot as plt +import matplotlib.colors as mcolors +LABELS_CMAP = mcolors.ListedColormap(['white', 'red', 'dodgerblue', 'gold', 'greenyellow', 'blueviolet']) -except ImportError: - print("Could not import matplotlib -- skimage.viewer not available.") -from base import CanvasToolBase +from skimage.viewer.canvastools.base import CanvasToolBase __all__ = ['PaintTool'] @@ -192,7 +188,6 @@ class CenteredWindow(object): if __name__ == '__main__': np.testing.rundocs() - import matplotlib.pyplot as plt from skimage import data image = data.camera() diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 2d8c7175..50b7601c 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,9 +1,12 @@ """ Base class for Plugins that interact with ImageViewer. """ -from ..qt import QtGui -from ..qt.QtCore import Qt +from warnings import warn +import numpy as np + +from ..qt import QtGui +from ..qt.QtCore import Qt, Signal from ..utils import RequiredAttr, init_qtapp @@ -71,14 +74,24 @@ class Plugin(QtGui.QDialog): name = 'Plugin' image_viewer = RequiredAttr("%s is not attached to ImageViewer" % name) - def __init__(self, image_filter=None, height=0, width=400, useblit=True): + # Signals used when viewers are linked to the Plugin output. + image_changed = Signal(np.ndarray) + _started = Signal(int) + + def __init__(self, image_filter=None, height=0, width=400, useblit=True, + dock='bottom'): init_qtapp() super(Plugin, self).__init__() + self.dock = dock + self.image_viewer = None # If subclass defines `image_filter` method ignore input. if not hasattr(self, 'image_filter'): self.image_filter = image_filter + elif image_filter is not None: + warn("If the Plugin class defines an `image_filter` method, " + "then the `image_filter` argument is ignored.") self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) @@ -109,7 +122,7 @@ class Plugin(QtGui.QDialog): self.image_viewer = image_viewer self.image_viewer.plugins.append(self) #TODO: Always passing image as first argument may be bad assumption. - self.arguments.append(self.image_viewer.original_image) + self.arguments = [self.image_viewer.original_image] # Call filter so that filtered image matches widget values self.filter_image() @@ -155,12 +168,22 @@ class Plugin(QtGui.QDialog): kwargs = dict([(name, self._get_value(a)) for name, a in self.keyword_arguments.items()]) filtered = self.image_filter(*arguments, **kwargs) + self.display_filtered_image(filtered) + self.image_changed.emit(filtered) def _get_value(self, param): # If param is a widget, return its `val` attribute. return param if not hasattr(param, 'val') else param.val + def _update_original_image(self, image): + """Update the original image argument passed to the filter function. + + This method is called by the viewer when the original image is updated. + """ + self.arguments[0] = image + self.filter_image() + @property def filtered_image(self): """Return filtered image.""" @@ -183,6 +206,17 @@ class Plugin(QtGui.QDialog): """ setattr(self, name, value) + def show(self, main_window=True): + """Show plugin.""" + super(Plugin, self).show() + self.activateWindow() + self.raise_() + + # Emit signal with x-hint so new windows can be displayed w/o overlap. + size = self.frameGeometry() + x_hint = size.x() + size.width() + self._started.emit(x_hint) + def closeEvent(self, event): """On close disconnect all artists and events from ImageViewer. diff --git a/skimage/viewer/plugins/color_histogram.py b/skimage/viewer/plugins/color_histogram.py index 166b2805..39a75004 100644 --- a/skimage/viewer/plugins/color_histogram.py +++ b/skimage/viewer/plugins/color_histogram.py @@ -10,8 +10,9 @@ from ..canvastools import RectangleTool class ColorHistogram(PlotPlugin): name = 'Color Histogram' - def __init__(self, **kwargs): + def __init__(self, max_pct=0.99, **kwargs): super(ColorHistogram, self).__init__(height=400, **kwargs) + self.max_pct = max_pct print(self.help()) @@ -30,7 +31,7 @@ class ColorHistogram(PlotPlugin): normed=True) # Clip bin heights that dominate a-b histogram - max_val = pct_total_area(hist, percentile=99) + max_val = pct_total_area(hist, percentile=self.max_pct) hist = exposure.rescale_intensity(hist, in_range=(0, max_val)) self.ax.imshow(hist, extent=ab_extents, cmap=plt.cm.gray) @@ -55,12 +56,12 @@ class ColorHistogram(PlotPlugin): self.image_viewer.image = color.lab2rgb(lab_masked) -def pct_total_area(image, percentile=80): +def pct_total_area(image, percentile=0.80): """Return threshold value based on percentage of total area. The specified percent of pixels less than the given intensity threshold. """ - idx = int((image.size - 1) * percentile / 100.0) + idx = int((image.size - 1) * percentile) sorted_pixels = np.sort(image.flat) return sorted_pixels[idx] diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index c9ceedc5..0d555eb5 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -60,7 +60,7 @@ class LineProfile(PlotPlugin): if not self._limit_type is None: self.ax.set_ylim(self.limits) - h, w = image.shape + h, w = image.shape[0:2] x = [w / 3, 2 * w / 3] y = [h / 2] * 2 @@ -71,7 +71,9 @@ class LineProfile(PlotPlugin): self.line_tool.end_points = np.transpose([x, y]) scan_data = profile_line(image, self.line_tool.end_points) - self.profile = self.ax.plot(scan_data, 'k-')[0] + + self.reset_axes(scan_data) + self._autoscale_view() def help(self): @@ -80,18 +82,18 @@ class LineProfile(PlotPlugin): "Select and drag ends of the scan line to adjust it.") return '\n'.join(helpstr) - def get_profile(self): + def get_profiles(self): """Return intensity profile of the selected line. Returns ------- end_points: (2, 2) array The positions ((x1, y1), (x2, y2)) of the line ends. - profile: 1d array - Profile of intensity values. + profile: list of 1d arrays + Profile of intensity values. Length 1 (grayscale) or 3 (rgb). """ - profile = self.profile.get_ydata() - return self.line_tool.end_points, profile + profiles = [data.get_ydata() for data in self.profile] + return self.line_tool.end_points, profiles def _autoscale_view(self): if self.limits is None: @@ -105,25 +107,50 @@ class LineProfile(PlotPlugin): scan = profile_line(self.image_viewer.original_image, end_points, linewidth=self.line_tool.linewidth) - self.profile.set_xdata(np.arange(scan.shape[0])) - self.profile.set_ydata(scan) + if scan.shape[1] != len(self.profile): + self.reset_axes(scan) + + for i in range(len(scan[0])): + self.profile[i].set_xdata(np.arange(scan.shape[0])) + self.profile[i].set_ydata(scan[:, i]) self.ax.relim() - if self.useblit: - self.ax.draw_artist(self.profile) - self._autoscale_view() self.redraw() + def reset_axes(self, scan_data): + # Clear lines out + for line in self.ax.lines: + self.ax.lines = [] + + if scan_data.shape[1] == 1: + self.profile = self.ax.plot(scan_data, 'k-') + else: + self.profile = self.ax.plot(scan_data[:, 0], 'r-', + scan_data[:, 1], 'g-', + scan_data[:, 2], 'b-') + + +def _calc_vert(img, x1, x2, y1, y2, linewidth): + # Quick calculation if perfectly horizontal + pixels = img[min(y1, y2): max(y1, y2) + 1, + x1 - linewidth / 2: x1 + linewidth / 2 + 1] + + # Reverse index if necessary + if y2 > y1: + pixels = pixels[::-1, :] + + return pixels.mean(axis=1)[:, np.newaxis] + def profile_line(img, end_points, linewidth=1): """Return the intensity profile of an image measured along a scan line. Parameters ---------- - img : 2d array - The image. + img : 2d or 3d array + The image, in grayscale (2d) or RGB (3d) format. end_points: (2, 2) list End points ((x1, y1), (x2, y2)) of scan line. linewidth: int @@ -139,17 +166,18 @@ def profile_line(img, end_points, linewidth=1): x1, y1 = point1 = np.asarray(point1, dtype=float) x2, y2 = point2 = np.asarray(point2, dtype=float) dx, dy = point2 - point1 + channels = 1 + if img.ndim == 3: + channels = 3 - # Quick calculation if perfectly horizontal or vertical (remove?) + # Quick calculation if perfectly vertical; shortcuts div0 error if x1 == x2: - pixels = img[min(y1, y2): max(y1, y2) + 1, - x1 - linewidth / 2: x1 + linewidth / 2 + 1] - intensities = pixels.mean(axis=1) - return intensities - elif y1 == y2: - pixels = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, - min(x1, x2): max(x1, x2) + 1] - intensities = pixels.mean(axis=0) + if channels == 1: + img = img[:, :, np.newaxis] + + img = np.rollaxis(img, -1) + intensities = np.hstack([_calc_vert(im, x1, x2, y1, y2, linewidth) + for im in img]) return intensities theta = np.arctan2(dy, dx) @@ -157,7 +185,7 @@ def profile_line(img, end_points, linewidth=1): b = y1 - a * x1 length = np.hypot(dx, dy) - line_x = np.linspace(min(x1, x2), max(x1, x2), np.ceil(length)) + line_x = np.linspace(x2, x1, np.ceil(length)) line_y = line_x * a + b y_width = abs(linewidth * np.cos(theta) / 2) perp_ys = np.array([np.linspace(yi - y_width, @@ -165,7 +193,17 @@ def profile_line(img, end_points, linewidth=1): perp_xs = - a * perp_ys + (line_x + a * line_y)[:, np.newaxis] perp_lines = np.array([perp_ys, perp_xs]) - pixels = ndi.map_coordinates(img, perp_lines) + if img.ndim == 3: + pixels = [ndi.map_coordinates(img[..., i], perp_lines) + for i in range(3)] + pixels = np.transpose(np.asarray(pixels), (1, 2, 0)) + else: + pixels = ndi.map_coordinates(img, perp_lines) + pixels = pixels[..., np.newaxis] + intensities = pixels.mean(axis=1) - return intensities + if intensities.ndim == 1: + return intensities[..., np.newaxis] + else: + return intensities diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 278f072c..dc5ca060 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -2,7 +2,8 @@ from warnings import warn from skimage.util.dtype import dtype_range from .base import Plugin -from ..utils import ClearColormap +from ..utils import ClearColormap, update_axes_image +from skimage._shared import six __all__ = ['OverlayPlugin'] @@ -66,7 +67,8 @@ class OverlayPlugin(Plugin): self._overlay_plot = ax.imshow(image, cmap=self.cmap, vmin=vmin, vmax=vmax) else: - self._overlay_plot.set_array(image) + update_axes_image(self._overlay_plot, image) + self.image_viewer.redraw() @property @@ -76,7 +78,8 @@ class OverlayPlugin(Plugin): @color.setter def color(self, index): # Update colormap whenever color is changed. - if isinstance(index, basestring) and index not in self.color_names: + if isinstance(index, six.string_types) and \ + index not in self.color_names: raise ValueError("%s not defined in OverlayPlugin.colors" % index) else: name = self.color_names[index] diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index c120756c..0ce5df73 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -17,6 +17,13 @@ class PlotPlugin(Plugin): See base Plugin class for additional details. """ + def __init__(self, image_filter=None, height=150, width=400, **kwargs): + super(PlotPlugin, self).__init__(image_filter=image_filter, + height=height, width=width, **kwargs) + + self._height = height + self._width = width + def attach(self, image_viewer): super(PlotPlugin, self).attach(image_viewer) # Add plot for displaying intensity profile. @@ -26,10 +33,12 @@ class PlotPlugin(Plugin): """Redraw plot.""" self.canvas.draw_idle() - def add_plot(self, height=4, width=4): - self.fig, self.ax = new_plot(figsize=(height, width)) + def add_plot(self): + self.fig, self.ax = new_plot() + self.fig.set_figwidth(self._width / float(self.fig.dpi)) + self.fig.set_figheight(self._height / float(self.fig.dpi)) + self.canvas = self.fig.canvas - self.canvas.setMinimumHeight(150) #TODO: Converted color is slightly different than Qt background. qpalette = QtGui.QPalette() qcolor = qpalette.color(QtGui.QPalette.Window) diff --git a/skimage/viewer/qt/QtCore.py b/skimage/viewer/qt/QtCore.py index 897b8760..19b92a53 100644 --- a/skimage/viewer/qt/QtCore.py +++ b/skimage/viewer/qt/QtCore.py @@ -4,6 +4,19 @@ 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 - Qt = None + # 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/utils/__init__.py b/skimage/viewer/utils/__init__.py index 5af24064..bb67a43f 100644 --- a/skimage/viewer/utils/__init__.py +++ b/skimage/viewer/utils/__init__.py @@ -1 +1 @@ -from core import * +from .core import * diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index e4985a9e..3b9b33fb 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -23,7 +23,8 @@ from ..qt import QtGui __all__ = ['init_qtapp', 'start_qtapp', 'RequiredAttr', 'figimage', - 'LinearColormap', 'ClearColormap', 'FigureCanvas', 'new_plot'] + 'LinearColormap', 'ClearColormap', 'FigureCanvas', 'new_plot', + 'update_axes_image'] QApp = None @@ -35,29 +36,53 @@ def init_qtapp(): The QApplication needs to be initialized before creating any QWidgets """ global QApp + QApp = QtGui.QApplication.instance() if QApp is None: QApp = QtGui.QApplication([]) + return QApp -def start_qtapp(): +def is_event_loop_running(app=None): + """Return True if event loop is running.""" + if app is None: + app = init_qtapp() + if hasattr(app, '_in_event_loop'): + return app._in_event_loop + else: + return False + + +def start_qtapp(app=None): """Start Qt mainloop""" - QApp.exec_() + if app is None: + app = init_qtapp() + if not is_event_loop_running(app): + app._in_event_loop = True + app.exec_() + app._in_event_loop = False + else: + app._in_event_loop = True class RequiredAttr(object): """A class attribute that must be set before use.""" - def __init__(self, msg): + instances = dict() + + def __init__(self, msg='Required attribute not set', init_val=None): + self.instances[self, None] = init_val self.msg = msg - self.val = None def __get__(self, obj, objtype): - if self.val is None: + value = self.instances[self, obj] + if value is None: + # Should raise an error but that causes issues with the buildbot. warnings.warn(self.msg) - return self.val + self.__set__(obj, self.init_val) + return value - def __set__(self, obj, val): - self.val = val + def __set__(self, obj, value): + self.instances[self, obj] = value class LinearColormap(LinearSegmentedColormap): @@ -179,3 +204,22 @@ def figimage(image, scale=1, dpi=None, **kwargs): ax.set_axis_off() ax.imshow(image, **kwargs) return fig, ax + + +def update_axes_image(image_axes, image): + """Update the image displayed by an image plot. + + This sets the image plot's array and updates its shape appropriately + + Parameters + ---------- + image_axes : `matplotlib.image.AxesImage` + Image axes to update. + image : array + Image array. + """ + image_axes.set_array(image) + + # Adjust size if new image shape doesn't match the original + h, w = image.shape[:2] + image_axes.set_extent((0, w, h, 0)) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 9d6261c6..c3ffeb1e 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -2,7 +2,7 @@ ImageViewer class for viewing and interacting with images. """ from ..qt import QtGui -from ..qt import QtCore +from ..qt.QtCore import Qt, Signal from skimage import io, img_as_float from skimage.util.dtype import dtype_range @@ -11,6 +11,7 @@ import numpy as np from .. import utils from ..widgets import Slider from ..utils import dialogs +from ..plugins.base import Plugin __all__ = ['ImageViewer', 'CollectionViewer'] @@ -59,6 +60,15 @@ class ImageViewer(QtGui.QMainWindow): >>> # viewer.show() """ + + dock_areas = {'top': Qt.TopDockWidgetArea, + 'bottom': Qt.BottomDockWidgetArea, + 'left': Qt.LeftDockWidgetArea, + 'right': Qt.RightDockWidgetArea} + + # Signal that the original image has been changed + original_image_changed = Signal(np.ndarray) + def __init__(self, image): # Start main loop utils.init_qtapp() @@ -66,21 +76,28 @@ class ImageViewer(QtGui.QMainWindow): #TODO: Add ImageViewer to skimage.io window manager - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle("Image Viewer") self.file_menu = QtGui.QMenu('&File', self) self.file_menu.addAction('Open file', self.open_file, - QtCore.Qt.CTRL + QtCore.Qt.Key_O) + Qt.CTRL + Qt.Key_O) self.file_menu.addAction('Save to file', self.save_to_file, - QtCore.Qt.CTRL + QtCore.Qt.Key_S) + Qt.CTRL + Qt.Key_S) self.file_menu.addAction('Quit', self.close, - QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + Qt.CTRL + Qt.Key_Q) self.menuBar().addMenu(self.file_menu) self.main_widget = QtGui.QWidget() self.setCentralWidget(self.main_widget) + if isinstance(image, Plugin): + plugin = image + image = plugin.filtered_image + plugin.image_changed.connect(self._update_original_image) + # When plugin is started, start + plugin._started.connect(self._show) + self.fig, self.ax = utils.figimage(image) self.canvas = self.fig.canvas self.canvas.setParent(self) @@ -88,9 +105,7 @@ class ImageViewer(QtGui.QMainWindow): self.ax.autoscale(enable=False) self._image_plot = self.ax.images[0] - - self.original_image = image - self.image = image.copy() + self._update_original_image(image) self.plugins = [] self.layout = QtGui.QVBoxLayout(self.main_widget) @@ -107,16 +122,48 @@ class ImageViewer(QtGui.QMainWindow): def __add__(self, plugin): """Add plugin to ImageViewer""" plugin.attach(self) + self.original_image_changed.connect(plugin._update_original_image) + + if plugin.dock: + location = self.dock_areas[plugin.dock] + dock_location = Qt.DockWidgetArea(location) + dock = QtGui.QDockWidget() + dock.setWidget(plugin) + dock.setWindowTitle(plugin.name) + self.addDockWidget(dock_location, dock) + + horiz = (self.dock_areas['left'], self.dock_areas['right']) + dimension = 'width' if location in horiz else 'height' + self._add_widget_size(plugin, dimension=dimension) + return self + def _add_widget_size(self, widget, dimension='width'): + widget_size = widget.sizeHint() + viewer_size = self.frameGeometry() + + dx = dy = 0 + if dimension == 'width': + dx = widget_size.width() + elif dimension == 'height': + dy = widget_size.height() + + w = viewer_size.width() + h = viewer_size.height() + self.resize(w + dx, h + dy) + def open_file(self): """Open image file and display in viewer.""" filename = dialogs.open_file_dialog() if filename is None: return image = io.imread(filename) + self._update_original_image(image) + + def _update_original_image(self, image): self.original_image = image # update saved image - self.image = image # update displayed image + self.image = image.copy() # update displayed image + self.original_image_changed.emit(image) def save_to_file(self): """Save current image to file. @@ -149,27 +196,22 @@ class ImageViewer(QtGui.QMainWindow): def closeEvent(self, event): self.close() - def auto_layout(self): - """Move viewer to top-left and align plugin on right edge of viewer.""" - size = self.geometry() - self.move(0, 0) - w = size.width() - y = 0 - #TODO: Layout isn't quite correct for multiple plugins (overlaps). + def _show(self, x=0): + self.move(x, 0) for p in self.plugins: - p.move(w, y) - y += p.geometry().height() + p.show() + super(ImageViewer, self).show() + self.activateWindow() + self.raise_() - def show(self): + def show(self, main_window=True): """Show ImageViewer and attached plugins. This behaves much like `matplotlib.pyplot.show` and `QWidget.show`. """ - self.auto_layout() - for p in self.plugins: - p.show() - super(ImageViewer, self).show() - utils.start_qtapp() + self._show() + if main_window: + utils.start_qtapp() def redraw(self): self.canvas.draw_idle() @@ -181,13 +223,10 @@ class ImageViewer(QtGui.QMainWindow): @image.setter def image(self, image): self._img = image - self._image_plot.set_array(image) + utils.update_axes_image(self._image_plot, image) - # Adjust size if new image shape doesn't match the original - h, w = image.shape[:2] - # update data coordinates (otherwise pixel coordinates are off) - self._image_plot.set_extent((0, w, h, 0)) # update display (otherwise image doesn't fill the canvas) + h, w = image.shape[:2] self.ax.set_xlim(0, w) self.ax.set_ylim(h, 0) @@ -248,7 +287,7 @@ class CollectionViewer(ImageViewer): ---------- image_collection : list of images List of images to be displayed. - update_on : {'on_slide' | 'on_release'} + update_on : {'move' | 'release'} Control whether image is updated on slide or release of the image slider. Using 'on_release' will give smoother behavior when displaying large images or when writing a plugin/subclass that requires heavy @@ -296,7 +335,7 @@ class CollectionViewer(ImageViewer): This method can be overridden or extended in subclasses and plugins to react to image changes. """ - self.image = image + self._update_original_image(image) def keyPressEvent(self, event): if type(event) == QtGui.QKeyEvent: diff --git a/skimage/viewer/widgets/__init__.py b/skimage/viewer/widgets/__init__.py index 6552a313..efa9fe9d 100644 --- a/skimage/viewer/widgets/__init__.py +++ b/skimage/viewer/widgets/__init__.py @@ -1,2 +1,2 @@ -from core import * -from history import * +from .core import * +from .history import * diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index ad69bfcf..b9714d38 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -81,7 +81,7 @@ class Slider(BaseWidget): Range of slider values. value : float Default slider value. If None, use midpoint between `low` and `high`. - value : {'float' | 'int'} + value_type : {'float' | 'int'} Numeric type of slider value. ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. @@ -90,12 +90,12 @@ class Slider(BaseWidget): is typically set when the widget is added to a plugin. orientation : {'horizontal' | 'vertical'} Slider orientation. - update_on : {'move' | 'release'} + update_on : {'release' | 'move'} Control when callback function is called: on slider move or release. """ def __init__(self, name, low=0.0, high=1.0, value=None, value_type='float', ptype='kwarg', callback=None, max_edit_width=60, - orientation='horizontal', update_on='move'): + orientation='horizontal', update_on='release'): super(Slider, self).__init__(name, ptype, callback) if value is None: diff --git a/viewer_examples/plugins/canny_simple.py b/viewer_examples/plugins/canny_simple.py index 912401e1..c26ca08d 100644 --- a/viewer_examples/plugins/canny_simple.py +++ b/viewer_examples/plugins/canny_simple.py @@ -12,9 +12,9 @@ image = data.camera() # You can create a UI for a filter just by passing a filter function... plugin = OverlayPlugin(image_filter=canny) # ... and adding widgets to adjust parameter values. -plugin += Slider('sigma', 0, 5, update_on='release') -plugin += Slider('low threshold', 0, 255, update_on='release') -plugin += Slider('high threshold', 0, 255, update_on='release') +plugin += Slider('sigma', 0, 5) +plugin += Slider('low threshold', 0, 255) +plugin += Slider('high threshold', 0, 255) # ... and we can also add buttons to save the overlay: plugin += SaveButtons(name='Save overlay to:') diff --git a/viewer_examples/plugins/collection_overlay.py b/viewer_examples/plugins/collection_overlay.py new file mode 100644 index 00000000..4c7002f2 --- /dev/null +++ b/viewer_examples/plugins/collection_overlay.py @@ -0,0 +1,21 @@ +""" +============================================== +``CollectionViewer`` with an ``OverlayPlugin`` +============================================== + +Demo of a CollectionViewer for viewing collections of images with an +overlay plugin. + +""" +from skimage import data + +from skimage.viewer import CollectionViewer +from skimage.viewer.plugins.canny import CannyPlugin + + +img_collection = [data.camera(), data.coins(), data.text()] + +viewer = CollectionViewer(img_collection) +viewer += CannyPlugin() + +viewer.show() diff --git a/viewer_examples/plugins/collection_plugin.py b/viewer_examples/plugins/collection_plugin.py new file mode 100644 index 00000000..65ff1f21 --- /dev/null +++ b/viewer_examples/plugins/collection_plugin.py @@ -0,0 +1,33 @@ +""" +================================== +``CollectionViewer`` with a plugin +================================== + +Demo of a CollectionViewer for viewing collections of images with the +`autolevel` rank filter connected as a plugin. + +""" +from skimage import data +from skimage.filter import rank +from skimage.morphology import disk + +from skimage.viewer import CollectionViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.plugins.base import Plugin + + +# Wrap autolevel function to make the disk size a filter argument. +def autolevel(image, disk_size): + return rank.autolevel(image, disk(disk_size)) + + +img_collection = [data.camera(), data.coins(), data.text()] + +plugin = Plugin(image_filter=autolevel) +plugin += Slider('disk_size', 2, 8, value_type='int') +plugin.name = "Autolevel" + +viewer = CollectionViewer(img_collection) +viewer += plugin + +viewer.show() diff --git a/viewer_examples/plugins/color_histogram.py b/viewer_examples/plugins/color_histogram.py index 0b654b3e..6b091d69 100644 --- a/viewer_examples/plugins/color_histogram.py +++ b/viewer_examples/plugins/color_histogram.py @@ -5,5 +5,5 @@ from skimage import data image = data.load('color.png') viewer = ImageViewer(image) -viewer += ColorHistogram() +viewer += ColorHistogram(dock='right') viewer.show() diff --git a/viewer_examples/plugins/lineprofile_rgb.py b/viewer_examples/plugins/lineprofile_rgb.py new file mode 100644 index 00000000..86b71d5d --- /dev/null +++ b/viewer_examples/plugins/lineprofile_rgb.py @@ -0,0 +1,9 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.lineprofile import LineProfile + + +image = data.chelsea() +viewer = ImageViewer(image) +viewer += LineProfile() +viewer.show() diff --git a/viewer_examples/plugins/median_filter.py b/viewer_examples/plugins/median_filter.py index a20ad8f3..36593c3d 100644 --- a/viewer_examples/plugins/median_filter.py +++ b/viewer_examples/plugins/median_filter.py @@ -10,7 +10,7 @@ image = data.coins() viewer = ImageViewer(image) plugin = Plugin(image_filter=median_filter) -plugin += Slider('radius', 2, 10, value_type='int', update_on='release') +plugin += Slider('radius', 2, 10, value_type='int') plugin += SaveButtons() plugin += OKCancelButtons() diff --git a/viewer_examples/plugins/probabilistic_hough.py b/viewer_examples/plugins/probabilistic_hough.py new file mode 100644 index 00000000..98052f87 --- /dev/null +++ b/viewer_examples/plugins/probabilistic_hough.py @@ -0,0 +1,46 @@ +import numpy as np + +from skimage import data +from skimage import draw +from skimage.transform import probabilistic_hough_line + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.plugins.overlayplugin import OverlayPlugin +from skimage.viewer.plugins.canny import CannyPlugin + + +def line_image(shape, lines): + image = np.zeros(shape, dtype=bool) + for end_points in lines: + # hough lines returns (x, y) points, draw.line wants (row, columns) + end_points = np.asarray(end_points)[:, ::-1] + image[draw.line(*np.ravel(end_points))] = 1 + return image + + +def hough_lines(image, *args, **kwargs): + # Set threshold to 0.5 since we're working with a binary image (from canny) + lines = probabilistic_hough_line(image, threshold=0.5, *args, **kwargs) + image = line_image(image.shape, lines) + return image + + +image = data.camera() +canny_viewer = ImageViewer(image) +canny_plugin = CannyPlugin() +canny_viewer += canny_plugin + +hough_plugin = OverlayPlugin(image_filter=hough_lines) +hough_plugin.name = 'Hough Lines' + +hough_plugin += Slider('line length', 0, 100) +hough_plugin += Slider('line gap', 0, 20) + +# Passing a plugin to a viewer connects the output of the plugin to the viewer. +hough_viewer = ImageViewer(canny_plugin) +hough_viewer += hough_plugin + +# Show viewers displays both viewers since `hough_viewer` is connected to +# `canny_viewer` through `canny_plugin` +canny_viewer.show() diff --git a/viewer_examples/plugins/watershed_demo.py b/viewer_examples/plugins/watershed_demo.py index 683e8a30..612ec6c9 100644 --- a/viewer_examples/plugins/watershed_demo.py +++ b/viewer_examples/plugins/watershed_demo.py @@ -7,6 +7,7 @@ from skimage.viewer import ImageViewer from skimage.viewer.widgets import history from skimage.viewer.plugins.labelplugin import LabelPainter + class OKCancelButtons(history.OKCancelButtons): def update_original_image(self):