diff --git a/.gitignore b/.gitignore index 1bd4352c..2623ea74 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ doc/source/auto_examples/images/plot_*.png doc/source/auto_examples/images/thumb doc/source/auto_examples/applications/ doc/source/_static/random.js - +.idea/ +*.log diff --git a/.travis.yml b/.travis.yml index 28104a17..f50ba1ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,16 +7,17 @@ language: erlang env: - - PYTHON=python PYSUF='' - # - PYTHON=python3 PYSUF=3 : python3-numpy not currently available + - PYTHON=python PYSUF='' PYVER=2.7 + - PYTHON=python3 PYSUF='3' PYVER=3.2 install: - # - sudo apt-get build-dep $PYTHON-numpy + - sudo apt-get update # needed for python3-numpy - sudo apt-get install $PYTHON-dev - sudo apt-get install $PYTHON-numpy - sudo apt-get install $PYTHON-scipy - sudo apt-get install $PYTHON-setuptools - sudo apt-get install $PYTHON-nose - - sudo apt-get install cython + - sudo easy_install$PYSUF pip + - sudo pip-$PYVER install cython - sudo apt-get install libfreeimage3 - $PYTHON setup.py build - sudo $PYTHON setup.py install @@ -24,5 +25,5 @@ script: # Change into an innocuous directory and find tests from installation - mkdir for_test - cd for_test - - nosetests --exe -v --cover-package=skimage skimage + - nosetests-$PYVER --exe -v --cover-package=skimage skimage diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index c73b14e2..fd0656db 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -117,3 +117,22 @@ - Petter Strandmark Perimeter calculation in regionprops. + +- Olivier Debeir + Rank filters (8- and 16-bits) using sliding window. + +- Luis Pedro Coelho + imread plugin + +- Steven Silvester, Karel Zuiderveld + Adaptive Histogram Equalization + +- Anders Boesen Lindbo Larsen + Dense DAISY feature description, circle perimeter drawing. + +- François Boulogne + Andres Method for circle perimeter, ellipse perimeter drawing. + Circular Hough Transform + +- Thouis Jones + Vectorized operators for arrays of 16-bit ints. diff --git a/DEVELOPMENT.txt b/DEVELOPMENT.txt index 509cf322..daffe705 100644 --- a/DEVELOPMENT.txt +++ b/DEVELOPMENT.txt @@ -1,8 +1,7 @@ Development process ------------------- -:doc:`Read this overview ` of how to use Git with -``skimage``. Here's the long and short of it: +Here's the long and short of it: * Go to `https://github.com/scikit-image/scikit-image `_ and follow the @@ -11,9 +10,12 @@ Development process branch name will appear in the merge message, use a sensible name such as 'transform-speedups'. * Commit locally as you progress. - * Push your changes back to github and create a pull request by - clicking "request pull" in GitHub. - * Optionally, mail the mailing list, explaining your changes. + * Push your changes back to GitHub and create a Pull Request by + clicking 'Pull Request' in GitHub. + * Optionally, post on the `mailing list `_ to explain your changes. + +Read these :doc:`detailed documents ` on how to use Git with +``scikit-image`` (``_). .. note:: @@ -33,8 +35,9 @@ project on how they manage to keep review overhead to a minimum: http://mail.scipy.org/pipermail/ipython-dev/2010-October/006746.html Guidelines -`````````` - * All code should have tests (see "Test coverage" below for more details). +---------- + + * All code should have tests (see `test coverage`_ below for more details). * All code should be documented, to the same `standard `_ as NumPy and SciPy. For new functionality, always add an example to the @@ -47,7 +50,8 @@ Guidelines * Examples in the gallery should have a maximum figure width of 8 inches. Stylistic Guidelines -```````````````````` +-------------------- + * Use numpy data types instead of strings (``np.uint8`` instead of ``"uint8"``). @@ -77,10 +81,14 @@ Stylistic Guidelines hough(canny(my_image)) + * Use `Py_ssize_t` as data type for all indexing, shape and size variables in + C/C++ and Cython code. + Test coverage -````````````` +------------- + Tests for a module should ideally cover all code in that module, -i.e. statement coverage should be at 100%. +i.e., statement coverage should be at 100%. To measure the test coverage, install `coverage.py `__ @@ -98,5 +106,6 @@ detailing the test coverage:: ... Bugs -```` -Please `report bugs on Github `_. +---- + +Please `report bugs on GitHub `_. diff --git a/README.rst b/README.md similarity index 91% rename from README.rst rename to README.md index 508db396..64a7dfe9 100644 --- a/README.rst +++ b/README.md @@ -29,8 +29,3 @@ this path to your PYTHONPATH variable and compiling the extensions: License ------- Please read LICENSE.txt in this directory. - -Contact -------- -Stefan van der Walt - diff --git a/RELEASE.txt b/RELEASE.txt index ce05e24e..e42d1c85 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -12,7 +12,7 @@ How to make a new release of ``skimage`` - Edit ``doc/source/themes/agogo/static/docversions.js`` and commit - Build a clean version of the docs. Run ``make`` in the root dir, then - ``rm build -rf; make html`` in the docs. + ``rm -rf build; make html`` in the docs. - Run ``make html`` again to copy the newly generated ``random.js`` into place. Double check ``random.js``, otherwise the skimage.org front page gets broken! diff --git a/TASKS.txt b/TASKS.txt index a2fcffe2..0046b6a3 100644 --- a/TASKS.txt +++ b/TASKS.txt @@ -15,14 +15,14 @@ How to contribute to ``skimage`` cell_profiler -Developing Open Source is great fun! Join us on the `skimage mailing +Developing Open Source is great fun! Join us on the `scikit-image mailing list `_ and tell us which of the following challenges you'd like to solve. * Mentoring is available for those new to scientific programming in Python. -* The technical detail of the `development process`_ is given below. -* :doc:`How to use GitHub ` when developing skimage -* If you're looking something to implement, you can find a list of `requested features on github `__. In addition, you can browse the `open issues on github `__. +* If you're looking for something to implement, you can find a list of `requested features on GitHub `__. In addition, you can browse the `open issues on GitHub `__. +* The technical detail of the `development process`_ is summed up below. + Refer to the :doc:`gitwash ` for a step-by-step tutorial. .. contents:: :local: diff --git a/bento.info b/bento.info index a6424eb1..10ee84f2 100644 --- a/bento.info +++ b/bento.info @@ -1,5 +1,5 @@ Name: scikit-image -Version: 0.7.2 +Version: 0.8.0 Summary: Image processing routines for SciPy Url: http://scikit-image.org DownloadUrl: http://github.com/scikit-image/scikit-image @@ -64,6 +64,9 @@ Library: Extension: skimage.filter._ctmf Sources: skimage/filter/_ctmf.pyx + Extension: skimage.filter._denoise_cy + Sources: + skimage/filter/_denoise_cy.pyx Extension: skimage.morphology.ccomp Sources: skimage/morphology/ccomp.pyx @@ -88,6 +91,9 @@ Library: Extension: skimage.morphology._greyreconstruct Sources: skimage/morphology/_greyreconstruct.pyx + Extension: skimage.feature.corner_cy + Sources: + skimage/feature/corner_cy.pyx Extension: skimage.feature._texture Sources: skimage/feature/_texture.pyx @@ -115,6 +121,36 @@ Library: Extension: skimage._shared.geometry Sources: skimage/_shared/geometry.pyx + Extension: skimage.filter.rank._core16 + Sources: + skimage/filter/rank/_core16.pyx + Extension: skimage.filter.rank._crank8 + Sources: + skimage/filter/rank/_crank8.pyx + Extension: skimage.filter.rank._crank16 + Sources: + skimage/filter/rank/_crank16.pyx + Extension: skimage.filter.rank._core8 + Sources: + skimage/filter/rank/_core8.pyx + Extension: skimage.filter.rank.rank + Sources: + skimage/filter/rank/rank.pyx + Extension: skimage.filter.rank.bilateral_rank + Sources: + skimage/filter/rank/bilateral_rank.pyx + Extension: skimage.filter.rank._crank16_percentiles + Sources: + skimage/filter/rank/_crank16_percentiles.pyx + Extension: skimage.filter.rank.percentile_rank + Sources: + skimage/filter/rank/percentile_rank.pyx + Extension: skimage.filter.rank._crank8_percentiles + Sources: + skimage/filter/rank/_crank8_percentiles.pyx + Extension: skimage.filter.rank._crank16_bilateral + Sources: + skimage/filter/rank/_crank16_bilateral.pyx Executable: skivi Module: skimage.scripts.skivi diff --git a/check_bento_build.py b/check_bento_build.py index 108b0aad..34b2272a 100644 --- a/check_bento_build.py +++ b/check_bento_build.py @@ -5,7 +5,7 @@ import os import re -RE_CYTHON = re.compile("config.add_extension\(['\"]([\S]+)['\"]") +RE_CYTHON = re.compile("config.add_extension\(\s*['\"]([\S]+)['\"]") BENTO_TEMPLATE = """ Extension: {module_path} @@ -23,7 +23,7 @@ def each_setup_in_pkg(top_dir): def each_cy_in_setup(top_dir): - """Yield path and name for each cython extension package's setup file.""" + """Yield path for each cython extension package's setup file.""" for dir_path, f in each_setup_in_pkg(top_dir): text = f.read() match = RE_CYTHON.findall(text) @@ -38,30 +38,27 @@ def each_cy_in_setup(top_dir): else: path = dir_path full_path = os.path.join(path, cy_file) - yield full_path, cy_file + yield full_path def each_cy_in_bento(bento_file='bento.info'): - """Yield path and name for each cython extension in bento info file.""" + """Yield path for each cython extension in bento info file.""" with open(bento_file) as f: for line in f: line = line.strip() if line.startswith('Extension:'): - parts = line.split('.') - ext_name = parts[-1] path = line.lstrip('Extension:').strip() - yield path, ext_name + yield path def remove_common_extensions(cy_bento, cy_setup): - for ext_name in cy_bento.keys(): - if ext_name in cy_setup: - spath = cy_setup.pop(ext_name) - bpath = cy_bento.pop(ext_name) - if not spath.replace(os.path.sep, '.') == bpath: - print "Mismatched paths:" - print " setup.py: ", spath - print " bento.info:", bpath + # normalize so that cy_setup and cy_bento have the same separator + cy_setup = set(ext.replace('/', '.') for ext in cy_setup) + cy_setup_diff = cy_setup.difference(cy_bento) + cy_setup_diff = set(ext.replace('.', '/') for ext in cy_setup_diff) + cy_bento_diff = cy_bento.difference(cy_setup) + return cy_bento_diff, cy_setup_diff + def print_results(cy_bento, cy_setup): def info(text): @@ -69,28 +66,30 @@ def print_results(cy_bento, cy_setup): print(text) print('-' * len(text)) - print "Bento errors:" - print "-------------" + if not (cy_bento or cy_setup): + print "bento.info and setup.py files match." if cy_bento: - info("The following extensions in 'bento.info' were not found:") - print('\n'.join(cy_bento.keys())) + info("Extensions found in 'bento.info' but not in any 'setup.py:") + print('\n'.join(cy_bento)) if cy_setup: - info("The following cython files exist but were not in 'bento.info':") + info("Extensions found in a 'setup.py' but not in any 'bento.info:") print('\n'.join(cy_setup)) info("Consider adding the following to the 'bento.info' Library:") - for ext_name, dir_path in cy_setup.iteritems(): - print BENTO_TEMPLATE.format(module_path=dir_path.replace('/', '.'), + for dir_path in cy_setup: + module_path = dir_path.replace('/', '.') + print BENTO_TEMPLATE.format(module_path=module_path, dir_path=dir_path) + if __name__ == '__main__': # All cython extensions defined in 'setup.py' files. - cy_setup = dict((ext, path) for path, ext in each_cy_in_setup('skimage')) + cy_setup = set(each_cy_in_setup('skimage')) # All cython extensions defined 'bento.info' file. - cy_bento = dict((ext, path) for path, ext in each_cy_in_bento()) + cy_bento = set(each_cy_in_bento()) - remove_common_extensions(cy_bento, cy_setup) + cy_bento, cy_setup = remove_common_extensions(cy_bento, cy_setup) print_results(cy_bento, cy_setup) diff --git a/doc/__init__.py b/doc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/doc/examples/applications/plot_coins_segmentation.py b/doc/examples/applications/plot_coins_segmentation.py index f46bbd33..0d1d8a34 100644 --- a/doc/examples/applications/plot_coins_segmentation.py +++ b/doc/examples/applications/plot_coins_segmentation.py @@ -90,12 +90,8 @@ plt.title('Filling the holes') Small spurious objects are easily removed by setting a minimum size for valid objects. """ - -label_objects, nb_labels = ndimage.label(fill_coins) -sizes = np.bincount(label_objects.ravel()) -mask_sizes = sizes > 20 -mask_sizes[0] = 0 -coins_cleaned = mask_sizes[label_objects] +from skimage import morphology +coins_cleaned = morphology.remove_small_objects(fill_coins, 21) plt.figure(figsize=(4, 3)) plt.imshow(coins_cleaned, cmap=plt.cm.gray, interpolation='nearest') @@ -149,8 +145,7 @@ plt.title('markers') Finally, we use the watershed transform to fill regions of the elevation map starting from the markers determined above: """ -from skimage.morphology import watershed -segmentation = watershed(elevation_map, markers) +segmentation = morphology.watershed(elevation_map, markers) plt.figure(figsize=(4, 3)) plt.imshow(segmentation, cmap=plt.cm.gray, interpolation='nearest') diff --git a/doc/examples/applications/plot_rank_filters.py b/doc/examples/applications/plot_rank_filters.py new file mode 100644 index 00000000..c182671d --- /dev/null +++ b/doc/examples/applications/plot_rank_filters.py @@ -0,0 +1,719 @@ +""" +============ +Rank filters +============ + +Rank filters are non-linear filters using the local greylevels ordering to +compute the filtered value. This ensemble of filters share a common base: the +local grey-level histogram extraction computed on the neighborhood of a pixel +(defined by a 2D structuring element). If the filtered value is taken as the +middle value of the histogram, we get the classical median filter. + +Rank filters can be used for several purposes such as: + +* image quality enhancement + e.g. image smoothing, sharpening + +* image pre-processing + e.g. noise reduction, contrast enhancement + +* feature extraction + e.g. border detection, isolated point detection + +* post-processing + e.g. small object removal, object grouping, contour smoothing + +Some well known filters are specific cases of rank filters [1]_ e.g. +morphological dilation, morphological erosion, median filters. + +The different implementation availables in `skimage` are compared. + +In this example, we will see how to filter a greylevel image using some of the +linear and non-linear filters availables in skimage. We use the `camera` +image from `skimage.data`. + +.. [1] Pierre Soille, On morphological operators based on rank filters, Pattern + Recognition 35 (2002) 527-535. + +""" + +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data + +ima = data.camera() +hist = np.histogram(ima, bins=np.arange(0, 256)) + +plt.figure(figsize=(8, 3)) +plt.subplot(1, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray, interpolation='nearest') +plt.axis('off') +plt.subplot(1, 2, 2) +plt.plot(hist[1][:-1], hist[0], lw=2) +plt.title('histogram of grey values') + +""" + +.. image:: PLOT2RST.current_figure + +Noise removal +============= + +Some noise is added to the image, 1% of pixels are randomly set to 255, 1% are +randomly set to 0. The **median** filter is applied to remove the noise. + +.. note:: + + there are different implementations of median filter : + `skimage.filter.median_filter` and `skimage.filter.rank.median` + +""" + +noise = np.random.random(ima.shape) +nima = data.camera() +nima[noise > 0.99] = 255 +nima[noise < 0.01] = 0 + +from skimage.filter.rank import median +from skimage.morphology import disk + +fig = plt.figure(figsize=[10, 7]) + +lo = median(nima, disk(1)) +hi = median(nima, disk(5)) +ext = median(nima, disk(20)) +plt.subplot(2, 2, 1) +plt.imshow(nima, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('noised image') +plt.subplot(2, 2, 2) +plt.imshow(lo, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('median $r=1$') +plt.subplot(2, 2, 3) +plt.imshow(hi, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('median $r=5$') +plt.subplot(2, 2, 4) +plt.imshow(ext, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('median $r=20$') + +""" + +.. image:: PLOT2RST.current_figure + +The added noise is efficiently removed, as the image defaults are small (1 pixel +wide), a small filter radius is sufficient. As the radius is increasing, objects +with a bigger size are filtered as well, such as the camera tripod. The median +filter is commonly used for noise removal because borders are preserved. + +Image smoothing +================ + +The example hereunder shows how a local **mean** smoothes the camera man image. + +""" + +from skimage.filter.rank import mean + +fig = plt.figure(figsize=[10, 7]) + +loc_mean = mean(nima, disk(10)) +plt.subplot(1, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('original') +plt.subplot(1, 2, 2) +plt.imshow(loc_mean, cmap=plt.cm.gray, vmin=0, vmax=255) +plt.xlabel('local mean $r=10$') + +""" + +.. image:: PLOT2RST.current_figure + +One may be interested in smoothing an image while preserving important borders +(median filters already achieved this), here we use the **bilateral** filter +that restricts the local neighborhood to pixel having a greylevel similar to +the central one. + +.. note:: + + a different implementation is available for color images in + `skimage.filter.denoise_bilateral`. + +""" + +from skimage.filter.rank import bilateral_mean + +ima = data.camera() +selem = disk(10) + +bilat = bilateral_mean(ima.astype(np.uint16), disk(20), s0=10, s1=10) + +# display results +fig = plt.figure(figsize=[10, 7]) +plt.subplot(2, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray) +plt.xlabel('original') +plt.subplot(2, 2, 3) +plt.imshow(bilat, cmap=plt.cm.gray) +plt.xlabel('bilateral mean') +plt.subplot(2, 2, 2) +plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.subplot(2, 2, 4) +plt.imshow(bilat[200:350, 350:450], cmap=plt.cm.gray) + +""" + +.. image:: PLOT2RST.current_figure + +One can see that the large continuous part of the image (e.g. sky) is smoothed +whereas other details are preserved. + + +Contrast enhancement +==================== + +We compare here how the global histogram equalization is applied locally. + +The equalized image [2]_ has a roughly linear cumulative distribution function +for each pixel neighborhood. The local version [3]_ of the histogram +equalization emphasizes every local greylevel variations. + +.. [2] http://en.wikipedia.org/wiki/Histogram_equalization +.. [3] http://en.wikipedia.org/wiki/Adaptive_histogram_equalization + +""" + +from skimage import exposure +from skimage.filter import rank + +ima = data.camera() +# equalize globally and locally +glob = exposure.equalize(ima) * 255 +loc = rank.equalize(ima, disk(20)) + +# extract histogram for each image +hist = np.histogram(ima, bins=np.arange(0, 256)) +glob_hist = np.histogram(glob, bins=np.arange(0, 256)) +loc_hist = np.histogram(loc, bins=np.arange(0, 256)) + +plt.figure(figsize=(10, 10)) +plt.subplot(321) +plt.imshow(ima, cmap=plt.cm.gray, interpolation='nearest') +plt.axis('off') +plt.subplot(322) +plt.plot(hist[1][:-1], hist[0], lw=2) +plt.title('histogram of grey values') +plt.subplot(323) +plt.imshow(glob, cmap=plt.cm.gray, interpolation='nearest') +plt.axis('off') +plt.subplot(324) +plt.plot(glob_hist[1][:-1], glob_hist[0], lw=2) +plt.title('histogram of grey values') +plt.subplot(325) +plt.imshow(loc, cmap=plt.cm.gray, interpolation='nearest') +plt.axis('off') +plt.subplot(326) +plt.plot(loc_hist[1][:-1], loc_hist[0], lw=2) +plt.title('histogram of grey values') + +""" + +.. image:: PLOT2RST.current_figure + +another way to maximize the number of greylevels used for an image is to apply +a local autoleveling, i.e. here a pixel greylevel is proportionally remapped +between local minimum and local maximum. + +The following example shows how local autolevel enhances the camara man picture. + +""" + +from skimage.filter.rank import autolevel + +ima = data.camera() +selem = disk(10) + +auto = autolevel(ima.astype(np.uint16), disk(20)) + +# display results +fig = plt.figure(figsize=[10, 7]) +plt.subplot(1, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray) +plt.xlabel('original') +plt.subplot(1, 2, 2) +plt.imshow(auto, cmap=plt.cm.gray) +plt.xlabel('local autolevel') + +""" + +.. image:: PLOT2RST.current_figure + +This filter is very sensitive to local outlayers, see the little white spot in +the sky left part. This is due to a local maximum which is very high comparing +to the rest of the neighborhood. One can moderate this using the percentile +version of the autolevel filter which uses given percentiles (one inferior, +one superior) in place of local minimum and maximum. The example below +illustrates how the percentile parameters influence the local autolevel result. + +""" + +from skimage.filter.rank import percentile_autolevel + +image = data.camera() + +selem = disk(20) +loc_autolevel = autolevel(image, selem=selem) +loc_perc_autolevel0 = percentile_autolevel(image, selem=selem, p0=.00, p1=1.0) +loc_perc_autolevel1 = percentile_autolevel(image, selem=selem, p0=.01, p1=.99) +loc_perc_autolevel2 = percentile_autolevel(image, selem=selem, p0=.05, p1=.95) +loc_perc_autolevel3 = percentile_autolevel(image, selem=selem, p0=.1, p1=.9) + +fig, axes = plt.subplots(nrows=3, figsize=(7, 8)) +ax0, ax1, ax2 = axes +plt.gray() + +ax0.imshow(np.hstack((image, loc_autolevel))) +ax0.set_title('original / autolevel') + +ax1.imshow( + np.hstack((loc_perc_autolevel0, loc_perc_autolevel1)), vmin=0, vmax=255) +ax1.set_title('percentile autolevel 0%,1%') +ax2.imshow( + np.hstack((loc_perc_autolevel2, loc_perc_autolevel3)), vmin=0, vmax=255) +ax2.set_title('percentile autolevel 5% and 10%') + +for ax in axes: + ax.axis('off') + +""" + +.. image:: PLOT2RST.current_figure + +The morphological contrast enhancement filter replaces the central pixel by the +local maximum if the original pixel value is closest to local maximum, otherwise +by the minimum local. + +""" + +from skimage.filter.rank import morph_contr_enh + +ima = data.camera() + +enh = morph_contr_enh(ima, disk(5)) + +# display results +fig = plt.figure(figsize=[10, 7]) +plt.subplot(2, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray) +plt.xlabel('original') +plt.subplot(2, 2, 3) +plt.imshow(enh, cmap=plt.cm.gray) +plt.xlabel('local morphlogical contrast enhancement') +plt.subplot(2, 2, 2) +plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.subplot(2, 2, 4) +plt.imshow(enh[200:350, 350:450], cmap=plt.cm.gray) + +""" + +.. image:: PLOT2RST.current_figure + +The percentile version of the local morphological contrast enhancement uses +percentile *p0* and *p1* instead of the local minimum and maximum. + +""" + +from skimage.filter.rank import percentile_morph_contr_enh + +ima = data.camera() + +penh = percentile_morph_contr_enh(ima, disk(5), p0=.1, p1=.9) + +# display results +fig = plt.figure(figsize=[10, 7]) +plt.subplot(2, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray) +plt.xlabel('original') +plt.subplot(2, 2, 3) +plt.imshow(penh, cmap=plt.cm.gray) +plt.xlabel('local percentile morphlogical\n contrast enhancement') +plt.subplot(2, 2, 2) +plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.subplot(2, 2, 4) +plt.imshow(penh[200:350, 350:450], cmap=plt.cm.gray) + +""" + +.. image:: PLOT2RST.current_figure + +Image threshold +=============== + +The Otsu's threshold [1]_ method can be applied locally using the local +greylevel distribution. In the example below, for each pixel, an "optimal" +threshold is determined by maximizing the variance between two classes of pixels +of the local neighborhood defined by a structuring element. + +The example compares the local threshold with the global threshold +`skimage.filter.threshold_otsu`. + +.. note:: + + Local thresholding is much slower than global one. There exists a function + for global Otsu thresholding: `skimage.filter.threshold_otsu`. + +.. [1] http://en.wikipedia.org/wiki/Otsu's_method + +""" + +from skimage.filter.rank import otsu +from skimage.filter import threshold_otsu + +p8 = data.page() + +radius = 10 +selem = disk(radius) + +# t_loc_otsu is an image +t_loc_otsu = otsu(p8, selem) +loc_otsu = p8 >= t_loc_otsu + +# t_glob_otsu is a scalar +t_glob_otsu = threshold_otsu(p8) +glob_otsu = p8 >= t_glob_otsu + +plt.figure() +plt.subplot(2, 2, 1) +plt.imshow(p8, cmap=plt.cm.gray) +plt.xlabel('original') +plt.colorbar() +plt.subplot(2, 2, 2) +plt.imshow(t_loc_otsu, cmap=plt.cm.gray) +plt.xlabel('local Otsu ($radius=%d$)' % radius) +plt.colorbar() +plt.subplot(2, 2, 3) +plt.imshow(p8 >= t_loc_otsu, cmap=plt.cm.gray) +plt.xlabel('original>=local Otsu' % t_glob_otsu) +plt.subplot(2, 2, 4) +plt.imshow(glob_otsu, cmap=plt.cm.gray) +plt.xlabel('global Otsu ($t=%d$)' % t_glob_otsu) + +""" + +.. image:: PLOT2RST.current_figure + +The following example shows how local Otsu's threshold handles a global level +shift applied to a synthetic image . + +""" + +n = 100 +theta = np.linspace(0, 10 * np.pi, n) +x = np.sin(theta) +m = (np.tile(x, (n, 1)) * np.linspace(0.1, 1, n) * 128 + 128).astype(np.uint8) + +radius = 10 +t = rank.otsu(m, disk(radius)) +plt.figure() +plt.subplot(1, 2, 1) +plt.imshow(m) +plt.xlabel('original') +plt.subplot(1, 2, 2) +plt.imshow(m >= t, interpolation='nearest') +plt.xlabel('local Otsu ($radius=%d$)' % radius) + +""" + +.. image:: PLOT2RST.current_figure + +Image morphology +================ + +Local maximum and local minimum are the base operators for greylevel +morphology. + +.. note:: + + `skimage.dilate` and `skimage.erode` are equivalent filters (see below for + comparison). + +Here is an example of the classical morphological greylevel filters: opening, +closing and morphological gradient. + +""" + +from skimage.filter.rank import maximum, minimum, gradient + +ima = data.camera() + +closing = maximum(minimum(ima, disk(5)), disk(5)) +opening = minimum(maximum(ima, disk(5)), disk(5)) +grad = gradient(ima, disk(5)) + +# display results +fig = plt.figure(figsize=[10, 7]) +plt.subplot(2, 2, 1) +plt.imshow(ima, cmap=plt.cm.gray) +plt.xlabel('original') +plt.subplot(2, 2, 2) +plt.imshow(closing, cmap=plt.cm.gray) +plt.xlabel('greylevel closing') +plt.subplot(2, 2, 3) +plt.imshow(opening, cmap=plt.cm.gray) +plt.xlabel('greylevel opening') +plt.subplot(2, 2, 4) +plt.imshow(grad, cmap=plt.cm.gray) +plt.xlabel('morphological gradient') + +""" + +.. image:: PLOT2RST.current_figure + +Feature extraction +=================== + +Local histogram can be exploited to compute local entropy, which is related to +the local image complexity. Entropy is computed using base 2 logarithm i.e. the +filter returns the minimum number of bits needed to encode local greylevel +distribution. + +`skimage.rank.entropy` returns local entropy on a given structuring element. +The following example shows this filter applied on 8- and 16- bit images. + +.. note:: + + to better use the available image bit, the function returns 10x entropy for + 8-bit images and 1000x entropy for 16-bit images. + +""" + +from skimage import data +from skimage.filter.rank import entropy +from skimage.morphology import disk +import numpy as np +import matplotlib.pyplot as plt + +# defining a 8- and a 16-bit test images +a8 = data.camera() +a16 = data.camera().astype(np.uint16) * 4 + +ent8 = entropy(a8, disk(5)) # pixel value contain 10x the local entropy +ent16 = entropy(a16, disk(5)) # pixel value contain 1000x the local entropy + +# display results +plt.figure(figsize=(10, 10)) + +plt.subplot(2, 2, 1) +plt.imshow(a8, cmap=plt.cm.gray) +plt.xlabel('8-bit image') +plt.colorbar() + +plt.subplot(2, 2, 2) +plt.imshow(ent8, cmap=plt.cm.jet) +plt.xlabel('entropy*10') +plt.colorbar() + +plt.subplot(2, 2, 3) +plt.imshow(a16, cmap=plt.cm.gray) +plt.xlabel('16-bit image') +plt.colorbar() + +plt.subplot(2, 2, 4) +plt.imshow(ent16, cmap=plt.cm.jet) +plt.xlabel('entropy*1000') +plt.colorbar() + +""" + +.. image:: PLOT2RST.current_figure + +Implementation +================ + +The central part of the `skimage.rank` filters is build on a sliding window that +update local greylevel histogram. This approach limits the algorithm complexity +to O(n) where n is the number of image pixels. The complexity is also limited +with respect to the structuring element size. + +""" + +from time import time + +from scipy.ndimage.filters import percentile_filter +from skimage.morphology import dilation +from skimage.filter import median_filter +from skimage.filter.rank import median, maximum + + +def exec_and_timeit(func): + """Decorator that returns both function results and execution time.""" + def wrapper(*arg): + t1 = time() + res = func(*arg) + t2 = time() + ms = (t2 - t1) * 1000.0 + return (res, ms) + return wrapper + + +@exec_and_timeit +def cr_med(image, selem): + return median(image=image, selem=selem) + + +@exec_and_timeit +def cr_max(image, selem): + return maximum(image=image, selem=selem) + + +@exec_and_timeit +def cm_dil(image, selem): + return dilation(image=image, selem=selem) + + +@exec_and_timeit +def ctmf_med(image, radius): + return median_filter(image=image, radius=radius) + + +@exec_and_timeit +def ndi_med(image, n): + return percentile_filter(image, 50, size=n * 2 - 1) + +""" + +Comparison between + +* `rank.maximum` +* `cmorph.dilate` + +on increasing structuring element size + +""" + +a = data.camera() + +rec = [] +e_range = range(1, 20, 2) +for r in e_range: + elem = disk(r + 1) + rc, ms_rc = cr_max(a, elem) + rcm, ms_rcm = cm_dil(a, elem) + rec.append((ms_rc, ms_rcm)) + +rec = np.asarray(rec) + +plt.figure() +plt.title('increasing element size') +plt.ylabel('time (ms)') +plt.xlabel('element radius') +plt.plot(e_range, rec) +plt.legend(['crank.maximum', 'cmorph.dilate']) + +""" + +and increasing image size + +.. image:: PLOT2RST.current_figure + +""" + +r = 9 +elem = disk(r + 1) + +rec = [] +s_range = range(100, 1000, 100) +for s in s_range: + a = (np.random.random((s, s)) * 256).astype('uint8') + (rc, ms_rc) = cr_max(a, elem) + (rcm, ms_rcm) = cm_dil(a, elem) + rec.append((ms_rc, ms_rcm)) + +rec = np.asarray(rec) + +plt.figure() +plt.title('increasing image size') +plt.ylabel('time (ms)') +plt.xlabel('image size') +plt.plot(s_range, rec) +plt.legend(['crank.maximum', 'cmorph.dilate']) + + +""" + +.. image:: PLOT2RST.current_figure + +Comparison between: + +* `rank.median` +* `ctmf.median_filter` +* `ndimage.percentile` + +on increasing structuring element size + +""" + +a = data.camera() + +rec = [] +e_range = range(2, 30, 4) +for r in e_range: + elem = disk(r + 1) + rc, ms_rc = cr_med(a, elem) + rctmf, ms_rctmf = ctmf_med(a, r) + rndi, ms_ndi = ndi_med(a, r) + rec.append((ms_rc, ms_rctmf, ms_ndi)) + +rec = np.asarray(rec) + +plt.figure() +plt.title('increasing element size') +plt.plot(e_range, rec) +plt.legend(['rank.median', 'ctmf.median_filter', 'ndimage.percentile']) +plt.ylabel('time (ms)') +plt.xlabel('element radius') + +""" +.. image:: PLOT2RST.current_figure + +comparison of outcome of the three methods + +""" + +plt.figure() +plt.imshow(np.hstack((rc, rctmf, rndi))) +plt.xlabel('rank.median vs ctmf.median_filter vs ndimage.percentile') + +""" +.. image:: PLOT2RST.current_figure + +and increasing image size + +""" + +r = 9 +elem = disk(r + 1) + +rec = [] +s_range = [100, 200, 500, 1000] +for s in s_range: + a = (np.random.random((s, s)) * 256).astype('uint8') + (rc, ms_rc) = cr_med(a, elem) + rctmf, ms_rctmf = ctmf_med(a, r) + rndi, ms_ndi = ndi_med(a, r) + rec.append((ms_rc, ms_rctmf, ms_ndi)) + +rec = np.asarray(rec) + +plt.figure() +plt.title('increasing image size') +plt.plot(s_range, rec) +plt.legend(['rank.median', 'ctmf.median_filter', 'ndimage.percentile']) +plt.ylabel('time (ms)') +plt.xlabel('image size') + +""" +.. image:: PLOT2RST.current_figure + +""" + +plt.show() diff --git a/doc/examples/plot_16bitbilateral.py b/doc/examples/plot_16bitbilateral.py new file mode 100644 index 00000000..473fcadd --- /dev/null +++ b/doc/examples/plot_16bitbilateral.py @@ -0,0 +1,47 @@ +""" +============================== +Bilateral mean +============================== +This example compares + +* local mean +* percentile mean +* bilateral mean + +build on the local histogram distribution +local mean uses all pixels belonging to the structuring element to compute average gray level, +percentile mean uses only values between percentiles p0 and p1 (here 10% and 90%), +whereas bilateral mean uses only pixels of the structuring element having a gray level situated inside +g-s0 and g+s1 (here g-500 and g+500). +The filters are applied on a 16 bit image (actual bitdepth is 12bit). + +Percentile and usual mean give here similar results, these filters smooth the complete image (background and details). +Bilateral mean exhibits a high filtering rate for continuous area (i.e. background) while image higher frequencies +remains untouched. + +""" +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data +from skimage.morphology import disk +import skimage.filter.rank as rank + +a16 = (data.coins()).astype('uint16') * 16 +selem = disk(20) + +f1 = rank.percentile_mean(a16, selem=selem, p0=.1, p1=.9) +f2 = rank.bilateral_mean(a16, selem=selem, s0=500, s1=500) +f3 = rank.mean(a16, selem=selem) + +# display results +fig, axes = plt.subplots(nrows=3, figsize=(15, 10)) +ax0, ax1, ax2 = axes + +ax0.imshow(np.hstack((a16, f1))) +ax0.set_title('percentile mean') +ax1.imshow(np.hstack((a16, f2))) +ax1.set_title('bilateral mean') +ax2.imshow(np.hstack((a16, f3))) +ax2.set_title('local mean') +plt.show() diff --git a/doc/examples/plot_circular_hough_transform.py b/doc/examples/plot_circular_hough_transform.py new file mode 100755 index 00000000..d2f8f2ae --- /dev/null +++ b/doc/examples/plot_circular_hough_transform.py @@ -0,0 +1,72 @@ +""" +======================== +Circular Hough Transform +======================== + +The Hough transform in its simplest form is a `method to detect +straight lines `__ +but it can also be used to detect circles. + +In the following example, the Hough transform is used to detect +coin positions and match their edges. We provide a range of +plausible radii. For each radius, two circles are extracted and +we finally keep the five most prominent candidates. +The result shows that coin positions are well-detected. + + +Algorithm overview +------------------ + +Given a black circle on a white background, we first guess its +radius (or a range of radii) to construct a new circle. +This circle is applied on each black pixel of the original picture +and the coordinates of this circle are voting in an accumulator. +From this geometrical construction, the original circle center +position receives the highest score. + +Note that the accumulator size is built to be larger than the +original picture in order to detect centers outside the frame. +Its size is extended by two times the larger radius. + +""" + + +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data, filter, color +from skimage.transform import hough_circle +from skimage.feature import peak_local_max +from skimage.draw import circle_perimeter + +# Load picture and detect edges +image = data.coins()[0:95, 70:370] +edges = filter.canny(image, sigma=3, low_threshold=10, high_threshold=50) + +fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) + +# Detect two radii +hough_radii = np.arange(15, 30, 2) +hough_res = hough_circle(edges, hough_radii) + +centers = [] +accums = [] +radii = [] + +for radius, h in zip(hough_radii, hough_res): + # For each radius, extract two circles + peaks = peak_local_max(h, num_peaks=2) + centers.extend(peaks - hough_radii.max()) + accums.extend(h[peaks[:, 0], peaks[:, 1]]) + radii.extend([radius, radius]) + +# Draw the most prominent 5 circles +image = color.gray2rgb(image) +for idx in np.argsort(accums)[::-1][:5]: + center_x, center_y = centers[idx] + radius = radii[idx] + cx, cy = circle_perimeter(center_y, center_x, radius) + image[cy, cx] = (220, 20, 20) + +ax.imshow(image, cmap=plt.cm.gray) +plt.show() diff --git a/doc/examples/plot_corner.py b/doc/examples/plot_corner.py new file mode 100644 index 00000000..30f1f0cf --- /dev/null +++ b/doc/examples/plot_corner.py @@ -0,0 +1,37 @@ +""" +================ +Corner detection +================ + +Detect corner points using the Harris corner detector and determine subpixel +position of corners. + +.. [1] http://en.wikipedia.org/wiki/Corner_detection +.. [2] http://en.wikipedia.org/wiki/Interest_point_detection + +""" + +from matplotlib import pyplot as plt + +from skimage import data +from skimage.feature import corner_harris, corner_subpix, corner_peaks +from skimage.transform import warp, AffineTransform +from skimage.draw import ellipse + +tform = AffineTransform(scale=(1.3, 1.1), rotation=1, shear=0.7, + translation=(210, 50)) +image = warp(data.checkerboard(), tform.inverse, output_shape=(350, 350)) +rr, cc = ellipse(310, 175, 10, 100) +image[rr, cc] = 1 +image[180:230, 10:60] = 1 +image[230:280, 60:110] = 1 + +coords = corner_peaks(corner_harris(image), min_distance=5) +coords_subpix = corner_subpix(image, coords, window_size=13) + +plt.gray() +plt.imshow(image, interpolation='nearest') +plt.plot(coords[:, 1], coords[:, 0], '.b', markersize=3) +plt.plot(coords_subpix[:, 1], coords_subpix[:, 0], '+r', markersize=15) +plt.axis((0, 350, 350, 0)) +plt.show() diff --git a/doc/examples/plot_daisy.py b/doc/examples/plot_daisy.py new file mode 100644 index 00000000..af78103d --- /dev/null +++ b/doc/examples/plot_daisy.py @@ -0,0 +1,28 @@ +""" +=============================== +Dense DAISY feature description +=============================== + +The DAISY local image descriptor is based on gradient orientation histograms +similar to the SIFT descriptor. It is formulated in a way that allows for fast +dense extraction which is useful for e.g. bag-of-features image +representations. + +In this example a limited number of DAISY descriptors are extracted at a large +scale for illustrative purposes. +""" + +from skimage.feature import daisy +from skimage import data +import matplotlib.pyplot as plt + + +img = data.camera() +descs, descs_img = daisy(img, step=180, radius=58, rings=2, histograms=6, + orientations=8, visualize=True) + +plt.axis('off') +plt.imshow(descs_img) +descs_num = descs.shape[0] * descs.shape[1] +plt.title('%i DAISY descriptors extracted:' % descs_num) +plt.show() diff --git a/doc/examples/plot_denoise.py b/doc/examples/plot_denoise.py new file mode 100644 index 00000000..debb2ea6 --- /dev/null +++ b/doc/examples/plot_denoise.py @@ -0,0 +1,68 @@ +""" +============================= +Denoising the picture of Lena +============================= + +In this example, we denoise a noisy version of the picture of Lena using the +total variation and bilateral denoising filter. + +These algorithms typically produce "posterized" images with flat domains +separated by sharp edges. It is possible to change the degree of posterization +by controlling the tradeoff between denoising and faithfulness to the original +image. + +Total variation filter +---------------------- + +The result of this filter is an image that has a minimal total variation norm, +while being as close to the initial image as possible. The total variation is +the L1 norm of the gradient of the image. + +Bilateral filter +---------------- + +A bilateral filter is an edge-preserving and noise reducing filter. It averages +pixels based on their spatial closeness and radiometric similarity. + +""" + +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data, color, img_as_float +from skimage.filter import denoise_tv_chambolle, denoise_bilateral + +lena = img_as_float(data.lena()) +lena = lena[220:300, 220:320] + +noisy = lena + 0.6 * lena.std() * np.random.random(lena.shape) +noisy = np.clip(noisy, 0, 1) + +fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(8, 5)) + +plt.gray() + +ax[0, 0].imshow(noisy) +ax[0, 0].axis('off') +ax[0, 0].set_title('noisy') +ax[0, 1].imshow(denoise_tv_chambolle(noisy, weight=0.1, multichannel=True)) +ax[0, 1].axis('off') +ax[0, 1].set_title('TV') +ax[0, 2].imshow(denoise_bilateral(noisy, sigma_range=0.05, sigma_spatial=15)) +ax[0, 2].axis('off') +ax[0, 2].set_title('Bilateral') + +ax[1, 0].imshow(denoise_tv_chambolle(noisy, weight=0.2, multichannel=True)) +ax[1, 0].axis('off') +ax[1, 0].set_title('(more) TV') +ax[1, 1].imshow(denoise_bilateral(noisy, sigma_range=0.1, sigma_spatial=15)) +ax[1, 1].axis('off') +ax[1, 1].set_title('(more) Bilateral') +ax[1, 2].imshow(lena) +ax[1, 2].axis('off') +ax[1, 2].set_title('original') + +fig.subplots_adjust(wspace=0.02, hspace=0.2, + top=0.9, bottom=0.05, left=0, right=1) + +plt.show() diff --git a/doc/examples/plot_entropy.py b/doc/examples/plot_entropy.py new file mode 100644 index 00000000..f019d79c --- /dev/null +++ b/doc/examples/plot_entropy.py @@ -0,0 +1,44 @@ +""" +=================== +Entropy +=================== + + +""" +from skimage import data +from skimage.filter.rank import entropy +from skimage.morphology import disk +import numpy as np +import matplotlib.pyplot as plt + +# defining a 8- and a 16-bit test images +a8 = data.camera() +a16 = data.camera().astype(np.uint16)*4 + +ent8 = entropy(a8,disk(5)) # pixel value contain 10x the local entropy +ent16 = entropy(a16,disk(5)) # pixel value contain 1000x the local entropy + +# display results +plt.figure(figsize=(10, 10)) + +plt.subplot(2,2,1) +plt.imshow(a8, cmap=plt.cm.gray) +plt.xlabel('8-bit image') +plt.colorbar() + +plt.subplot(2,2,2) +plt.imshow(ent8, cmap=plt.cm.jet) +plt.xlabel('entropy*10') +plt.colorbar() + +plt.subplot(2,2,3) +plt.imshow(a16, cmap=plt.cm.gray) +plt.xlabel('16-bit image') +plt.colorbar() + +plt.subplot(2,2,4) +plt.imshow(ent16, cmap=plt.cm.jet) +plt.xlabel('entropy*1000') +plt.colorbar() +plt.show() + diff --git a/doc/examples/plot_equalize.py b/doc/examples/plot_equalize.py index 1e1291d8..3569c4ee 100644 --- a/doc/examples/plot_equalize.py +++ b/doc/examples/plot_equalize.py @@ -18,18 +18,18 @@ that fall within the 2nd and 98th percentiles [2]_. """ -from skimage import data -from skimage.util.dtype import dtype_range +from skimage import data, img_as_float from skimage import exposure import matplotlib.pyplot as plt - import numpy as np + def plot_img_and_hist(img, axes, bins=256): """Plot an image along with its histogram and cumulative histogram. """ + img = img_as_float(img) ax_img, ax_hist = axes ax_cdf = ax_hist.twinx() @@ -38,16 +38,16 @@ def plot_img_and_hist(img, axes, bins=256): ax_img.set_axis_off() # Display histogram - ax_hist.hist(img.ravel(), bins=bins) + ax_hist.hist(img.ravel(), bins=bins, histtype='step', color='black') ax_hist.ticklabel_format(axis='y', style='scientific', scilimits=(0, 0)) ax_hist.set_xlabel('Pixel intensity') - - xmin, xmax = dtype_range[img.dtype.type] - ax_hist.set_xlim(xmin, xmax) + ax_hist.set_xlim(0, 1) + ax_hist.set_yticks([]) # Display cumulative distribution img_cdf, bins = exposure.cumulative_distribution(img, bins) ax_cdf.plot(bins, img_cdf, 'r') + ax_cdf.set_yticks([]) return ax_img, ax_hist, ax_cdf @@ -61,25 +61,33 @@ p98 = np.percentile(img, 98) img_rescale = exposure.rescale_intensity(img, in_range=(p2, p98)) # Equalization -img_eq = exposure.equalize(img) +img_eq = exposure.equalize_hist(img) +# Adaptive Equalization +img_adapteq = exposure.equalize_adapthist(img, clip_limit=0.03) # Display results -f, axes = plt.subplots(2, 3, figsize=(8, 4)) +f, axes = plt.subplots(2, 4, figsize=(8, 4)) ax_img, ax_hist, ax_cdf = plot_img_and_hist(img, axes[:, 0]) ax_img.set_title('Low contrast image') + +y_min, y_max = ax_hist.get_ylim() ax_hist.set_ylabel('Number of pixels') +ax_hist.set_yticks(np.linspace(0, y_max, 5)) ax_img, ax_hist, ax_cdf = plot_img_and_hist(img_rescale, axes[:, 1]) ax_img.set_title('Contrast stretching') ax_img, ax_hist, ax_cdf = plot_img_and_hist(img_eq, axes[:, 2]) ax_img.set_title('Histogram equalization') -ax_cdf.set_ylabel('Fraction of total intensity') +ax_img, ax_hist, ax_cdf = plot_img_and_hist(img_adapteq, axes[:, 3]) +ax_img.set_title('Adaptive equalization') + +ax_cdf.set_ylabel('Fraction of total intensity') +ax_cdf.set_yticks(np.linspace(0, 1, 5)) # prevent overlap of y-axis labels plt.subplots_adjust(wspace=0.4) plt.show() - diff --git a/doc/examples/plot_harris.py b/doc/examples/plot_harris.py deleted file mode 100644 index 6212ac4a..00000000 --- a/doc/examples/plot_harris.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -=============================================================================== -Harris Corner detector -=============================================================================== - -The Harris corner filter [1]_ detects "interest points" [2]_ using edge -detection in multiple directions. - -.. [1] http://en.wikipedia.org/wiki/Corner_detection -.. [2] http://en.wikipedia.org/wiki/Interest_point_detection -""" -import numpy as np -from matplotlib import pyplot as plt - -from skimage import data, img_as_float -from skimage.feature import harris - - -def plot_harris_points(image, filtered_coords): - """ plots corners found in image""" - - plt.imshow(image) - y, x = np.transpose(filtered_coords) - plt.plot(x, y, 'b.') - plt.axis('off') - -# display results -plt.figure(figsize=(8, 3)) -im_lena = img_as_float(data.lena()) -im_text = img_as_float(data.text()) - -filtered_coords = harris(im_lena, min_distance=4) - -plt.axes([0, 0, 0.3, 0.95]) -plot_harris_points(im_lena, filtered_coords) - -filtered_coords = harris(im_text, min_distance=4) - -plt.axes([0.2, 0, 0.77, 1]) -plot_harris_points(im_text, filtered_coords) - -plt.show() diff --git a/doc/examples/plot_hough_transform.py b/doc/examples/plot_hough_transform.py index 5416d662..b74132da 100644 --- a/doc/examples/plot_hough_transform.py +++ b/doc/examples/plot_hough_transform.py @@ -59,7 +59,7 @@ References ''' -from skimage.transform import hough, probabilistic_hough +from skimage.transform import hough, hough_peaks, probabilistic_hough from skimage.filter import canny from skimage import data @@ -81,11 +81,11 @@ h, theta, d = hough(image) plt.figure(figsize=(8, 4)) -plt.subplot(121) +plt.subplot(131) plt.imshow(image, cmap=plt.cm.gray) plt.title('Input image') -plt.subplot(122) +plt.subplot(132) plt.imshow(np.log(1 + h), extent=[np.rad2deg(theta[-1]), np.rad2deg(theta[0]), d[-1], d[0]], @@ -94,6 +94,15 @@ plt.title('Hough transform') plt.xlabel('Angles (degrees)') plt.ylabel('Distance (pixels)') +plt.subplot(133) +plt.imshow(image, cmap=plt.cm.gray) +rows, cols = image.shape +for _, angle, dist in zip(*hough_peaks(h, theta, d)): + y0 = (dist - 0 * np.cos(angle)) / np.sin(angle) + y1 = (dist - cols * np.cos(angle)) / np.sin(angle) + plt.plot((0, cols), (y0, y1), '-r') +plt.axis((0, cols, rows, 0)) +plt.title('Detected lines') # Line finding, using the Probabilistic Hough Transform @@ -118,7 +127,6 @@ for line in lines: p0, p1 = line plt.plot((p0[0], p1[0]), (p0[1], p1[1])) -plt.title('Lines found with PHT') +plt.title('Probabilistic Hough') plt.axis('image') plt.show() - diff --git a/doc/examples/plot_join_segmentations.py b/doc/examples/plot_join_segmentations.py new file mode 100644 index 00000000..02600ba6 --- /dev/null +++ b/doc/examples/plot_join_segmentations.py @@ -0,0 +1,69 @@ +""" +========================================== +Find the intersection of two segmentations +========================================== + +When segmenting an image, you may want to combine multiple alternative +segmentations. The `skimage.segmentation.join_segmentations` function +computes the join of two segmentations, in which a pixel is placed in +the same segment if and only if it is in the same segment in _both_ +segmentations. +""" + +import numpy as np +from scipy import ndimage as nd +import matplotlib.pyplot as plt +import matplotlib as mpl + +from skimage.filter import sobel +from skimage.segmentation import slic, join_segmentations +from skimage.morphology import watershed + +from skimage import data + +coins = data.coins() + +# make segmentation using edge-detection and watershed +edges = sobel(coins) +markers = np.zeros_like(coins) +foreground, background = 1, 2 +markers[coins < 30] = background +markers[coins > 150] = foreground + +ws = watershed(edges, markers) +seg1 = nd.label(ws == foreground)[0] + +# make segmentation using SLIC superpixels + +# make the RGB equivalent of `coins` +coins_colour = np.tile(coins[..., np.newaxis], (1, 1, 3)) +seg2 = slic(coins_colour, n_segments=30, max_iter=160, sigma=1, ratio=9, + convert2lab=False) + +# combine the two +segj = join_segmentations(seg1, seg2) + +### Display the result ### + +# make a random colormap for a set number of values +def random_cmap(im): + np.random.seed(9) + cmap_array = np.concatenate( + (np.zeros((1, 3)), np.random.rand(np.ceil(im.max()), 3))) + return mpl.colors.ListedColormap(cmap_array) + +# show the segmentations +fig, axes = plt.subplots(ncols=4, figsize=(9, 2.5)) +axes[0].imshow(coins, cmap=plt.cm.gray, interpolation='nearest') +axes[0].set_title('Image') +axes[1].imshow(seg1, cmap=random_cmap(seg1), interpolation='nearest') +axes[1].set_title('Sobel+Watershed') +axes[2].imshow(seg2, cmap=random_cmap(seg2), interpolation='nearest') +axes[2].set_title('SLIC superpixels') +axes[3].imshow(segj, cmap=random_cmap(segj), interpolation='nearest') +axes[3].set_title('Join') + +for ax in axes: + ax.axis('off') +plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) +plt.show() diff --git a/doc/examples/plot_lena_tv_denoise.py b/doc/examples/plot_lena_tv_denoise.py deleted file mode 100644 index bce07845..00000000 --- a/doc/examples/plot_lena_tv_denoise.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -==================================================== -Denoising the picture of Lena using total variation -==================================================== - -In this example, we denoise a noisy version of the picture of Lena -using the total variation denoising filter. The result of this filter -is an image that has a minimal total variation norm, while being as -close to the initial image as possible. The total variation is the L1 -norm of the gradient of the image, and minimizing the total variation -typically produces "posterized" images with flat domains separated by -sharp edges. - -It is possible to change the degree of posterization by controlling -the tradeoff between denoising and faithfulness to the original image. - -""" - -import numpy as np -import matplotlib.pyplot as plt - -from skimage import data, color, img_as_ubyte -from skimage.filter import tv_denoise - -l = img_as_ubyte(color.rgb2gray(data.lena())) -l = l[230:290, 220:320] - -noisy = l + 0.4 * l.std() * np.random.random(l.shape) - -tv_denoised = tv_denoise(noisy, weight=10) - -plt.figure(figsize=(8, 2)) - -plt.subplot(131) -plt.imshow(noisy, cmap=plt.cm.gray, vmin=40, vmax=220) -plt.axis('off') -plt.title('noisy', fontsize=20) -plt.subplot(132) -plt.imshow(tv_denoised, cmap=plt.cm.gray, vmin=40, vmax=220) -plt.axis('off') -plt.title('TV denoising', fontsize=20) - -tv_denoised = tv_denoise(noisy, weight=50) -plt.subplot(133) -plt.imshow(tv_denoised, cmap=plt.cm.gray, vmin=40, vmax=220) -plt.axis('off') -plt.title('(more) TV denoising', fontsize=20) - -plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, bottom=0, left=0, - right=1) -plt.show() diff --git a/doc/examples/plot_local_equalize.py b/doc/examples/plot_local_equalize.py new file mode 100644 index 00000000..33b2c2e4 --- /dev/null +++ b/doc/examples/plot_local_equalize.py @@ -0,0 +1,84 @@ +""" +=============================== +Local Histogram Equalization +=============================== + +This examples enhances an image with low contrast, using a method called +*local histogram equalization*, which "spreads out the most frequent intensity +values" in an image . +The equalized image [1]_ has a roughly linear cumulative distribution function for each pixel neighborhood. +The local version [2]_ of the histogram equalization emphasized every local graylevel variations. + +.. [1] http://en.wikipedia.org/wiki/Histogram_equalization +.. [2] http://en.wikipedia.org/wiki/Adaptive_histogram_equalization + +""" + +from skimage import data +from skimage.util.dtype import dtype_range +from skimage import exposure +from skimage.morphology import disk + +import matplotlib.pyplot as plt + +import numpy as np +from skimage.filter import rank + + +def plot_img_and_hist(img, axes, bins=256): + """Plot an image along with its histogram and cumulative histogram. + + """ + ax_img, ax_hist = axes + ax_cdf = ax_hist.twinx() + + # Display image + ax_img.imshow(img, cmap=plt.cm.gray) + ax_img.set_axis_off() + + # Display histogram + ax_hist.hist(img.ravel(), bins=bins) + ax_hist.ticklabel_format(axis='y', style='scientific', scilimits=(0, 0)) + ax_hist.set_xlabel('Pixel intensity') + + xmin, xmax = dtype_range[img.dtype.type] + ax_hist.set_xlim(xmin, xmax) + + # Display cumulative distribution + img_cdf, bins = exposure.cumulative_distribution(img, bins) + ax_cdf.plot(bins, img_cdf, 'r') + + return ax_img, ax_hist, ax_cdf + + +# Load an example image +img = data.moon() + +# Contrast stretching +p2 = np.percentile(img, 2) +p98 = np.percentile(img, 98) +img_rescale = exposure.equalize_hist(img) + +# Equalization +selem = disk(30) +img_eq = rank.equalize(img, selem=selem) + + +# Display results +f, axes = plt.subplots(2, 3, figsize=(8, 4)) + +ax_img, ax_hist, ax_cdf = plot_img_and_hist(img, axes[:, 0]) +ax_img.set_title('Low contrast image') +ax_hist.set_ylabel('Number of pixels') + +ax_img, ax_hist, ax_cdf = plot_img_and_hist(img_rescale, axes[:, 1]) +ax_img.set_title('Global equalise') + +ax_img, ax_hist, ax_cdf = plot_img_and_hist(img_eq, axes[:, 2]) +ax_img.set_title('Local equalize') +ax_cdf.set_ylabel('Fraction of total intensity') + + +# prevent overlap of y-axis labels +plt.subplots_adjust(wspace=0.4) +plt.show() diff --git a/doc/examples/plot_local_otsu.py b/doc/examples/plot_local_otsu.py new file mode 100644 index 00000000..968ce6e1 --- /dev/null +++ b/doc/examples/plot_local_otsu.py @@ -0,0 +1,49 @@ +""" +===================== +Local Otsu Threshold +===================== +This example shows how Otsu's threshold [1]_ method can be applied locally. +For each pixel, an "optimal" threshold is determined by maximizing the variance between two classes of pixels +of the local neighborhood defined by a structuring element. + +The example compares the local threshold with the global threshold. + +.. note: local threshold is much slower than global one. + +.. [1] http://en.wikipedia.org/wiki/Otsu's_method + +""" +import matplotlib.pyplot as plt + +from skimage import data +from skimage.morphology.selem import disk +import skimage.filter.rank as rank +from skimage.filter import threshold_otsu + + +p8 = data.page() + +radius = 10 +selem = disk(radius) + +loc_otsu = rank.otsu(p8, selem) +t_glob_otsu = threshold_otsu(p8) +glob_otsu = p8 >= t_glob_otsu + + +plt.figure() +plt.subplot(2, 2, 1) +plt.imshow(p8, cmap=plt.cm.gray) +plt.xlabel('original') +plt.colorbar() +plt.subplot(2, 2, 2) +plt.imshow(loc_otsu, cmap=plt.cm.gray) +plt.xlabel('local Otsu ($radius=%d$)' % radius) +plt.colorbar() +plt.subplot(2, 2, 3) +plt.imshow(p8 >= loc_otsu, cmap=plt.cm.gray) +plt.xlabel('original>=local Otsu' % t_glob_otsu) +plt.subplot(2, 2, 4) +plt.imshow(glob_otsu, cmap=plt.cm.gray) +plt.xlabel('global Otsu ($t=%d$)' % t_glob_otsu) +plt.show() diff --git a/doc/examples/plot_marked_watershed.py b/doc/examples/plot_marked_watershed.py new file mode 100644 index 00000000..e97280c7 --- /dev/null +++ b/doc/examples/plot_marked_watershed.py @@ -0,0 +1,54 @@ +""" +================================ +Markers for watershed transform +================================ + +The watershed is a classical algorithm used for **segmentation**, that +is, for separating different objects in an image. + +Here a marker image is build from the region of low gradient inside the image. + +See Wikipedia_ for more details on the algorithm. + +.. _Wikipedia: http://en.wikipedia.org/wiki/Watershed_(image_processing) + +""" + +from scipy import ndimage +import matplotlib.pyplot as plt +from skimage.morphology import watershed, disk +from skimage import data + +# original data +from skimage.filter import rank + +image = data.camera() + +# denoise image +denoised = rank.median(image, disk(2)) + +# find continuous region (low gradient) --> markers +markers = rank.gradient(denoised, disk(5)) < 10 +markers = ndimage.label(markers)[0] + +#local gradient +gradient = rank.gradient(denoised, disk(2)) + +# process the watershed +labels = watershed(gradient, markers) + +# display results +fig, axes = plt.subplots(ncols=4, figsize=(8, 2.7)) +ax0, ax1, ax2, ax3 = axes + +ax0.imshow(image, cmap=plt.cm.gray, interpolation='nearest') +ax1.imshow(gradient, cmap=plt.cm.spectral, interpolation='nearest') +ax2.imshow(markers, cmap=plt.cm.spectral, interpolation='nearest') +ax3.imshow(image, cmap=plt.cm.gray, interpolation='nearest') +ax3.imshow(labels, cmap=plt.cm.spectral, interpolation='nearest', alpha=.7) + +for ax in axes: + ax.axis('off') + +plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) +plt.show() diff --git a/doc/examples/plot_random_walker_segmentation.py b/doc/examples/plot_random_walker_segmentation.py index 6d194293..7d9f4fa8 100644 --- a/doc/examples/plot_random_walker_segmentation.py +++ b/doc/examples/plot_random_walker_segmentation.py @@ -19,7 +19,6 @@ values, and use the random walker for the segmentation. .. [1] *Random walks for image segmentation*, Leo Grady, IEEE Trans. Pattern Anal. Mach. Intell. 2006 Nov; 28(11):1768-83 """ -print __doc__ import numpy as np from scipy import ndimage diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index 99bfefcc..418a0903 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -63,8 +63,8 @@ import matplotlib.pyplot as plt import numpy as np from skimage.data import lena -from skimage.segmentation import felzenszwalb, \ - visualize_boundaries, slic, quickshift +from skimage.segmentation import felzenszwalb, slic, quickshift +from skimage.segmentation import mark_boundaries from skimage.util import img_as_float img = img_as_float(lena()[::2, ::2]) @@ -80,11 +80,11 @@ fig, ax = plt.subplots(1, 3) fig.set_size_inches(8, 3, forward=True) plt.subplots_adjust(0.05, 0.05, 0.95, 0.95, 0.05, 0.05) -ax[0].imshow(visualize_boundaries(img, segments_fz)) +ax[0].imshow(mark_boundaries(img, segments_fz)) ax[0].set_title("Felzenszwalbs's method") -ax[1].imshow(visualize_boundaries(img, segments_slic)) +ax[1].imshow(mark_boundaries(img, segments_slic)) ax[1].set_title("SLIC") -ax[2].imshow(visualize_boundaries(img, segments_quick)) +ax[2].imshow(mark_boundaries(img, segments_quick)) ax[2].set_title("Quickshift") for a in ax: a.set_xticks(()) diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 9e586209..25a48ebe 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -13,7 +13,7 @@ This example shows how to fill several different shapes: import matplotlib.pyplot as plt -from skimage.draw import line, polygon, circle, ellipse +from skimage.draw import line, polygon, circle, circle_perimeter, ellipse import numpy as np @@ -42,5 +42,9 @@ img[rr,cc,:] = (255, 255, 0) rr, cc = ellipse(300, 300, 100, 200, img.shape) img[rr,cc,2] = 255 +# circle +rr, cc = circle_perimeter(120, 400, 50) +img[rr, cc, :] = (255, 0, 255) + plt.imshow(img) -plt.show() \ No newline at end of file +plt.show() diff --git a/doc/examples/plot_watershed.py b/doc/examples/plot_watershed.py index a1cd18cf..50857b2c 100644 --- a/doc/examples/plot_watershed.py +++ b/doc/examples/plot_watershed.py @@ -26,7 +26,7 @@ See Wikipedia_ for more details on the algorithm. """ import numpy as np -from scipy import ndimage + import matplotlib.pyplot as plt from skimage.morphology import watershed, is_local_maximum diff --git a/doc/ext/plot2rst.py b/doc/ext/plot2rst.py index 65c77521..00560be4 100644 --- a/doc/ext/plot2rst.py +++ b/doc/ext/plot2rst.py @@ -69,6 +69,7 @@ import os import shutil import token import tokenize +import traceback import numpy as np import matplotlib @@ -247,7 +248,16 @@ def write_gallery(gallery_index, src_dir, rst_dir, cfg, depth=0): gallery_index.write(TOCTREE_TEMPLATE % (sub_dir + '\n '.join(ex_names))) for src_name in examples: - write_example(src_name, src_dir, rst_dir, cfg) + + try: + write_example(src_name, src_dir, rst_dir, cfg) + except Exception: + print "Exception raised while running:" + print "%s in %s" % (src_name, src_dir) + print '~' * 60 + traceback.print_exc() + print '~' * 60 + continue link_name = sub_dir.pjoin(src_name) link_name = link_name.replace(os.path.sep, '_') diff --git a/doc/release/release_0.8.txt b/doc/release/release_0.8.txt new file mode 100644 index 00000000..d146659c --- /dev/null +++ b/doc/release/release_0.8.txt @@ -0,0 +1,71 @@ +Announcement: scikits-image 0.8.0 +================================= + +We're happy to announce the 8th version of scikit-image! + +scikit-image is an image processing toolbox for SciPy that includes algorithms +for segmentation, geometric transformations, color space manipulation, +analysis, filtering, morphology, feature detection, and more. + +For more information, examples, and documentation, please visit our website: + + http://scikit-image.org + + +New Features +------------ + +- New rank filter package with many new functions and a very fast underlying + local histogram algorithm, especially for large structuring elements + `skimage.filter.rank.*` +- New function for small object removal + `skimage.morphology.remove_small_objects` +- New circular hough transformation `skimage.transform.hough_circle` +- New function to draw circle perimeter `skimage.draw.circle_perimeter` and + ellipse perimeter `skimage.draw.ellipse_perimeter` +- New dense DAISY feature descriptor `skimage.feature.daisy` +- New bilateral filter `skimage.filter.denoise_bilateral` +- New faster TV denoising filter based on split-Bregman algorithm + `skimage.filter.denoise_tv_bregman` +- New linear hough peak detection `skimage.transform.hough_peaks` +- New Scharr edge detection `skimage.filter.scharr` +- New geometric image scaling as convenience function + `skimage.transform.rescale` +- New theme for documentation and website +- Faster median filter through vectorization `skimage.filter.median_filter` +- Grayscale images supported for SLIC segmentation +- Unified peak detection with more options `skimage.feature.peak_local_max` +- `imread` can read images via URL and knows more formats `skimage.io.imread` + +Additionally, this release adds lots of bug fixes, new examples, and +performance enhancements. + + +Contributors to this release +---------------------------- + +This release was only possible due to the efforts of many contributors, both +new and old. + +- Adam Ginsburg +- Anders Boesen Lindbo Larsen +- Andreas Mueller +- Christoph Gohlke +- Christos Psaltis +- Colin Lea +- François Boulogne +- Jan Margeta +- Johannes Schönberger +- Josh Warner (Mac) +- Juan Nunez-Iglesias +- Luis Pedro Coelho +- Marianne Corvellec +- Matt McCormick +- Nicolas Pinto +- Olivier Debeir +- Paul Ivanov +- Sergey Karayev +- Stefan van der Walt +- Steven Silvester +- Thouis (Ray) Jones +- Tony S Yu diff --git a/doc/source/_static/docversions.js b/doc/source/_static/docversions.js index 0b414325..fde9437b 100644 --- a/doc/source/_static/docversions.js +++ b/doc/source/_static/docversions.js @@ -1,5 +1,5 @@ function insert_version_links() { - var labels = ['dev', '0.7.0', '0.6', '0.5', '0.4', '0.3']; + var labels = ['dev', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; for (i = 0; i < labels.length; i++){ open_list = '
  • ' diff --git a/doc/source/user_guide/data_types.txt b/doc/source/user_guide/data_types.txt index 9801c244..c3b87b2e 100644 --- a/doc/source/user_guide/data_types.txt +++ b/doc/source/user_guide/data_types.txt @@ -71,11 +71,6 @@ issued:: float64 to uint8 array([ 0, 128, 255], dtype=uint8) -Wherever possible, functions should try to handle input without explicit -conversion. For example, there is no need to force values to a specific type -for doing a convolution; a plotting function, on the other hand, needs to know -the range of the input. - Output types ============ diff --git a/setup.py b/setup.py index e4a7fc07..22971ba7 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ MAINTAINER_EMAIL = 'stefan@sun.ac.za' URL = 'http://scikit-image.org' LICENSE = 'Modified BSD' DOWNLOAD_URL = 'http://github.com/scikit-image/scikit-image' -VERSION = '0.7.2' +VERSION = '0.8.0' PYTHON_VERSION = (2, 5) DEPENDENCIES = { 'numpy': (1, 6), @@ -27,6 +27,7 @@ DEPENDENCIES = { import os import sys +import re import setuptools from numpy.distutils.core import setup try: @@ -72,8 +73,8 @@ def get_package_version(package): for version_attr in ('version', 'VERSION', '__version__'): if hasattr(package, version_attr) \ and isinstance(getattr(package, version_attr), str): - version_info = getattr(package, version_attr) - for part in version_info.split('.'): + version_info = getattr(package, version_attr, '') + for part in re.split('\D+', version_info): try: version.append(int(part)) except ValueError: @@ -137,7 +138,7 @@ if __name__ == "__main__": configuration=configuration, - packages=setuptools.find_packages(), + packages=setuptools.find_packages(exclude=['doc']), include_package_data=True, zip_safe=False, # the package can run out of an .egg file diff --git a/skimage/__init__.py b/skimage/__init__.py index 1ac121b3..daac238c 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -52,6 +52,8 @@ img_as_ubyte """ import os.path as _osp +import imp as _imp +import functools as _functools pkg_dir = _osp.abspath(_osp.dirname(__file__)) data_dir = _osp.join(pkg_dir, 'data') @@ -62,30 +64,28 @@ except ImportError: __version__ = "unbuilt-dev" -def _setup_test(verbose=False): - import functools +try: + _imp.find_module('nose') +except ImportError: + def _test(verbose=False): + """This would invoke the skimage test suite, but nose couldn't be + imported so the test suite can not run. + """ + raise ImportError("Could not load nose. Unit tests not available.") +else: + def _test(verbose=False): + """Invoke the skimage test suite.""" + import nose + args = ['', pkg_dir, '--exe'] + if verbose: + args.extend(['-v', '-s']) + nose.run('skimage', argv=args) - args = ['', pkg_dir, '--exe'] - if verbose: - args.extend(['-v', '-s']) - - try: - import nose as _nose - except ImportError: - def broken_test_func(): - """This would invoke the skimage test suite, but nose couldn't be - imported so the test suite can not run. - """ - raise ImportError("Could not load nose. Unit tests not available.") - return broken_test_func - else: - f = functools.partial(_nose.run, 'skimage', argv=args) - f.__doc__ = 'Invoke the skimage test suite.' - return f - - -test = _setup_test() -test_verbose = _setup_test(verbose=True) +# do not use `test` as function name as this leads to a recursion problem with +# the nose test suite +test = _test +test_verbose = _functools.partial(test, verbose=True) +test_verbose.__doc__ = test.__doc__ def get_log(name=None): diff --git a/skimage/_build.py b/skimage/_build.py index 8f255f29..771b04b9 100644 --- a/skimage/_build.py +++ b/skimage/_build.py @@ -4,6 +4,13 @@ import hashlib import subprocess +# WindowsError is not defined on unix systems +try: + WindowsError +except NameError: + WindowsError = None + + def cython(pyx_files, working_path=''): """Use Cython to convert the given files to C. diff --git a/skimage/_shared/geometry.pxd b/skimage/_shared/geometry.pxd index afdc6b5b..3379318c 100644 --- a/skimage/_shared/geometry.pxd +++ b/skimage/_shared/geometry.pxd @@ -1,6 +1,6 @@ -cdef unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, +cdef unsigned char point_in_polygon(Py_ssize_t nr_verts, double *xp, double *yp, double x, double y) -cdef void points_in_polygon(int nr_verts, double *xp, double *yp, - int nr_points, double *x, double *y, +cdef void points_in_polygon(Py_ssize_t nr_verts, double *xp, double *yp, + Py_ssize_t nr_points, double *x, double *y, unsigned char *result) diff --git a/skimage/_shared/geometry.pyx b/skimage/_shared/geometry.pyx index 3f4850b0..beb07e14 100644 --- a/skimage/_shared/geometry.pyx +++ b/skimage/_shared/geometry.pyx @@ -4,8 +4,8 @@ #cython: wraparound=False -cdef inline unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, - double x, double y): +cdef inline unsigned char point_in_polygon(Py_ssize_t nr_verts, double *xp, + double *yp, double x, double y): """Test whether point lies inside a polygon. Parameters @@ -17,9 +17,9 @@ cdef inline unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, x, y : double Coordinates of point. """ - cdef int i + cdef Py_ssize_t i cdef unsigned char c = 0 - cdef int j = nr_verts - 1 + cdef Py_ssize_t j = nr_verts - 1 for i in range(nr_verts): if ( (((yp[i] <= y) and (y < yp[j])) or @@ -31,8 +31,8 @@ cdef inline unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, return c -cdef void points_in_polygon(int nr_verts, double *xp, double *yp, - int nr_points, double *x, double *y, +cdef void points_in_polygon(Py_ssize_t nr_verts, double *xp, double *yp, + Py_ssize_t nr_points, double *x, double *y, unsigned char *result): """Test whether points lie inside a polygon. @@ -49,6 +49,6 @@ cdef void points_in_polygon(int nr_verts, double *xp, double *yp, result : unsigned char array Test results for each point. """ - cdef int n + cdef Py_ssize_t n for n in range(nr_points): result[n] = point_in_polygon(nr_verts, xp, yp, x[n], y[n]) diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd index f43ff25e..c5f32b6a 100644 --- a/skimage/_shared/interpolation.pxd +++ b/skimage/_shared/interpolation.pxd @@ -1,24 +1,27 @@ -cdef double nearest_neighbour_interpolation(double* image, int rows, - int cols, double r, +cdef double nearest_neighbour_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, char mode, double cval) -cdef double bilinear_interpolation(double* image, int rows, int cols, +cdef double bilinear_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, double r, double c, char mode, double cval) cdef double quadratic_interpolation(double x, double[3] f) -cdef double biquadratic_interpolation(double* image, int rows, int cols, +cdef double biquadratic_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, double r, double c, char mode, double cval) cdef double cubic_interpolation(double x, double[4] f) -cdef double bicubic_interpolation(double* image, int rows, int cols, +cdef double bicubic_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, double r, double c, char mode, double cval) -cdef double get_pixel(double* image, int rows, int cols, int r, int c, - char mode, double cval) +cdef double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, Py_ssize_t r, + Py_ssize_t c, char mode, double cval) -cdef int coord_map(int dim, int coord, char mode) +cdef double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, Py_ssize_t dims, + Py_ssize_t r, Py_ssize_t c, Py_ssize_t d, char mode, double cval) + +cdef Py_ssize_t coord_map(Py_ssize_t dim, Py_ssize_t coord, char mode) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index e0fd0067..a8b96014 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -5,12 +5,12 @@ from libc.math cimport ceil, floor -cdef inline int round(double r): - return ((r + 0.5) if (r > 0.0) else (r - 0.5)) +cdef inline Py_ssize_t round(double r): + return ((r + 0.5) if (r > 0.0) else (r - 0.5)) -cdef inline double nearest_neighbour_interpolation(double* image, int rows, - int cols, double r, +cdef inline double nearest_neighbour_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, char mode, double cval): """Nearest neighbour interpolation at a given position in the image. @@ -35,13 +35,12 @@ cdef inline double nearest_neighbour_interpolation(double* image, int rows, """ - return get_pixel(image, rows, cols, round(r), round(c), - mode, cval) + return get_pixel2d(image, rows, cols, round(r), round(c), mode, cval) -cdef inline double bilinear_interpolation(double* image, int rows, int cols, - double r, double c, char mode, - double cval): +cdef inline double bilinear_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, + char mode, double cval): """Bilinear interpolation at a given position in the image. Parameters @@ -64,18 +63,18 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, """ cdef double dr, dc - cdef int minr, minc, maxr, maxc + cdef Py_ssize_t minr, minc, maxr, maxc - minr = floor(r) - minc = floor(c) - maxr = ceil(r) - maxc = ceil(c) + minr = floor(r) + minc = floor(c) + maxr = ceil(r) + maxc = ceil(c) dr = r - minr dc = c - minc - top = (1 - dc) * get_pixel(image, rows, cols, minr, minc, mode, cval) \ - + dc * get_pixel(image, rows, cols, minr, maxc, mode, cval) - bottom = (1 - dc) * get_pixel(image, rows, cols, maxr, minc, mode, cval) \ - + dc * get_pixel(image, rows, cols, maxr, maxc, mode, cval) + top = (1 - dc) * get_pixel2d(image, rows, cols, minr, minc, mode, cval) \ + + dc * get_pixel2d(image, rows, cols, minr, maxc, mode, cval) + bottom = (1 - dc) * get_pixel2d(image, rows, cols, maxr, minc, mode, cval) \ + + dc * get_pixel2d(image, rows, cols, maxr, maxc, mode, cval) return (1 - dr) * top + dr * bottom @@ -98,9 +97,9 @@ cdef inline double quadratic_interpolation(double x, double[3] f): return f[1] - 0.25 * (f[0] - f[2]) * x -cdef inline double biquadratic_interpolation(double* image, int rows, int cols, - double r, double c, char mode, - double cval): +cdef inline double biquadratic_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, + char mode, double cval): """Biquadratic interpolation at a given position in the image. Parameters @@ -123,8 +122,8 @@ cdef inline double biquadratic_interpolation(double* image, int rows, int cols, """ - cdef int r0 = round(r) - cdef int c0 = round(c) + cdef Py_ssize_t r0 = round(r) + cdef Py_ssize_t c0 = round(c) if r < 0: r0 -= 1 if c < 0: @@ -139,12 +138,12 @@ cdef inline double biquadratic_interpolation(double* image, int rows, int cols, cdef double fc[3], fr[3] - cdef int pr, pc + cdef Py_ssize_t pr, pc # row-wise cubic interpolation for pr in range(r0, r0 + 3): for pc in range(c0, c0 + 3): - fc[pc - c0] = get_pixel(image, rows, cols, pr, pc, mode, cval) + fc[pc - c0] = get_pixel2d(image, rows, cols, pr, pc, mode, cval) fr[pr - r0] = quadratic_interpolation(xc, fc) # cubic interpolation for interpolated values of each row @@ -174,9 +173,9 @@ cdef inline double cubic_interpolation(double x, double[4] f): (3.0 * (f[1] - f[2]) + f[3] - f[0]))) -cdef inline double bicubic_interpolation(double* image, int rows, int cols, - double r, double c, char mode, - double cval): +cdef inline double bicubic_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, + char mode, double cval): """Bicubic interpolation at a given position in the image. Parameters @@ -199,8 +198,8 @@ cdef inline double bicubic_interpolation(double* image, int rows, int cols, """ - cdef int r0 = r - 1 - cdef int c0 = c - 1 + cdef Py_ssize_t r0 = r - 1 + cdef Py_ssize_t c0 = c - 1 if r < 0: r0 -= 1 if c < 0: @@ -211,20 +210,20 @@ cdef inline double bicubic_interpolation(double* image, int rows, int cols, cdef double fc[4], fr[4] - cdef int pr, pc + cdef Py_ssize_t pr, pc # row-wise cubic interpolation for pr in range(r0, r0 + 4): for pc in range(c0, c0 + 4): - fc[pc - c0] = get_pixel(image, rows, cols, pr, pc, mode, cval) + fc[pc - c0] = get_pixel2d(image, rows, cols, pr, pc, mode, cval) fr[pr - r0] = cubic_interpolation(xc, fc) # cubic interpolation for interpolated values of each row return cubic_interpolation(xr, fr) -cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, - char mode, double cval): +cdef inline double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t c, char mode, double cval): """Get a pixel from the image, taking wrapping mode into consideration. Parameters @@ -255,7 +254,42 @@ cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, return image[coord_map(rows, r, mode) * cols + coord_map(cols, c, mode)] -cdef inline int coord_map(int dim, int coord, char mode): +cdef inline double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t dims, Py_ssize_t r, Py_ssize_t c, Py_ssize_t d, + char mode, double cval): + """Get a pixel from the image, taking wrapping mode into consideration. + + Parameters + ---------- + image : double array + Input image. + rows, cols, dims : int + Shape of image. + r, c, d : int + Position at which to get the pixel. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Pixel value at given position. + + """ + if mode == 'C': + if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): + return cval + else: + return image[r * cols * dims + c * dims + d] + else: + return image[coord_map(rows, r, mode) * cols * dims + + coord_map(cols, c, mode) * dims + + d] + + +cdef inline Py_ssize_t coord_map(Py_ssize_t dim, Py_ssize_t coord, char mode): """ Wrap a coordinate, according to a given mode. @@ -274,20 +308,20 @@ cdef inline int coord_map(int dim, int coord, char mode): if mode == 'R': # reflect if coord < 0: # How many times times does the coordinate wrap? - if (-coord / dim) % 2 != 0: - return dim - (-coord % dim) + if (-coord / dim) % 2 != 0: + return dim - (-coord % dim) else: - return (-coord % dim) + return (-coord % dim) elif coord > dim: - if (coord / dim) % 2 != 0: - return (dim - (coord % dim)) + if (coord / dim) % 2 != 0: + return (dim - (coord % dim)) else: - return (coord % dim) + return (coord % dim) elif mode == 'W': # wrap if coord < 0: - return (dim - (-coord % dim)) + return (dim - (-coord % dim)) elif coord > dim: - return (coord % dim) + return (coord % dim) elif mode == 'N': # nearest if coord < 0: return 0 diff --git a/skimage/_shared/transform.pxd b/skimage/_shared/transform.pxd index 0edc22a4..ccb16ff3 100644 --- a/skimage/_shared/transform.pxd +++ b/skimage/_shared/transform.pxd @@ -2,4 +2,4 @@ cimport numpy as cnp cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, - int r0, int c0, int r1, int c1) + 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 ba0efc71..8ce2ab67 100644 --- a/skimage/_shared/transform.pyx +++ b/skimage/_shared/transform.pyx @@ -6,7 +6,7 @@ cimport numpy as cnp cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, - int r0, int c0, int r1, int c1): + 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 new file mode 100644 index 00000000..4075ddb4 --- /dev/null +++ b/skimage/_shared/utils.py @@ -0,0 +1,43 @@ +import warnings +import functools + + +__all__ = ['deprecated'] + + +class deprecated(object): + """Decorator to mark deprecated functions with warning. + + Adapted from . + + Parameters + ---------- + alt_func : str + If given, tell user what function to use instead. + behavior : {'warn', 'raise'} + Behavior during call to deprecated function: 'warn' = warn user that + function is deprecated; 'raise' = raise error. + """ + + def __init__(self, alt_func=None, behavior='warn'): + self.alt_func = alt_func + self.behavior = behavior + + def __call__(self, func): + + msg = "Call to deprecated function `%s`." % func.__name__ + if self.alt_func is not None: + msg = msg + " Use `%s` instead." % self.alt_func + + @functools.wraps(func) + def wrapped(*args, **kwargs): + if self.behavior == 'warn': + warnings.warn_explicit(msg, + category=DeprecationWarning, + filename=func.func_code.co_filename, + lineno=func.func_code.co_firstlineno + 1) + elif self.behavior == 'raise': + raise DeprecationWarning(msg) + return func(*args, **kwargs) + + return wrapped diff --git a/skimage/_shared/vectorized_ops.h b/skimage/_shared/vectorized_ops.h new file mode 100644 index 00000000..ab9647ea --- /dev/null +++ b/skimage/_shared/vectorized_ops.h @@ -0,0 +1,110 @@ +/* Intrinsic declarations */ +#if defined(__SSE2__) +#include +#elif defined(__MMX__) +#include +#elif defined(__ALTIVEC__) +#include +#endif + +/* Compiler peculiarities */ +#if defined(__GNUC__) +#include +#elif defined(_MSC_VER) +#define inline __inline +typedef unsigned __int16 uint16_t; +#endif + +/** + * Add 16 unsigned 16-bit integers using SSE2, MMX or Altivec, if + * available. + */ +#if defined(__SSE2__) +static inline void add16(uint16_t *dest, uint16_t *src) +{ + __m128i *d, *s; + d = (__m128i *) dest; + s = (__m128i *) src; + *d = _mm_add_epi16(*d, *s); + d++; s++; + *d = _mm_add_epi16(*d, *s); +} +#elif defined(__MMX__) +static inline void add16(uint16_t *dest, uint16_t *src) +{ + __m64 *d, *s; + d = (__m64 *) dest; + s = (__m64 *) src; + *d = _mm_add_pi16(*d, *s); + d++; s++; + *d = _mm_add_pi16(*d, *s); + d++; s++; + *d = _mm_add_pi16(*d, *s); + d++; s++; + *d = _mm_add_pi16(*d, *s); +} +#elif defined(__ALTIVEC__) +static inline void add16(uint16_t *dest, uint16_t *src) +{ + vector unsigned short *d, *s; + d = (vector unsigned short *) dest; + s = (vector unsigned short *) src; + *d = vec_add(*d, *s); + d++; s++; + *d = vec_add(*d, *s); +} +#else +static inline void add16(uint16_t *dest, uint16_t *src) +{ + int i; + + for (i = 0; i < 16; i++) dest[i] += src[i]; +} +#endif + +/** + * Subtract 16 unsigned 16-bit integers using SSE2, MMX or Altivec, if + * available. + */ +#if defined(__SSE2__) +static inline void sub16(uint16_t *dest, uint16_t *src) +{ + __m128i *d, *s; + d = (__m128i *) dest; + s = (__m128i *) src; + *d = _mm_sub_epi16(*d, *s); + d++; s++; + *d = _mm_sub_epi16(*d, *s); +} +#elif defined(__MMX__) +static inline void sub16(uint16_t *dest, uint16_t *src) +{ + __m64 *d, *s; + d = (__m64 *) dest; + s = (__m64 *) src; + *d = _mm_sub_pi16(*d, *s); + d++; s++; + *d = _mm_sub_pi16(*d, *s); + d++; s++; + *d = _mm_sub_pi16(*d, *s); + d++; s++; + *d = _mm_sub_pi16(*d, *s); +} +#elif defined(__ALTIVEC__) +static inline void sub16(uint16_t *dest, uint16_t *src) +{ + vector unsigned short *d, *s; + d = (vector unsigned short *) dest; + s = (vector unsigned short *) src; + *d = vec_sub(*d, *s); + d++; s++; + *d = vec_sub(*d, *s); +} +#else +static inline void sub16(uint16_t *dest, uint16_t *src) +{ + int i; + + for (i = 0; i < 16; i++) dest[i] -= src[i]; +} +#endif diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index ec5551d6..4186682d 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -45,7 +45,7 @@ from __future__ import division __all__ = ['convert_colorspace', 'rgb2hsv', 'hsv2rgb', 'rgb2xyz', 'xyz2rgb', 'rgb2rgbcie', 'rgbcie2rgb', 'rgb2grey', 'rgb2gray', 'gray2rgb', - 'xyz2lab', 'lab2xyz', 'lab2rgb', 'rgb2lab' + 'xyz2lab', 'lab2xyz', 'lab2rgb', 'rgb2lab', 'is_rgb', 'is_gray' ] __docformat__ = "restructuredtext en" @@ -55,6 +55,30 @@ from scipy import linalg from ..util import dtype +def is_rgb(image): + """Test whether the image is RGB or RGBA. + + Parameters + ---------- + image : ndarray + Input image. + + """ + return (image.ndim == 3 and image.shape[2] in (3, 4)) + + +def is_gray(image): + """Test whether the image is gray (i.e. has only one color band). + + Parameters + ---------- + image : ndarray + Input image. + + """ + return image.ndim == 2 + + def convert_colorspace(arr, fromspace, tospace): """Convert an image array to a new color space. @@ -263,8 +287,8 @@ sb_primaries = np.array([1. / 155, 1. / 190, 1. / 225]) * 1e5 # From sRGB specification xyz_from_rgb = np.array([[0.412453, 0.357580, 0.180423], - [0.212671, 0.715160, 0.072169], - [0.019334, 0.119193, 0.950227]]) + [0.212671, 0.715160, 0.072169], + [0.019334, 0.119193, 0.950227]]) rgb_from_xyz = linalg.inv(xyz_from_rgb) @@ -281,7 +305,7 @@ rgbcie_from_rgb = np.dot(rgbcie_from_xyz, xyz_from_rgb) rgb_from_rgbcie = np.dot(rgb_from_xyz, xyz_from_rgbcie) -grey_from_rgb = np.array([[0.2125, 0.7154, 0.0721], +gray_from_rgb = np.array([[0.2125, 0.7154, 0.0721], [0, 0, 0], [0, 0, 0]]) @@ -354,7 +378,13 @@ def xyz2rgb(xyz): >>> lena_xyz = rgb2xyz(lena) >>> lena_rgb = xyz2rgb(lena_xyz) """ - return _convert(rgb_from_xyz, xyz) + # Follow the algorithm from http://www.easyrgb.com/index.php + # except we don't multiply/divide by 100 in the conversion + arr = _convert(rgb_from_xyz, xyz) + mask = arr > 0.0031308 + arr[mask] = 1.055 * np.power(arr[mask], 1 / 2.4) - 0.055 + arr[~mask] *= 12.92 + return arr def rgb2xyz(rgb): @@ -390,7 +420,13 @@ def rgb2xyz(rgb): >>> lena = data.lena() >>> lena_xyz = rgb2xyz(lena) """ - return _convert(xyz_from_rgb, rgb) + # Follow the algorithm from http://www.easyrgb.com/index.php + # except we don't multiply/divide by 100 in the conversion + arr = _prepare_colorarray(rgb).copy() + mask = arr > 0.04045 + arr[mask] = np.power((arr[mask] + 0.055) / 1.055, 2.4) + arr[~mask] /= 12.92 + return _convert(xyz_from_rgb, arr) def rgb2rgbcie(rgb): @@ -458,7 +494,7 @@ def rgbcie2rgb(rgbcie): return _convert(rgb_from_rgbcie, rgbcie) -def rgb2grey(rgb): +def rgb2gray(rgb): """Compute luminance of an RGB image. Parameters @@ -475,7 +511,7 @@ def rgb2grey(rgb): Raises ------ ValueError - If `rgb2grey` is not a 3-D array of shape (.., .., 3) or + If `rgb2gray` is not a 3-D array of shape (.., .., 3) or (.., .., 4). References @@ -493,21 +529,21 @@ def rgb2grey(rgb): Examples -------- - >>> from skimage.color import rgb2grey + >>> from skimage.color import rgb2gray >>> from skimage import data >>> lena = data.lena() - >>> lena_grey = rgb2grey(lena) + >>> lena_gray = rgb2gray(lena) """ if rgb.ndim == 2: return rgb - return _convert(grey_from_rgb, rgb[:, :, :3])[..., 0] + return _convert(gray_from_rgb, rgb[:, :, :3])[..., 0] -rgb2gray = rgb2grey +rgb2grey = rgb2gray def gray2rgb(image): - """Create an RGB representation of a grey-level image. + """Create an RGB representation of a gray-level image. Parameters ---------- @@ -525,11 +561,12 @@ def gray2rgb(image): If the input is not 2-dimensional. """ - if image.ndim != 2: - raise ValueError('Gray-level image should be two-dimensional.') - - M, N = image.shape - return np.dstack((image, image, image)) + if is_rgb(image): + return image + elif is_gray(image): + return np.dstack((image, image, image)) + else: + raise ValueError("Input image expected to be RGB, RGBA or gray.") def xyz2lab(xyz): diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 316d801a..962fa9fc 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -25,10 +25,11 @@ from skimage.color import ( convert_colorspace, rgb2grey, gray2rgb, xyz2lab, lab2xyz, - lab2rgb, rgb2lab + lab2rgb, rgb2lab, + is_rgb, is_gray ) -from skimage import data_dir +from skimage import data_dir, data import colorsys @@ -112,10 +113,14 @@ class TestColorconv(TestCase): # XYZ to RGB def test_xyz2rgb_conversion(self): - # only roundtrip test, we checked rgb2xyz above already assert_almost_equal(xyz2rgb(rgb2xyz(self.colbars_array)), self.colbars_array) + # RGB<->XYZ roundtrip on another image + def test_xyz_rgb_roundtrip(self): + img_rgb = img_as_float(self.img_rgb) + assert_array_almost_equal(xyz2rgb(rgb2xyz(img_rgb)), img_rgb) + # RGB to RGB CIE def test_rgb2rgbcie_conversion(self): gt = np.array([[[ 0.1488856 , 0.18288098, 0.19277574], @@ -174,11 +179,29 @@ class TestColorconv(TestCase): assert_array_almost_equal(lab2xyz(self.lab_array), self.xyz_array, decimal=3) + def test_rgb2lab_brucelindbloom(self): + """ + Test the RGB->Lab conversion by comparing to the calculator on the + authoritative Bruce Lindbloom + [website](http://brucelindbloom.com/index.html?ColorCalculator.html). + """ + # Obtained with D65 white point, sRGB model and gamma + gt_for_colbars = np.array([ + [100,0,0], + [97.1393, -21.5537, 94.4780], + [91.1132, -48.0875, -14.1312], + [87.7347, -86.1827, 83.1793], + [60.3242, 98.2343, -60.8249], + [53.2408, 80.0925, 67.2032], + [32.2970, 79.1875, -107.8602], + [0,0,0]]).T + gt_array = np.swapaxes(gt_for_colbars.reshape(3, 4, 2), 0, 2) + assert_array_almost_equal(rgb2lab(self.colbars_array), gt_array, decimal=2) + def test_lab_rgb_roundtrip(self): img_rgb = img_as_float(self.img_rgb) assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb) - def test_gray2rgb(): x = np.array([0, 0.5, 1]) assert_raises(ValueError, gray2rgb, x) @@ -196,5 +219,23 @@ def test_gray2rgb(): assert_equal(z[..., 0], x) assert_equal(z[0, 1, :], [128, 128, 128]) + +def test_gray2rgb_rgb(): + x = np.random.random((5, 5, 4)) + y = gray2rgb(x) + assert_equal(x, y) + + +def test_is_rgb(): + color = data.lena() + gray = data.camera() + + assert is_rgb(color) + assert not is_gray(color) + + assert is_gray(gray) + assert not is_gray(color) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index 5e51d353..d2fba7db 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -114,3 +114,16 @@ def page(): """ return load("page.png") + + +def clock(): + """Motion blurred clock. + + This photograph of a wall clock was taken while moving the camera in an + aproximately horizontal direction. It may be used to illustrate + inverse filters and deconvolution. + + Released into the public domain by the photographer (Stefan van der Walt). + + """ + return load("clock_motion.png") diff --git a/skimage/data/clock_motion.png b/skimage/data/clock_motion.png new file mode 100644 index 00000000..3a81bd74 Binary files /dev/null and b/skimage/data/clock_motion.png differ diff --git a/skimage/data/tests/test_data.py b/skimage/data/tests/test_data.py index b5f6bcaa..f660c69c 100644 --- a/skimage/data/tests/test_data.py +++ b/skimage/data/tests/test_data.py @@ -34,6 +34,11 @@ def test_page(): data.page() +def test_page(): + """ Test that "clock" image can be loaded. """ + data.clock() + + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/draw/__init__.py b/skimage/draw/__init__.py index 4eac9b63..4aa6bf35 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -1,2 +1,5 @@ -from ._draw import line, polygon, ellipse, circle +from ._draw import line, polygon, ellipse, ellipse_perimeter, \ + circle, circle_perimeter, set_color + + bresenham = line diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index ca0c502a..8a2572c4 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -1,14 +1,16 @@ -import numpy as np +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import math +import numpy as np + +cimport numpy as cnp from libc.math cimport sqrt -cimport numpy as np -cimport cython from skimage._shared.geometry cimport point_in_polygon -@cython.boundscheck(False) -@cython.wraparound(False) -def line(int y, int x, int y2, int x2): +def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): """Generate line pixel coordinates. Parameters @@ -26,26 +28,31 @@ def line(int y, int x, int y2, int x2): ``img[rr, cc] = 1``. """ - cdef np.ndarray[np.int32_t, ndim=1, mode="c"] rr, cc - cdef int steep = 0 - cdef int dx = abs(x2 - x) - cdef int dy = abs(y2 - y) - cdef int sx, sy, d, i + cdef cnp.ndarray[cnp.intp_t, ndim=1, mode="c"] rr, cc - if (x2 - x) > 0: sx = 1 - else: sx = -1 - if (y2 - y) > 0: sy = 1 - else: sy = -1 + cdef char steep = 0 + cdef Py_ssize_t dx = abs(x2 - x) + cdef Py_ssize_t dy = abs(y2 - y) + cdef Py_ssize_t sx, sy, d, i + + if (x2 - x) > 0: + sx = 1 + else: + sx = -1 + if (y2 - y) > 0: + sy = 1 + else: + sy = -1 if dy > dx: steep = 1 - x,y = y,x - dx,dy = dy,dx - sx,sy = sy,sx + x, y = y, x + dx, dy = dy, dx + sx, sy = sy, sx d = (2 * dy) - dx - rr = np.zeros(int(dx) + 1, dtype=np.int32) - cc = np.zeros(int(dx) + 1, dtype=np.int32) + rr = np.zeros(int(dx) + 1, dtype=np.intp) + cc = np.zeros(int(dx) + 1, dtype=np.intp) for i in range(dx): if steep: @@ -66,9 +73,6 @@ def line(int y, int x, int y2, int x2): return rr, cc -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.nonecheck(False) def polygon(y, x, shape=None): """Generate coordinates of pixels within polygon. @@ -89,26 +93,28 @@ def polygon(y, x, shape=None): Pixel coordinates of polygon. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. + """ - cdef int nr_verts = x.shape[0] - cdef int minr = max(0, y.min()) - cdef int maxr = math.ceil(y.max()) - cdef int minc = max(0, x.min()) - cdef int maxc = math.ceil(x.max()) + + cdef Py_ssize_t nr_verts = x.shape[0] + cdef Py_ssize_t minr = int(max(0, y.min())) + cdef Py_ssize_t maxr = int(math.ceil(y.max())) + cdef Py_ssize_t minc = int(max(0, x.min())) + cdef Py_ssize_t maxc = int(math.ceil(x.max())) # make sure output coordinates do not exceed image size if shape is not None: - maxr = min(shape[0]-1, maxr) - maxc = min(shape[1]-1, maxc) + maxr = min(shape[0] - 1, maxr) + maxc = min(shape[1] - 1, maxc) - cdef int r, c + cdef Py_ssize_t r, c #: make contigous arrays for r, c coordinates - cdef np.ndarray contiguous_rdata, contiguous_cdata + cdef cnp.ndarray contiguous_rdata, contiguous_cdata contiguous_rdata = np.ascontiguousarray(y, 'double') contiguous_cdata = np.ascontiguousarray(x, 'double') - cdef np.double_t* rptr = contiguous_rdata.data - cdef np.double_t* cptr = contiguous_cdata.data + cdef cnp.double_t* rptr = contiguous_rdata.data + cdef cnp.double_t* cptr = contiguous_cdata.data #: output coordinate arrays cdef list rr = list() @@ -123,19 +129,19 @@ def polygon(y, x, shape=None): return np.array(rr), np.array(cc) -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.nonecheck(False) -@cython.cdivision(True) -def ellipse(double cy, double cx, double b, double a, shape=None): +def ellipse(double cy, double cx, double yradius, double xradius, shape=None): """Generate coordinates of pixels within ellipse. Parameters ---------- cy, cx : double Centre coordinate of ellipse. - b, a: double - Minor and major semi-axes. ``(x/a)**2 + (y/b)**2 = 1``. + yradius, xradius : double + Minor and major semi-axes. ``(x/xradius)**2 + (y/yradius)**2 = 1``. + shape : tuple, optional + image shape which is used to determine maximum extents of output pixel + coordinates. This is useful for ellipses which exceed the image size. + By default the full extents of the ellipse are used. Returns ------- @@ -143,18 +149,20 @@ def ellipse(double cy, double cx, double b, double a, shape=None): Pixel coordinates of ellipse. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. + """ - cdef int minr = max(0, cy-b) - cdef int maxr = math.ceil(cy+b) - cdef int minc = max(0, cx-a) - cdef int maxc = math.ceil(cx+a) + + cdef Py_ssize_t minr = int(max(0, cy - yradius)) + cdef Py_ssize_t maxr = int(math.ceil(cy + yradius)) + cdef Py_ssize_t minc = int(max(0, cx - xradius)) + cdef Py_ssize_t maxc = int(math.ceil(cx + xradius)) # make sure output coordinates do not exceed image size if shape is not None: - maxr = min(shape[0]-1, maxr) - maxc = min(shape[1]-1, maxc) + maxr = min(shape[0] - 1, maxr) + maxc = min(shape[1] - 1, maxc) - cdef int r, c + cdef Py_ssize_t r, c #: output coordinate arrays cdef list rr = list() @@ -162,7 +170,7 @@ def ellipse(double cy, double cx, double b, double a, shape=None): for r in range(minr, maxr+1): for c in range(minc, maxc+1): - if sqrt(((r - cy)/b)**2 + ((c - cx)/a)**2) < 1: + if sqrt(((r - cy) / yradius)**2 + ((c - cx) / xradius)**2) < 1: rr.append(r) cc.append(c) @@ -178,6 +186,10 @@ def circle(double cy, double cx, double radius, shape=None): Centre coordinate of circle. radius: double Radius of circle. + shape : tuple, optional + image shape which is used to determine maximum extents of output pixel + coordinates. This is useful for circles which exceed the image size. + By default the full extents of the circle are used. Returns ------- @@ -185,5 +197,203 @@ def circle(double cy, double cx, double radius, shape=None): Pixel coordinates of circle. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. + Notes + ----- + This function is a wrapper for skimage.draw.ellipse() """ + return ellipse(cy, cx, radius, radius, shape) + + +def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, + method='bresenham'): + """Generate circle perimeter coordinates. + + Parameters + ---------- + cy, cx : int + Centre coordinate of circle. + radius: int + Radius of circle. + method : {'bresenham', 'andres'}, optional + bresenham : Bresenham method + andres : Andres method + + + Returns + ------- + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the circle perimeter. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Notes + ----- + Andres method presents the advantage that concentric + circles create a disc whereas Bresenham can make holes. There + is also less distortions when Andres circles are rotated. + Bresenham method is also known as midpoint circle algorithm. + + References + ---------- + .. [1] J.E. Bresenham, "Algorithm for computer control of a digital + plotter", 4 (1965) 25-30. + .. [2] E. Andres, "Discrete circles, rings and spheres", 18 (1994) 695-706. + + """ + + cdef list rr = list() + cdef list cc = list() + + cdef Py_ssize_t x = 0 + cdef Py_ssize_t y = radius + cdef Py_ssize_t d = 0 + cdef char cmethod + if method == 'bresenham': + d = 3 - 2 * radius + cmethod = 'b' + elif method == 'andres': + d = radius - 1 + cmethod = 'a' + else: + raise ValueError('Wrong method') + + while y >= x: + rr.extend([y, -y, y, -y, x, -x, x, -x]) + cc.extend([x, x, -x, -x, y, y, -y, -y]) + + if cmethod == 'b': + if d < 0: + d += 4 * x + 6 + else: + d += 4 * (x - y) + 10 + y -= 1 + x += 1 + elif cmethod == 'a': + if d >= 2 * (x - 1): + d = d - 2 * x + x = x + 1 + elif d <= 2 * (radius - y): + d = d + 2 * y - 1 + y = y - 1 + else: + d = d + 2 * (y - x - 1) + y = y - 1 + x = x + 1 + + return np.array(rr) + cy, np.array(cc) + cx + + +def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, + Py_ssize_t xradius): + """Generate ellipse perimeter coordinates. + + Parameters + ---------- + cy, cx : int + Centre coordinate of ellipse. + yradius, xradius: int + Main radial values. + + Returns + ------- + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the circle perimeter. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + References + ---------- + .. [1] J. Kennedy "A fast Bresenham type algorithm for + drawing ellipses". + + """ + + # If both radii == 0, return the center to avoid infinite loop in 2nd set + if xradius == 0 and yradius == 0: + return np.array(cy), np.array(cx) + + # a and b are xradius an yradius compute 2a^2 and 2b^2 + cdef Py_ssize_t twoasquared = 2 * xradius**2 + cdef Py_ssize_t twobsquared = 2 * yradius**2 + + # Pixels + cdef list px = list() + cdef list py = list() + + # First set of points: + # start at the top + cdef Py_ssize_t x = xradius + cdef Py_ssize_t y = 0 + + cdef Py_ssize_t err = 0 + cdef Py_ssize_t xstop = twobsquared * xradius + cdef Py_ssize_t ystop = 0 + cdef Py_ssize_t xchange = yradius * yradius * (1 - 2 * xradius) + cdef Py_ssize_t ychange = xradius * xradius + + while xstop > ystop: + px.extend([x, -x, -x, x]) + py.extend([y, y, -y, -y]) + y += 1 + ystop += twoasquared + err += ychange + ychange += twoasquared + if (2 * err + xchange) > 0: + x -= 1 + xstop -= twobsquared + err += xchange + xchange += twobsquared + + # Second set of points: + x = 0 + y = yradius + + err = 0 + xstop = 0 + ystop = twoasquared * yradius + xchange = yradius * yradius + ychange = xradius * xradius * (1 - 2 * yradius) + + while xstop <= ystop: + px.extend([x, -x, -x, x]) + py.extend([y, y, -y, -y]) + x += 1 + xstop += twobsquared + err += xchange + xchange += twobsquared + if (2 * err + ychange) > 0: + y -= 1 + ystop -= twoasquared + err += ychange + ychange += twobsquared + + return np.array(py) + cy, np.array(px) + cx + + +def set_color(img, coords, color): + """Set pixel color in the image at the given coordinates. + + Coordinates that exceed the shape of the image will be ignored. + + Parameters + ---------- + img : (M, N, D) ndarray + Image + coords : ((P,) ndarray, (P,) ndarray) + Coordinates of pixels to be colored. + color : (D,) ndarray + Color to be assigned to coordinates in the image. + + Returns + ------- + img : (M, N, D) ndarray + The updated image. + + """ + + rr, cc = coords + rr_inside = np.logical_and(rr >= 0, rr < img.shape[0]) + cc_inside = np.logical_and(cc >= 0, cc < img.shape[1]) + inside = np.logical_and(rr_inside, cc_inside) + img[rr[inside], cc[inside]] = color diff --git a/skimage/draw/setup.py b/skimage/draw/setup.py index 5b8e237d..1414fca8 100644 --- a/skimage/draw/setup.py +++ b/skimage/draw/setup.py @@ -15,7 +15,7 @@ def configuration(parent_package='', top_path=None): cython(['_draw.pyx'], working_path=base_path) config.add_extension('_draw', sources=['_draw.c'], - include_dirs=[get_numpy_include_dirs(), '../shared']) + include_dirs=[get_numpy_include_dirs(), '../_shared']) return config diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 9e6ca8e8..ef09747c 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -1,7 +1,7 @@ from numpy.testing import assert_array_equal import numpy as np -from skimage.draw import line, polygon, circle, ellipse +from skimage.draw import line, polygon, circle, circle_perimeter, ellipse, ellipse_perimeter def test_line_horizontal(): @@ -150,6 +150,68 @@ def test_circle(): assert_array_equal(img, img_) +def test_circle_perimeter_bresenham(): + img = np.zeros((15, 15), 'uint8') + rr, cc = circle_perimeter(7, 7, 0, method='bresenham') + img[rr, cc] = 1 + assert(np.sum(img) == 1) + assert(img[7][7] == 1) + + img = np.zeros((17, 15), 'uint8') + rr, cc = circle_perimeter(7, 7, 7, method='bresenham') + img[rr, cc] = 1 + img_ = np.array( + [[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] + ) + assert_array_equal(img, img_) + +def test_circle_perimeter_andres(): + img = np.zeros((15, 15), 'uint8') + rr, cc = circle_perimeter(7, 7, 0, method='andres') + img[rr, cc] = 1 + assert(np.sum(img) == 1) + assert(img[7][7] == 1) + + img = np.zeros((17, 15), 'uint8') + rr, cc = circle_perimeter(7, 7, 7, method='andres') + img[rr, cc] = 1 + img_ = np.array( + [[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] + ) + assert_array_equal(img, img_) + def test_ellipse(): img = np.zeros((15, 15), 'uint8') @@ -176,6 +238,50 @@ def test_ellipse(): assert_array_equal(img, img_) +def test_ellipse_perimeter(): + img = np.zeros((30, 15), 'uint8') + rr, cc = ellipse_perimeter(15, 7, 0, 0) + img[rr, cc] = 1 + assert(np.sum(img) == 1) + assert(img[15][7] == 1) + + img = np.zeros((30, 15), 'uint8') + rr, cc = ellipse_perimeter(15, 7, 14, 6) + img[rr, cc] = 1 + img_ = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0]] + ) + + assert_array_equal(img, img_) if __name__ == "__main__": from numpy.testing import run_module_suite diff --git a/skimage/exposure/__init__.py b/skimage/exposure/__init__.py index 7e19d317..ae75c982 100644 --- a/skimage/exposure/__init__.py +++ b/skimage/exposure/__init__.py @@ -1,2 +1,3 @@ -from .exposure import histogram, equalize, cumulative_distribution -from .exposure import rescale_intensity +from .exposure import histogram, equalize, equalize_hist +from .exposure import rescale_intensity, cumulative_distribution +from ._adapthist import equalize_adapthist diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py new file mode 100644 index 00000000..8a825435 --- /dev/null +++ b/skimage/exposure/_adapthist.py @@ -0,0 +1,326 @@ +""" +Adapted code from "Contrast Limited Adaptive Histogram Equalization" by Karel +Zuiderveld , Graphics Gems IV, Academic Press, 1994. + +http://tog.acm.org/resources/GraphicsGems/gems.html#gemsvi + +The Graphics Gems code is copyright-protected. In other words, you cannot +claim the text of the code as your own and resell it. Using the code is +permitted in any program, product, or library, non-commercial or commercial. +Giving credit is not required, though is a nice gesture. The code comes as-is, +and if there are any flaws or problems with any Gems code, nobody involved with +Gems - authors, editors, publishers, or webmasters - are to be held +responsible. Basically, don't be a jerk, and remember that anything free +comes with no guarantee. +""" +import numpy as np +import skimage +from skimage import color +from skimage.exposure import rescale_intensity +from skimage.util import view_as_blocks + + +MAX_REG_X = 16 # max. # contextual regions in x-direction */ +MAX_REG_Y = 16 # max. # contextual regions in y-direction */ +NR_OF_GREY = 16384 # number of grayscale levels to use in CLAHE algorithm + + +def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, + nbins=256): + """Contrast Limited Adaptive Histogram Equalization. + + Parameters + ---------- + image : array-like + Input image. + ntiles_x : int, optional + Number of tile regions in the X direction. Ranges between 2 and 16. + ntiles_y : int, optional + Number of tile regions in the Y direction. Ranges between 2 and 16. + clip_limit : float: optional + Clipping limit, normalized between 0 and 1 (higher values give more + contrast). + nbins : int, optional + Number of gray bins for histogram ("dynamic range"). + + Returns + ------- + out : ndarray + Equalized image. + + Notes + ----- + * The algorithm relies on an image whose rows and columns are even + multiples of the number of tiles, so the extra rows and columns are left + at their original values, thus preserving the input image shape. + * For color images, the following steps are performed: + - The image is converted to LAB color space + - The CLAHE algorithm is run on the L channel + - The image is converted back to RGB space and returned + * For RGBA images, the original alpha channel is removed. + + References + ---------- + .. [1] http://tog.acm.org/resources/GraphicsGems/gems.html#gemsvi + .. [2] https://en.wikipedia.org/wiki/CLAHE#CLAHE + """ + args = [None, ntiles_x, ntiles_y, clip_limit * nbins, nbins] + if image.ndim > 2: + lab_img = color.rgb2lab(skimage.img_as_float(image)) + l_chan = lab_img[:, :, 0] + l_chan /= np.max(np.abs(l_chan)) + l_chan = skimage.img_as_uint(l_chan) + args[0] = rescale_intensity(l_chan, out_range=(0, NR_OF_GREY - 1)) + new_l = _clahe(*args).astype(float) + new_l = rescale_intensity(new_l, out_range=(0, 100)) + lab_img[:new_l.shape[0], :new_l.shape[1], 0] = new_l + image = color.lab2rgb(lab_img) + image = rescale_intensity(image, out_range=(0, 1)) + else: + image = skimage.img_as_uint(image) + args[0] = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) + out = _clahe(*args) + image[:out.shape[0], :out.shape[1]] = out + image = rescale_intensity(image) + return image + + +def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): + """Contrast Limited Adaptive Histogram Equalization. + + Parameters + ---------- + image : array-like + Input image. + ntiles_x : int, optional + Number of tile regions in the X direction. Ranges between 2 and 16. + ntiles_y : int, optional + Number of tile regions in the Y direction. Ranges between 2 and 16. + clip_limit : float, optional + Normalized clipping limit (higher values give more contrast). + nbins : int, optional + Number of gray bins for histogram ("dynamic range"). + + Returns + ------- + out : ndarray + Equalized image. + + The number of "effective" greylevels in the output image is set by `nbins`; + selecting a small value (eg. 128) speeds up processing and still produce + an output image of good quality. The output image will have the same + minimum and maximum value as the input image. A clip limit smaller than 1 + results in standard (non-contrast limited) AHE. + """ + ntiles_x = min(ntiles_x, MAX_REG_X) + ntiles_y = min(ntiles_y, MAX_REG_Y) + ntiles_y = max(ntiles_x, 2) + ntiles_x = max(ntiles_y, 2) + + if clip_limit == 1.0: + return image # is OK, immediately returns original image. + + map_array = np.zeros((ntiles_y, ntiles_x, nbins), dtype=int) + + y_res = image.shape[0] - image.shape[0] % ntiles_y + x_res = image.shape[1] - image.shape[1] % ntiles_x + image = image[: y_res, : x_res] + + x_size = image.shape[1] / ntiles_x # Actual size of contextual regions + y_size = image.shape[0] / ntiles_y + n_pixels = x_size * y_size + + if clip_limit > 0.0: # Calculate actual cliplimit + clip_limit = int(clip_limit * (x_size * y_size) / nbins) + if clip_limit < 1: + clip_limit = 1 + else: + clip_limit = NR_OF_GREY # Large value, do not clip (AHE) + + bin_size = 1 + NR_OF_GREY / nbins + aLUT = np.arange(NR_OF_GREY) + aLUT /= bin_size + img_blocks = view_as_blocks(image, (y_size, x_size)) + + # Calculate greylevel mappings for each contextual region + for y in range(ntiles_y): + for x in range(ntiles_x): + sub_img = img_blocks[y, x] + hist = aLUT[sub_img.ravel()] + hist = np.bincount(hist) + hist = np.append(hist, np.zeros(nbins - hist.size, dtype=int)) + hist = clip_histogram(hist, clip_limit) + hist = map_histogram(hist, 0, NR_OF_GREY - 1, n_pixels) + map_array[y, x] = hist + + # Interpolate greylevel mappings to get CLAHE image + ystart = 0 + for y in range(ntiles_y + 1): + xstart = 0 + if y == 0: # special case: top row + ystep = y_size / 2.0 + yU = 0 + yB = 0 + elif y == ntiles_y: # special case: bottom row + ystep = y_size / 2.0 + yU = ntiles_y - 1 + yB = yU + else: # default values + ystep = y_size + yU = y - 1 + yB = yB + 1 + + for x in range(ntiles_x + 1): + if x == 0: # special case: left column + xstep = x_size / 2.0 + xL = 0 + xR = 0 + elif x == ntiles_x: # special case: right column + xstep = x_size / 2.0 + xL = ntiles_x - 1 + xR = xL + else: # default values + xstep = x_size + xL = x - 1 + xR = xL + 1 + + mapLU = map_array[yU, xL] + mapRU = map_array[yU, xR] + mapLB = map_array[yB, xL] + mapRB = map_array[yB, xR] + + xslice = np.arange(xstart, xstart + xstep) + yslice = np.arange(ystart, ystart + ystep) + interpolate(image, xslice, yslice, + mapLU, mapRU, mapLB, mapRB, aLUT) + + xstart += xstep # set pointer on next matrix */ + + ystart += ystep + + return image + + +def clip_histogram(hist, clip_limit): + """Perform clipping of the histogram and redistribution of bins. + + The histogram is clipped and the number of excess pixels is counted. + Afterwards the excess pixels are equally redistributed across the + whole histogram (providing the bin count is smaller than the cliplimit). + + Parameters + ---------- + hist : ndarray + Histogram array. + clip_limit : int + Maximum allowed bin count. + + Returns + ------- + hist : ndarray + Clipped histogram. + """ + # calculate total number of excess pixels + excess_mask = hist > clip_limit + excess = hist[excess_mask] + n_excess = excess.sum() - excess.size * clip_limit + + # Second part: clip histogram and redistribute excess pixels in each bin + bin_incr = int(n_excess / hist.size) # average binincrement + upper = clip_limit - bin_incr # Bins larger than upper set to cliplimit + + hist[excess_mask] = clip_limit + + low_mask = hist < upper + n_excess -= hist[low_mask].size * bin_incr + hist[low_mask] += bin_incr + + mid_mask = (hist >= upper) & (hist < clip_limit) + mid = hist[mid_mask] + n_excess -= mid.size * clip_limit - mid.sum() + hist[mid_mask] = clip_limit + + while n_excess > 0: # Redistribute remaining excess + index = 0 + while n_excess > 0 and index < hist.size: + step_size = int(hist[hist < clip_limit].size / n_excess) + step_size = max(step_size, 1) + indices = np.arange(index, hist.size, step_size) + under = hist[indices] < clip_limit + hist[under] += 1 + n_excess -= hist[under].size + index += 1 + + return hist + + +def map_histogram(hist, min_val, max_val, n_pixels): + """Calculate the equalized lookup table (mapping). + + It does so by cumulating the input histogram. + + Parameters + ---------- + hist : ndarray + Clipped histogram. + min_val : int + Minimum value for mapping. + max_val : int + Maximum value for mapping. + n_pixels : int + Number of pixels in the region. + + Returns + ------- + out : ndarray + Mapped intensity LUT. + """ + out = np.cumsum(hist).astype(float) + scale = ((float)(max_val - min_val)) / n_pixels + out *= scale + out += min_val + out[out > max_val] = max_val + return out.astype(int) + + +def interpolate(image, xslice, yslice, + mapLU, mapRU, mapLB, mapRB, aLUT): + """Find the new grayscale level for a region using bilinear interpolation. + + Parameters + ---------- + image : ndarray + Full image. + xslice, yslice : array-like + Indices of the region. + map* : ndarray + Mappings of greylevels from histograms. + aLUT : ndarray + Maps grayscale levels in image to histogram levels. + + Returns + ------- + out : ndarray + Original image with the subregion replaced. + + Notes + ----- + This function calculates the new greylevel assignments of pixels within + a submatrix of the image. This is done by a bilinear interpolation between + four different mappings in order to eliminate boundary artifacts. + """ + norm = xslice.size * yslice.size # Normalization factor + # interpolation weight matrices + x_coef, y_coef = np.meshgrid(np.arange(xslice.size), + np.arange(yslice.size)) + x_inv_coef, y_inv_coef = x_coef[:, ::-1] + 1, y_coef[::-1] + 1 + + view = image[yslice[0]: yslice[-1] + 1, xslice[0]: xslice[-1] + 1] + im_slice = aLUT[view] + new = ((y_inv_coef * (x_inv_coef * mapLU[im_slice] + + x_coef * mapRU[im_slice]) + + y_coef * (x_inv_coef * mapLB[im_slice] + + x_coef * mapRB[im_slice])) + / norm) + view[:, :] = new + return image diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index bffe660a..c7f1e712 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -2,6 +2,9 @@ import numpy as np from skimage import img_as_float from skimage.util.dtype import dtype_range +import skimage.color as color +from skimage.util.dtype import convert +from skimage._shared.utils import deprecated __all__ = ['histogram', 'cumulative_distribution', 'equalize', @@ -76,7 +79,12 @@ def cumulative_distribution(image, nbins=256): return img_cdf, bin_centers +@deprecated('equalize_hist') def equalize(image, nbins=256): + return equalize_hist(image, nbins) + + +def equalize_hist(image, nbins=256): """Return image after histogram equalization. Parameters diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index b6ed0b12..086739de 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -1,21 +1,23 @@ import numpy as np from numpy.testing import assert_array_almost_equal as assert_close - import skimage from skimage import data from skimage import exposure +from skimage.color import rgb2gray +from skimage.util.dtype import dtype_range # Test histogram equalization # =========================== # squeeze image intensities to lower image contrast -test_img = exposure.rescale_intensity(data.camera() / 5. + 100) +test_img = skimage.img_as_float(data.camera()) +test_img = exposure.rescale_intensity(test_img / 5. + 100) def test_equalize_ubyte(): img = skimage.img_as_ubyte(test_img) - img_eq = exposure.equalize(img) + img_eq = exposure.equalize_hist(img) cdf, bin_edges = exposure.cumulative_distribution(img_eq) check_cdf_slope(cdf) @@ -23,7 +25,7 @@ def test_equalize_ubyte(): def test_equalize_float(): img = skimage.img_as_float(test_img) - img_eq = exposure.equalize(img) + img_eq = exposure.equalize_hist(img) cdf, bin_edges = exposure.cumulative_distribution(img_eq) check_cdf_slope(cdf) @@ -71,6 +73,99 @@ def test_rescale_out_range(): assert_close(out, [0, 63, 127]) +# Test adaptive histogram equalization +# ==================================== + +def test_adapthist_scalar(): + '''Test a scalar uint8 image + ''' + img = skimage.img_as_ubyte(data.moon()) + adapted = exposure.equalize_adapthist(img, clip_limit=0.02) + assert adapted.min() == 0 + assert adapted.max() == (1 << 16) - 1 + assert img.shape == adapted.shape + full_scale = skimage.exposure.rescale_intensity(skimage.img_as_uint(img)) + + assert_almost_equal = np.testing.assert_almost_equal + assert_almost_equal(peak_snr(full_scale, adapted), 101.231, 3) + assert_almost_equal(norm_brightness_err(full_scale, adapted), + 0.041, 3) + return img, adapted + + +def test_adapthist_grayscale(): + '''Test a grayscale float image + ''' + img = skimage.img_as_float(data.lena()) + img = rgb2gray(img) + img = np.dstack((img, img, img)) + adapted = exposure.equalize_adapthist(img, 10, 9, clip_limit=0.01, + nbins=128) + assert_almost_equal = np.testing.assert_almost_equal + assert img.shape == adapted.shape + assert_almost_equal(peak_snr(img, adapted), 97.531, 3) + assert_almost_equal(norm_brightness_err(img, adapted), 0.0313, 3) + return data, adapted + + +def test_adapthist_color(): + '''Test an RGB color uint16 image + ''' + img = skimage.img_as_uint(data.lena()) + adapted = exposure.equalize_adapthist(img, clip_limit=0.01) + assert_almost_equal = np.testing.assert_almost_equal + assert adapted.min() == 0 + assert adapted.max() == 1.0 + assert img.shape == adapted.shape + full_scale = skimage.exposure.rescale_intensity(img) + assert_almost_equal(peak_snr(full_scale, adapted), 102.940, 3) + assert_almost_equal(norm_brightness_err(full_scale, adapted), + 0.0110, 3) + return data, adapted + + +def peak_snr(img1, img2): + '''Peak signal to noise ratio of two images + + Parameters + ---------- + img1 : array-like + img2 : array-like + + Returns + ------- + peak_snr : float + Peak signal to noise ratio + ''' + if img1.ndim == 3: + img1, img2 = rgb2gray(img1.copy()), rgb2gray(img2.copy()) + img1 = skimage.img_as_float(img1) + img2 = skimage.img_as_float(img2) + mse = 1. / img1.size * np.square(img1 - img2).sum() + _, max_ = dtype_range[img1.dtype.type] + return 20 * np.log(max_ / mse) + + +def norm_brightness_err(img1, img2): + '''Normalized Absolute Mean Brightness Error between two images + + Parameters + ---------- + img1 : array-like + img2 : array-like + + Returns + ------- + norm_brightness_error : float + Normalized absolute mean brightness error + ''' + if img1.ndim == 3: + img1, img2 = rgb2gray(img1), rgb2gray(img2) + ambe = np.abs(img1.mean() - img2.mean()) + nbe = ambe / dtype_range[img1.dtype.type][1] + return nbe + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 1597e9a8..9f1e5f92 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -1,5 +1,8 @@ +from ._daisy import daisy from ._hog import hog from .texture import greycomatrix, greycoprops, local_binary_pattern from .peak import peak_local_max -from ._harris import harris +from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, + corner_foerstner, corner_subpix, corner_peaks) +from .corner_cy import corner_moravec from .template import match_template diff --git a/skimage/feature/_daisy.py b/skimage/feature/_daisy.py new file mode 100644 index 00000000..1a97de8f --- /dev/null +++ b/skimage/feature/_daisy.py @@ -0,0 +1,223 @@ +import numpy as np +from scipy import sqrt, pi, arctan2, cos, sin, exp +from scipy.ndimage import gaussian_filter +import skimage.color +from skimage import img_as_float, draw + + +def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8, + normalization='l1', sigmas=None, ring_radii=None, visualize=False): + '''Extract DAISY feature descriptors densely for the given image. + + DAISY is a feature descriptor similar to SIFT formulated in a way that + allows for fast dense extraction. Typically, this is practical for + bag-of-features image representations. + + The implementation follows Tola et al. [1]_ but deviate on the following + points: + + * Histogram bin contribution are smoothed with a circular Gaussian + window over the tonal range (the angular range). + * The sigma values of the spatial Gaussian smoothing in this code do not + match the sigma values in the original code by Tola et al. [2]_. In + their code, spatial smoothing is applied to both the input image and + the center histogram. However, this smoothing is not documented in [1]_ + and, therefore, it is omitted. + + Parameters + ---------- + img : (M, N) array + Input image (greyscale). + step : int, optional + Distance between descriptor sampling points. + radius : int, optional + Radius (in pixels) of the outermost ring. + rings : int, optional + Number of rings. + histograms : int, optional + Number of histograms sampled per ring. + orientations : int, optional + Number of orientations (bins) per histogram. + normalization : [ 'l1' | 'l2' | 'daisy' | 'off' ], optional + How to normalize the descriptors + + * 'l1': L1-normalization of each descriptor. + * 'l2': L2-normalization of each descriptor. + * 'daisy': L2-normalization of individual histograms. + * 'off': Disable normalization. + + sigmas : 1D array of float, optional + Standard deviation of spatial Gaussian smoothing for the center + histogram and for each ring of histograms. The array of sigmas should + be sorted from the center and out. I.e. the first sigma value defines + the spatial smoothing of the center histogram and the last sigma value + defines the spatial smoothing of the outermost ring. Specifying sigmas + overrides the following parameter. + + ``rings = len(sigmas) - 1`` + + ring_radii : 1D array of int, optional + Radius (in pixels) for each ring. Specifying ring_radii overrides the + following two parameters. + + ``rings = len(ring_radii)`` + ``radius = ring_radii[-1]`` + + If both sigmas and ring_radii are given, they must satisfy the + following predicate since no radius is needed for the center + histogram. + + ``len(ring_radii) == len(sigmas) + 1`` + + visualize : bool, optional + Generate a visualization of the DAISY descriptors + + Returns + ------- + descs : array + Grid of DAISY descriptors for the given image as an array + dimensionality (P, Q, R) where + + ``P = ceil((M - radius*2) / step)`` + ``Q = ceil((N - radius*2) / step)`` + ``R = (rings * histograms + 1) * orientations`` + + descs_img : (M, N, 3) array (only if visualize==True) + Visualization of the DAISY descriptors. + + References + ---------- + .. [1] Tola et al. "Daisy: An efficient dense descriptor applied to wide- + baseline stereo." Pattern Analysis and Machine Intelligence, IEEE + Transactions on 32.5 (2010): 815-830. + .. [2] http://cvlab.epfl.ch/alumni/tola/daisy.html + ''' + + # Validate image format. + if img.ndim > 2: + raise ValueError('Only grey-level images are supported.') + if img.dtype.kind != 'f': + img = img_as_float(img) + + # Validate parameters. + if sigmas is not None and ring_radii is not None \ + and len(sigmas) - 1 != len(ring_radii): + raise ValueError('len(sigmas)-1 != len(ring_radii)') + if ring_radii is not None: + rings = len(ring_radii) + radius = ring_radii[-1] + if sigmas is not None: + rings = len(sigmas) - 1 + if sigmas is None: + sigmas = [radius * (i + 1) / float(2 * rings) for i in range(rings)] + if ring_radii is None: + ring_radii = [radius * (i + 1) / float(rings) for i in range(rings)] + if normalization not in ['l1', 'l2', 'daisy', 'off']: + raise ValueError('Invalid normalization method.') + + # Compute image derivatives. + dx = np.zeros(img.shape) + dy = np.zeros(img.shape) + dx[:, :-1] = np.diff(img, n=1, axis=1) + dy[:-1, :] = np.diff(img, n=1, axis=0) + + # Compute gradient orientation and magnitude and their contribution + # to the histograms. + grad_mag = sqrt(dx ** 2 + dy ** 2) + grad_ori = arctan2(dy, dx) + orientation_kappa = orientations / pi + orientation_angles = [2 * o * pi / orientations - pi + for o in range(orientations)] + hist = np.empty((orientations,) + img.shape, dtype=float) + for i, o in enumerate(orientation_angles): + # Weigh bin contribution by the circular normal distribution + hist[i, :, :] = exp(orientation_kappa * cos(grad_ori - o)) + # Weigh bin contribution by the gradient magnitude + hist[i, :, :] = np.multiply(hist[i, :, :], grad_mag) + + # Smooth orientation histograms for the center and all rings. + sigmas = [sigmas[0]] + sigmas + hist_smooth = np.empty((rings + 1,) + hist.shape, dtype=float) + for i in range(rings + 1): + for j in range(orientations): + hist_smooth[i, j, :, :] = gaussian_filter(hist[j, :, :], + sigma=sigmas[i]) + + # Assemble descriptor grid. + theta = [2 * pi * j / histograms for j in range(histograms)] + desc_dims = (rings * histograms + 1) * orientations + descs = np.empty((desc_dims, img.shape[0] - 2 * radius, + img.shape[1] - 2 * radius)) + descs[:orientations, :, :] = hist_smooth[0, :, radius:-radius, + radius:-radius] + idx = orientations + for i in range(rings): + for j in range(histograms): + y_min = radius + int(round(ring_radii[i] * sin(theta[j]))) + y_max = descs.shape[1] + y_min + x_min = radius + int(round(ring_radii[i] * cos(theta[j]))) + x_max = descs.shape[2] + x_min + descs[idx:idx + orientations, :, :] = hist_smooth[i + 1, :, + y_min:y_max, + x_min:x_max] + idx += orientations + descs = descs[:, ::step, ::step] + descs = descs.swapaxes(0, 1).swapaxes(1, 2) + + # Normalize descriptors. + if normalization != 'off': + descs += 1e-10 + if normalization == 'l1': + descs /= np.sum(descs, axis=2)[:, :, np.newaxis] + elif normalization == 'l2': + descs /= sqrt(np.sum(descs ** 2, axis=2))[:, :, np.newaxis] + elif normalization == 'daisy': + for i in range(0, desc_dims, orientations): + norms = sqrt(np.sum(descs[:, :, i:i + orientations] ** 2, + axis=2)) + descs[:, :, i:i + orientations] /= norms[:, :, np.newaxis] + + if visualize: + descs_img = skimage.color.gray2rgb(img) + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + # Draw center histogram sigma + color = (1, 0, 0) + desc_y = i * step + radius + desc_x = j * step + radius + coords = draw.circle_perimeter(desc_y, desc_x, int(sigmas[0])) + draw.set_color(descs_img, coords, color) + max_bin = np.max(descs[i, j, :]) + for o_num, o in enumerate(orientation_angles): + # Draw center histogram bins + bin_size = descs[i, j, o_num] / max_bin + dy = sigmas[0] * bin_size * sin(o) + dx = sigmas[0] * bin_size * cos(o) + coords = draw.line(desc_y, desc_x, int(desc_y + dy), + int(desc_x + dx)) + draw.set_color(descs_img, coords, color) + for r_num, r in enumerate(ring_radii): + color_offset = float(1 + r_num) / rings + color = (1 - color_offset, 1, color_offset) + for t_num, t in enumerate(theta): + # Draw ring histogram sigmas + hist_y = desc_y + int(round(r * sin(t))) + hist_x = desc_x + int(round(r * cos(t))) + coords = draw.circle_perimeter(hist_y, hist_x, + int(sigmas[r_num + 1])) + draw.set_color(descs_img, coords, color) + for o_num, o in enumerate(orientation_angles): + # Draw histogram bins + bin_size = descs[i, j, orientations + r_num * + histograms * orientations + + t_num * orientations + o_num] + bin_size /= max_bin + dy = sigmas[r_num + 1] * bin_size * sin(o) + dx = sigmas[r_num + 1] * bin_size * cos(o) + coords = draw.line(hist_y, hist_x, + int(hist_y + dy), + int(hist_x + dx)) + draw.set_color(descs_img, coords, color) + return descs, descs_img + else: + return descs diff --git a/skimage/feature/_harris.py b/skimage/feature/_harris.py deleted file mode 100644 index ae30a29e..00000000 --- a/skimage/feature/_harris.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Harris corner detector - -Inspired from Solem's implementation -http://www.janeriksolem.net/2009/01/harris-corner-detector-in-python.html -""" -from scipy import ndimage - -from . import peak - - -def _compute_harris_response(image, eps=1e-6, gaussian_deviation=1): - """Compute the Harris corner detector response function - for each pixel in the image - - Parameters - ---------- - image : ndarray of floats - Input image. - - eps : float, optional - Normalisation factor. - - gaussian_deviation : integer, optional - Standard deviation used for the Gaussian kernel. - - Returns - -------- - image : (M, N) ndarray - Harris image response - """ - if len(image.shape) == 3: - image = image.mean(axis=2) - - # derivatives - image = ndimage.gaussian_filter(image, gaussian_deviation) - imx = ndimage.sobel(image, axis=0, mode='constant') - imy = ndimage.sobel(image, axis=1, mode='constant') - - Wxx = ndimage.gaussian_filter(imx * imx, 1.5, mode='constant') - Wxy = ndimage.gaussian_filter(imx * imy, 1.5, mode='constant') - Wyy = ndimage.gaussian_filter(imy * imy, 1.5, mode='constant') - - # determinant and trace - Wdet = Wxx * Wyy - Wxy**2 - Wtr = Wxx + Wyy - # Alternate formula for Harris response. - # Alison Noble, "Descriptions of Image Surfaces", PhD thesis (1989) - harris = Wdet / (Wtr + eps) - - return harris - - -def harris(image, min_distance=10, threshold=0.1, eps=1e-6, - gaussian_deviation=1): - """Return corners from a Harris response image - - Parameters - ---------- - image : ndarray of floats - Input image. - - min_distance : int, optional - Minimum number of pixels separating interest points and image boundary. - - threshold : float, optional - Relative threshold impacting the number of interest points. - - eps : float, optional - Normalisation factor. - - gaussian_deviation : integer, optional - Standard deviation used for the Gaussian kernel. - - Returns - ------- - coordinates : (N, 2) array - (row, column) coordinates of interest points. - - Examples - ------- - >>> square = np.zeros([10,10]) - >>> square[2:8,2:8] = 1 - >>> square - array([[ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]) - >>> harris(square, min_distance=1) - - Corners of the square - - array([[3, 3], - [3, 6], - [6, 3], - [6, 6]]) - """ - - harrisim = _compute_harris_response(image, eps=eps, - gaussian_deviation=gaussian_deviation) - coordinates = peak.peak_local_max(harrisim, min_distance=min_distance, - threshold_rel=threshold) - return coordinates diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index ce7db462..9fa018b7 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -142,8 +142,10 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), centre = tuple([y * cy + cy // 2, x * cx + cx // 2]) dx = radius * cos(float(o) / orientations * np.pi) dy = radius * sin(float(o) / orientations * np.pi) - rr, cc = draw.bresenham(centre[0] - dy, centre[1] - dx, - centre[0] + dy, centre[1] + dx) + rr, cc = draw.bresenham(int(centre[0] - dx), + int(centre[1] - dy), + int(centre[0] + dx), + int(centre[1] + dy)) hog_image[rr, cc] += orientation_histogram[y, x, o] """ diff --git a/skimage/feature/_template.pyx b/skimage/feature/_template.pyx index 58d48524..03695959 100644 --- a/skimage/feature/_template.pyx +++ b/skimage/feature/_template.pyx @@ -1,3 +1,8 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + """ Template matching using normalized cross-correlation. @@ -30,24 +35,31 @@ the image window *before* squaring.) .. [2] J. P. Lewis, "Fast Normalized Cross-Correlation", Industrial Light and Magic. """ -import cython -cimport numpy as np + import numpy as np from scipy.signal import fftconvolve -from skimage.transform import integral + +cimport numpy as cnp from libc.math cimport sqrt, fabs from skimage._shared.transform cimport integrate -@cython.boundscheck(False) -def match_template(np.ndarray[float, ndim=2, mode="c"] image, - np.ndarray[float, ndim=2, mode="c"] template): - cdef np.ndarray[float, ndim=2, mode="c"] corr - cdef np.ndarray[float, ndim=2, mode="c"] image_sat - cdef np.ndarray[float, ndim=2, mode="c"] image_sqr_sat +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 template_mean = np.mean(template) cdef float template_ssd cdef float inv_area + cdef Py_ssize_t r, c, r_end, c_end + cdef Py_ssize_t template_rows = template.shape[0] + cdef Py_ssize_t template_cols = template.shape[1] + cdef float den, window_sqr_sum, window_mean_sqr, window_sum image_sat = integral.integral_image(image) image_sqr_sat = integral.integral_image(image**2) @@ -63,24 +75,23 @@ def match_template(np.ndarray[float, ndim=2, mode="c"] image, mode="valid"), dtype=np.float32) - cdef int i, j - cdef float den, window_sqr_sum, window_mean_sqr, window_sum, - # move window through convolution results, normalizing in the process - for i in range(corr.shape[0]): - for j in range(corr.shape[1]): - # subtract 1 because `i_end` and `j_end` are used for indexing into - # summed-area table, instead of slicing windows of the image. - i_end = i + template.shape[0] - 1 - j_end = j + template.shape[1] - 1 - window_sum = integrate(image_sat, i, j, i_end, j_end) + # move window through convolution results, normalizing in the process + for r in range(corr.shape[0]): + for c in range(corr.shape[1]): + # subtract 1 because `i_end` and `c_end` are used for indexing into + # summed-area table, instead of slicing windows of the image. + r_end = r + template_rows - 1 + c_end = c + template_cols - 1 + + window_sum = integrate(image_sat, r, c, r_end, c_end) window_mean_sqr = window_sum * window_sum * inv_area - window_sqr_sum = integrate(image_sqr_sat, i, j, i_end, j_end) + window_sqr_sum = integrate(image_sqr_sat, r, c, r_end, c_end) if window_sqr_sum <= window_mean_sqr: - corr[i, j] = 0 + corr[r, c] = 0 continue den = sqrt((window_sqr_sum - window_mean_sqr) * template_ssd) - corr[i, j] /= den - return corr + corr[r, c] /= den + return corr diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 70a446bb..f98ed4ca 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -3,21 +3,20 @@ #cython: nonecheck=False #cython: wraparound=False import numpy as np -cimport numpy as np +cimport numpy as cnp from libc.math cimport sin, cos, abs from skimage._shared.interpolation cimport bilinear_interpolation -def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, - negative_indices=False, mode='c'] image, - np.ndarray[dtype=np.float64_t, ndim=1, - negative_indices=False, mode='c'] distances, - np.ndarray[dtype=np.float64_t, ndim=1, - negative_indices=False, mode='c'] angles, +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, - np.ndarray[dtype=np.uint32_t, ndim=4, - negative_indices=False, mode='c'] out - ): + cnp.ndarray[dtype=cnp.uint32_t, ndim=4, + negative_indices=False, mode='c'] out): """Perform co-occurrence matrix accumulation. Parameters @@ -37,23 +36,26 @@ def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, the results of the GLCM computation. """ + cdef: - np.int32_t a_inx, d_idx - np.int32_t r, c, rows, cols, row, col - np.int32_t i, j + Py_ssize_t a_idx, d_idx, r, c, rows, cols, row, col + cnp.uint8_t i, j + cnp.float64_t angle, distance rows = image.shape[0] cols = image.shape[1] - for a_idx, angle in enumerate(angles): - for d_idx, distance in enumerate(distances): + for a_idx in range(len(angles)): + angle = angles[a_idx] + for d_idx in range(len(distances)): + distance = distances[d_idx] for r in range(rows): for c in range(cols): i = image[r, c] # compute the location of the offset pixel row = r + (sin(angle) * distance + 0.5) - col = c + (cos(angle) * distance + 0.5); + col = c + (cos(angle) * distance + 0.5) # make sure the offset is within bounds if row >= 0 and row < rows and \ @@ -79,7 +81,7 @@ cdef inline int _bit_rotate_right(int value, int length): return (value >> 1) | ((value & 1) << (length - 1)) -def _local_binary_pattern(np.ndarray[double, ndim=2] image, +def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, int P, float R, char method='D'): """Gray scale and rotation invariant LBP (Local Binary Patterns). @@ -109,25 +111,25 @@ def _local_binary_pattern(np.ndarray[double, ndim=2] image, """ # texture weights - cdef np.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype=np.int32) + cdef cnp.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype=np.int32) # local position of texture elements rp = - R * np.sin(2 * np.pi * np.arange(P, dtype=np.double) / P) cp = R * np.cos(2 * np.pi * np.arange(P, dtype=np.double) / P) - cdef np.ndarray[double, ndim=2] coords = np.round(np.vstack([rp, cp]).T, 5) + cdef cnp.ndarray[double, ndim=2] coords = np.round(np.vstack([rp, cp]).T, 5) # pre allocate arrays for computation - cdef np.ndarray[double, ndim=1] texture = np.zeros(P, np.double) - cdef np.ndarray[char, ndim=1] signed_texture = np.zeros(P, np.int8) - cdef np.ndarray[int, ndim=1] rotation_chain = np.zeros(P, np.int32) + cdef cnp.ndarray[double, ndim=1] texture = np.zeros(P, np.double) + cdef cnp.ndarray[char, ndim=1] signed_texture = np.zeros(P, np.int8) + cdef cnp.ndarray[int, ndim=1] rotation_chain = np.zeros(P, np.int32) output_shape = (image.shape[0], image.shape[1]) - cdef np.ndarray[double, ndim=2] output = np.zeros(output_shape, np.double) + cdef cnp.ndarray[double, ndim=2] output = np.zeros(output_shape, np.double) - cdef int rows = image.shape[0] - cdef int cols = image.shape[1] + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] cdef double lbp - cdef int r, c, changes, i + cdef Py_ssize_t r, c, changes, i for r in range(image.shape[0]): for c in range(image.shape[1]): for i in range(P): diff --git a/skimage/feature/corner.py b/skimage/feature/corner.py new file mode 100644 index 00000000..12d3fb70 --- /dev/null +++ b/skimage/feature/corner.py @@ -0,0 +1,505 @@ +import numpy as np +from scipy import ndimage +from scipy import stats +from skimage.color import rgb2grey +from skimage.util import img_as_float +from skimage.feature import peak_local_max + + +def _compute_derivatives(image): + """Compute derivatives in x and y direction using the Sobel operator. + + Parameters + ---------- + image : ndarray + Input image. + + Returns + ------- + imx : ndarray + Derivative in x-direction. + imy : ndarray + Derivative in y-direction. + + """ + + imy = ndimage.sobel(image, axis=0, mode='constant', cval=0) + imx = ndimage.sobel(image, axis=1, mode='constant', cval=0) + + return imx, imy + + +def _compute_auto_correlation(image, sigma): + """Compute auto-correlation matrix using sum of squared differences. + + Parameters + ---------- + image : ndarray + Input image. + sigma : float + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + Axx : ndarray + Element of the auto-correlation matrix for each pixel in input image. + Axy : ndarray + Element of the auto-correlation matrix for each pixel in input image. + Ayy : ndarray + Element of the auto-correlation matrix for each pixel in input image. + + """ + + if image.ndim == 3: + image = img_as_float(rgb2grey(image)) + + imx, imy = _compute_derivatives(image) + + # structure tensore + Axx = ndimage.gaussian_filter(imx * imx, sigma, mode='constant', cval=0) + Axy = ndimage.gaussian_filter(imx * imy, sigma, mode='constant', cval=0) + Ayy = ndimage.gaussian_filter(imy * imy, sigma, mode='constant', cval=0) + + return Axx, Axy, Ayy + + +def corner_kitchen_rosenfeld(image): + """Compute Kitchen and Rosenfeld corner measure response image. + + The corner measure is calculated as follows:: + + (imxx * imy**2 + imyy * imx**2 - 2 * imxy * imx * imy) + ------------------------------------------------------ + (imx**2 + imy**2) + + Where imx and imy are the first and imxx, imxy, imyy the second derivatives. + + Parameters + ---------- + image : ndarray + Input image. + + Returns + ------- + response : ndarray + Kitchen and Rosenfeld response image. + + """ + + imx, imy = _compute_derivatives(image) + imxx, imxy = _compute_derivatives(imx) + imyx, imyy = _compute_derivatives(imy) + + response = (imxx * imy**2 + imyy * imx**2 - 2 * imxy * imx * imy) \ + / (imx**2 + imy**2) + + return response + + +def corner_harris(image, method='k', k=0.05, eps=1e-6, sigma=1): + """Compute Harris corner measure response image. + + This corner detector uses information from the auto-correlation matrix A:: + + A = [(imx**2) (imx*imy)] = [Axx Axy] + [(imx*imy) (imy**2)] [Axy Ayy] + + Where imx and imy are the first derivatives averaged with a gaussian filter. + The corner measure is then defined as:: + + det(A) - k * trace(A)**2 + + or:: + + 2 * det(A) / (trace(A) + eps) + + Parameters + ---------- + image : ndarray + Input image. + method : {'k', 'eps'}, optional + Method to compute the response image from the auto-correlation matrix. + k : float, optional + Sensitivity factor to separate corners from edges, typically in range + `[0, 0.2]`. Small values of k result in detection of sharp corners. + eps : float, optional + Normalisation factor (Noble's corner measure). + sigma : float, optional + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + response : ndarray + Harris response image. + + References + ---------- + ..[1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/harris.htm + ..[2] http://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_harris, corner_peaks + >>> square = np.zeros([10, 10]) + >>> square[2:8, 2:8] = 1 + >>> square + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> corner_peaks(corner_harris(square), min_distance=1) + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) + + """ + + Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + + # determinant + detA = Axx * Ayy - Axy**2 + # trace + traceA = Axx + Ayy + + if method == 'k': + response = detA - k * traceA**2 + else: + response = 2 * detA / (traceA + eps) + + return response + + +def corner_shi_tomasi(image, sigma=1): + """Compute Shi-Tomasi (Kanade-Tomasi) corner measure response image. + + This corner detector uses information from the auto-correlation matrix A:: + + A = [(imx**2) (imx*imy)] = [Axx Axy] + [(imx*imy) (imy**2)] [Axy Ayy] + + Where imx and imy are the first derivatives averaged with a gaussian filter. + The corner measure is then defined as the smaller eigenvalue of A:: + + ((Axx + Ayy) - sqrt((Axx - Ayy)**2 + 4 * Axy**2)) / 2 + + Parameters + ---------- + image : ndarray + Input image. + sigma : float, optional + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + response : ndarray + Shi-Tomasi response image. + + References + ---------- + ..[1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/harris.htm + ..[2] http://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_shi_tomasi, corner_peaks + >>> square = np.zeros([10, 10]) + >>> square[2:8, 2:8] = 1 + >>> square + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> corner_peaks(corner_shi_tomasi(square), min_distance=1) + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) + + """ + + Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + + # minimum eigenvalue of A + response = ((Axx + Ayy) - np.sqrt((Axx - Ayy)**2 + 4 * Axy**2)) / 2 + + return response + + +def corner_foerstner(image, sigma=1): + """Compute Foerstner corner measure response image. + + This corner detector uses information from the auto-correlation matrix A:: + + A = [(imx**2) (imx*imy)] = [Axx Axy] + [(imx*imy) (imy**2)] [Axy Ayy] + + Where imx and imy are the first derivatives averaged with a gaussian filter. + The corner measure is then defined as:: + + w = det(A) / trace(A) (size of error ellipse) + q = 4 * det(A) / trace(A)**2 (roundness of error ellipse) + + Parameters + ---------- + image : ndarray + Input image. + sigma : float, optional + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + w : ndarray + Error ellipse sizes. + q : ndarray + Roundness of error ellipse. + + References + ---------- + ..[1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/\ + foerstner87.fast.pdf + ..[2] http://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_foerstner, corner_peaks + >>> square = np.zeros([10, 10]) + >>> square[2:8, 2:8] = 1 + >>> square + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> w, q = corner_foerstner(square) + >>> accuracy_thresh = 0.5 + >>> roundness_thresh = 0.3 + >>> foerstner = (q > roundness_thresh) * (w > accuracy_thresh) * w + >>> corner_peaks(foerstner, min_distance=1) + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) + + """ + + Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + + # determinant + detA = Axx * Ayy - Axy**2 + # trace + traceA = Axx + Ayy + + w = detA / traceA + q = 4 * detA / traceA**2 + + return w, q + + +def corner_subpix(image, corners, window_size=11, alpha=0.99): + """Determine subpixel position of corners. + + Parameters + ---------- + image : ndarray + Input image. + corners : (N, 2) ndarray + Corner coordinates `(row, col)`. + window_size : int, optional + Search window size for subpixel estimation. + alpha : float, optional + Significance level for point classification. + + Returns + ------- + positions : (N, 2) ndarray + Subpixel corner positions. NaN for "not classified" corners. + + References + ---------- + ..[1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/\ + foerstner87.fast.pdf + ..[2] http://en.wikipedia.org/wiki/Corner_detection + + """ + + # window extent in one direction + wext = (window_size - 1) / 2 + + # normal equation arrays + N_dot = np.zeros((2, 2), dtype=np.double) + N_edge = np.zeros((2, 2), dtype=np.double) + b_dot = np.zeros((2, ), dtype=np.double) + b_edge = np.zeros((2, ), dtype=np.double) + + # critical statistical test values + redundancy = window_size**2 - 2 + t_crit_dot = stats.f.isf(1 - alpha, redundancy, redundancy) + t_crit_edge = stats.f.isf(alpha, redundancy, redundancy) + + # coordinates of pixels within window + y, x = np.mgrid[- wext:wext + 1, - wext:wext + 1] + + corners_subpix = np.zeros_like(corners, dtype=np.double) + + for i, (y0, x0) in enumerate(corners): + + # crop window around corner + border for sobel operator + miny = y0 - wext - 1 + maxy = y0 + wext + 2 + minx = x0 - wext - 1 + maxx = x0 + wext + 2 + window = image[miny:maxy, minx:maxx] + + winx, winy = _compute_derivatives(window) + + # compute gradient suares and remove border + winx_winx = (winx * winx)[1:-1, 1:-1] + winx_winy = (winx * winy)[1:-1, 1:-1] + winy_winy = (winy * winy)[1:-1, 1:-1] + + # sum of squared differences (mean instead of gaussian filter) + Axx = np.sum(winx_winx) + Axy = np.sum(winx_winy) + Ayy = np.sum(winy_winy) + + # sum of squared differences weighted with coordinates + # (mean instead of gaussian filter) + bxx_x = np.sum(winx_winx * x) + bxx_y = np.sum(winx_winx * y) + bxy_x = np.sum(winx_winy * x) + bxy_y = np.sum(winx_winy * y) + byy_x = np.sum(winy_winy * x) + byy_y = np.sum(winy_winy * y) + + # normal equations for subpixel position + N_dot[0, 0] = Axx + N_dot[0, 1] = N_dot[1, 0] = - Axy + N_dot[1, 1] = Ayy + + N_edge[0, 0] = Ayy + N_edge[0, 1] = N_edge[1, 0] = Axy + N_edge[1, 1] = Axx + + b_dot[:] = bxx_y - bxy_x, byy_x - bxy_y + b_edge[:] = byy_y + bxy_x, bxx_x + bxy_y + + # estimated positions + est_dot = np.linalg.solve(N_dot, b_dot) + est_edge = np.linalg.solve(N_edge, b_edge) + + # residuals + ry_dot = y - est_dot[0] + rx_dot = x - est_dot[1] + ry_edge = y - est_edge[0] + rx_edge = x - est_edge[1] + # squared residuals + rxx_dot = rx_dot * rx_dot + rxy_dot = rx_dot * ry_dot + ryy_dot = ry_dot * ry_dot + rxx_edge = rx_edge * rx_edge + rxy_edge = rx_edge * ry_edge + ryy_edge = ry_edge * ry_edge + + # determine corner class (dot or edge) + # variance for different models + var_dot = np.sum(winx_winx * ryy_dot - 2 * winx_winy * rxy_dot \ + + winy_winy * rxx_dot) + var_edge = np.sum(winy_winy * ryy_edge + 2 * winx_winy * rxy_edge \ + + winx_winx * rxx_edge) + # test value (F-distributed) + t = var_edge / var_dot + # 1 for edge, -1 for dot, 0 for "not classified" + corner_class = (t < t_crit_edge) - (t > t_crit_dot) + + if corner_class == - 1: + corners_subpix[i, :] = y0 + est_dot[0], x0 + est_dot[1] + elif corner_class == 0: + corners_subpix[i, :] = np.nan, np.nan + elif corner_class == 1: + corners_subpix[i, :] = y0 + est_edge[0], x0 + est_edge[1] + + return corners_subpix + + +def corner_peaks(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, + exclude_border=True, indices=True, num_peaks=np.inf, + footprint=None, labels=None): + """Find corners in corner measure response image. + + This differs from `skimage.feature.peak_local_max` in that it suppresses + multiple connected peaks with the same accumulator value. + + Parameters + ---------- + See `skimage.feature.peak_local_max`. + + Returns + ------- + See `skimage.feature.peak_local_max`. + + Examples + -------- + >>> from skimage.feature import peak_local_max, corner_peaks + >>> response = np.zeros((5, 5)) + >>> response[2:4, 2:4] = 1 + >>> response + array([[ 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0.], + [ 0., 0., 1., 1., 0.], + [ 0., 0., 1., 1., 0.], + [ 0., 0., 0., 0., 0.]]) + >>> peak_local_max(response, exclude_border=False) + array([[2, 2], + [2, 3], + [3, 2], + [3, 3]]) + >>> corner_peaks(response, exclude_border=False) + array([[2, 2]]) + >>> corner_peaks(response, exclude_border=False, min_distance=0) + array([[2, 2], + [2, 3], + [3, 2], + [3, 3]]) + + """ + + peaks = peak_local_max(image, min_distance=min_distance, + threshold_abs=threshold_abs, + threshold_rel=threshold_rel, + exclude_border=exclude_border, + indices=False, num_peaks=np.inf, + footprint=footprint, labels=labels) + if min_distance > 0: + coords = np.transpose(peaks.nonzero()) + for r, c in coords: + if peaks[r, c]: + peaks[r - min_distance:r + min_distance + 1, + c - min_distance:c + min_distance + 1] = False + peaks[r, c] = True + + if indices is True: + return np.transpose(peaks.nonzero()) + else: + return peaks diff --git a/skimage/feature/corner_cy.pyx b/skimage/feature/corner_cy.pyx new file mode 100644 index 00000000..71c748f8 --- /dev/null +++ b/skimage/feature/corner_cy.pyx @@ -0,0 +1,91 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +cimport numpy as cnp +from libc.float cimport DBL_MAX + +from skimage.color import rgb2grey +from skimage.util import img_as_float + + +def corner_moravec(image, Py_ssize_t window_size=1): + """Compute Moravec corner measure response image. + + This is one of the simplest corner detectors and is comparatively fast but + has several limitations (e.g. not rotation invariant). + + Parameters + ---------- + image : ndarray + Input image. + window_size : int, optional + Window size. + + Returns + ------- + response : ndarray + Moravec response image. + + References + ---------- + ..[1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/moravec.htm + ..[2] http://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import moravec, peak_local_max + >>> square = np.zeros([7, 7]) + >>> square[3, 3] = 1 + >>> square + array([[ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 1., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.]]) + >>> moravec(square) + array([[ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 1., 1., 1., 0., 0.], + [ 0., 0., 1., 2., 1., 0., 0.], + [ 0., 0., 1., 1., 1., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0.]]) + """ + + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + + cdef cnp.ndarray[dtype=cnp.double_t, ndim=2, mode='c'] cimage, out + + if image.ndim == 3: + cimage = rgb2grey(image) + cimage = np.ascontiguousarray(img_as_float(image)) + + out = np.zeros(image.shape, dtype=np.double) + + cdef double* image_data = cimage.data + cdef double* out_data = out.data + + cdef double msum, min_msum + cdef Py_ssize_t r, c, br, bc, mr, mc, a, b + for r in range(2 * window_size, rows - 2 * window_size): + for c in range(2 * window_size, cols - 2 * window_size): + min_msum = DBL_MAX + for br in range(r - window_size, r + window_size + 1): + for bc in range(c - window_size, c + window_size + 1): + if br != r and bc != c: + msum = 0 + for mr in range(- window_size, window_size + 1): + for mc in range(- window_size, window_size + 1): + a = (r + mr) * cols + c + mc + b = (br + mr) * cols + bc + mc + msum += (image_data[a] - image_data[b]) ** 2 + min_msum = min(msum, min_msum) + + out_data[r * cols + c] = min_msum + + return out diff --git a/skimage/feature/peak.py b/skimage/feature/peak.py index 4765974e..9c7a934f 100644 --- a/skimage/feature/peak.py +++ b/skimage/feature/peak.py @@ -1,46 +1,67 @@ -import warnings import numpy as np -from scipy import ndimage +import scipy.ndimage as ndi +from ..filter import rank_order -def peak_local_max(image, min_distance=10, threshold='deprecated', - threshold_abs=0, threshold_rel=0.1, num_peaks=np.inf): - """Return coordinates of peaks in an image. +def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, + exclude_border=True, indices=True, num_peaks=np.inf, + footprint=None, labels=None): + """ + Find peaks in an image, and return them as coordinates or a boolean array. Peaks are the local maxima in a region of `2 * min_distance + 1` (i.e. peaks are separated by at least `min_distance`). - NOTE: If peaks are flat (i.e. multiple pixels have exact same intensity), - the coordinates of all pixels are returned. + NOTE: If peaks are flat (i.e. multiple adjacent pixels have identical + intensities), the coordinates of all such pixels are returned. Parameters ---------- image : ndarray of floats Input image. min_distance : int - Minimum number of pixels separating peaks and image boundary. - threshold : float - Deprecated. See `threshold_rel`. + Minimum number of pixels separating peaks in a region of `2 * + min_distance + 1` (i.e. peaks are separated by at least + `min_distance`). If `exclude_border` is True, this value also excludes + a border `min_distance` from the image boundary. + To find the maximum number of peaks, use `min_distance=1`. threshold_abs : float Minimum intensity of peaks. threshold_rel : float Minimum intensity of peaks calculated as `max(image) * threshold_rel`. + exclude_border : bool + If True, `min_distance` excludes peaks from the border of the image as + well as from each other. + indices : bool + If True, the output will be a matrix representing peak coordinates. + If False, the output will be a boolean matrix shaped as `image.shape` + with peaks present at True elements. num_peaks : int Maximum number of peaks. When the number of peaks exceeds `num_peaks`, - return `num_peaks` coordinates based on peak intensity. + return `num_peaks` peaks based on highest peak intensity. + footprint : ndarray of bools, optional + If provided, `footprint == 1` represents the local region within which + to search for peaks at every point in `image`. Overrides + `min_distance`, except for border exclusion if `exclude_border=True`. + labels : ndarray of ints, optional + If provided, each unique region `labels == value` represents a unique + region to search for peaks. Zero is reserved for background. Returns ------- - coordinates : (N, 2) array - (row, column) coordinates of peaks. + output : (N, 2) array or ndarray of bools + + * If `indices = True` : (row, column) coordinates of peaks. + * If `indices = False` : Boolean array shaped like `image`, with peaks + represented by True values. Notes ----- - The peak local maximum function returns the coordinates of local peaks (maxima) - in a image. A maximum filter is used for finding local maxima. This operation - dilates the original image. After comparison between dilated and original image, - peak_local_max function returns the coordinates of peaks where - dilated image = original. + The peak local maximum function returns the coordinates of local peaks + (maxima) in a image. A maximum filter is used for finding local maxima. + This operation dilates the original image. After comparison between + dilated and original image, peak_local_max function returns the + coordinates of peaks where dilated image = original. Examples -------- @@ -64,35 +85,70 @@ def peak_local_max(image, min_distance=10, threshold='deprecated', array([[3, 2]]) """ + out = np.zeros_like(image, dtype=np.bool) + # In the case of labels, recursively build and return an output + # operating on each label separately + if labels is not None: + label_values = np.unique(labels) + # Reorder label values to have consecutive integers (no gaps) + if np.any(np.diff(label_values) != 1): + mask = labels >= 1 + labels[mask] = 1 + rank_order(labels[mask])[0].astype(labels.dtype) + labels = labels.astype(np.int32) + + # New values for new ordering + label_values = np.unique(labels) + for label in label_values[label_values != 0]: + maskim = (labels == label) + out += peak_local_max(image * maskim, min_distance=min_distance, + threshold_abs=threshold_abs, + threshold_rel=threshold_rel, + exclude_border=exclude_border, + indices=False, num_peaks=np.inf, + footprint=footprint, labels=None) + + if indices is True: + return np.transpose(out.nonzero()) + else: + return out.astype(np.bool) + if np.all(image == image.flat[0]): - return [] + if indices is True: + return [] + else: + return out + image = image.copy() # Non maximum filter - size = 2 * min_distance + 1 - image_max = ndimage.maximum_filter(image, size=size, mode='constant') + if footprint is not None: + image_max = ndi.maximum_filter(image, footprint=footprint, + mode='constant') + else: + size = 2 * min_distance + 1 + image_max = ndi.maximum_filter(image, size=size, mode='constant') mask = (image == image_max) image *= mask - # Remove the image borders - image[:min_distance] = 0 - image[-min_distance:] = 0 - image[:, :min_distance] = 0 - image[:, -min_distance:] = 0 + if exclude_border: + # Remove the image borders + image[:min_distance] = 0 + image[-min_distance:] = 0 + image[:, :min_distance] = 0 + image[:, -min_distance:] = 0 - if not threshold == 'deprecated': - msg = "`threshold` parameter deprecated; use `threshold_rel instead." - warnings.warn(msg, DeprecationWarning) - threshold_rel = threshold # find top peak candidates above a threshold peak_threshold = max(np.max(image.ravel()) * threshold_rel, threshold_abs) - image_t = (image > peak_threshold) * 1 # get coordinates of peaks - coordinates = np.transpose(image_t.nonzero()) + coordinates = np.transpose((image > peak_threshold).nonzero()) if coordinates.shape[0] > num_peaks: intensities = image[coordinates[:, 0], coordinates[:, 1]] idx_maxsort = np.argsort(intensities)[::-1] coordinates = coordinates[idx_maxsort][:num_peaks] - return coordinates + if indices is True: + return coordinates + else: + out[coordinates[:, 0], coordinates[:, 1]] = True + return out diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index 6f820163..e769621d 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -12,9 +12,12 @@ def configuration(parent_package='', top_path=None): config = Configuration('feature', parent_package, top_path) config.add_data_dir('tests') + cython(['corner_cy.pyx'], working_path=base_path) cython(['_texture.pyx'], working_path=base_path) cython(['_template.pyx'], working_path=base_path) + config.add_extension('corner_cy', sources=['corner_cy.c'], + include_dirs=[get_numpy_include_dirs()]) config.add_extension('_texture', sources=['_texture.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) config.add_extension('_template', sources=['_template.c'], diff --git a/skimage/feature/tests/test_corner.py b/skimage/feature/tests/test_corner.py new file mode 100644 index 00000000..a7ee01ff --- /dev/null +++ b/skimage/feature/tests/test_corner.py @@ -0,0 +1,116 @@ +import numpy as np +from numpy.testing import assert_array_equal + +from skimage import data +from skimage import img_as_float + +from skimage.feature import (corner_moravec, corner_harris, corner_shi_tomasi, + corner_subpix, peak_local_max, corner_peaks) + + +def test_square_image(): + im = np.zeros((50, 50)).astype(float) + im[:25, :25] = 1. + + # Moravec + results = peak_local_max(corner_moravec(im)) + # interest points along edge + assert len(results) == 57 + + # Harris + results = peak_local_max(corner_harris(im)) + # interest at corner + assert len(results) == 1 + + # Shi-Tomasi + results = peak_local_max(corner_shi_tomasi(im)) + # interest at corner + assert len(results) == 1 + + +def test_noisy_square_image(): + im = np.zeros((50, 50)).astype(float) + im[:25, :25] = 1. + np.random.seed(seed=1234) + im = im + np.random.uniform(size=im.shape) * .2 + + # Moravec + results = peak_local_max(corner_moravec(im)) + # undefined number of interest points + assert results.any() + + # Harris + results = peak_local_max(corner_harris(im, sigma=1.5)) + assert len(results) == 1 + + # Shi-Tomasi + results = peak_local_max(corner_shi_tomasi(im, sigma=1.5)) + assert len(results) == 1 + + +def test_squared_dot(): + im = np.zeros((50, 50)) + im[4:8, 4:8] = 1 + im = img_as_float(im) + + # Moravec fails + + # Harris + results = peak_local_max(corner_harris(im)) + assert (results == np.array([[6, 6]])).all() + + # Shi-Tomasi + results = peak_local_max(corner_shi_tomasi(im)) + assert (results == np.array([[6, 6]])).all() + + +def test_rotated_lena(): + """ + The harris filter should yield the same results with an image and it's + rotation. + """ + im = img_as_float(data.lena().mean(axis=2)) + im_rotated = im.T + + # Moravec + results = peak_local_max(corner_moravec(im)) + results_rotated = peak_local_max(corner_moravec(im_rotated)) + assert (np.sort(results[:, 0]) == np.sort(results_rotated[:, 1])).all() + assert (np.sort(results[:, 1]) == np.sort(results_rotated[:, 0])).all() + + # Harris + results = peak_local_max(corner_harris(im)) + results_rotated = peak_local_max(corner_harris(im_rotated)) + assert (np.sort(results[:, 0]) == np.sort(results_rotated[:, 1])).all() + assert (np.sort(results[:, 1]) == np.sort(results_rotated[:, 0])).all() + + # Shi-Tomasi + results = peak_local_max(corner_shi_tomasi(im)) + results_rotated = peak_local_max(corner_shi_tomasi(im_rotated)) + assert (np.sort(results[:, 0]) == np.sort(results_rotated[:, 1])).all() + assert (np.sort(results[:, 1]) == np.sort(results_rotated[:, 0])).all() + + +def test_subpix(): + img = np.zeros((50, 50)) + img[:25,:25] = 255 + img[25:,25:] = 255 + corner = peak_local_max(corner_harris(img), num_peaks=1) + subpix = corner_subpix(img, corner) + assert_array_equal(subpix[0], (24.5, 24.5)) + + +def test_corner_peaks(): + response = np.zeros((5, 5)) + response[2:4, 2:4] = 1 + + corners = corner_peaks(response, exclude_border=False) + assert len(corners) == 1 + + corners = corner_peaks(response, exclude_border=False, min_distance=0) + assert len(corners) == 4 + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_daisy.py b/skimage/feature/tests/test_daisy.py new file mode 100644 index 00000000..40781a64 --- /dev/null +++ b/skimage/feature/tests/test_daisy.py @@ -0,0 +1,95 @@ +import numpy as np +from numpy.testing import assert_raises, assert_almost_equal +from numpy import sqrt, ceil + +from skimage import data +from skimage import img_as_float +from skimage.feature import daisy + + +def test_daisy_color_image_unsupported_error(): + img = np.zeros((20, 20, 3)) + assert_raises(ValueError, daisy, img) + + +def test_daisy_desc_dims(): + img = img_as_float(data.lena()[:128, :128].mean(axis=2)) + rings = 2 + histograms = 4 + orientations = 3 + descs = daisy(img, rings=rings, histograms=histograms, + orientations=orientations) + assert(descs.shape[2] == (rings * histograms + 1) * orientations) + + rings = 4 + histograms = 5 + orientations = 13 + descs = daisy(img, rings=rings, histograms=histograms, + orientations=orientations) + assert(descs.shape[2] == (rings * histograms + 1) * orientations) + + +def test_descs_shape(): + img = img_as_float(data.lena()[:256, :256].mean(axis=2)) + radius = 20 + step = 8 + descs = daisy(img, radius=radius, step=step) + assert(descs.shape[0] == ceil((img.shape[0] - radius * 2) / float(step))) + assert(descs.shape[1] == ceil((img.shape[1] - radius * 2) / float(step))) + + img = img[:-1, :-2] + radius = 5 + step = 3 + descs = daisy(img, radius=radius, step=step) + assert(descs.shape[0] == ceil((img.shape[0] - radius * 2) / float(step))) + assert(descs.shape[1] == ceil((img.shape[1] - radius * 2) / float(step))) + + +def test_daisy_incompatible_sigmas_and_radii(): + img = img_as_float(data.lena()[:128, :128].mean(axis=2)) + sigmas = [1, 2] + radii = [1, 2] + assert_raises(ValueError, daisy, img, sigmas=sigmas, ring_radii=radii) + + +def test_daisy_normalization(): + img = img_as_float(data.lena()[:64, :64].mean(axis=2)) + + descs = daisy(img, normalization='l1') + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + assert_almost_equal(np.sum(descs[i, j, :]), 1) + descs_ = daisy(img) + assert_almost_equal(descs, descs_) + + descs = daisy(img, normalization='l2') + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + assert_almost_equal(sqrt(np.sum(descs[i, j, :] ** 2)), 1) + + orientations = 8 + descs = daisy(img, orientations=orientations, normalization='daisy') + desc_dims = descs.shape[2] + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + for k in range(0, desc_dims, orientations): + assert_almost_equal(sqrt(np.sum( + descs[i, j, k:k + orientations] ** 2)), 1) + + img = np.zeros((50, 50)) + descs = daisy(img, normalization='off') + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + assert_almost_equal(np.sum(descs[i, j, :]), 0) + + assert_raises(ValueError, daisy, img, normalization='does_not_exist') + + +def test_daisy_visualization(): + img = img_as_float(data.lena()[:128, :128].mean(axis=2)) + descs, descs_img = daisy(img, visualize=True) + assert(descs_img.shape == (128, 128, 3)) + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_harris.py b/skimage/feature/tests/test_harris.py deleted file mode 100644 index 43bf28a3..00000000 --- a/skimage/feature/tests/test_harris.py +++ /dev/null @@ -1,49 +0,0 @@ -import numpy as np - -from skimage import data -from skimage import img_as_float - -from skimage.feature import harris - - -def test_square_image(): - im = np.zeros((50, 50)).astype(float) - im[:25, :25] = 1. - results = harris(im) - assert results.any() - assert len(results) == 1 - - -def test_noisy_square_image(): - im = np.zeros((50, 50)).astype(float) - im[:25, :25] = 1. - im = im + np.random.uniform(size=im.shape) * .5 - results = harris(im) - assert results.any() - assert len(results) == 1 - - -def test_squared_dot(): - im = np.zeros((50, 50)) - im[4:8, 4:8] = 1 - im = img_as_float(im) - results = harris(im, min_distance=3) - assert (results == np.array([[6, 6]])).all() - - -def test_rotated_lena(): - """ - The harris filter should yield the same results with an image and it's - rotation. - """ - im = img_as_float(data.lena().mean(axis=2)) - results = harris(im) - im_rotated = im.T - results_rotated = harris(im_rotated) - assert (np.sort(results[:, 0]) == np.sort(results_rotated[:, 1])).all() - assert (np.sort(results[:, 1]) == np.sort(results_rotated[:, 0])).all() - - -if __name__ == '__main__': - from numpy import testing - testing.run_module_suite() diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index 6f2d4cdf..cfef2da0 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -102,7 +102,7 @@ def test_hog_orientations_circle(): width = height = 100 image = np.zeros((height, width)) - rr, cc = draw.circle(height/2, width/2, width/3) + rr, cc = draw.circle(int(height / 2), int(width / 2), int(width / 3)) image[rr, cc] = 100 image = ndimage.gaussian_filter(image, 2) diff --git a/skimage/feature/tests/test_peak.py b/skimage/feature/tests/test_peak.py index 13457781..3ef1f12d 100644 --- a/skimage/feature/tests/test_peak.py +++ b/skimage/feature/tests/test_peak.py @@ -1,9 +1,17 @@ import numpy as np from numpy.testing import assert_array_almost_equal as assert_close - +import scipy.ndimage from skimage.feature import peak +def test_trivial_case(): + trivial = np.zeros((25, 25)) + peak_indices = peak.peak_local_max(trivial, min_distance=1, indices=True) + assert not peak_indices # inherent boolean-ness of empty list + peaks = peak.peak_local_max(trivial, min_distance=1, indices=False) + assert (peaks.astype(np.bool) == trivial).all() + + def test_noisy_peaks(): peak_locations = [(7, 7), (7, 13), (13, 7), (13, 13)] @@ -70,6 +78,45 @@ def test_num_peaks(): assert (3, 5) in peaks_limited +def test_reorder_labels(): + np.random.seed(21) + image = np.random.uniform(size=(40, 60)) + i, j = np.mgrid[0:40, 0:60] + labels = 1 + (i >= 20) + (j >= 30) * 2 + labels[labels == 4] = 5 + i, j = np.mgrid[-3:4, -3:4] + footprint = (i * i + j * j <= 9) + expected = np.zeros(image.shape, float) + for imin, imax in ((0, 20), (20, 40)): + for jmin, jmax in ((0, 30), (30, 60)): + expected[imin:imax, jmin:jmax] = scipy.ndimage.maximum_filter( + image[imin:imax, jmin:jmax], footprint=footprint) + expected = (expected == image) + result = peak.peak_local_max(image, labels=labels, min_distance=1, + threshold_rel=0, footprint=footprint, + indices=False, exclude_border=False) + assert (result == expected).all() + + +def test_indices_with_labels(): + np.random.seed(21) + image = np.random.uniform(size=(40, 60)) + i, j = np.mgrid[0:40, 0:60] + labels = 1 + (i >= 20) + (j >= 30) * 2 + i, j = np.mgrid[-3:4, -3:4] + footprint = (i * i + j * j <= 9) + expected = np.zeros(image.shape, float) + for imin, imax in ((0, 20), (20, 40)): + for jmin, jmax in ((0, 30), (30, 60)): + expected[imin:imax, jmin:jmax] = scipy.ndimage.maximum_filter( + image[imin:imax, jmin:jmax], footprint=footprint) + expected = (expected == image) + result = peak.peak_local_max(image, labels=labels, min_distance=1, + threshold_rel=0, footprint=footprint, + indices=True, exclude_border=False) + assert (result == np.transpose(expected.nonzero())).all() + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index bdbbb531..f32c3b23 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,7 +1,9 @@ from .lpi_filter import * from .ctmf import median_filter from ._canny import canny -from .edges import sobel, hsobel, vsobel, hprewitt, vprewitt, prewitt -from ._tv_denoise import tv_denoise +from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, + hprewitt, vprewitt) +from ._denoise import denoise_tv_chambolle, tv_denoise +from ._denoise_cy import denoise_bilateral, denoise_tv_bregman from ._rank_order import rank_order from .thresholding import threshold_otsu, threshold_adaptive diff --git a/skimage/filter/_ctmf.pyx b/skimage/filter/_ctmf.pyx index c133d8d6..1a03ff5a 100644 --- a/skimage/filter/_ctmf.pyx +++ b/skimage/filter/_ctmf.pyx @@ -10,14 +10,20 @@ Copyright (c) 2009-2011 Broad Institute All rights reserved. Original author: Lee Kamentsky ''' + import numpy as np -cimport numpy as np + +cimport numpy as cnp cimport cython from libc.stdlib cimport malloc, free from libc.string cimport memset -np.import_array() + +cdef extern from "../_shared/vectorized_ops.h": + void add16(cnp.uint16_t *dest, cnp.uint16_t *src) + void sub16(cnp.uint16_t *dest, cnp.uint16_t *src) + ############################################################################## # @@ -39,7 +45,7 @@ np.import_array() DTYPE_UINT32 = np.uint32 DTYPE_BOOL = np.bool -ctypedef np.uint16_t pixel_count_t +ctypedef cnp.uint16_t pixel_count_t ########### # @@ -54,15 +60,15 @@ ctypedef np.uint16_t pixel_count_t ########### cdef struct HistogramPiece: - np.uint16_t coarse[16] - np.uint16_t fine[256] + cnp.uint16_t coarse[16] + cnp.uint16_t fine[256] cdef struct Histogram: - HistogramPiece top_left # top-left corner - HistogramPiece top_right # top-right corner - HistogramPiece edge # leading/trailing edge - HistogramPiece bottom_left # bottom-left corner - HistogramPiece bottom_right # bottom-right corner + HistogramPiece top_left # top-left corner + HistogramPiece top_right # top-right corner + HistogramPiece edge # leading/trailing edge + HistogramPiece bottom_left # bottom-left corner + HistogramPiece bottom_right # bottom-right corner # The pixel count has the number of pixels histogrammed in # each of the five compartments for this position. This changes @@ -80,27 +86,27 @@ cdef struct PixelCount: # relative offsets from the octagon center # cdef struct SCoord: - np.int32_t stride # add the stride to the memory location - np.int32_t x - np.int32_t y + Py_ssize_t stride # add the stride to the memory location + Py_ssize_t x + Py_ssize_t y cdef struct Histograms: void *memory # pointer to the allocated memory Histogram *histogram # pointer to the histogram memory PixelCount *pixel_count # pointer to the pixel count memory - np.uint8_t *data # pointer to the image data - np.uint8_t *mask # pointer to the image mask - np.uint8_t *output # pointer to the output array - np.int32_t column_count # number of columns represented by this + cnp.uint8_t *data # pointer to the image data + cnp.uint8_t *mask # pointer to the image mask + cnp.uint8_t *output # pointer to the output array + Py_ssize_t column_count # number of columns represented by this # structure - np.int32_t stripe_length # number of columns including "radius" before + Py_ssize_t stripe_length # number of columns including "radius" before # and after - np.int32_t row_count # number of rows available in image - np.int32_t current_column # the column being processed - np.int32_t current_row # the row being processed - np.int32_t current_stride # offset in data and mask to current location - np.int32_t radius # the "radius" of the octagon - np.int32_t a_2 # 1/2 of the length of a side of the octagon + Py_ssize_t row_count # number of rows available in image + Py_ssize_t current_column # the column being processed + Py_ssize_t current_row # the row being processed + Py_ssize_t current_stride # offset in data and mask to current location + Py_ssize_t radius # the "radius" of the octagon + Py_ssize_t a_2 # 1/2 of the length of a side of the octagon # # # The strides are the offsets in the array to the points that need to @@ -123,83 +129,83 @@ cdef struct Histograms: # # x --> # - SCoord last_top_left # (-) left side of octagon's top - 1 row - SCoord top_left # (+) -1 row from trailing edge top - SCoord last_top_right # (-) right side of octagon's top - 1 col - 1 row - SCoord top_right # (+) -1 col -1 row from leading edge top - SCoord last_leading_edge # (-) leading edge (right) top stride - 1 row - SCoord leading_edge # (+) leading edge bottom stride - SCoord last_bottom_right # (-) leading edge bottom - 1 col - SCoord bottom_right # (+) right side of octagon's bottom - 1 col - SCoord last_bottom_left # (-) trailing edge bottom - 1 col - SCoord bottom_left # (+) left side of octagon's bottom - 1 col + SCoord last_top_left # (-) left side of octagon's top - 1 row + SCoord top_left # (+) -1 row from trailing edge top + SCoord last_top_right # (-) right side of octagon's top - 1 col - 1 row + SCoord top_right # (+) -1 col -1 row from leading edge top + SCoord last_leading_edge # (-) leading edge (right) top stride - 1 row + SCoord leading_edge # (+) leading edge bottom stride + SCoord last_bottom_right # (-) leading edge bottom - 1 col + SCoord bottom_right # (+) right side of octagon's bottom - 1 col + SCoord last_bottom_left # (-) trailing edge bottom - 1 col + SCoord bottom_left # (+) left side of octagon's bottom - 1 col - np.int32_t row_stride # stride between one row and the next - np.int32_t col_stride # stride between one column and the next + Py_ssize_t row_stride # stride between one row and the next + Py_ssize_t col_stride # stride between one column and the next # The accumulator holds the running histogram # HistogramPiece accumulator # # The running count of pixels in the accumulator # - np.uint32_t accumulator_count + Py_ssize_t accumulator_count # # The percent of pixels within the octagon whose value is # less than or equal to the median-filtered value (e.g. for # median, this is 50, for lower quartile it's 25) # - np.int32_t percent + Py_ssize_t percent # # last_update_column keeps track of the column # of the last update # to the fine histogram accumulator. Short-term, the median # stays in one coarse block so only one fine histogram might # need to be updated # - np.int32_t last_update_column[16] + Py_ssize_t last_update_column[16] ############################################################################ # # allocate_histograms - allocates the Histograms structure for the run # ############################################################################ -cdef Histograms *allocate_histograms(np.int32_t rows, - np.int32_t columns, - np.int32_t row_stride, - np.int32_t col_stride, - np.int32_t radius, - np.int32_t percent, - np.uint8_t *data, - np.uint8_t *mask, - np.uint8_t *output): +cdef Histograms *allocate_histograms(Py_ssize_t rows, + Py_ssize_t columns, + Py_ssize_t row_stride, + Py_ssize_t col_stride, + Py_ssize_t radius, + Py_ssize_t percent, + cnp.uint8_t *data, + cnp.uint8_t *mask, + cnp.uint8_t *output): cdef: - unsigned int adjusted_stripe_length = columns + 2*radius + 1 - unsigned int memory_size + Py_ssize_t adjusted_stripe_length = columns + 2*radius + 1 + Py_ssize_t memory_size void *ptr Histograms *ph - size_t roundoff - int a + Py_ssize_t roundoff + Py_ssize_t a SCoord *psc memory_size = (adjusted_stripe_length * - (sizeof(Histogram) + sizeof(PixelCount))+ - sizeof(Histograms)+32) + (sizeof(Histogram) + sizeof(PixelCount)) + + sizeof(Histograms) + 32) ptr = malloc(memory_size) memset(ptr, 0, memory_size) - ph = ptr + ph = ptr if not ptr: return ph ph.memory = ptr - ptr = (ph+1) - ph.pixel_count = ptr - ptr = (ph.pixel_count + adjusted_stripe_length) + ptr = (ph + 1) + ph.pixel_count = ptr + ptr = (ph.pixel_count + adjusted_stripe_length) # # Align histogram memory to a 32-byte boundary # - roundoff = ptr + roundoff = ptr roundoff += 31 roundoff -= roundoff % 32 - ptr = roundoff - ph.histogram = ptr + ptr = roundoff + ph.histogram = ptr # # Fill in the statistical things we keep around # @@ -228,7 +234,7 @@ cdef Histograms *allocate_histograms(np.int32_t rows, # a_2 is the offset from the center to each of the octagon # corners # - a = (radius * 2.0 / 2.414213) + a = (radius * 2.0 / 2.414213) a_2 = a / 2 if a_2 == 0: a_2 = 1 @@ -322,34 +328,18 @@ cdef void set_stride(Histograms *ph, SCoord *psc): # a column that is "radius" to the left. # ############################################################################ -cdef inline np.int32_t tl_br_colidx(Histograms *ph, np.int32_t colidx): +cdef inline Py_ssize_t tl_br_colidx(Histograms *ph, Py_ssize_t colidx): return (colidx + 3*ph.radius + ph.current_row) % ph.stripe_length -cdef inline np.int32_t tr_bl_colidx(Histograms *ph, np.int32_t colidx): +cdef inline Py_ssize_t tr_bl_colidx(Histograms *ph, Py_ssize_t colidx): return (colidx + 3*ph.radius + ph.row_count-ph.current_row) % \ ph.stripe_length -cdef inline np.int32_t leading_edge_colidx(Histograms *ph, np.int32_t colidx): +cdef inline Py_ssize_t leading_edge_colidx(Histograms *ph, Py_ssize_t colidx): return (colidx + 5*ph.radius) % ph.stripe_length -cdef inline np.int32_t trailing_edge_colidx(Histograms *ph, np.int32_t colidx): +cdef inline Py_ssize_t trailing_edge_colidx(Histograms *ph, Py_ssize_t colidx): return (colidx + 3*ph.radius - 1) % ph.stripe_length -# -# add16 - add 16 consecutive integers -# -# Add an array of 16 16-bit integers to an accumulator of 16 16-bit integers -# -# TO_DO - optimize using SIMD instructions -# -cdef inline void add16(np.uint16_t *dest, np.uint16_t *src): - cdef int i - for i in range(16): - dest[i] += src[i] - -cdef inline void sub16(np.uint16_t *dest, np.uint16_t *src): - cdef int i - for i in range(16): - dest[i] -= src[i] ############################################################################ # @@ -360,9 +350,8 @@ cdef inline void sub16(np.uint16_t *dest, np.uint16_t *src): # colidx - the index of the column to add # ############################################################################ -cdef inline void accumulate_coarse_histogram(Histograms *ph, np.int32_t colidx): - cdef: - int offset +cdef inline void accumulate_coarse_histogram(Histograms *ph, Py_ssize_t colidx): + cdef Py_ssize_t offset offset = tr_bl_colidx(ph, colidx) if ph.pixel_count[offset].top_right > 0: @@ -383,9 +372,8 @@ cdef inline void accumulate_coarse_histogram(Histograms *ph, np.int32_t colidx): # for a given column # ############################################################################ -cdef inline void deaccumulate_coarse_histogram(Histograms *ph, np.int32_t colidx): - cdef: - int offset +cdef inline void deaccumulate_coarse_histogram(Histograms *ph, Py_ssize_t colidx): + cdef Py_ssize_t offset # # The trailing diagonals don't appear until here # @@ -414,11 +402,11 @@ cdef inline void deaccumulate_coarse_histogram(Histograms *ph, np.int32_t colidx # ############################################################################ cdef inline void accumulate_fine_histogram(Histograms *ph, - np.int32_t colidx, - np.uint32_t fineidx): + Py_ssize_t colidx, + Py_ssize_t fineidx): cdef: - int fineoffset = fineidx * 16 - int offset + Py_ssize_t fineoffset = fineidx * 16 + Py_ssize_t offset offset = tr_bl_colidx(ph, colidx) add16(ph.accumulator.fine + fineoffset, @@ -438,11 +426,11 @@ cdef inline void accumulate_fine_histogram(Histograms *ph, # ############################################################################ cdef inline void deaccumulate_fine_histogram(Histograms *ph, - np.int32_t colidx, - np.uint32_t fineidx): + Py_ssize_t colidx, + Py_ssize_t fineidx): cdef: - int fineoffset = fineidx * 16 - int offset + Py_ssize_t fineoffset = fineidx * 16 + Py_ssize_t offset # # The trailing diagonals don't appear until here @@ -470,10 +458,7 @@ cdef inline void deaccumulate_fine_histogram(Histograms *ph, ############################################################################ cdef inline void accumulate(Histograms *ph): - cdef: - int i - int j - np.int32_t accumulator + cdef cnp.int32_t accumulator accumulate_coarse_histogram(ph, ph.current_column) deaccumulate_coarse_histogram(ph, ph.current_column) @@ -497,11 +482,11 @@ cdef inline void accumulate(Histograms *ph): # to choose remains to be done. ############################################################################ -cdef inline void update_fine(Histograms *ph, int fineidx): +cdef inline void update_fine(Histograms *ph, Py_ssize_t fineidx): cdef: - int first_update_column = ph.last_update_column[fineidx]+1 - int update_limit = ph.current_column+1 - int i + Py_ssize_t first_update_column = ph.last_update_column[fineidx]+1 + Py_ssize_t update_limit = ph.current_column+1 + Py_ssize_t i for i in range(first_update_column, update_limit): accumulate_fine_histogram(ph, i, fineidx) @@ -526,23 +511,23 @@ cdef inline void update_histogram(Histograms *ph, SCoord *last_coord, SCoord *coord): cdef: - np.int32_t current_column = ph.current_column - np.int32_t current_row = ph.current_row - np.int32_t current_stride = ph.current_stride - np.int32_t column_count = ph.column_count - np.int32_t row_count = ph.row_count - np.uint8_t value - np.int32_t stride - np.int32_t x - np.int32_t y + Py_ssize_t current_column = ph.current_column + Py_ssize_t current_row = ph.current_row + Py_ssize_t current_stride = ph.current_stride + Py_ssize_t column_count = ph.column_count + Py_ssize_t row_count = ph.row_count + cnp.uint8_t value + Py_ssize_t stride + Py_ssize_t x + Py_ssize_t y x = last_coord.x + current_column y = last_coord.y + current_row stride = current_stride+last_coord.stride - if (x >= 0 and x < column_count and - y >= 0 and y < row_count and - ph.mask[stride]): + if (x >= 0 and x < column_count and \ + y >= 0 and y < row_count and \ + ph.mask[stride]): value = ph.data[stride] pixel_count[0] -= 1 hist_piece.fine[value] -= 1 @@ -552,9 +537,9 @@ cdef inline void update_histogram(Histograms *ph, y = coord.y + current_row stride = current_stride + coord.stride - if (x >= 0 and x < column_count and - y >= 0 and y < row_count and - ph.mask[stride]): + if (x >= 0 and x < column_count and \ + y >= 0 and y < row_count and \ + ph.mask[stride]): value = ph.data[stride] pixel_count[0] += 1 hist_piece.fine[value] += 1 @@ -567,21 +552,21 @@ cdef inline void update_histogram(Histograms *ph, ############################################################################ cdef inline void update_current_location(Histograms *ph): cdef: - np.int32_t current_column = ph.current_column - np.int32_t radius = ph.radius - np.int32_t top_left_off = tl_br_colidx(ph, current_column) - np.int32_t top_right_off = tr_bl_colidx(ph, current_column) - np.int32_t bottom_left_off = tr_bl_colidx(ph, current_column) - np.int32_t bottom_right_off = tl_br_colidx(ph, current_column) - np.int32_t leading_edge_off = leading_edge_colidx(ph, current_column) - np.int32_t *coarse_histogram - np.int32_t *fine_histogram - np.int32_t last_xoff - np.int32_t last_yoff - np.int32_t last_stride - np.int32_t xoff - np.int32_t yoff - np.int32_t stride + Py_ssize_t current_column = ph.current_column + Py_ssize_t radius = ph.radius + Py_ssize_t top_left_off = tl_br_colidx(ph, current_column) + Py_ssize_t top_right_off = tr_bl_colidx(ph, current_column) + Py_ssize_t bottom_left_off = tr_bl_colidx(ph, current_column) + Py_ssize_t bottom_right_off = tl_br_colidx(ph, current_column) + Py_ssize_t leading_edge_off = leading_edge_colidx(ph, current_column) + cnp.int32_t *coarse_histogram + cnp.int32_t *fine_histogram + Py_ssize_t last_xoff + Py_ssize_t last_yoff + Py_ssize_t last_stride + Py_ssize_t xoff + Py_ssize_t yoff + Py_ssize_t stride update_histogram(ph, &ph.histogram[top_left_off].top_left, &ph.pixel_count[top_left_off].top_left, @@ -614,18 +599,20 @@ cdef inline void update_current_location(Histograms *ph): # ############################################################################ -cdef inline np.uint8_t find_median(Histograms *ph): +cdef inline cnp.uint8_t find_median(Histograms *ph): cdef: - np.uint32_t pixels_below # of pixels below the median - int i - int j - int k - np.uint32_t accumulator + Py_ssize_t pixels_below # of pixels below the median + Py_ssize_t i + Py_ssize_t j + Py_ssize_t k + cnp.uint32_t accumulator if ph.accumulator_count == 0: return 0 - pixels_below = ((ph.accumulator_count * ph.percent + 50) - / 100) # +50 for roundoff + + # +50 for roundoff + pixels_below = (ph.accumulator_count * ph.percent + 50) / 100 + if pixels_below > 0: pixels_below -= 1 @@ -637,10 +624,10 @@ cdef inline np.uint8_t find_median(Histograms *ph): accumulator -= ph.accumulator.coarse[i] update_fine(ph, i) - for j in range(i*16,(i+1)*16): + for j in range(i*16, (i + 1)*16): accumulator += ph.accumulator.fine[j] if accumulator > pixels_below: - return j + return j return 0 @@ -659,30 +646,30 @@ cdef inline np.uint8_t find_median(Histograms *ph): # output - array to be filled with filtered pixels # ############################################################################ -cdef int c_median_filter(np.int32_t rows, - np.int32_t columns, - np.int32_t row_stride, - np.int32_t col_stride, - np.int32_t radius, - np.int32_t percent, - np.uint8_t *data, - np.uint8_t *mask, - np.uint8_t *output): +cdef int c_median_filter(Py_ssize_t rows, + Py_ssize_t columns, + Py_ssize_t row_stride, + Py_ssize_t col_stride, + Py_ssize_t radius, + Py_ssize_t percent, + cnp.uint8_t *data, + cnp.uint8_t *mask, + cnp.uint8_t *output): cdef: Histograms *ph Histogram *phistogram - int row - int col - int i - np.int32_t top_left_off - np.int32_t top_right_off - np.int32_t bottom_left_off - np.int32_t bottom_right_off + Py_ssize_t row + Py_ssize_t col + Py_ssize_t i + Py_ssize_t top_left_off + Py_ssize_t top_right_off + Py_ssize_t bottom_left_off + Py_ssize_t bottom_right_off ph = allocate_histograms(rows, columns, row_stride, col_stride, radius, percent, data, mask, output) if not ph: - return 1 + return 1 for row in range(-radius, rows): # @@ -721,7 +708,7 @@ cdef int c_median_filter(np.int32_t rows, # Update locations and coarse accumulator for the octagon # for points before 0 # - for col in range(-radius, 0 if row >=0 else columns+radius): + for col in range(-radius, 0 if row >= 0 else columns+radius): ph.current_column = col ph.current_stride = row * row_stride + col * col_stride update_current_location(ph) @@ -742,16 +729,18 @@ cdef int c_median_filter(np.int32_t rows, ph.current_stride = row * row_stride + col * col_stride update_current_location(ph) - free_histograms(ph) return 0 -def median_filter( - np.ndarray[dtype=np.uint8_t, ndim=2, negative_indices=False, mode='c'] data, - np.ndarray[dtype=np.uint8_t, ndim=2, negative_indices=False, mode='c'] mask, - np.ndarray[dtype=np.uint8_t, ndim=2, negative_indices=False, mode='c'] output, - int radius, - np.int32_t percent): + +def median_filter(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] data, + cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] mask, + cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] output, + int radius, + cnp.int32_t percent): """Median filter with octagon shape and masking. Parameters @@ -773,10 +762,10 @@ def median_filter( """ if percent < 0: - raise ValueError('Median filter percent = %d is less than zero' % \ + raise ValueError('Median filter percent = %d is less than zero' % percent) if percent > 100: - raise ValueError('Median filter percent = %d is greater than 100' % \ + raise ValueError('Median filter percent = %d is greater than 100' % percent) if data.shape[0] != mask.shape[0] or data.shape[1] != mask.shape[1]: raise ValueError('Data shape (%d, %d) is not mask shape (%d, %d)' % @@ -786,10 +775,12 @@ def median_filter( raise ValueError('Data shape (%d, %d) is not output shape (%d, %d)' % (data.shape[0], data.shape[1], output.shape[0], output.shape[1])) - if c_median_filter(data.shape[0], data.shape[1], - data.strides[0], data.strides[1], + if c_median_filter(data.shape[0], + data.shape[1], + data.strides[0], + data.strides[1], radius, percent, - data.data, - mask.data, - output.data): + data.data, + mask.data, + output.data): raise MemoryError('Failed to allocate scratchpad memory') diff --git a/skimage/filter/_tv_denoise.py b/skimage/filter/_denoise.py similarity index 55% rename from skimage/filter/_tv_denoise.py rename to skimage/filter/_denoise.py index 3302319c..87850759 100644 --- a/skimage/filter/_tv_denoise.py +++ b/skimage/filter/_denoise.py @@ -1,47 +1,46 @@ import numpy as np from skimage import img_as_float +from skimage._shared.utils import deprecated -def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): - """ - Perform total-variation denoising on 3-D arrays +def _denoise_tv_chambolle_3d(im, weight=100, eps=2.e-4, n_iter_max=200): + """Perform total-variation denoising on 3D images. Parameters ---------- - im: ndarray - 3-D input data to be denoised - - weight: float, optional - denoising weight. The greater ``weight``, the more denoising (at - the expense of fidelity to ``input``) - - eps: float, optional - relative difference of the value of the cost function that determines + im : ndarray + 3-D input data to be denoised. + weight : float, optional + Denoising weight. The greater `weight`, the more denoising (at + the expense of fidelity to `input`). + eps : float, optional + Relative difference of the value of the cost function that determines the stop criterion. The algorithm stops when: (E_(n-1) - E_n) < eps * E_0 - n_iter_max: int, optional - maximal number of iterations used for the optimization. + n_iter_max : int, optional + Maximal number of iterations used for the optimization. Returns ------- - out: ndarray - denoised array of floats + out : ndarray + Denoised array of floats. Notes ----- - Rudin, Osher and Fatemi algorithm + Rudin, Osher and Fatemi algorithm. Examples - --------- - First build synthetic noisy data + -------- >>> x, y, z = np.ogrid[0:40, 0:40, 0:40] - >>> mask = (x -22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 + >>> mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 >>> mask = mask.astype(np.float) - >>> mask += 0.2*np.random.randn(*mask.shape) - >>> res = tv_denoise_3d(mask, weight=100) + >>> mask += 0.2 * np.random.randn(*mask.shape) + >>> res = denoise_tv(mask, weight=100) + """ + px = np.zeros_like(im) py = np.zeros_like(im) pz = np.zeros_like(im) @@ -85,57 +84,53 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): return out -def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): - """ - Perform total-variation denoising +def _denoise_tv_chambolle_2d(im, weight=50, eps=2.e-4, n_iter_max=200): + """Perform total-variation denoising on 2D images. Parameters ---------- - im: ndarray - input data to be denoised - - weight: float, optional - denoising weight. The greater ``weight``, the more denoising (at - the expense of fidelity to ``input``) - - eps: float, optional - relative difference of the value of the cost function that determines + im : ndarray + Input data to be denoised. + weight : float, optional + Denoising weight. The greater `weight`, the more denoising (at + the expense of fidelity to `input`) + eps : float, optional + Relative difference of the value of the cost function that determines the stop criterion. The algorithm stops when: (E_(n-1) - E_n) < eps * E_0 - n_iter_max: int, optional - maximal number of iterations used for the optimization. + n_iter_max : int, optional + Maximal number of iterations used for the optimization. Returns ------- - out: ndarray - denoised array of floats + out : ndarray + Denoised array of floats. Notes ----- The principle of total variation denoising is explained in - http://en.wikipedia.org/wiki/Total_variation_denoising + http://en.wikipedia.org/wiki/Total_variation_denoising. This code is an implementation of the algorithm of Rudin, Fatemi and Osher that was proposed by Chambolle in [1]_. References ---------- - .. [1] A. Chambolle, An algorithm for total variation minimization and applications, Journal of Mathematical Imaging and Vision, Springer, 2004, 20, 89-97. Examples - --------- - >>> import scipy - >>> lena = scipy.lena() - >>> import scipy - >>> lena = scipy.lena().astype(np.float) - >>> lena += 0.5 * lena.std()*np.random.randn(*lena.shape) - >>> denoised_lena = tv_denoise(lena, weight=60.0) + -------- + >>> from skimage import color, data + >>> lena = color.rgb2gray(data.lena()) + >>> lena += 0.5 * lena.std() * np.random.randn(*lena.shape) + >>> denoised_lena = denoise_tv(lena, weight=60) + """ + px = np.zeros_like(im) py = np.zeros_like(im) gx = np.zeros_like(im) @@ -172,37 +167,41 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): return out -def tv_denoise(im, weight=50, eps=2.e-4, n_iter_max=200): - """ - Perform total-variation denoising on 2-d and 3-d images +def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, + multichannel=False): + """Perform total-variation denoising on 2D and 3D images. Parameters ---------- - im: ndarray (2d or 3d) of ints, uints or floats - input data to be denoised. `im` can be of any numeric type, + im : ndarray (2d or 3d) of ints, uints or floats + Input data to be denoised. `im` can be of any numeric type, but it is cast into an ndarray of floats for the computation of the denoised image. - - weight: float, optional - denoising weight. The greater ``weight``, the more denoising (at - the expense of fidelity to ``input``) - - eps: float, optional - relative difference of the value of the cost function that + weight : float, optional + Denoising weight. The greater `weight`, the more denoising (at + the expense of fidelity to `input`). + eps : float, optional + Relative difference of the value of the cost function that determines the stop criterion. The algorithm stops when: (E_(n-1) - E_n) < eps * E_0 - n_iter_max: int, optional - maximal number of iterations used for the optimization. + n_iter_max : int, optional + Maximal number of iterations used for the optimization. + multichannel : bool, optional + Apply total-variation denoising separately for each channel. This + option should be true for color images, otherwise the denoising is + also applied in the 3rd dimension. Returns ------- - out: ndarray - denoised array of floats + out : ndarray + Denoised image. Notes ----- + Make sure to set the multichannel parameter appropriately for color images. + The principle of total variation denoising is explained in http://en.wikipedia.org/wiki/Total_variation_denoising @@ -217,36 +216,48 @@ def tv_denoise(im, weight=50, eps=2.e-4, n_iter_max=200): References ---------- - .. [1] A. Chambolle, An algorithm for total variation minimization and applications, Journal of Mathematical Imaging and Vision, Springer, 2004, 20, 89-97. Examples - --------- - >>> import scipy - >>> # 2D example using lena - >>> lena = scipy.lena() - >>> import scipy - >>> lena = scipy.lena().astype(np.float) - >>> lena += 0.5 * lena.std()*np.random.randn(*lena.shape) - >>> denoised_lena = tv_denoise(lena, weight=60) - >>> # 3D example on synthetic data + -------- + 2D example on Lena image: + + >>> from skimage import color, data + >>> lena = color.rgb2gray(data.lena()) + >>> lena += 0.5 * lena.std() * np.random.randn(*lena.shape) + >>> denoised_lena = denoise_tv(lena, weight=60) + + 3D example on synthetic data: + >>> x, y, z = np.ogrid[0:40, 0:40, 0:40] - >>> mask = (x -22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 + >>> mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 >>> mask = mask.astype(np.float) >>> mask += 0.2*np.random.randn(*mask.shape) - >>> res = tv_denoise_3d(mask, weight=100) + >>> res = denoise_tv(mask, weight=100) + """ + im_type = im.dtype if not im_type.kind == 'f': im = img_as_float(im) if im.ndim == 2: - out = _tv_denoise_2d(im, weight, eps, n_iter_max) + out = _denoise_tv_chambolle_2d(im, weight, eps, n_iter_max) elif im.ndim == 3: - out = _tv_denoise_3d(im, weight, eps, n_iter_max) + if multichannel: + out = np.zeros_like(im) + for c in range(im.shape[2]): + out[..., c] = _denoise_tv_chambolle_2d(im[..., c], weight, eps, + n_iter_max) + else: + out = _denoise_tv_chambolle_3d(im, weight, eps, n_iter_max) else: raise ValueError('only 2-d and 3-d images may be denoised with this ' 'function') return out + + +tv_denoise = deprecated('skimage.filter.denoise_tv_chambolle')\ + (denoise_tv_chambolle) diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/filter/_denoise_cy.pyx new file mode 100644 index 00000000..5a85e84a --- /dev/null +++ b/skimage/filter/_denoise_cy.pyx @@ -0,0 +1,320 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +import numpy as np +from libc.math cimport exp, fabs, sqrt +from libc.stdlib cimport malloc, free +from libc.float cimport DBL_MAX +from skimage._shared.interpolation cimport get_pixel3d +from skimage.util import img_as_float +from skimage._shared.utils import deprecated + + +cdef inline double _gaussian_weight(double sigma, double value): + return exp(-0.5 * (value / sigma)**2) + + +cdef double* _compute_color_lut(Py_ssize_t bins, double sigma, double max_value): + + cdef: + double* color_lut = malloc(bins * sizeof(double)) + Py_ssize_t b + + for b in range(bins): + color_lut[b] = _gaussian_weight(sigma, b * max_value / bins) + + return color_lut + + +cdef double* _compute_range_lut(Py_ssize_t win_size, double sigma): + + cdef: + double* range_lut = malloc(win_size**2 * sizeof(double)) + Py_ssize_t kr, kc + Py_ssize_t window_ext = (win_size - 1) / 2 + double dist + + for kr in range(win_size): + for kc in range(win_size): + dist = sqrt((kr - window_ext)**2 + (kc - window_ext)**2) + range_lut[kr * win_size + kc] = _gaussian_weight(sigma, dist) + + return range_lut + + +def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, + double sigma_spatial=1, Py_ssize_t bins=10000, + mode='constant', double cval=0): + """Denoise image using bilateral filter. + + This is an edge-preserving and noise reducing denoising filter. It averages + pixels based on their spatial closeness and radiometric similarity. + + Spatial closeness is measured by the gaussian function of the euclidian + distance between two pixels and a certain standard deviation + (`sigma_spatial`). + + Radiometric similarity is measured by the gaussian function of the euclidian + distance between two color values and a certain standard deviation + (`sigma_range`). + + Parameters + ---------- + image : ndarray + Input image. + win_size : int + Window size for filtering. + sigma_range : float + Standard deviation for grayvalue/color distance (radiometric + similarity). A larger value results in averaging of pixels with larger + radiometric differences. Note, that the image will be converted using + the `img_as_float` function and thus the standard deviation is in + respect to the range `[0, 1]`. + sigma_spatial : float + Standard deviation for range distance. A larger value results in + averaging of pixels with larger spatial differences. + bins : int + Number of discrete values for gaussian weights of color filtering. + A larger value results in improved accuracy. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + denoised : ndarray + Denoised image. + + References + ---------- + .. [1] http://users.soe.ucsc.edu/~manduchi/Papers/ICCV98.pdf + + """ + + image = np.atleast_3d(img_as_float(image)) + + cdef: + Py_ssize_t rows = image.shape[0] + Py_ssize_t cols = image.shape[1] + Py_ssize_t dims = image.shape[2] + Py_ssize_t window_ext = (win_size - 1) / 2 + + double max_value = image.max() + + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] cimage = \ + np.ascontiguousarray(image) + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] out = \ + np.zeros((rows, cols, dims), dtype=np.double) + + double* image_data = cimage.data + double* out_data = out.data + + double* color_lut = _compute_color_lut(bins, sigma_range, max_value) + double* range_lut = _compute_range_lut(win_size, sigma_spatial) + + Py_ssize_t r, c, d, wr, wc, kr, kc, rr, cc, pixel_addr + double value, weight, dist, total_weight, csigma_range, color_weight, \ + range_weight + double dist_scale = bins / dims / max_value + double* values = malloc(dims * sizeof(double)) + double* centres = malloc(dims * sizeof(double)) + double* total_values = malloc(dims * sizeof(double)) + + if sigma_range is None: + csigma_range = image.std() + else: + csigma_range = sigma_range + + if mode not in ('constant', 'wrap', 'reflect', 'nearest'): + raise ValueError("Invalid mode specified. Please use " + "`constant`, `nearest`, `wrap` or `reflect`.") + cdef char cmode = ord(mode[0].upper()) + + for r in range(rows): + for c in range(cols): + pixel_addr = r * cols * dims + c * dims + total_weight = 0 + for d in range(dims): + total_values[d] = 0 + centres[d] = image_data[pixel_addr + d] + for wr in range(-window_ext, window_ext + 1): + rr = wr + r + kr = wr + window_ext + for wc in range(-window_ext, window_ext + 1): + cc = wc + c + kc = wc + window_ext + + # save pixel values for all dims and compute euclidian + # distance between centre stack and current position + dist = 0 + for d in range(dims): + value = get_pixel3d(image_data, rows, cols, dims, + rr, cc, d, cmode, cval) + values[d] = value + dist += (centres[d] - value)**2 + dist = sqrt(dist) + + range_weight = range_lut[kr * win_size + kc] + color_weight = color_lut[(dist * dist_scale)] + + weight = range_weight * color_weight + for d in range(dims): + total_values[d] += values[d] * weight + total_weight += weight + for d in range(dims): + out_data[pixel_addr + d] = total_values[d] / total_weight + + free(color_lut) + free(range_lut) + free(values) + free(centres) + free(total_values) + + return np.squeeze(out) + + +def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): + """Perform total-variation denoising using split-Bregman optimization. + + Total-variation denoising (also know as total-variation regularization) + tries to find an image with less total-variation under the constraint + of being similar to the input image, which is controlled by the + regularization parameter. + + Parameters + ---------- + image : ndarray + Input data to be denoised (converted using img_as_float`). + weight : float, optional + Denoising weight. The smaller the `weight`, the more denoising (at + the expense of less similarity to the `input`). The regularization + parameter `lambda` is chosen as `2 * weight`. + eps : float, optional + Relative difference of the value of the cost function that determines + the stop criterion. The algorithm stops when:: + + SUM((u(n) - u(n-1))**2) < eps + + max_iter: int, optional + Maximal number of iterations used for the optimization. + + Returns + ------- + u : ndarray + Denoised image. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Total_variation_denoising + .. [2] Tom Goldstein and Stanley Osher, "The Split Bregman Method For L1 + Regularized Problems", + ftp://ftp.math.ucla.edu/pub/camreport/cam08-29.pdf + .. [3] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising + using Split Bregman" in Image Processing On Line on 2012–05–19, + http://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf + + """ + + image = np.atleast_3d(img_as_float(image)) + + cdef: + Py_ssize_t rows = image.shape[0] + Py_ssize_t cols = image.shape[1] + Py_ssize_t dims = image.shape[2] + Py_ssize_t rows2 = rows + 2 + Py_ssize_t cols2 = cols + 2 + Py_ssize_t r, c, k + + Py_ssize_t total = rows * cols * dims + + shape_ext = (rows2, cols2, dims) + + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] cimage = \ + np.ascontiguousarray(image) + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] u = \ + np.zeros(shape_ext, dtype=np.double) + + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] dx = \ + np.zeros(shape_ext, dtype=np.double) + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] dy = \ + np.zeros(shape_ext, dtype=np.double) + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] bx = \ + np.zeros(shape_ext, dtype=np.double) + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] by = \ + np.zeros(shape_ext, dtype=np.double) + + double ux, uy, uprev, unew, bxx, byy, dxx, dyy, s + int i = 0 + double lam = 2 * weight + double rmse = DBL_MAX + double norm = (weight + 4 * lam) + + u[1:-1, 1:-1] = image + + # reflect image + u[0, 1:-1] = image[1, :] + u[1:-1, 0] = image[:, 1] + u[-1, 1:-1] = image[-2, :] + u[1:-1, -1] = image[:, -2] + + while i < max_iter and rmse > eps: + + rmse = 0 + + for k in range(dims): + for r in range(1, rows + 1): + for c in range(1, cols + 1): + + uprev = u[r, c, k] + + # forward derivatives + ux = u[r, c + 1, k] - uprev + uy = u[r + 1, c, k] - uprev + + # Gauss-Seidel method + unew = ( + lam * ( + + u[r + 1, c, k] + + u[r - 1, c, k] + + u[r, c + 1, k] + + u[r, c - 1, k] + + + dx[r, c - 1, k] + - dx[r, c, k] + + dy[r - 1, c, k] + - dy[r, c, k] + + - bx[r, c - 1, k] + + bx[r, c, k] + - by[r - 1, c, k] + + by[r, c, k] + ) + weight * cimage[r - 1, c - 1, k] + ) / norm + u[r, c, k] = unew + + # update root mean square error + rmse += (unew - uprev)**2 + + bxx = bx[r, c, k] + byy = by[r, c, k] + + s = sqrt((ux + bxx)**2 + (uy + byy)**2) + dxx = s * lam * (ux + bxx) / (s * lam + 1) + dyy = s * lam * (uy + byy) / (s * lam + 1) + + dx[r, c, k] = dxx + dy[r, c, k] = dyy + + bx[r, c, k] += ux - dxx + by[r, c, k] += uy - dyy + + rmse = sqrt(rmse / total) + i += 1 + + return np.squeeze(u[1:-1, 1:-1]) diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index 134aa796..fcd9f548 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -1,6 +1,7 @@ -"""edges.py - Sobel edge filter +"""edges.py - Edge filters -Originally part of CellProfiler, code licensed under both GPL and BSD licenses. +Sobel and Prewitt filters originally part of CellProfiler, code licensed under +both GPL and BSD licenses. Website: http://www.cellprofiler.org Copyright (c) 2003-2009 Massachusetts Institute of Technology Copyright (c) 2009-2011 Broad Institute @@ -34,13 +35,13 @@ def _mask_filter_result(result, mask): def sobel(image, mask=None): - """Calculate the absolute magnitude Sobel to find edges. + """Find the edge magnitude using the Sobel transform. Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process. - mask : array_like, dtype=bool, optional + mask : 2-D array, optional An optional mask to limit the application to a certain area. Note that pixels surrounding masked regions are also masked to prevent masked regions from affecting the result. @@ -48,7 +49,7 @@ def sobel(image, mask=None): Returns ------- output : ndarray - The Sobel edge map. + The Sobel edge map. Notes ----- @@ -67,9 +68,9 @@ def hsobel(image, mask=None): Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process. - mask : array_like, dtype=bool, optional + mask : 2-D array, optional An optional mask to limit the application to a certain area. Note that pixels surrounding masked regions are also masked to prevent masked regions from affecting the result. @@ -77,7 +78,7 @@ def hsobel(image, mask=None): Returns ------- output : ndarray - The Sobel edge map. + The Sobel edge map. Notes ----- @@ -102,9 +103,9 @@ def vsobel(image, mask=None): Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process - mask : array_like, dtype=bool, optional + mask : 2-D array, optional An optional mask to limit the application to a certain area Note that pixels surrounding masked regions are also masked to prevent masked regions from affecting the result. @@ -112,7 +113,7 @@ def vsobel(image, mask=None): Returns ------- output : ndarray - The Sobel edge map. + The Sobel edge map. Notes ----- @@ -132,14 +133,14 @@ def vsobel(image, mask=None): return _mask_filter_result(result, mask) -def prewitt(image, mask=None): - """Find the edge magnitude using the Prewitt transform. +def scharr(image, mask=None): + """Find the edge magnitude using the Scharr transform. Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process. - mask : array_like, dtype=bool, optional + mask : 2-D array, optional An optional mask to limit the application to a certain area. Note that pixels surrounding masked regions are also masked to prevent masked regions from affecting the result. @@ -147,7 +148,119 @@ def prewitt(image, mask=None): Returns ------- output : ndarray - The Prewitt edge map. + The Scharr edge map. + + Notes + ----- + Take the square root of the sum of the squares of the horizontal and + vertical Scharrs to get a magnitude that's somewhat insensitive to + direction. + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical Optimization + of Kernel Based Image Derivatives. + + """ + return np.sqrt(hscharr(image, mask)**2 + vscharr(image, mask)**2) + + +def hscharr(image, mask=None): + """Find the horizontal edges of an image using the Scharr transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : ndarray + The Scharr edge map. + + Notes + ----- + We use the following kernel and return the absolute value of the + result at each point:: + + 3 10 3 + 0 0 0 + -3 -10 -3 + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical Optimization + of Kernel Based Image Derivatives. + + """ + image = img_as_float(image) + result = np.abs(convolve(image, + np.array([[ 3, 10, 3], + [ 0, 0, 0], + [-3, -10, -3]]).astype(float) / 16.0)) + return _mask_filter_result(result, mask) + + +def vscharr(image, mask=None): + """Find the vertical edges of an image using the Scharr transform. + + Parameters + ---------- + image : 2-D array + Image to process + mask : 2-D array, optional + An optional mask to limit the application to a certain area + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : ndarray + The Scharr edge map. + + Notes + ----- + We use the following kernel and return the absolute value of the + result at each point:: + + 3 0 -3 + 10 0 -10 + 3 0 -3 + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical Optimization + of Kernel Based Image Derivatives. + + """ + image = img_as_float(image) + result = np.abs(convolve(image, + np.array([[ 3, 0, -3], + [10, 0, -10], + [ 3, 0, -3]]).astype(float) / 16.0)) + return _mask_filter_result(result, mask) + + +def prewitt(image, mask=None): + """Find the edge magnitude using the Prewitt transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : ndarray + The Prewitt edge map. Notes ----- @@ -162,9 +275,9 @@ def hprewitt(image, mask=None): Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process. - mask : array_like, dtype=bool, optional + mask : 2-D array, optional An optional mask to limit the application to a certain area. Note that pixels surrounding masked regions are also masked to prevent masked regions from affecting the result. @@ -172,7 +285,7 @@ def hprewitt(image, mask=None): Returns ------- output : ndarray - The Prewitt edge map. + The Prewitt edge map. Notes ----- @@ -197,9 +310,9 @@ def vprewitt(image, mask=None): Parameters ---------- - image : array_like, dtype=float + image : 2-D array Image to process. - mask : array_like, dtype=bool, optional + mask : 2-D array, optional An optional mask to limit the application to a certain area. Note that pixels surrounding masked regions are also masked to prevent masked regions from affecting the result. @@ -207,7 +320,7 @@ def vprewitt(image, mask=None): Returns ------- output : ndarray - The Prewitt edge map. + The Prewitt edge map. Notes ----- diff --git a/skimage/filter/rank/.gitignore b/skimage/filter/rank/.gitignore new file mode 100644 index 00000000..08a24d5c --- /dev/null +++ b/skimage/filter/rank/.gitignore @@ -0,0 +1 @@ +demo/ \ No newline at end of file diff --git a/skimage/filter/rank/README.rst b/skimage/filter/rank/README.rst new file mode 100644 index 00000000..cdf8205c --- /dev/null +++ b/skimage/filter/rank/README.rst @@ -0,0 +1,32 @@ +To do +----- + +* add simple examples, adapt documentation on existing examples +* add/check existing doc +* adapting tests for each type of filter + +General remarks +--------------- + +Basically these filters compute local histogram for each pixel. A histogram is +built using a moving window in order to limit redundant computation. The path +followed by the moving window is given hereunder + + ...-----------------------\ +/--------------------------/ +\-------------------------- ... + +We compare cmorph.dilate to this histogram based method to show how +computational costs increase with respect to image size or structuring element +size. This implementation gives better results for large structuring elements. + +The local histogram is updated at each pixel as the structuring element window +moves by, i.e. only those pixels entering and leaving the structuring element +update the local histogram. The histogram size is 8-bit (256 bins) for 8-bit +images and 2 to 12-bit (up to 4096 bins) for 16-bit images depending on the +maximum value of the image. Pixel values higher than 4095 raise a ValueError. + +The filter is applied up to the image border, the neighboorhood used is adjusted +accordingly. The user may provide a mask image (same size as input image) where +non zero values are the part of the image participating in the histogram +computation. By default the entire image is filtered. diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py new file mode 100644 index 00000000..30d936db --- /dev/null +++ b/skimage/filter/rank/__init__.py @@ -0,0 +1,3 @@ +from .rank import * +from .percentile_rank import * +from .bilateral_rank import * diff --git a/skimage/filter/rank/_core16.pxd b/skimage/filter/rank/_core16.pxd new file mode 100644 index 00000000..5586aea1 --- /dev/null +++ b/skimage/filter/rank/_core16.pxd @@ -0,0 +1,20 @@ +cimport numpy as cnp + + +ctypedef cnp.uint16_t dtype_t + + +cdef int int_max(int a, int b) +cdef int int_min(int a, int b) + + +# 16-bit core kernel receives extra information about data bitdepth +cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, + Py_ssize_t, Py_ssize_t, Py_ssize_t, float, + float, Py_ssize_t, Py_ssize_t), + cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask, + cnp.ndarray[dtype_t, ndim=2] out, + char shift_x, char shift_y, Py_ssize_t bitdepth, + float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except * diff --git a/skimage/filter/rank/_core16.pyx b/skimage/filter/rank/_core16.pyx new file mode 100644 index 00000000..36bc500d --- /dev/null +++ b/skimage/filter/rank/_core16.pyx @@ -0,0 +1,255 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +import numpy as np + +cimport numpy as cnp +from libc.stdlib cimport malloc, free +from _core8 cimport is_in_mask + + +cdef inline int int_max(int a, int b): + return a if a >= b else b + + +cdef inline int int_min(int a, int b): + return a if a <= b else b + + +cdef inline void histogram_increment(Py_ssize_t * histo, float * pop, + dtype_t value): + histo[value] += 1 + pop[0] += 1 + + +cdef inline void histogram_decrement(Py_ssize_t * histo, float * pop, + dtype_t value): + histo[value] -= 1 + pop[0] -= 1 + + +cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, + Py_ssize_t, Py_ssize_t, Py_ssize_t, float, + float, Py_ssize_t, Py_ssize_t), + cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask, + cnp.ndarray[dtype_t, ndim=2] out, + char shift_x, char shift_y, Py_ssize_t bitdepth, + float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except *: + """Compute histogram for each pixel neighborhood, apply kernel function and + use kernel function return value for output image. + """ + + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + cdef Py_ssize_t srows = selem.shape[0] + cdef Py_ssize_t scols = selem.shape[1] + + cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) + shift_y + cdef Py_ssize_t centre_c = int(selem.shape[1] / 2) + shift_x + + # check that structuring element center is inside the element bounding box + assert centre_r >= 0 + assert centre_c >= 0 + assert centre_r < srows + assert centre_c < scols + assert bitdepth in range(2, 13) + + maxbin_list = [0, 0, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096] + midbin_list = [0, 0, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048] + + # set maxbin and midbin + cdef Py_ssize_t maxbin = maxbin_list[bitdepth] + cdef Py_ssize_t midbin = midbin_list[bitdepth] + + assert (image < maxbin).all() + + # define pointers to the data + cdef dtype_t * out_data = out.data + cdef dtype_t * image_data = image.data + cdef cnp.uint8_t * mask_data = mask.data + + # define local variable types + cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row + # number of pixels actually inside the neighborhood (float) + cdef float pop + + # allocate memory with malloc + cdef Py_ssize_t max_se = srows * scols + + # number of element in each attack border + cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w + + # the current local histogram distribution + cdef Py_ssize_t * histo = malloc(maxbin * sizeof(Py_ssize_t)) + + # these lists contain the relative pixel row and column for each of the 4 + # attack borders east, west, north and south e.g. se_e_r lists the rows of + # the east structuring element border + cdef Py_ssize_t * se_e_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_e_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_w_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_w_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_n_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_n_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_s_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_s_c = malloc(max_se * sizeof(Py_ssize_t)) + + # build attack and release borders + # by using difference along axis + t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) + t_e = np.diff(t, axis=1) == -1 + + t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) + t_w = np.diff(t, axis=1) == 1 + + t = np.vstack((selem, np.zeros((1, selem.shape[1])))) + t_s = np.diff(t, axis=0) == -1 + + t = np.vstack((np.zeros((1, selem.shape[1])), selem)) + t_n = np.diff(t, axis=0) == 1 + + num_se_n = num_se_s = num_se_e = num_se_w = 0 + + for r in range(srows): + for c in range(scols): + if t_e[r, c]: + se_e_r[num_se_e] = r - centre_r + se_e_c[num_se_e] = c - centre_c + num_se_e += 1 + if t_w[r, c]: + se_w_r[num_se_w] = r - centre_r + se_w_c[num_se_w] = c - centre_c + num_se_w += 1 + if t_n[r, c]: + se_n_r[num_se_n] = r - centre_r + se_n_c[num_se_n] = c - centre_c + num_se_n += 1 + if t_s[r, c]: + se_s_r[num_se_s] = r - centre_r + se_s_c[num_se_s] = c - centre_c + num_se_s += 1 + + # initial population and histogram + for i in range(maxbin): + histo[i] = 0 + + pop = 0 + + for r in range(srows): + for c in range(scols): + rr = r - centre_r + cc = c - centre_c + if selem[r, c]: + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + r = 0 + c = 0 + # kernel ------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + bitdepth, maxbin, midbin, p0, p1, s0, s1) + # kernel ------------------------------------------- + + # main loop + r = 0 + for even_row in range(0, rows, 2): + # ---> west to east + for c in range(1, cols): + for s in range(num_se_e): + rr = r + se_e_r[s] + cc = c + se_e_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_w): + rr = r + se_w_r[s] + cc = c + se_w_c[s] - 1 + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ------------------------------------------- + out_data[r * cols + c] = kernel( + histo, pop, image_data[r * cols + c], + bitdepth, maxbin, midbin, p0, p1, s0, s1) + # kernel ------------------------------------------- + + r += 1 # pass to the next row + if r >= rows: + break + + # ---> north to south + for s in range(num_se_s): + rr = r + se_s_r[s] + cc = c + se_s_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_n): + rr = r + se_n_r[s] - 1 + cc = c + se_n_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + bitdepth, maxbin, midbin, p0, p1, s0, s1) + # kernel ------------------------------------------- + + # ---> east to west + for c in range(cols - 2, -1, -1): + for s in range(num_se_w): + rr = r + se_w_r[s] + cc = c + se_w_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_e): + rr = r + se_e_r[s] + cc = c + se_e_c[s] + 1 + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ------------------------------------------- + out_data[r * cols + c] = kernel( + histo, pop, image_data[r * cols + c], + bitdepth, maxbin, midbin, p0, p1, s0, s1) + # kernel ------------------------------------------- + + r += 1 # pass to the next row + if r >= rows: + break + + # ---> north to south + for s in range(num_se_s): + rr = r + se_s_r[s] + cc = c + se_s_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_n): + rr = r + se_n_r[s] - 1 + cc = c + se_n_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + bitdepth, maxbin, midbin, p0, p1, s0, s1) + # kernel ------------------------------------------- + + # release memory allocated by malloc + + free(se_e_r) + free(se_e_c) + free(se_w_r) + free(se_w_c) + free(se_n_r) + free(se_n_c) + free(se_s_r) + free(se_s_c) + + free(histo) diff --git a/skimage/filter/rank/_core8.pxd b/skimage/filter/rank/_core8.pxd new file mode 100644 index 00000000..d3b6d8c2 --- /dev/null +++ b/skimage/filter/rank/_core8.pxd @@ -0,0 +1,25 @@ +cimport numpy as cnp + + +ctypedef cnp.uint8_t dtype_t + + +cdef dtype_t uint8_max(dtype_t a, dtype_t b) +cdef dtype_t uint8_min(dtype_t a, dtype_t b) + + +cdef dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t c, + dtype_t * mask) + + +# 8-bit core kernel receives extra information about data inferior and superior +# percentiles +cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, + float, Py_ssize_t, Py_ssize_t), + cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask, + cnp.ndarray[dtype_t, ndim=2] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1) except * diff --git a/skimage/filter/rank/_core8.pyx b/skimage/filter/rank/_core8.pyx new file mode 100644 index 00000000..eca47891 --- /dev/null +++ b/skimage/filter/rank/_core8.pyx @@ -0,0 +1,257 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +import numpy as np + +cimport numpy as cnp +from libc.stdlib cimport malloc, free + + +cdef inline dtype_t uint8_max(dtype_t a, dtype_t b): + return a if a >= b else b + + +cdef inline dtype_t uint8_min(dtype_t a, dtype_t b): + return a if a <= b else b + + +cdef inline void histogram_increment(Py_ssize_t * histo, float * pop, + dtype_t value): + histo[value] += 1 + pop[0] += 1 + + +cdef inline void histogram_decrement(Py_ssize_t * histo, float * pop, + dtype_t value): + histo[value] -= 1 + pop[0] -= 1 + + +cdef inline dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t c, + dtype_t * mask): + """Check whether given coordinate is within image and mask is true.""" + if r < 0 or r > rows - 1 or c < 0 or c > cols - 1: + return 0 + else: + if mask[r * cols + c]: + return 1 + else: + return 0 + + +cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, + float, Py_ssize_t, Py_ssize_t), + cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask, + cnp.ndarray[dtype_t, ndim=2] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1) except *: + """Compute histogram for each pixel neighborhood, apply kernel function and + use kernel function return value for output image. + """ + + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + cdef Py_ssize_t srows = selem.shape[0] + cdef Py_ssize_t scols = selem.shape[1] + + cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) + shift_y + cdef Py_ssize_t centre_c = int(selem.shape[1] / 2) + shift_x + + # check that structuring element center is inside the element bounding box + assert centre_r >= 0 + assert centre_c >= 0 + assert centre_r < srows + assert centre_c < scols + + # define pointers to the data + + cdef dtype_t * out_data = out.data + cdef dtype_t * image_data = image.data + cdef dtype_t * mask_data = mask.data + + # define local variable types + cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row + + # number of pixels actually inside the neighborhood (float) + cdef float pop + + # allocate memory with malloc + cdef Py_ssize_t max_se = srows * scols + + # number of element in each attack border + cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w + + # the current local histogram distribution + cdef Py_ssize_t * histo = malloc(256 * sizeof(Py_ssize_t)) + + # these lists contain the relative pixel row and column for each of the 4 + # attack borders east, west, north and south e.g. se_e_r lists the rows of + # the east structuring element border + cdef Py_ssize_t * se_e_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_e_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_w_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_w_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_n_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_n_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_s_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_s_c = malloc(max_se * sizeof(Py_ssize_t)) + + # build attack and release borders + # by using difference along axis + t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) + t_e = np.diff(t, axis=1) == -1 + + t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) + t_w = np.diff(t, axis=1) == 1 + + t = np.vstack((selem, np.zeros((1, selem.shape[1])))) + t_s = np.diff(t, axis=0) == -1 + + t = np.vstack((np.zeros((1, selem.shape[1])), selem)) + t_n = np.diff(t, axis=0) == 1 + + num_se_n = num_se_s = num_se_e = num_se_w = 0 + + for r in range(srows): + for c in range(scols): + if t_e[r, c]: + se_e_r[num_se_e] = r - centre_r + se_e_c[num_se_e] = c - centre_c + num_se_e += 1 + if t_w[r, c]: + se_w_r[num_se_w] = r - centre_r + se_w_c[num_se_w] = c - centre_c + num_se_w += 1 + if t_n[r, c]: + se_n_r[num_se_n] = r - centre_r + se_n_c[num_se_n] = c - centre_c + num_se_n += 1 + if t_s[r, c]: + se_s_r[num_se_s] = r - centre_r + se_s_c[num_se_s] = c - centre_c + num_se_s += 1 + + # initial population and histogram (kernel is centered on the first row and + # column) + for i in range(256): + histo[i] = 0 + + pop = 0 + + for r in range(srows): + for c in range(scols): + rr = r - centre_r + cc = c - centre_c + if selem[r, c]: + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + r = 0 + c = 0 + # kernel ------------------------------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + p0, p1, s0, s1) + # kernel ------------------------------------------------------------------- + + # main loop + r = 0 + for even_row in range(0, rows, 2): + # ---> west to east + for c in range(1, cols): + for s in range(num_se_e): + rr = r + se_e_r[s] + cc = c + se_e_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_w): + rr = r + se_w_r[s] + cc = c + se_w_c[s] - 1 + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ----------------------------------------------------------- + out_data[r * cols + c] = \ + kernel(histo, pop, image_data[r * cols + c], p0, p1, s0, s1) + # kernel ----------------------------------------------------------- + + r += 1 # pass to the next row + if r >= rows: + break + + # ---> north to south + for s in range(num_se_s): + rr = r + se_s_r[s] + cc = c + se_s_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_n): + rr = r + se_n_r[s] - 1 + cc = c + se_n_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel --------------------------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + p0, p1, s0, s1) + # kernel --------------------------------------------------------------- + + # ---> east to west + for c in range(cols - 2, -1, -1): + for s in range(num_se_w): + rr = r + se_w_r[s] + cc = c + se_w_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_e): + rr = r + se_e_r[s] + cc = c + se_e_c[s] + 1 + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel ----------------------------------------------------------- + out_data[r * cols + c] = kernel( + histo, pop, image_data[r * cols + c], p0, p1, s0, s1) + # kernel ----------------------------------------------------------- + + r += 1 # pass to the next row + if r >= rows: + break + + # ---> north to south + for s in range(num_se_s): + rr = r + se_s_r[s] + cc = c + se_s_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_increment(histo, &pop, image_data[rr * cols + cc]) + + for s in range(num_se_n): + rr = r + se_n_r[s] - 1 + cc = c + se_n_c[s] + if is_in_mask(rows, cols, rr, cc, mask_data): + histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + + # kernel --------------------------------------------------------------- + out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + p0, p1, s0, s1) + # kernel --------------------------------------------------------------- + + # release memory allocated by malloc + + free(se_e_r) + free(se_e_c) + free(se_w_r) + free(se_w_c) + free(se_n_r) + free(se_n_c) + free(se_s_r) + free(se_s_c) + + free(histo) diff --git a/skimage/filter/rank/_crank16.pyx b/skimage/filter/rank/_crank16.pyx new file mode 100644 index 00000000..232d6812 --- /dev/null +++ b/skimage/filter/rank/_crank16.pyx @@ -0,0 +1,422 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log +from skimage.filter.rank._core16 cimport _core16 + + +# ----------------------------------------------------------------- +# kernels uint16 take extra parameter for defining the bitdepth +# ----------------------------------------------------------------- + + +ctypedef cnp.uint16_t dtype_t + + +cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i, imin, imax, delta + + if pop: + for i in range(maxbin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(maxbin): + if histo[i]: + imin = i + break + delta = imax - imin + if delta > 0: + return (1. * (maxbin - 1) * (g - imin) / delta) + else: + return (imax - imin) + + +cdef inline dtype_t kernel_bottomhat(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + + if pop: + for i in range(maxbin): + if histo[i]: + break + + return (g - i) + else: + return (0) + +cdef inline dtype_t kernel_equalize(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float sum = 0. + + if pop: + for i in range(maxbin): + sum += histo[i] + if i >= g: + break + + return (((maxbin - 1) * sum) / pop) + else: + return (0) + + +cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(maxbin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(maxbin): + if histo[i]: + imin = i + break + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_maximum(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + + if pop: + for i in range(maxbin - 1, -1, -1): + if histo[i]: + return (i) + + return (0) + + +cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(maxbin): + mean += histo[i] * i + return (mean / pop) + else: + return (0) + + +cdef inline dtype_t kernel_meansubstraction(Py_ssize_t * histo, + float pop, + dtype_t g, + Py_ssize_t bitdepth, + Py_ssize_t maxbin, + Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(maxbin): + mean += histo[i] * i + return ((g - mean / pop) / 2. + (midbin - 1)) + else: + return (0) + + +cdef inline dtype_t kernel_median(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float sum = pop / 2.0 + + if pop: + for i in range(maxbin): + if histo[i]: + sum -= histo[i] + if sum < 0: + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_minimum(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + + if pop: + for i in range(maxbin): + if histo[i]: + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_modal(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t hmax = 0, imax = 0 + + if pop: + for i in range(maxbin): + if histo[i] > hmax: + hmax = histo[i] + imax = i + return (imax) + else: + return (0) + + +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, + float pop, + dtype_t g, + Py_ssize_t bitdepth, + Py_ssize_t maxbin, + Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(maxbin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(maxbin): + if histo[i]: + imin = i + break + if imax - g < g - imin: + return (imax) + else: + return (imin) + else: + return (0) + + +cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + return (pop) + + +cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(maxbin): + mean += histo[i] * i + return (g > (mean / pop)) + else: + return (0) + + +cdef inline dtype_t kernel_tophat(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + + if pop: + for i in range(maxbin - 1, -1, -1): + if histo[i]: + break + + return (i - g) + else: + return (0) + +cdef inline dtype_t kernel_entropy(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float e, p + + if pop: + e = 0. + + for i in range(maxbin): + p = histo[i] / pop + if p > 0: + e -= p * log(p) / 0.6931471805599453 + + return e * 1000 + else: + return (0) + +# ----------------------------------------------------------------- +# python wrappers +# ----------------------------------------------------------------- + + +def autolevel(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def bottomhat(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_bottomhat, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def equalize(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_equalize, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def gradient(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_gradient, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def maximum(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_maximum, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def mean(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def meansubstraction(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_meansubstraction, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def median(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_median, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def minimum(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_minimum, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def modal(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_modal, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def pop(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def threshold(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_threshold, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def tophat(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_tophat, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) + + +def entropy(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): + _core16(kernel_entropy, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0, 0, 0, 0) diff --git a/skimage/filter/rank/_crank16_bilateral.pyx b/skimage/filter/rank/_crank16_bilateral.pyx new file mode 100644 index 00000000..e431e42b --- /dev/null +++ b/skimage/filter/rank/_crank16_bilateral.pyx @@ -0,0 +1,82 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from skimage.filter.rank._core16 cimport _core16 + + +# ----------------------------------------------------------------- +# kernels uint16 take extra parameter for defining the bitdepth +# ----------------------------------------------------------------- + + +ctypedef cnp.uint16_t dtype_t + + +cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, bilat_pop = 0 + cdef float mean = 0. + + if pop: + for i in range(maxbin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + mean += histo[i] * i + if bilat_pop: + return (mean / bilat_pop) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, bilat_pop = 0 + + if pop: + for i in range(maxbin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + return (bilat_pop) + else: + return (0) + + +# ----------------------------------------------------------------- +# python wrappers +# ----------------------------------------------------------------- + + +def mean(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, int s0=1, int s1=1): + """average greylevel (clipped on uint8) + """ + _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, + bitdepth, 0., 0., s0, s1) + + +def pop(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, int s0=1, int s1=1): + """returns the number of actual pixels of the structuring element inside + the mask + """ + _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, + bitdepth, .0, .0, s0, s1) diff --git a/skimage/filter/rank/_crank16_percentiles.pyx b/skimage/filter/rank/_crank16_percentiles.pyx new file mode 100644 index 00000000..0ab71353 --- /dev/null +++ b/skimage/filter/rank/_crank16_percentiles.pyx @@ -0,0 +1,330 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from skimage.filter.rank._core16 cimport _core16, int_min, int_max + + +# ----------------------------------------------------------------- +# kernels uint16 (SOFT version using percentiles) +# ----------------------------------------------------------------- + + +ctypedef cnp.uint16_t dtype_t + + +cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(maxbin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range(maxbin - 1, -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + + delta = imax - imin + if delta > 0: + return (1.0 * (maxbin - 1) + * (int_min(int_max(imin, g), imax) + - imin) / delta) + else: + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(maxbin): + sum += histo[i] + if sum >= p0 * pop: + imin = i + break + sum = 0 + for i in range((maxbin - 1), -1, -1): + sum += histo[i] + if sum >= p1 * pop: + imax = i + break + + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(maxbin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + + if n > 0: + return (1.0 * mean / n) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t kernel_mean_substraction(Py_ssize_t * histo, + float pop, + dtype_t g, + Py_ssize_t bitdepth, + Py_ssize_t maxbin, + Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(maxbin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + if n > 0: + return ((g - (mean / n)) * .5 + midbin) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, + float pop, + dtype_t g, + Py_ssize_t bitdepth, + Py_ssize_t maxbin, + Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(maxbin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range((maxbin - 1), -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + if g > imax: + return imax + if g < imin: + return imin + if imax - g < g - imin: + return imax + else: + return imin + else: + return (0) + + +cdef inline dtype_t kernel_percentile(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i + cdef float sum = 0. + + if pop: + for i in range(maxbin): + sum += histo[i] + if sum >= p0 * pop: + break + + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i, sum, n + + if pop: + sum = 0 + n = 0 + for i in range(maxbin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + return (n) + else: + return (0) + + +cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, + dtype_t g, Py_ssize_t bitdepth, + Py_ssize_t maxbin, Py_ssize_t midbin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i + cdef float sum = 0. + + if pop: + for i in range(maxbin): + sum += histo[i] + if sum >= p0 * pop: + break + + return ((maxbin - 1) * (g >= i)) + else: + return (0) + + +# ----------------------------------------------------------------- +# python wrappers +# ----------------------------------------------------------------- + + +def autolevel(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """bottom hat + """ + _core16(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def gradient(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return p0,p1 percentile gradient + """ + _core16(kernel_gradient, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def mean(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return mean between [p0 and p1] percentiles + """ + _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def mean_substraction(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return original - mean between [p0 and p1] percentiles *.5 +127 + """ + _core16( + kernel_mean_substraction, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """reforce contrast using percentiles + """ + _core16(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def percentile(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return p0 percentile + """ + _core16(kernel_percentile, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def pop(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return nb of pixels between [p0 and p1] + """ + _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) + + +def threshold(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[cnp.uint8_t, ndim=2] selem, + cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, int bitdepth=8, + float p0=0., float p1=0.): + """return (maxbin-1) if g > percentile p0 + """ + _core16(kernel_threshold, image, selem, mask, out, shift_x, shift_y, + bitdepth, p0, p1, 0, 0) diff --git a/skimage/filter/rank/_crank8.pyx b/skimage/filter/rank/_crank8.pyx new file mode 100644 index 00000000..cb7febac --- /dev/null +++ b/skimage/filter/rank/_crank8.pyx @@ -0,0 +1,483 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log +from skimage.filter.rank._core8 cimport _core8 + + +# ----------------------------------------------------------------- +# kernels uint8 +# ----------------------------------------------------------------- + + +ctypedef cnp.uint8_t dtype_t + + +cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, delta + + if pop: + for i in range(255, -1, -1): + if histo[i]: + imax = i + break + for i in range(256): + if histo[i]: + imin = i + break + delta = imax - imin + if delta > 0: + return (255. * (g - imin) / delta) + else: + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_bottomhat(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(256): + if histo[i]: + break + + return (g - i) + else: + return (0) + + +cdef inline dtype_t kernel_equalize(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float sum = 0. + + if pop: + for i in range(256): + sum += histo[i] + if i >= g: + break + + return ((255 * sum) / pop) + else: + return (0) + + +cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(255, -1, -1): + if histo[i]: + imax = i + break + for i in range(256): + if histo[i]: + imin = i + break + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_maximum(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(255, -1, -1): + if histo[i]: + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(256): + mean += histo[i] * i + return (mean / pop) + else: + return (0) + + +cdef inline dtype_t kernel_meansubstraction(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(256): + mean += histo[i] * i + return ((g - mean / pop) / 2. + 127) + else: + return (0) + + +cdef inline dtype_t kernel_median(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float sum = pop / 2.0 + + if pop: + for i in range(256): + if histo[i]: + sum -= histo[i] + if sum < 0: + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_minimum(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(256): + if histo[i]: + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_modal(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t hmax = 0, imax = 0 + + if pop: + for i in range(256): + if histo[i] > hmax: + hmax = histo[i] + imax = i + return (imax) + else: + return (0) + + +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(255, -1, -1): + if histo[i]: + imax = i + break + for i in range(256): + if histo[i]: + imin = i + break + if imax - g < g - imin: + return (imax) + else: + return (imin) + else: + return (0) + + +cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + return (pop) + + +cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float mean = 0. + + if pop: + for i in range(256): + mean += histo[i] * i + return (g > (mean / pop)) + else: + return (0) + + +cdef inline dtype_t kernel_tophat(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(255, -1, -1): + if histo[i]: + break + + return (i - g) + else: + return (0) + +cdef inline dtype_t kernel_noise_filter(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t min_i + + # early stop if at least one pixel of the neighborhood has the same g + if histo[g] > 0: + return 0 + + for i in range(g, -1, -1): + if histo[i]: + break + min_i = g - i + for i in range(g, 256): + if histo[i]: + break + if i - g < min_i: + return (i - g) + else: + return min_i + + +cdef inline dtype_t kernel_entropy(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float e, p + + if pop: + e = 0. + + for i in range(256): + p = histo[i] / pop + if p > 0: + e -= p * log(p) / 0.6931471805599453 + + return e * 10 + else: + return (0) + +cdef inline dtype_t kernel_otsu(Py_ssize_t * histo, float pop, dtype_t g, + float p0, float p1, Py_ssize_t s0, + Py_ssize_t s1): + cdef Py_ssize_t i + cdef Py_ssize_t max_i + cdef float P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b + cdef float mu = 0. + + # compute local mean + if pop: + for i in range(256): + mu += histo[i] * i + mu = (mu / pop) + else: + return (0) + + # maximizing the between class variance + max_i = 0 + q1 = histo[0] / pop + m1 = 0. + max_sigma_b = 0. + + for i in range(1, 256): + P = histo[i] / pop + new_q1 = q1 + P + if new_q1 > 0: + mu1 = (q1 * mu1 + i * P) / new_q1 + mu2 = (mu - new_q1 * mu1) / (1. - new_q1) + sigma_b = new_q1 * (1. - new_q1) * (mu1 - mu2) ** 2 + if sigma_b > max_sigma_b: + max_sigma_b = sigma_b + max_i = i + q1 = new_q1 + + return max_i + + +# ----------------------------------------------------------------- +# python wrappers +# used only internally +# ----------------------------------------------------------------- + + +def autolevel(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def bottomhat(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_bottomhat, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def equalize(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_equalize, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def gradient(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_gradient, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def maximum(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_maximum, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def mean(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_mean, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def meansubstraction(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_meansubstraction, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def median(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_median, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def minimum(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_minimum, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def modal(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_modal, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def pop(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_pop, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def threshold(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_threshold, image, selem, mask, out, shift_x, shift_y, 0, 0, + 0, 0) + + +def tophat(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_tophat, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def noise_filter(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_noise_filter, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def entropy(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_entropy, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) + + +def otsu(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): + _core8(kernel_otsu, image, selem, mask, out, shift_x, shift_y, + 0, 0, 0, 0) diff --git a/skimage/filter/rank/_crank8_percentiles.pyx b/skimage/filter/rank/_crank8_percentiles.pyx new file mode 100644 index 00000000..d085957e --- /dev/null +++ b/skimage/filter/rank/_crank8_percentiles.pyx @@ -0,0 +1,294 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from skimage.filter.rank._core8 cimport _core8, uint8_max, uint8_min + + +# ----------------------------------------------------------------- +# kernels uint8 (SOFT version using percentiles) +# ----------------------------------------------------------------- + + +ctypedef cnp.uint8_t dtype_t + + +cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + imin = 0 + imax = 255 + + for i in range(256): + sum += histo[i] + if sum > (p0 * pop): + imin = i + break + sum = 0 + for i in range(255, -1, -1): + sum += histo[i] + if sum > (p1 * pop): + imax = i + break + delta = imax - imin + if delta > 0: + return (255 * (uint8_min(uint8_max(imin, g), imax) + - imin) / delta) + else: + return (imax - imin) + else: + return (128) + + +cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(256): + sum += histo[i] + if sum >= p0 * pop: + imin = i + break + sum = 0 + for i in range(255, -1, -1): + sum += histo[i] + if sum >= p1 * pop: + imax = i + break + + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(256): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + if n > 0: + return (1.0 * mean / n) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t kernel_mean_substraction(Py_ssize_t * histo, + float pop, + dtype_t g, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(256): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + if n > 0: + return ((g - (mean / n)) * .5 + 127) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, + float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(256): + sum += histo[i] + if sum >= p0 * pop: + imin = i + break + sum = 0 + for i in range(255, -1, -1): + sum += histo[i] + if sum >= p1 * pop: + imax = i + break + if g > imax: + return imax + if g < imin: + return imin + if imax - g < g - imin: + return imax + else: + return imin + else: + return (0) + + +cdef inline dtype_t kernel_percentile(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i + cdef float sum = 0. + + if pop: + for i in range(256): + sum += histo[i] + if sum >= p0 * pop: + break + + return (i) + else: + return (0) + + +cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i, sum, n + + if pop: + sum = 0 + n = 0 + for i in range(256): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + return (n) + else: + return (0) + + +cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, + dtype_t g, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef int i + cdef float sum = 0. + + if pop: + for i in range(256): + sum += histo[i] + if sum >= p0 * pop: + break + + return (255 * (g >= i)) + else: + return (0) + + +# ----------------------------------------------------------------- +# python wrappers +# ----------------------------------------------------------------- + + +def autolevel(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """autolevel + """ + _core8(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, p0, p1, + 0, 0) + + +def gradient(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return p0,p1 percentile gradient + """ + _core8(kernel_gradient, image, selem, mask, out, shift_x, shift_y, p0, p1, + 0, 0) + + +def mean(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return mean between [p0 and p1] percentiles + """ + _core8(kernel_mean, image, selem, mask, out, shift_x, shift_y, p0, p1, + 0, 0) + + +def mean_substraction(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return original - mean between [p0 and p1] percentiles *.5 +127 + """ + _core8(kernel_mean_substraction, image, selem, mask, out, shift_x, shift_y, + p0, p1, 0, 0) + + +def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """reforce contrast using percentiles + """ + _core8(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, + p0, p1, 0, 0) + + +def percentile(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return p0 percentile + """ + _core8(kernel_percentile, image, selem, mask, out, shift_x, shift_y, + p0, p1, 0, 0) + + +def pop(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return nb of pixels between [p0 and p1] + """ + _core8(kernel_pop, image, selem, mask, out, shift_x, shift_y, p0, p1, + 0, 0) + + +def threshold(cnp.ndarray[dtype_t, ndim=2] image, + cnp.ndarray[dtype_t, ndim=2] selem, + cnp.ndarray[dtype_t, ndim=2] mask=None, + cnp.ndarray[dtype_t, ndim=2] out=None, + char shift_x=0, char shift_y=0, float p0=0., float p1=0.): + """return 255 if g > percentile p0 + """ + _core8(kernel_threshold, image, selem, mask, out, shift_x, shift_y, p0, p1, + 0, 0) diff --git a/skimage/filter/rank/bilateral_rank.pyx b/skimage/filter/rank/bilateral_rank.pyx new file mode 100644 index 00000000..04f6cb35 --- /dev/null +++ b/skimage/filter/rank/bilateral_rank.pyx @@ -0,0 +1,192 @@ +"""Approximate bilateral rank filter for local (custom kernel) mean. + +The local histogram is computed using a sliding window similar to the method +described in [1]_. + +Input image must be 16-bit with a value < 4096 (i.e. 12 bit), +the number of histogram bins is determined from the +maximum value present in the image. + +The pixel neighborhood is defined by: + +* the given structuring element +* an interval [g-s0,g+s1] in greylevel around g the processed pixel greylevel + +The kernel is flat (i.e. each pixel belonging to the neighborhood contributes +equally). + +Result image is 16-bit with respect to the input image. + +References +---------- + +.. [1] Huang, T. ,Yang, G. ; Tang, G.. "A fast two-dimensional + median filtering algorithm", IEEE Transactions on Acoustics, Speech and + Signal Processing, Feb 1979. Volume: 27 , Issue: 1, Page(s): 13 - 18. + +""" + +import numpy as np +from skimage import img_as_ubyte +from skimage.filter.rank import _crank16_bilateral +from skimage.filter.rank.generic import find_bitdepth + + +__all__ = ['bilateral_mean', 'bilateral_pop'] + + +def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, s0, s1): + selem = img_as_ubyte(selem) + image = np.ascontiguousarray(image) + + if mask is None: + mask = np.ones(image.shape, dtype=np.uint8) + else: + mask = np.ascontiguousarray(mask) + mask = img_as_ubyte(mask) + + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + + if image.dtype == np.uint8: + if func8 is None: + raise TypeError("Not implemented for uint8 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint8) + func8(image, selem, shift_x=shift_x, shift_y=shift_y, + mask=mask, out=out, s0=s0, s1=s1) + elif image.dtype == np.uint16: + if func16 is None: + raise TypeError("Not implemented for uint16 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint16) + bitdepth = find_bitdepth(image) + if bitdepth > 11: + raise ValueError("Only uint16 <4096 image (12bit) supported.") + func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + bitdepth=bitdepth + 1, out=out, s0=s0, s1=s1) + else: + raise TypeError("Only uint8 and uint16 image supported.") + + return out + + +def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, s0=10, s1=10): + """Apply a flat kernel bilateral filter. + + This is an edge-preserving and noise reducing denoising filter. It averages + pixels based on their spatial closeness and radiometric similarity. + + Spatial closeness is measured by considering only the local pixel + neighborhood given by a structuring element (selem). + + Radiometric similarity is defined by the greylevel interval [g-s0,g+s1] + where g is the current pixel greylevel. Only pixels belonging to the + structuring element AND having a greylevel inside this interval are + averaged. Return greyscale local bilateral_mean of an image. + + Parameters + ---------- + image : ndarray + Image array (uint16). As the algorithm uses max. 12bit histogram, + an exception will be raised if image has a value > 4095 + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : (int) + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + s0, s1 : int + define the [s0, s1] interval to be considered for computing the value. + + Returns + ------- + out : uint16 array + The result of the local bilateral mean. + + See also + -------- + skimage.filter.denoise_bilateral() for a gaussian bilateral filter. + + Notes + ----- + + * input image are 16-bit only + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import bilateral_mean + >>> # Load test image / cast to uint16 + >>> ima = data.camera().astype(np.uint16) + >>> # bilateral filtering of cameraman image using a flat kernel + >>> bilat_ima = bilateral_mean(ima, disk(20), s0=10,s1=10) + """ + + return _apply(None, _crank16_bilateral.mean, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) + + +def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, s0=10, s1=10): + """Return the number (population) of pixels actually inside the bilateral + neighborhood, i.e. being inside the structuring element AND having a gray + level inside the interval [g-s0, g+s1]. + + Parameters + ---------- + image : ndarray + Image array (uint16). As the algorithm uses max. 12bit histogram, + an exception will be raised if image has a value > 4095 + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : (int) + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + s0, s1 : int + define the [s0, s1] interval to be considered for computing the value. + + Returns + ------- + out : uint16 array + the local number of pixels inside the bilateral neighborhood + + Notes + ----- + + * input image are 16-bit only + + Examples + -------- + >>> # Local mean + >>> from skimage.morphology import square + >>> import skimage.filter.rank as rank + >>> ima16 = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint16) + >>> rank.bilateral_pop(ima16, square(3), s0=10,s1=10) + array([[3, 4, 3, 4, 3], + [4, 4, 6, 4, 4], + [3, 6, 9, 6, 3], + [4, 4, 6, 4, 4], + [3, 4, 3, 4, 3]], dtype=uint16) + + """ + + return _apply(None, _crank16_bilateral.pop, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py new file mode 100644 index 00000000..94fc3130 --- /dev/null +++ b/skimage/filter/rank/generic.py @@ -0,0 +1,11 @@ +import numpy as np + + +def find_bitdepth(image): + """returns the max bith depth of a uint16 image + """ + umax = np.max(image) + if umax > 2: + return int(np.log2(umax)) + else: + return 1 diff --git a/skimage/filter/rank/percentile_rank.pyx b/skimage/filter/rank/percentile_rank.pyx new file mode 100644 index 00000000..7deae623 --- /dev/null +++ b/skimage/filter/rank/percentile_rank.pyx @@ -0,0 +1,396 @@ +"""Inferior and superior ranks, provided by the user, are passed to the kernel +function to provide a softer version of the rank filters. E.g. +percentile_autolevel will stretch image levels between percentile [p0, p1] +instead of using [min, max]. It means that isolated bright or dark pixels will +not produce halos. + +The local histogram is computed using a sliding window similar to the method +described in [1]_. + +Input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit), for 16-bit +input images, the number of histogram bins is determined from the maximum value +present in the image. + +Result image is 8 or 16-bit with respect to the input image. + +References +---------- + +.. [1] Huang, T. ,Yang, G. ; Tang, G.. "A fast two-dimensional + median filtering algorithm", IEEE Transactions on Acoustics, Speech and + Signal Processing, Feb 1979. Volume: 27 , Issue: 1, Page(s): 13 - 18. + +""" + +import numpy as np +from skimage import img_as_ubyte +from skimage.filter.rank.generic import find_bitdepth +from skimage.filter.rank import _crank16_percentiles, _crank8_percentiles + + +__all__ = ['percentile_autolevel', 'percentile_gradient', + 'percentile_mean', 'percentile_mean_substraction', + 'percentile_morph_contr_enh', 'percentile', 'percentile_pop', + 'percentile_threshold'] + + +def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, p0, p1): + selem = img_as_ubyte(selem) + image = np.ascontiguousarray(image) + + if mask is None: + mask = np.ones(image.shape, dtype=np.uint8) + else: + mask = np.ascontiguousarray(mask) + mask = img_as_ubyte(mask) + + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + + if image.dtype == np.uint8: + if func8 is None: + raise TypeError("Not implemented for uint8 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint8) + func8(image, selem, shift_x=shift_x, shift_y=shift_y, + mask=mask, out=out, p0=p0, p1=p1) + elif image.dtype == np.uint16: + if func16 is None: + raise TypeError("Not implemented for uint16 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint16) + bitdepth = find_bitdepth(image) + if bitdepth > 11: + raise ValueError("Only uint16 <4096 image (12bit) supported.") + func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + bitdepth=bitdepth + 1, out=out, p0=p0, p1=p1) + else: + raise TypeError("Only uint8 and uint16 image supported.") + + return out + + +def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local autolevel of an image. + + Autolevel is computed on the given structuring element. Only levels between + percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local autolevel : uint8 array or uint16 + The result of the local autolevel. + + """ + + return _apply( + _crank8_percentiles.autolevel, _crank16_percentiles.autolevel, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local percentile_gradient of an image. + + percentile_gradient is computed on the given structuring element. Only + levels between percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local percentile_gradient : uint8 array or uint16 + The result of the local percentile_gradient. + + """ + + return _apply(_crank8_percentiles.gradient, _crank16_percentiles.gradient, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_mean(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local mean of an image. + + Mean is computed on the given structuring element. Only levels between + percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local mean : uint8 array or uint16 + The result of the local mean. + + """ + + return _apply(_crank8_percentiles.mean, _crank16_percentiles.mean, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_mean_substraction(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, p0=.0, p1=1.): + """Return greyscale local mean_substraction of an image. + + mean_substraction is computed on the given structuring element. Only levels + between percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local mean_substraction : uint8 array or uint16 + The result of the local mean_substraction. + + """ + + return _apply(_crank8_percentiles.mean_substraction, + _crank16_percentiles.mean_substraction, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_morph_contr_enh( + image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local morph_contr_enh of an image. + + morph_contr_enh is computed on the given structuring element. Only levels + between percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local morph_contr_enh : uint8 array or uint16 + The result of the local morph_contr_enh. + + """ + + return _apply(_crank8_percentiles.morph_contr_enh, + _crank16_percentiles.morph_contr_enh, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, + p0=.0, p1=1.): + """Return greyscale local percentile of an image. + + percentile is computed on the given structuring element. Only levels between + percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local percentile : uint8 array or uint16 + The result of the local percentile. + + """ + + return _apply(_crank8_percentiles.percentile, + _crank16_percentiles.percentile, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_pop(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local pop of an image. + + pop is computed on the given structuring element. Only levels between + percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local pop : uint8 array or uint16 + The result of the local pop. + + """ + + return _apply(_crank8_percentiles.pop, _crank16_percentiles.pop, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=.0, p1=1.): + """Return greyscale local threshold of an image. + + threshold is computed on the given structuring element. Only levels between + percentiles [p0, p1] are used. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, as the + algorithm uses max. 12bit histogram, an exception will be raised if + image has a value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + local threshold : uint8 array or uint16 + The result of the local threshold. + + """ + + return _apply( + _crank8_percentiles.threshold, _crank16_percentiles.threshold, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) diff --git a/skimage/filter/rank/rank.pyx b/skimage/filter/rank/rank.pyx new file mode 100644 index 00000000..e8a4c8f1 --- /dev/null +++ b/skimage/filter/rank/rank.pyx @@ -0,0 +1,769 @@ +"""The local histogram is computed using a sliding window similar to the method +described in [1]_. + +Input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit), for 16-bit +input images, the number of histogram bins is determined from the maximum value +present in the image. + +Result image is 8 or 16-bit with respect to the input image. + +References +---------- + +.. [1] Huang, T. ,Yang, G. ; Tang, G.. "A fast two-dimensional + median filtering algorithm", IEEE Transactions on Acoustics, Speech and + Signal Processing, Feb 1979. Volume: 27 , Issue: 1, Page(s): 13 - 18. + +""" + +import numpy as np +from skimage import img_as_ubyte +from skimage.filter.rank import _crank8, _crank16 +from skimage.filter.rank.generic import find_bitdepth + + +__all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', + 'meansubstraction', 'median', 'minimum', 'modal', 'morph_contr_enh', + 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] + + +def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): + selem = img_as_ubyte(selem) + image = np.ascontiguousarray(image) + + if mask is None: + mask = np.ones(image.shape, dtype=np.uint8) + else: + mask = np.ascontiguousarray(mask) + mask = img_as_ubyte(mask) + + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + + if image.dtype == np.uint8: + if func8 is None: + raise TypeError("Not implemented for uint8 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint8) + func8(image, selem, shift_x=shift_x, shift_y=shift_y, + mask=mask, out=out) + elif image.dtype == np.uint16: + if func16 is None: + raise TypeError("Not implemented for uint16 image.") + if out is None: + out = np.zeros(image.shape, dtype=np.uint16) + bitdepth = find_bitdepth(image) + if bitdepth > 11: + raise ValueError("Only uint16 <4096 image (12bit) supported.") + func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + bitdepth=bitdepth + 1, out=out) + else: + raise TypeError("Only uint8 and uint16 image supported.") + + return out + + +def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Autolevel image using local histogram. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local autolevel. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import autolevel + >>> # Load test image + >>> ima = data.camera() + >>> # Stretch image contrast locally + >>> auto = autolevel(ima, disk(20)) + + """ + + return _apply(_crank8.autolevel, _crank16.autolevel, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns greyscale local bottomhat of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + local bottomhat : uint8 array or uint16 array depending on input image + The result of the local bottomhat. + + """ + + return _apply(_crank8.bottomhat, _crank16.bottomhat, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Equalize image using local histogram. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local equalize. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import equalize + >>> # Load test image + >>> ima = data.camera() + >>> # Local equalization + >>> equ = equalize(ima, disk(20)) + + """ + + return _apply(_crank8.equalize, _crank16.equalize, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local gradient of an image (i.e. local maximum - local + minimum). + + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local gradient. + + """ + + return _apply(_crank8.gradient, _crank16.gradient, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local maximum of an image. + + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local maximum. + + See also + -------- + skimage.morphology.dilation + + Note + ---- + * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) + * the lower algorithm complexity makes the rank.maximum() more efficient for + larger images and structuring elements + + """ + + return _apply(_crank8.maximum, _crank16.maximum, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local mean of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local mean. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import mean + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = mean(ima, disk(20)) + + """ + + return _apply(_crank8.mean, _crank16.mean, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def meansubstraction(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Return image substracted from its local mean. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local meansubstraction. + + """ + + return _apply(_crank8.meansubstraction, _crank16.meansubstraction, image, + selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local median of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local median. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import median + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = median(ima, disk(20)) + + """ + + return _apply(_crank8.median, _crank16.median, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local minimum of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local minimum. + + See also + -------- + skimage.morphology.erosion + + Note + ---- + * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) + * the lower algorithm complexity makes the rank.minimum() more efficient + for larger images and structuring elements + + """ + + return _apply(_crank8.minimum, _crank16.minimum, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local mode of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local modal. + + """ + + return _apply(_crank8.modal, _crank16.modal, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Enhance an image replacing each pixel by the local maximum if pixel + greylevel is closest to maximimum than local minimum OR local minimum + otherwise. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local morph_contr_enh. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import morph_contr_enh + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = morph_contr_enh(ima, disk(20)) + + """ + + return _apply(_crank8.morph_contr_enh, _crank16.morph_contr_enh, image, + selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return the number (population) of pixels actually inside the + neighborhood. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The number of pixels belonging to the neighborhood. + + Examples + -------- + >>> # Local mean + >>> from skimage.morphology import square + >>> import skimage.filter.rank as rank + >>> ima = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> rank.pop(ima, square(3)) + array([[4, 6, 6, 6, 4], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [4, 6, 6, 6, 4]], dtype=uint8) + + """ + + return _apply(_crank8.pop, _crank16.pop, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local threshold of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local threshold. + + Examples + -------- + >>> # Local threshold + >>> from skimage.morphology import square + >>> from skimage.filter.rank import threshold + >>> ima = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> threshold(ima, square(3)) + array([[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + """ + + return _apply(_crank8.threshold, _crank16.threshold, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local tophat of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The image tophat. + + """ + + return _apply(_crank8.tophat, _crank16.tophat, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def noise_filter(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Returns the noise feature as described in [Hashimoto12]_ + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + References + ---------- + .. [Hashimoto12] N. Hashimoto et al. Referenceless image quality evaluation + for whole slide imaging. J Pathol Inform 2012;3:9. + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The image noise. + + """ + + # ensure that the central pixel in the structuring element is empty + centre_r = int(selem.shape[0] / 2) + shift_y + centre_c = int(selem.shape[1] / 2) + shift_x + # make a local copy + selem_cpy = selem.copy() + selem_cpy[centre_r, centre_c] = 0 + + return _apply(_crank8.noise_filter, None, image, selem_cpy, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns the entropy [wiki_entropy]_ computed locally. Entropy is computed + using base 2 logarithm i.e. the filter returns the minimum number of + bits needed to encode local greylevel distribution. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + entropy x10 (uint8 images) and entropy x1000 (uint16 images) + + References + ---------- + .. [wiki_entropy] http://en.wikipedia.org/wiki/Entropy_(information_theory) + + Examples + -------- + >>> # Local entropy + >>> from skimage import data + >>> from skimage.filter.rank import entropy + >>> from skimage.morphology import disk + >>> # defining a 8- and a 16-bit test images + >>> a8 = data.camera() + >>> a16 = data.camera().astype(np.uint16) * 4 + >>> # pixel values contain 10x the local entropy + >>> ent8 = entropy(a8, disk(5)) + >>> # pixel values contain 1000x the local entropy + >>> ent16 = entropy(a16, disk(5)) + + """ + + return _apply(_crank8.entropy, _crank16.entropy, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns the Otsu's threshold value for each pixel. + + Parameters + ---------- + image : ndarray + Image array (uint8 array). + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array + Otsu's threshold values + + References + ---------- + .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method + + Notes + ----- + * input image are 8-bit only + + Examples + -------- + >>> # Local entropy + >>> from skimage import data + >>> from skimage.filter.rank import otsu + >>> from skimage.morphology import disk + >>> # defining a 8-bit test images + >>> a8 = data.camera() + >>> loc_otsu = otsu(a8, disk(5)) + >>> thresh_image = a8 >= loc_otsu + + """ + + return _apply(_crank8.otsu, None, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py new file mode 100644 index 00000000..43ff030f --- /dev/null +++ b/skimage/filter/rank/tests/test_rank.py @@ -0,0 +1,380 @@ +import numpy as np +from numpy.testing import run_module_suite, assert_array_equal, assert_raises + +from skimage import data +from skimage.morphology import cmorph, disk +from skimage.filter import rank + + +def test_random_sizes(): + # make sure the size is not a problem + + niter = 10 + elem = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8) + for m, n in np.random.random_integers(1, 100, size=(10, 2)): + mask = np.ones((m, n), dtype=np.uint8) + + image8 = np.ones((m, n), dtype=np.uint8) + out8 = np.empty_like(image8) + rank.mean(image=image8, selem=elem, mask=mask, out=out8, + shift_x=0, shift_y=0) + assert_array_equal(image8.shape, out8.shape) + rank.mean(image=image8, selem=elem, mask=mask, out=out8, + shift_x=+1, shift_y=+1) + assert_array_equal(image8.shape, out8.shape) + + image16 = np.ones((m, n), dtype=np.uint16) + out16 = np.empty_like(image8, dtype=np.uint16) + rank.mean(image=image16, selem=elem, mask=mask, out=out16, + shift_x=0, shift_y=0) + assert_array_equal(image16.shape, out16.shape) + rank.mean(image=image16, selem=elem, mask=mask, out=out16, + shift_x=+1, shift_y=+1) + assert_array_equal(image16.shape, out16.shape) + + rank.percentile_mean(image=image16, mask=mask, out=out16, + selem=elem, shift_x=0, shift_y=0, p0=.1, p1=.9) + assert_array_equal(image16.shape, out16.shape) + rank.percentile_mean(image=image16, mask=mask, out=out16, + selem=elem, shift_x=+1, shift_y=+1, p0=.1, p1=.9) + assert_array_equal(image16.shape, out16.shape) + + +def test_compare_with_cmorph_dilate(): + # compare the result of maximum filter with dilate + + image = (np.random.random((100, 100)) * 256).astype(np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + + for r in range(1, 20, 1): + elem = np.ones((r, r), dtype=np.uint8) + rank.maximum(image=image, selem=elem, out=out, mask=mask) + cm = cmorph.dilate(image=image, selem=elem) + assert_array_equal(out, cm) + + +def test_compare_with_cmorph_erode(): + # compare the result of maximum filter with erode + + image = (np.random.random((100, 100)) * 256).astype(np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + + for r in range(1, 20, 1): + elem = np.ones((r, r), dtype=np.uint8) + rank.minimum(image=image, selem=elem, out=out, mask=mask) + cm = cmorph.erode(image=image, selem=elem) + assert_array_equal(out, cm) + + +def test_bitdepth(): + # test the different bit depth for rank16 + + elem = np.ones((3, 3), dtype=np.uint8) + out = np.empty((100, 100), dtype=np.uint16) + mask = np.ones((100, 100), dtype=np.uint8) + + for i in range(5): + image = np.ones((100, 100), dtype=np.uint16) * 255 * 2 ** i + r = rank.percentile_mean(image=image, selem=elem, mask=mask, + out=out, shift_x=0, shift_y=0, p0=.1, p1=.9) + + +def test_population(): + # check the number of valid pixels in the neighborhood + + image = np.zeros((5, 5), dtype=np.uint8) + elem = np.ones((3, 3), dtype=np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + + rank.pop(image=image, selem=elem, out=out, mask=mask) + r = np.array([[4, 6, 6, 6, 4], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [4, 6, 6, 6, 4]]) + assert_array_equal(r, out) + + +def test_structuring_element8(): + # check the output for a custom structuring element + + r = np.array([[0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 255, 0, 0, 0], + [0, 0, 255, 255, 255, 0], + [0, 0, 0, 255, 255, 0], + [0, 0, 0, 0, 0, 0]]) + + # 8-bit + image = np.zeros((6, 6), dtype=np.uint8) + image[2, 2] = 255 + elem = np.asarray([[1, 1, 0], [1, 1, 1], [0, 0, 1]], dtype=np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=1, shift_y=1) + assert_array_equal(r, out) + + # 16-bit + image = np.zeros((6, 6), dtype=np.uint16) + image[2, 2] = 255 + out = np.empty_like(image) + + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=1, shift_y=1) + assert_array_equal(r, out) + + +def test_fail_on_bitdepth(): + # should fail because data bitdepth is too high for the function + + image = np.ones((100, 100), dtype=np.uint16) * 2 ** 12 + elem = np.ones((3, 3), dtype=np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + assert_raises(ValueError, rank.percentile_mean, image=image, + selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) + + +def test_pass_on_bitdepth(): + # should pass because data bitdepth is not too high for the function + + image = np.ones((100, 100), dtype=np.uint16) * 2 ** 11 + elem = np.ones((3, 3), dtype=np.uint8) + out = np.empty_like(image) + mask = np.ones(image.shape, dtype=np.uint8) + + +def test_inplace_output(): + # rank filters are not supposed to filter inplace + + selem = disk(20) + image = (np.random.random((500, 500)) * 256).astype(np.uint8) + out = image + assert_raises(NotImplementedError, rank.mean, image, selem, out=out) + + +def test_compare_autolevels(): + # compare autolevel and percentile autolevel with p0=0.0 and p1=1.0 + # should returns the same arrays + + image = data.camera() + + selem = disk(20) + loc_autolevel = rank.autolevel(image, selem=selem) + loc_perc_autolevel = rank.percentile_autolevel(image, selem=selem, + p0=.0, p1=1.) + + assert_array_equal(loc_autolevel, loc_perc_autolevel) + + +def test_compare_autolevels_16bit(): + # compare autolevel(16-bit) and percentile autolevel(16-bit) with p0=0.0 and + # p1=1.0 should returns the same arrays + + image = data.camera().astype(np.uint16) * 4 + + selem = disk(20) + loc_autolevel = rank.autolevel(image, selem=selem) + loc_perc_autolevel = rank.percentile_autolevel(image, selem=selem, + p0=.0, p1=1.) + + assert_array_equal(loc_autolevel, loc_perc_autolevel) + + +def test_compare_8bit_vs_16bit(): + # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of + # dynamic) should be identical + + image8 = data.camera() + image16 = image8.astype(np.uint16) + assert_array_equal(image8, image16) + + methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', + 'mean', 'meansubstraction', 'median', 'minimum', 'modal', + 'morph_contr_enh', 'pop', 'threshold', 'tophat'] + + for method in methods: + func = getattr(rank, method) + f8 = func(image8, disk(3)) + f16 = func(image16, disk(3)) + assert_array_equal(f8, f16) + + +def test_trivial_selem8(): + # check that min, max and mean returns identity if structuring element + # contains only central pixel + + image = np.zeros((5, 5), dtype=np.uint8) + out = np.zeros_like(image) + mask = np.ones_like(image, dtype=np.uint8) + image[2, 2] = 255 + image[2, 3] = 128 + image[1, 2] = 16 + + elem = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.uint8) + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.minimum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + + +def test_trivial_selem16(): + # check that min, max and mean returns identity if structuring element + # contains only central pixel + + image = np.zeros((5, 5), dtype=np.uint16) + out = np.zeros_like(image) + mask = np.ones_like(image, dtype=np.uint8) + image[2, 2] = 255 + image[2, 3] = 128 + image[1, 2] = 16 + + elem = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.uint8) + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.minimum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + + +def test_smallest_selem8(): + # check that min, max and mean returns identity if structuring element + # contains only central pixel + + image = np.zeros((5, 5), dtype=np.uint8) + out = np.zeros_like(image) + mask = np.ones_like(image, dtype=np.uint8) + image[2, 2] = 255 + image[2, 3] = 128 + image[1, 2] = 16 + + elem = np.array([[1]], dtype=np.uint8) + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.minimum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + + +def test_smallest_selem16(): + # check that min, max and mean returns identity if structuring element + # contains only central pixel + + image = np.zeros((5, 5), dtype=np.uint16) + out = np.zeros_like(image) + mask = np.ones_like(image, dtype=np.uint8) + image[2, 2] = 255 + image[2, 3] = 128 + image[1, 2] = 16 + + elem = np.array([[1]], dtype=np.uint8) + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.minimum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(image, out) + + +def test_empty_selem(): + # check that min, max and mean returns zeros if structuring element is empty + + image = np.zeros((5, 5), dtype=np.uint16) + out = np.zeros_like(image) + mask = np.ones_like(image, dtype=np.uint8) + res = np.zeros_like(image) + image[2, 2] = 255 + image[2, 3] = 128 + image[1, 2] = 16 + + elem = np.array([[0, 0, 0], [0, 0, 0]], dtype=np.uint8) + + rank.mean(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(res, out) + rank.minimum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(res, out) + rank.maximum(image=image, selem=elem, out=out, mask=mask, + shift_x=0, shift_y=0) + assert_array_equal(res, out) + + +def test_otsu(): + # test the local Otsu segmentation on a synthetic image + # (left to right ramp * sinus) + + test = np.tile( + [128, 145, 103, 127, 165, 83, 127, 185, 63, 127, 205, 43, + 127, 225, 23, 127], + (16, 1)) + test = test.astype(np.uint8) + res = np.tile([1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1], + (16, 1)) + selem = np.ones((6, 6), dtype=np.uint8) + th = 1 * (test >= rank.otsu(test, selem)) + assert_array_equal(th, res) + + +def test_entropy(): + # verify that entropy is coherent with bitdepth of the input data + + selem = np.ones((16, 16), dtype=np.uint8) + # 1 bit per pixel + data = np.tile(np.asarray([0, 1]), (100, 100)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 10) + + # 2 bit per pixel + data = np.tile(np.asarray([[0, 1], [2, 3]]), (10, 10)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 20) + + # 3 bit per pixel + data = np.tile( + np.asarray([[0, 1, 2, 3], [4, 5, 6, 7]]), (10, 10)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 30) + + # 4 bit per pixel + data = np.tile( + np.reshape(np.arange(16), (4, 4)), (10, 10)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 40) + + # 6 bit per pixel + data = np.tile( + np.reshape(np.arange(64), (8, 8)), (10, 10)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 60) + + # 8-bit per pixel + data = np.tile( + np.reshape(np.arange(256), (16, 16)), (10, 10)).astype(np.uint8) + assert(np.max(rank.entropy(data, selem)) == 80) + + # 12 bit per pixel + selem = np.ones((64, 64), dtype=np.uint8) + data = np.tile( + np.reshape(np.arange(4096), (64, 64)), (2, 2)).astype(np.uint16) + assert(np.max(rank.entropy(data, selem)) == 12000) + + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index 3e573aec..934d3f2e 100644 --- a/skimage/filter/setup.py +++ b/skimage/filter/setup.py @@ -13,9 +13,48 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['_ctmf.pyx'], working_path=base_path) + cython(['_denoise_cy.pyx'], working_path=base_path) + cython(['rank/_core8.pyx'], working_path=base_path) + cython(['rank/_core16.pyx'], working_path=base_path) + cython(['rank/_crank8.pyx'], working_path=base_path) + cython(['rank/_crank8_percentiles.pyx'], working_path=base_path) + cython(['rank/_crank16.pyx'], working_path=base_path) + cython(['rank/_crank16_percentiles.pyx'], working_path=base_path) + cython(['rank/_crank16_bilateral.pyx'], working_path=base_path) + cython(['rank/rank.pyx'], working_path=base_path) + cython(['rank/percentile_rank.pyx'], working_path=base_path) + cython(['rank/bilateral_rank.pyx'], working_path=base_path) config.add_extension('_ctmf', sources=['_ctmf.c'], - include_dirs=[get_numpy_include_dirs()]) + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_denoise_cy', sources=['_denoise_cy.c'], + include_dirs=[get_numpy_include_dirs(), '../_shared']) + config.add_extension('rank._core8', sources=['rank/_core8.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('rank._core16', sources=['rank/_core16.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('rank._crank8', sources=['rank/_crank8.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank._crank8_percentiles', sources=['rank/_crank8_percentiles.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('rank._crank16', sources=['rank/_crank16.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank._crank16_percentiles', sources=['rank/_crank16_percentiles.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank._crank16_bilateral', sources=['rank/_crank16_bilateral.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank.rank', sources=['rank/rank.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank.percentile_rank', sources=['rank/percentile_rank.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension( + 'rank.bilateral_rank', sources=['rank/bilateral_rank.c'], + include_dirs=[get_numpy_include_dirs()]) return config diff --git a/skimage/filter/tests/test_denoise.py b/skimage/filter/tests/test_denoise.py new file mode 100644 index 00000000..f81ba07b --- /dev/null +++ b/skimage/filter/tests/test_denoise.py @@ -0,0 +1,140 @@ +import numpy as np +from numpy.testing import run_module_suite, assert_raises, assert_equal + +from skimage import filter, data, color, img_as_float + + +np.random.seed(1234) + + +lena = img_as_float(data.lena()[:256, :256]) +lena_gray = color.rgb2gray(lena) + + +def test_denoise_tv_chambolle_2d(): + # lena image + img = lena_gray + # add noise to lena + img += 0.5 * img.std() * np.random.random(img.shape) + # clip noise so that it does not exceed allowed range for float images. + img = np.clip(img, 0, 1) + # denoise + denoised_lena = filter.denoise_tv_chambolle(img, weight=60.0) + # which dtype? + assert denoised_lena.dtype in [np.float, np.float32, np.float64] + from scipy import ndimage + grad = ndimage.morphological_gradient(img, size=((3, 3))) + grad_denoised = ndimage.morphological_gradient( + denoised_lena, size=((3, 3))) + # test if the total variation has decreased + assert grad_denoised.dtype == np.float + assert (np.sqrt((grad_denoised**2).sum()) + < np.sqrt((grad**2).sum()) / 2) + + +def test_denoise_tv_chambolle_multichannel(): + denoised0 = filter.denoise_tv_chambolle(lena[..., 0], weight=60.0) + denoised = filter.denoise_tv_chambolle(lena, weight=60.0, multichannel=True) + assert_equal(denoised[..., 0], denoised0) + + +def test_denoise_tv_chambolle_float_result_range(): + # lena image + img = lena_gray + int_lena = np.multiply(img, 255).astype(np.uint8) + assert np.max(int_lena) > 1 + denoised_int_lena = filter.denoise_tv_chambolle(int_lena, weight=60.0) + # test if the value range of output float data is within [0.0:1.0] + assert denoised_int_lena.dtype == np.float + assert np.max(denoised_int_lena) <= 1.0 + assert np.min(denoised_int_lena) >= 0.0 + + +def test_denoise_tv_chambolle_3d(): + """Apply the TV denoising algorithm on a 3D image representing a sphere.""" + x, y, z = np.ogrid[0:40, 0:40, 0:40] + mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 + mask = 100 * mask.astype(np.float) + mask += 60 + mask += 20 * np.random.random(mask.shape) + mask[mask < 0] = 0 + mask[mask > 255] = 255 + res = filter.denoise_tv_chambolle(mask.astype(np.uint8), weight=100) + assert res.dtype == np.float + assert res.std() * 255 < mask.std() + + # test wrong number of dimensions + assert_raises(ValueError, filter.denoise_tv_chambolle, + np.random.random((8, 8, 8, 8))) + + +def test_denoise_tv_bregman_2d(): + img = lena_gray + # add some random noise + img += 0.5 * img.std() * np.random.random(img.shape) + img = np.clip(img, 0, 1) + + out1 = filter.denoise_tv_bregman(img, weight=10) + out2 = filter.denoise_tv_bregman(img, weight=5) + + # make sure noise is reduced + assert img.std() > out1.std() + assert out1.std() > out2.std() + + +def test_denoise_tv_bregman_float_result_range(): + # lena image + img = lena_gray + int_lena = np.multiply(img, 255).astype(np.uint8) + assert np.max(int_lena) > 1 + denoised_int_lena = filter.denoise_tv_bregman(int_lena, weight=60.0) + # test if the value range of output float data is within [0.0:1.0] + assert denoised_int_lena.dtype == np.float + assert np.max(denoised_int_lena) <= 1.0 + assert np.min(denoised_int_lena) >= 0.0 + + +def test_denoise_tv_bregman_3d(): + img = lena + # add some random noise + img += 0.5 * img.std() * np.random.random(img.shape) + img = np.clip(img, 0, 1) + + out1 = filter.denoise_tv_bregman(img, weight=10) + out2 = filter.denoise_tv_bregman(img, weight=5) + + # make sure noise is reduced + assert img.std() > out1.std() + assert out1.std() > out2.std() + + +def test_denoise_bilateral_2d(): + img = lena_gray + # add some random noise + img += 0.5 * img.std() * np.random.random(img.shape) + img = np.clip(img, 0, 1) + + out1 = filter.denoise_bilateral(img, sigma_range=0.1, sigma_spatial=20) + out2 = filter.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) + + # make sure noise is reduced + assert img.std() > out1.std() + assert out1.std() > out2.std() + + +def test_denoise_bilateral_3d(): + img = lena + # add some random noise + img += 0.5 * img.std() * np.random.random(img.shape) + img = np.clip(img, 0, 1) + + out1 = filter.denoise_bilateral(img, sigma_range=0.1, sigma_spatial=20) + out2 = filter.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) + + # make sure noise is reduced + assert img.std() > out1.std() + assert out1.std() > out2.std() + + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/filter/tests/test_edges.py b/skimage/filter/tests/test_edges.py index a5d52aa5..628de16d 100644 --- a/skimage/filter/tests/test_edges.py +++ b/skimage/filter/tests/test_edges.py @@ -9,6 +9,7 @@ def test_sobel_zeros(): result = F.sobel(np.zeros((10, 10)), np.ones((10, 10), bool)) assert (np.all(result == 0)) + def test_sobel_mask(): """Sobel on a masked array should be zero""" np.random.seed(0) @@ -16,6 +17,7 @@ def test_sobel_mask(): np.zeros((10, 10), bool)) assert (np.all(result == 0)) + def test_sobel_horizontal(): """Sobel on an edge should be a horizontal line""" i, j = np.mgrid[-5:6, -5:6] @@ -26,6 +28,7 @@ def test_sobel_horizontal(): assert (np.all(result[i == 0] == 1)) assert (np.all(result[np.abs(i) > 1] == 0)) + def test_sobel_vertical(): """Sobel on a vertical edge should be a vertical line""" i, j = np.mgrid[-5:6, -5:6] @@ -41,6 +44,7 @@ def test_hsobel_zeros(): result = F.hsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) assert (np.all(result == 0)) + def test_hsobel_mask(): """Horizontal Sobel on a masked array should be zero""" np.random.seed(0) @@ -48,6 +52,7 @@ def test_hsobel_mask(): np.zeros((10, 10), bool)) assert (np.all(result == 0)) + def test_hsobel_horizontal(): """Horizontal Sobel on an edge should be a horizontal line""" i, j = np.mgrid[-5:6, -5:6] @@ -58,6 +63,7 @@ def test_hsobel_horizontal(): assert (np.all(result[i == 0] == 1)) assert (np.all(result[np.abs(i) > 1] == 0)) + def test_hsobel_vertical(): """Horizontal Sobel on a vertical edge should be zero""" i, j = np.mgrid[-5:6, -5:6] @@ -71,6 +77,7 @@ def test_vsobel_zeros(): result = F.vsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) assert (np.all(result == 0)) + def test_vsobel_mask(): """Vertical Sobel on a masked array should be zero""" np.random.seed(0) @@ -78,6 +85,7 @@ def test_vsobel_mask(): np.zeros((10, 10), bool)) assert (np.all(result == 0)) + def test_vsobel_vertical(): """Vertical Sobel on an edge should be a vertical line""" i, j = np.mgrid[-5:6, -5:6] @@ -88,6 +96,7 @@ def test_vsobel_vertical(): assert (np.all(result[j == 0] == 1)) assert (np.all(result[np.abs(j) > 1] == 0)) + def test_vsobel_horizontal(): """vertical Sobel on a horizontal edge should be zero""" i, j = np.mgrid[-5:6, -5:6] @@ -97,11 +106,114 @@ def test_vsobel_horizontal(): assert (np.all(np.abs(result) < eps)) +def test_scharr_zeros(): + """Scharr on an array of all zeros""" + result = F.scharr(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_scharr_mask(): + """Scharr on a masked array should be zero""" + np.random.seed(0) + result = F.scharr(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_scharr_horizontal(): + """Scharr on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.scharr(image) + # Fudge the eroded points + i[np.abs(j) == 5] = 10000 + assert (np.all(result[i == 0] == 1)) + assert (np.all(result[np.abs(i) > 1] == 0)) + + +def test_scharr_vertical(): + """Scharr on a vertical edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.scharr(image) + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + assert (np.all(result[np.abs(j) > 1] == 0)) + + +def test_hscharr_zeros(): + """Horizontal Scharr on an array of all zeros""" + result = F.hscharr(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_hscharr_mask(): + """Horizontal Scharr on a masked array should be zero""" + np.random.seed(0) + result = F.hscharr(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_hscharr_horizontal(): + """Horizontal Scharr on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.hscharr(image) + # Fudge the eroded points + i[np.abs(j) == 5] = 10000 + assert (np.all(result[i == 0] == 1)) + assert (np.all(result[np.abs(i) > 1] == 0)) + + +def test_hscharr_vertical(): + """Horizontal Scharr on a vertical edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.hscharr(image) + assert (np.all(result == 0)) + + +def test_vscharr_zeros(): + """Vertical Scharr on an array of all zeros""" + result = F.vscharr(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_vscharr_mask(): + """Vertical Scharr on a masked array should be zero""" + np.random.seed(0) + result = F.vscharr(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) + + +def test_vscharr_vertical(): + """Vertical Scharr on an edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.vscharr(image) + # Fudge the eroded points + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + assert (np.all(result[np.abs(j) > 1] == 0)) + + +def test_vscharr_horizontal(): + """vertical Scharr on a horizontal edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.vscharr(image) + eps = .000001 + assert (np.all(np.abs(result) < eps)) + + def test_prewitt_zeros(): """Prewitt on an array of all zeros""" result = F.prewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) assert (np.all(result == 0)) + def test_prewitt_mask(): """Prewitt on a masked array should be zero""" np.random.seed(0) @@ -110,6 +222,7 @@ def test_prewitt_mask(): eps = .000001 assert (np.all(np.abs(result) < eps)) + def test_prewitt_horizontal(): """Prewitt on an edge should be a horizontal line""" i, j = np.mgrid[-5:6, -5:6] @@ -121,6 +234,7 @@ def test_prewitt_horizontal(): assert (np.all(result[i == 0] == 1)) assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) + def test_prewitt_vertical(): """Prewitt on a vertical edge should be a vertical line""" i, j = np.mgrid[-5:6, -5:6] @@ -137,6 +251,7 @@ def test_hprewitt_zeros(): result = F.hprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) assert (np.all(result == 0)) + def test_hprewitt_mask(): """Horizontal prewitt on a masked array should be zero""" np.random.seed(0) @@ -145,6 +260,7 @@ def test_hprewitt_mask(): eps = .000001 assert (np.all(np.abs(result) < eps)) + def test_hprewitt_horizontal(): """Horizontal prewitt on an edge should be a horizontal line""" i, j = np.mgrid[-5:6, -5:6] @@ -156,6 +272,7 @@ def test_hprewitt_horizontal(): assert (np.all(result[i == 0] == 1)) assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) + def test_hprewitt_vertical(): """Horizontal prewitt on a vertical edge should be zero""" i, j = np.mgrid[-5:6, -5:6] @@ -170,6 +287,7 @@ def test_vprewitt_zeros(): result = F.vprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) assert (np.all(result == 0)) + def test_vprewitt_mask(): """Vertical prewitt on a masked array should be zero""" np.random.seed(0) @@ -177,6 +295,7 @@ def test_vprewitt_mask(): np.zeros((10, 10), bool)) assert (np.all(result == 0)) + def test_vprewitt_vertical(): """Vertical prewitt on an edge should be a vertical line""" i, j = np.mgrid[-5:6, -5:6] @@ -188,6 +307,7 @@ def test_vprewitt_vertical(): eps = .000001 assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) + def test_vprewitt_horizontal(): """Vertical prewitt on a horizontal edge should be zero""" i, j = np.mgrid[-5:6, -5:6] @@ -209,7 +329,7 @@ def test_horizontal_mask_line(): expected[1:-1, 1:-1] = 0.2 # constant gradient for most of image, expected[4:7, 1:-1] = 0 # but line and neighbors masked - for grad_func in (F.hprewitt, F.hsobel): + for grad_func in (F.hprewitt, F.hsobel, F.hscharr): result = grad_func(vgrad, mask) yield assert_close, result, expected @@ -226,7 +346,7 @@ def test_vertical_mask_line(): expected[1:-1, 1:-1] = 0.2 # constant gradient for most of image, expected[1:-1, 4:7] = 0 # but line and neighbors masked - for grad_func in (F.vprewitt, F.vsobel): + for grad_func in (F.vprewitt, F.vsobel, F.vscharr): result = grad_func(hgrad, mask) yield assert_close, result, expected diff --git a/skimage/filter/tests/test_tv_denoise.py b/skimage/filter/tests/test_tv_denoise.py deleted file mode 100644 index cc4fae7e..00000000 --- a/skimage/filter/tests/test_tv_denoise.py +++ /dev/null @@ -1,69 +0,0 @@ -import numpy as np -from numpy.testing import run_module_suite - -from skimage import filter, data, color - - -class TestTvDenoise(): - - def test_tv_denoise_2d(self): - """ - Apply the TV denoising algorithm on the lena image provided - by scipy - """ - # lena image - lena = color.rgb2gray(data.lena())[:256, :256] - # add noise to lena - lena += 0.5 * lena.std() * np.random.randn(*lena.shape) - # clip noise so that it does not exceed allowed range for float images. - lena = np.clip(lena, 0, 1) - # denoise - denoised_lena = filter.tv_denoise(lena, weight=60.0) - # which dtype? - assert denoised_lena.dtype in [np.float, np.float32, np.float64] - from scipy import ndimage - grad = ndimage.morphological_gradient(lena, size=((3, 3))) - grad_denoised = ndimage.morphological_gradient( - denoised_lena, size=((3, 3))) - # test if the total variation has decreased - assert grad_denoised.dtype == np.float - assert (np.sqrt((grad_denoised**2).sum()) - < np.sqrt((grad**2).sum()) / 2) - - def test_tv_denoise_float_result_range(self): - # lena image - lena = color.rgb2gray(data.lena())[:256, :256] - int_lena = np.multiply(lena, 255).astype(np.uint8) - assert np.max(int_lena) > 1 - denoised_int_lena = filter.tv_denoise(int_lena, weight=60.0) - # test if the value range of output float data is within [0.0:1.0] - assert denoised_int_lena.dtype == np.float - assert np.max(denoised_int_lena) <= 1.0 - assert np.min(denoised_int_lena) >= 0.0 - - def test_tv_denoise_3d(self): - """ - Apply the TV denoising algorithm on a 3D image representing - a sphere. - """ - x, y, z = np.ogrid[0:40, 0:40, 0:40] - mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 - mask = 100 * mask.astype(np.float) - mask += 60 - mask += 20 * np.random.randn(*mask.shape) - mask[mask < 0] = 0 - mask[mask > 255] = 255 - res = filter.tv_denoise(mask.astype(np.uint8), weight=100) - assert res.dtype == np.float - assert res.std() * 255 < mask.std() - - # test wrong number of dimensions - a = np.random.random((8, 8, 8, 8)) - try: - res = filter.tv_denoise(a) - except ValueError: - pass - - -if __name__ == "__main__": - run_module_suite() diff --git a/skimage/graph/_mcp.pxd b/skimage/graph/_mcp.pxd index 4222d877..b2e2a548 100644 --- a/skimage/graph/_mcp.pxd +++ b/skimage/graph/_mcp.pxd @@ -4,11 +4,11 @@ other cython modules can "cimport mcp" and subclass it. """ cimport heap -cimport numpy as np +cimport numpy as cnp ctypedef heap.BOOL_T BOOL_T -ctypedef unsigned char DIM_T -ctypedef np.float64_t FLOAT_T +ctypedef unsigned char DIM_T +ctypedef cnp.float64_t FLOAT_T cdef class MCP: cdef heap.FastUpdateBinaryHeap costs_heap @@ -23,7 +23,7 @@ cdef class MCP: cdef object flat_offsets cdef object offset_lengths cdef BOOL_T dirty - cdef BOOL_T use_start_cost + cdef BOOL_T use_start_cost # if use_start_cost is true, the cost of the starting element is added to # the cost of the path. Set to true by default in the base class... diff --git a/skimage/graph/_mcp.pyx b/skimage/graph/_mcp.pyx index 320493c2..4e5f6d52 100644 --- a/skimage/graph/_mcp.pyx +++ b/skimage/graph/_mcp.pyx @@ -1,5 +1,7 @@ -# -*- python -*- - +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False """Cython implementation of Dijkstra's minimum cost path algorithm, for use with data on a n-dimensional lattice. @@ -32,19 +34,19 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ -import cython -cimport numpy as np import numpy as np -cimport heap import heap -ctypedef np.int8_t OFFSET_T +cimport numpy as cnp +cimport heap + +ctypedef cnp.int8_t OFFSET_T OFFSET_D = np.int8 -ctypedef np.int16_t OFFSETS_INDEX_T +ctypedef cnp.int16_t OFFSETS_INDEX_T OFFSETS_INDEX_D = np.int16 -ctypedef np.int8_t EDGE_T +ctypedef cnp.int8_t EDGE_T EDGE_D = np.int8 -ctypedef np.intp_t INDEX_T +ctypedef cnp.intp_t INDEX_T INDEX_D = np.intp FLOAT_D = np.float64 @@ -317,7 +319,6 @@ cdef class MCP: FLOAT_T new_cost, FLOAT_T offset_length): return new_cost - @cython.boundscheck(False) def find_costs(self, starts, ends=None, find_all_ends=True): """ Find the minimum-cost path from the given starting points. @@ -366,7 +367,7 @@ cdef class MCP: cdef BOOL_T use_ends = 0 cdef INDEX_T num_ends cdef BOOL_T all_ends = find_all_ends - cdef np.ndarray[INDEX_T, ndim=1] flat_ends + cdef cnp.ndarray[INDEX_T, ndim=1] flat_ends starts = _normalize_indices(starts, self.costs_shape) if starts is None: raise ValueError('start points must all be within the costs array') @@ -385,18 +386,18 @@ cdef class MCP: # lookup and array-ify object attributes for fast use cdef heap.FastUpdateBinaryHeap costs_heap = self.costs_heap - cdef np.ndarray[FLOAT_T, ndim=1] flat_costs = self.flat_costs - cdef np.ndarray[FLOAT_T, ndim=1] flat_cumulative_costs = \ + cdef cnp.ndarray[FLOAT_T, ndim=1] flat_costs = self.flat_costs + cdef cnp.ndarray[FLOAT_T, ndim=1] flat_cumulative_costs = \ self.flat_cumulative_costs - cdef np.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ + cdef cnp.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ self.traceback_offsets - cdef np.ndarray[EDGE_T, ndim=2] flat_pos_edge_map = \ + cdef cnp.ndarray[EDGE_T, ndim=2] flat_pos_edge_map = \ self.flat_pos_edge_map - cdef np.ndarray[EDGE_T, ndim=2] flat_neg_edge_map = \ + cdef cnp.ndarray[EDGE_T, ndim=2] flat_neg_edge_map = \ self.flat_neg_edge_map - cdef np.ndarray[OFFSET_T, ndim=2] offsets = self.offsets - cdef np.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets - cdef np.ndarray[FLOAT_T, ndim=1] offset_lengths = self.offset_lengths + cdef cnp.ndarray[OFFSET_T, ndim=2] offsets = self.offsets + cdef cnp.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets + cdef cnp.ndarray[FLOAT_T, ndim=1] offset_lengths = self.offset_lengths cdef DIM_T dim = self.dim cdef int num_offsets = len(flat_offsets) @@ -514,7 +515,6 @@ cdef class MCP: self.dirty = 1 return cumulative_costs, traceback - @cython.boundscheck(False) def traceback(self, end): """traceback(end) @@ -555,12 +555,12 @@ cdef class MCP: raise ValueError('no minimum-cost path was found ' 'to the specified end point') - cdef np.ndarray[INDEX_T, ndim=1] position = \ + cdef cnp.ndarray[INDEX_T, ndim=1] position = \ np.array(ends[0], dtype=INDEX_D) - cdef np.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ + cdef cnp.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ self.traceback_offsets - cdef np.ndarray[OFFSET_T, ndim=2] offsets = self.offsets - cdef np.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets + cdef cnp.ndarray[OFFSET_T, ndim=2] offsets = self.offsets + cdef cnp.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets cdef OFFSETS_INDEX_T offset cdef DIM_T d diff --git a/skimage/graph/heap.pxd b/skimage/graph/heap.pxd index 51b4f79c..e31aa150 100644 --- a/skimage/graph/heap.pxd +++ b/skimage/graph/heap.pxd @@ -1,7 +1,7 @@ """ This is the definition file for heap.pyx. It contains the definitions of the heap classes, such that other cython modules can "cimport heap" and thus use the -C versions of pop(), push(), and value_of(): pop_fast(), push_fast() and +C versions of pop(), push(), and value_of(): pop_fast(), push_fast() and value_of_fast() """ @@ -14,16 +14,16 @@ ctypedef unsigned char LEVELS_T cdef class BinaryHeap: cdef readonly INDEX_T count - cdef readonly LEVELS_T levels, min_levels + cdef readonly LEVELS_T levels, min_levels cdef VALUE_T *_values cdef REFERENCE_T *_references cdef REFERENCE_T _popped_ref - + cdef void _add_or_remove_level(self, LEVELS_T add_or_remove) cdef void _update(self) cdef void _update_one(self, INDEX_T i) cdef void _remove(self, INDEX_T i) - + cdef INDEX_T push_fast(self, VALUE_T value, REFERENCE_T reference) cdef VALUE_T pop_fast(self) @@ -32,8 +32,7 @@ cdef class FastUpdateBinaryHeap(BinaryHeap): cdef INDEX_T *_crossref cdef BOOL_T _invalid_ref cdef BOOL_T _pushed - + cdef VALUE_T value_of_fast(self, REFERENCE_T reference) - cdef INDEX_T push_if_lower_fast(self, VALUE_T value, + cdef INDEX_T push_if_lower_fast(self, VALUE_T value, REFERENCE_T reference) - \ No newline at end of file diff --git a/skimage/io/_io.py b/skimage/io/_io.py index eea7e509..f8f395bd 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,6 +1,10 @@ __all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] +import os +import re +import urllib2 +import tempfile from io import BytesIO import numpy as np @@ -12,6 +16,14 @@ from skimage.color import rgb2grey # Shared image queue _image_stack = [] +URL_REGEX = re.compile(r'http://|https://|ftp://|file://|file:\\') + + +def is_url(filename): + """Return True if string is an http or ftp path.""" + return (isinstance(filename, basestring) and + URL_REGEX.match(filename) is not None) + class Image(np.ndarray): """Class representing Image data. @@ -88,7 +100,7 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, Parameters ---------- fname : string - Image file name, e.g. ``test.jpg``. + Image file name, e.g. ``test.jpg`` or URL. as_grey : bool If True, convert color images to grey-scale (32-bit floats). Images that are already in grey-scale format are not converted. @@ -117,7 +129,14 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, if flatten is not None: as_grey = flatten - img = call_plugin('imread', fname, plugin=plugin, **plugin_args) + if is_url(fname): + with tempfile.NamedTemporaryFile(delete=False) as f: + u = urllib2.urlopen(fname) + f.write(u.read()) + img = call_plugin('imread', f.name, plugin=plugin, **plugin_args) + os.remove(f.name) + else: + img = call_plugin('imread', fname, plugin=plugin, **plugin_args) if as_grey and getattr(img, 'ndim', 0) >= 3: img = rgb2grey(img) diff --git a/skimage/io/_plugins/_colormixer.pyx b/skimage/io/_plugins/_colormixer.pyx index ace45c94..64ac62ff 100644 --- a/skimage/io/_plugins/_colormixer.pyx +++ b/skimage/io/_plugins/_colormixer.pyx @@ -1,4 +1,7 @@ -# -*- python -*- +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False """Colour Mixer @@ -9,15 +12,14 @@ one. """ import cython -import numpy as np -cimport numpy as np + +cimport numpy as cnp from libc.math cimport exp, pow -@cython.boundscheck(False) -def add(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, - int channel, int amount): +def add(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, + Py_ssize_t channel, Py_ssize_t amount): """Add a given amount to a colour channel of `stateimg`, and store the result in `img`. Overflow is clipped. @@ -33,38 +35,37 @@ def add(np.ndarray[np.uint8_t, ndim=3] img, Value to add. """ - cdef int height = img.shape[0] - cdef int width = img.shape[1] - cdef int k = channel - cdef int n = amount + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] + cdef Py_ssize_t k = channel + cdef Py_ssize_t n = amount - cdef np.int16_t op_result + cdef cnp.int16_t op_result - cdef np.uint8_t lut[256] + cdef cnp.uint8_t lut[256] - cdef int i, j, l + cdef Py_ssize_t i, j, l with nogil: for l from 0 <= l < 256: - op_result = (l + n) + op_result = (l + n) if op_result > 255: op_result = 255 elif op_result < 0: op_result = 0 else: pass - lut[l] = op_result + lut[l] = op_result for i from 0 <= i < height: for j from 0 <= j < width: img[i, j, k] = lut[stateimg[i,j,k]] -@cython.boundscheck(False) -def multiply(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, - int channel, float amount): +def multiply(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, + Py_ssize_t channel, float amount): """Multiply a colour channel of `stateimg` by a certain amount, and store the result in `img`. Overflow is clipped. @@ -80,16 +81,16 @@ def multiply(np.ndarray[np.uint8_t, ndim=3] img, Multiplication factor. """ - cdef int height = img.shape[0] - cdef int width = img.shape[1] - cdef int k = channel + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] + cdef Py_ssize_t k = channel cdef float n = amount cdef float op_result - cdef np.uint8_t lut[256] + cdef cnp.uint8_t lut[256] - cdef int i, j, l + cdef Py_ssize_t i, j, l with nogil: @@ -101,17 +102,16 @@ def multiply(np.ndarray[np.uint8_t, ndim=3] img, op_result = 0 else: pass - lut[l] = op_result + lut[l] = op_result for i from 0 <= i < height: for j from 0 <= j < width: img[i,j,k] = lut[stateimg[i,j,k]] -@cython.boundscheck(False) -def brightness(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, - float factor, int offset): +def brightness(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, + float factor, Py_ssize_t offset): """Modify the brightness of an image. 'factor' is multiplied to all channels, which are then added by 'amount'. Overflow is clipped. @@ -129,13 +129,13 @@ def brightness(np.ndarray[np.uint8_t, ndim=3] img, """ - cdef int height = img.shape[0] - cdef int width = img.shape[1] + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] cdef float op_result - cdef np.uint8_t lut[256] + cdef cnp.uint8_t lut[256] - cdef int i, j, k + cdef Py_ssize_t i, j, k with nogil: for k from 0 <= k < 256: @@ -146,7 +146,7 @@ def brightness(np.ndarray[np.uint8_t, ndim=3] img, op_result = 0 else: pass - lut[k] = op_result + lut[k] = op_result for i from 0 <= i < height: for j from 0 <= j < width: @@ -155,27 +155,25 @@ def brightness(np.ndarray[np.uint8_t, ndim=3] img, img[i,j,2] = lut[stateimg[i,j,2]] -@cython.boundscheck(False) -@cython.cdivision(True) -def sigmoid_gamma(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, +def sigmoid_gamma(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, float alpha, float beta): - cdef int height = img.shape[0] - cdef int width = img.shape[1] + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] - cdef int i, j, k + cdef Py_ssize_t i, j, k cdef float c1 = 1 / (1 + exp(beta)) cdef float c2 = 1 / (1 + exp(beta - alpha)) - c1 - cdef np.uint8_t lut[256] + cdef cnp.uint8_t lut[256] with nogil: # compute the lut for k from 0 <= k < 256: - lut[k] = (((1 / (1 + exp(beta - (k / 255.) * alpha))) + lut[k] = (((1 / (1 + exp(beta - (k / 255.) * alpha))) - c1) * 255 / c2) for i from 0 <= i < height: for j from 0 <= j < width: @@ -184,17 +182,16 @@ def sigmoid_gamma(np.ndarray[np.uint8_t, ndim=3] img, img[i,j,2] = lut[stateimg[i,j,2]] -@cython.boundscheck(False) -def gamma(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, +def gamma(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, float gamma): - cdef int height = img.shape[0] - cdef int width = img.shape[1] + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] - cdef np.uint8_t lut[256] + cdef cnp.uint8_t lut[256] - cdef int i, j, k + cdef Py_ssize_t i, j, k if gamma == 0: gamma = 0.00000000000000000001 @@ -204,7 +201,7 @@ def gamma(np.ndarray[np.uint8_t, ndim=3] img, # compute the lut for k from 0 <= k < 256: - lut[k] = ((pow((k / 255.), gamma) * 255)) + lut[k] = ((pow((k / 255.), gamma) * 255)) for i from 0 <= i < height: for j from 0 <= j < width: @@ -213,7 +210,6 @@ def gamma(np.ndarray[np.uint8_t, ndim=3] img, img[i,j,2] = lut[stateimg[i,j,2]] -@cython.cdivision(True) cdef void rgb_2_hsv(float* RGB, float* HSV) nogil: cdef float R, G, B, H, S, V, MAX, MIN R = RGB[0] @@ -277,11 +273,10 @@ cdef void rgb_2_hsv(float* RGB, float* HSV) nogil: HSV[2] = V -@cython.cdivision(True) cdef void hsv_2_rgb(float* HSV, float* RGB) nogil: cdef float H, S, V cdef float f, p, q, t, r, g, b - cdef int hi + cdef Py_ssize_t hi H = HSV[0] S = HSV[1] @@ -422,9 +417,8 @@ def py_rgb_2_hsv(R, G, B): return (H, S, V) -@cython.boundscheck(False) -def hsv_add(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, +def hsv_add(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, float h_amt, float s_amt, float v_amt): """Modify the image color by specifying additive HSV Values. @@ -455,13 +449,13 @@ def hsv_add(np.ndarray[np.uint8_t, ndim=3] img, """ - cdef int height = img.shape[0] - cdef int width = img.shape[1] + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] cdef float HSV[3] cdef float RGB[3] - cdef int i, j + cdef Py_ssize_t i, j with nogil: for i from 0 <= i < height: @@ -483,14 +477,13 @@ def hsv_add(np.ndarray[np.uint8_t, ndim=3] img, RGB[1] *= 255 RGB[2] *= 255 - img[i, j, 0] = RGB[0] - img[i, j, 1] = RGB[1] - img[i, j, 2] = RGB[2] + img[i, j, 0] = RGB[0] + img[i, j, 1] = RGB[1] + img[i, j, 2] = RGB[2] -@cython.boundscheck(False) -def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img, - np.ndarray[np.uint8_t, ndim=3] stateimg, +def hsv_multiply(cnp.ndarray[cnp.uint8_t, ndim=3] img, + cnp.ndarray[cnp.uint8_t, ndim=3] stateimg, float h_amt, float s_amt, float v_amt): """Modify the image color by specifying multiplicative HSV Values. @@ -525,13 +518,13 @@ def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img, """ - cdef int height = img.shape[0] - cdef int width = img.shape[1] + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] cdef float HSV[3] cdef float RGB[3] - cdef int i, j + cdef Py_ssize_t i, j with nogil: for i from 0 <= i < height: @@ -553,6 +546,6 @@ def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img, RGB[1] *= 255 RGB[2] *= 255 - img[i, j, 0] = RGB[0] - img[i, j, 1] = RGB[1] - img[i, j, 2] = RGB[2] + img[i, j, 0] = RGB[0] + img[i, j, 1] = RGB[1] + img[i, j, 2] = RGB[2] diff --git a/skimage/io/_plugins/_histograms.pyx b/skimage/io/_plugins/_histograms.pyx index 41ec79a1..1f4344d1 100644 --- a/skimage/io/_plugins/_histograms.pyx +++ b/skimage/io/_plugins/_histograms.pyx @@ -1,7 +1,10 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np -import cython +cimport numpy as cnp cdef inline float tri_max(float a, float b, float c): @@ -18,8 +21,7 @@ cdef inline float tri_max(float a, float b, float c): return c -@cython.boundscheck(False) -def histograms(np.ndarray[np.uint8_t, ndim=3] img, int nbins): +def histograms(cnp.ndarray[cnp.uint8_t, ndim=3] img, int nbins): '''Calculate the channel histograms of the current image. Parameters @@ -39,10 +41,7 @@ def histograms(np.ndarray[np.uint8_t, ndim=3] img, int nbins): ''' cdef int width = img.shape[1] cdef int height = img.shape[0] - cdef np.ndarray[np.int32_t, ndim=1] r - cdef np.ndarray[np.int32_t, ndim=1] g - cdef np.ndarray[np.int32_t, ndim=1] b - cdef np.ndarray[np.int32_t, ndim=1] v + cdef cnp.ndarray[cnp.int32_t, ndim=1] r, g, b, v r = np.zeros((nbins,), dtype=np.int32) g = np.zeros((nbins,), dtype=np.int32) diff --git a/skimage/io/_plugins/imread_plugin.ini b/skimage/io/_plugins/imread_plugin.ini new file mode 100644 index 00000000..a6a5ddb1 --- /dev/null +++ b/skimage/io/_plugins/imread_plugin.ini @@ -0,0 +1,3 @@ +[imread] +description = Image reading and writing via imread +provides = imread, imsave diff --git a/skimage/io/_plugins/imread_plugin.py b/skimage/io/_plugins/imread_plugin.py new file mode 100644 index 00000000..323fbec8 --- /dev/null +++ b/skimage/io/_plugins/imread_plugin.py @@ -0,0 +1,44 @@ +__all__ = ['imread', 'imsave'] + +import numpy as np +from skimage.utils.dtype import convert + +try: + import imread as _imread +except ImportError: + raise ImportError("Imread could not be found" + "Please refer to http://pypi.python.org/pypi/imread/ " + "for further instructions.") + +def imread(fname, dtype=None): + """Load an image from file. + + Parameters + ---------- + fname : str + Name of input file + + """ + im = _imread.imread(fname) + if dtype is not None: + im = convert(im, dtype) + return im + +def imsave(fname, arr, format_str=None): + """Save an image to disk. + + Parameters + ---------- + fname : str + Name of destination file. + arr : ndarray of uint8 or uint16 + Array (image) to save. + format_str: str,optional + Format to save as. + + Notes + ----- + Currently, only 8-bit precision is supported. + """ + return _imread.imsave(fname, arr, formatstr=format_str) + diff --git a/skimage/io/_plugins/simpleitk_plugin.ini b/skimage/io/_plugins/simpleitk_plugin.ini new file mode 100644 index 00000000..75a6d995 --- /dev/null +++ b/skimage/io/_plugins/simpleitk_plugin.ini @@ -0,0 +1,3 @@ +[simpleitk] +description = Image reading and writing via SimpleITK +provides = imread, imsave diff --git a/skimage/io/_plugins/simpleitk_plugin.py b/skimage/io/_plugins/simpleitk_plugin.py new file mode 100644 index 00000000..90f7cbc6 --- /dev/null +++ b/skimage/io/_plugins/simpleitk_plugin.py @@ -0,0 +1,21 @@ +__all__ = ['imread', 'imsave'] + +try: + import SimpleITK as sitk +except ImportError: + raise ImportError("SimpleITK could not be found. " + "Please try " + " easy_install SimpleITK " + "or refer to " + " http://simpleitk.org/ " + "for further instructions.") + + +def imread(fname): + sitk_img = sitk.ReadImage(fname) + return sitk.GetArrayFromImage(sitk_img) + + +def imsave(fname, arr): + sitk_img = sitk.GetImageFromArray(arr, isVector=True) + sitk.WriteImage(sitk_img, fname) diff --git a/skimage/io/tests/test_imread.py b/skimage/io/tests/test_imread.py new file mode 100644 index 00000000..afb9eac9 --- /dev/null +++ b/skimage/io/tests/test_imread.py @@ -0,0 +1,73 @@ +import os.path +import numpy as np +from numpy.testing import * +from numpy.testing.decorators import skipif + +from tempfile import NamedTemporaryFile + +from skimage import data_dir +from skimage.io import imread, imsave, use_plugin, reset_plugins + +try: + import imread as _imread + use_plugin('imread') +except ImportError: + imread_available = False +else: + imread_available = True + + +def teardown(): + reset_plugins() + + +@skipif(not imread_available) +def test_imread_flatten(): + # a color image is flattened + img = imread(os.path.join(data_dir, 'color.png'), flatten=True) + assert img.ndim == 2 + assert img.dtype == np.float64 + img = imread(os.path.join(data_dir, 'camera.png'), flatten=True) + # check that flattening does not occur for an image that is grey already. + assert np.sctype2char(img.dtype) in np.typecodes['AllInteger'] + + +@skipif(not imread_available) +def test_imread_palette(): + img = imread(os.path.join(data_dir, 'palette_color.png')) + assert img.ndim == 3 + + +@skipif(not imread_available) +def test_bilevel(): + expected = np.zeros((10, 10), bool) + expected[::2] = 1 + + img = imread(os.path.join(data_dir, 'checker_bilevel.png')) + assert_array_equal(img, expected) + + +class TestSave: + def roundtrip(self, x, scaling=1): + f = NamedTemporaryFile(suffix='.png') + fname = f.name + f.close() + imsave(fname, x) + y = imread(fname) + + assert_array_almost_equal((x * scaling).astype(np.int32), y) + + @skipif(not imread_available) + def test_imsave_roundtrip(self): + dtype = np.uint8 + for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: + x = np.ones(shape, dtype=dtype) * np.random.random(shape) + + if np.issubdtype(dtype, float): + yield self.roundtrip, x, 255 + else: + x = (x * 255).astype(dtype) + yield self.roundtrip, x + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index 72f8496a..484784ee 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -1,7 +1,10 @@ +import os + from numpy.testing import * import numpy as np import skimage.io as io +from skimage import data_dir def test_stack_basic(): @@ -15,5 +18,15 @@ def test_stack_basic(): def test_stack_non_array(): io.push([[1, 2, 3]]) + +def test_imread_url(): + # tweak data path so that file URI works on both unix and windows. + data_path = data_dir.lstrip(os.path.sep) + data_path = data_path.replace(os.path.sep, '/') + image_url = 'file:///{0}/camera.png'.format(data_path) + image = io.imread(image_url) + assert image.shape == (512, 512) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/test_simpleitk.py b/skimage/io/tests/test_simpleitk.py new file mode 100644 index 00000000..4bb2cc23 --- /dev/null +++ b/skimage/io/tests/test_simpleitk.py @@ -0,0 +1,93 @@ +import os.path +import numpy as np +from numpy.testing import * +from numpy.testing.decorators import skipif + +from tempfile import NamedTemporaryFile + +from skimage import data_dir +from skimage.io import imread, imsave, use_plugin, reset_plugins + +try: + import SimpleITK as sitk + use_plugin('simpleitk') +except ImportError: + sitk_available = False +else: + sitk_available = True + + +def teardown(): + reset_plugins() + + +def setup_module(self): + """The effect of the `plugin.use` call may be overridden by later imports. + Call `use_plugin` directly before the tests to ensure that sitk is used. + + """ + try: + use_plugin('simpleitk') + except ImportError: + pass + + +@skipif(not sitk_available) +def test_imread_flatten(): + # a color image is flattened + img = imread(os.path.join(data_dir, 'color.png'), flatten=True) + assert img.ndim == 2 + assert img.dtype == np.float64 + img = imread(os.path.join(data_dir, 'camera.png'), flatten=True) + # check that flattening does not occur for an image that is grey already. + assert np.sctype2char(img.dtype) in np.typecodes['AllInteger'] + + +@skipif(not sitk_available) +def test_bilevel(): + expected = np.zeros((10, 10)) + expected[::2] = 255 + + img = imread(os.path.join(data_dir, 'checker_bilevel.png')) + assert_array_equal(img, expected) + + +@skipif(not sitk_available) +def test_imread_uint16(): + expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) + img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16.tif')) + assert np.issubdtype(img.dtype, np.uint16) + assert_array_almost_equal(img, expected) + + +@skipif(not sitk_available) +def test_imread_uint16_big_endian(): + expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) + img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16B.tif')) + assert_array_almost_equal(img, expected) + + +class TestSave: + def roundtrip(self, dtype, x): + f = NamedTemporaryFile(suffix='.mha') + fname = f.name + f.close() + imsave(fname, x) + y = imread(fname) + + assert_array_almost_equal(x, y) + + @skipif(not sitk_available) + def test_imsave_roundtrip(self): + for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: + for dtype in (np.uint8, np.uint16, np.float32, np.float64): + x = np.ones(shape, dtype=dtype) * np.random.random(shape) + + if np.issubdtype(dtype, float): + yield self.roundtrip, dtype, x + else: + x = (x * 255).astype(dtype) + yield self.roundtrip, dtype, x + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/measure/_find_contours.pyx b/skimage/measure/_find_contours.pyx index 4f1b3cee..d05d9aa7 100644 --- a/skimage/measure/_find_contours.pyx +++ b/skimage/measure/_find_contours.pyx @@ -1,10 +1,11 @@ -# -*- python -*- -# cython: cdivision=True - +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np -np.import_array() +cimport numpy as cnp + cdef inline double _get_fraction(double from_value, double to_value, double level): @@ -13,8 +14,8 @@ cdef inline double _get_fraction(double from_value, double to_value, return ((level - from_value) / (to_value - from_value)) -def iterate_and_store(np.ndarray[double, ndim=2] array, - double level, int vertex_connect_high): +def iterate_and_store(cnp.ndarray[double, ndim=2] array, + double level, Py_ssize_t vertex_connect_high): """Iterate across the given array in a marching-squares fashion, looking for segments that cross 'level'. If such a segment is found, its coordinates are added to a growing list of segments, @@ -27,7 +28,7 @@ def iterate_and_store(np.ndarray[double, ndim=2] array, raise ValueError("Input array must be at least 2x2.") cdef list arc_list = [] - cdef int n + cdef Py_ssize_t n # The plan is to iterate a 2x2 square across the input array. This means # that the upper-left corner of the square needs to iterate across a @@ -39,17 +40,18 @@ def iterate_and_store(np.ndarray[double, ndim=2] array, # index varies the fastest). # Current coords start at 0,0. - cdef int[2] coords + cdef Py_ssize_t[2] coords coords[0] = 0 coords[1] = 0 # Calculate the number of iterations we'll need - cdef int num_square_steps = (array.shape[0] - 1) * (array.shape[1] - 1) + cdef Py_ssize_t num_square_steps = (array.shape[0] - 1) \ + * (array.shape[1] - 1) cdef unsigned char square_case = 0 cdef tuple top, bottom, left, right cdef double ul, ur, ll, lr - cdef int r0, r1, c0, c1 + cdef Py_ssize_t r0, r1, c0, c1 for n in range(num_square_steps): # There are sixteen different possible square types, diagramed below. diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index f84e14dd..145f6052 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -1,14 +1,16 @@ -#cython: boundscheck=False -#cython: wraparound=False #cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np + +cimport numpy as cnp -def central_moments(np.ndarray[np.double_t, ndim=2] array, double cr, double cc, - int order): - cdef int p, q, r, c - cdef np.ndarray[np.double_t, ndim=2] mu +def central_moments(cnp.ndarray[cnp.double_t, ndim=2] array, double cr, + double cc, int order): + cdef Py_ssize_t p, q, r, c + cdef cnp.ndarray[cnp.double_t, ndim=2] mu mu = np.zeros((order + 1, order + 1), 'double') for p in range(order + 1): for q in range(order + 1): @@ -17,9 +19,10 @@ def central_moments(np.ndarray[np.double_t, ndim=2] array, double cr, double cc, mu[p,q] += array[r,c] * (r - cr) ** q * (c - cc) ** p return mu -def normalized_moments(np.ndarray[np.double_t, ndim=2] mu, int order): - cdef int p, q - cdef np.ndarray[np.double_t, ndim=2] nu + +def normalized_moments(cnp.ndarray[cnp.double_t, ndim=2] mu, int order): + cdef Py_ssize_t p, q + cdef cnp.ndarray[cnp.double_t, ndim=2] nu nu = np.zeros((order + 1, order + 1), 'double') for p in range(order + 1): for q in range(order + 1): @@ -29,8 +32,9 @@ def normalized_moments(np.ndarray[np.double_t, ndim=2] mu, int order): nu[p,q] = np.nan return nu -def hu_moments(np.ndarray[np.double_t, ndim=2] nu): - cdef np.ndarray[np.double_t, ndim=1] hu = np.zeros((7,), 'double') + +def hu_moments(cnp.ndarray[cnp.double_t, ndim=2] nu): + cdef cnp.ndarray[cnp.double_t, ndim=1] hu = np.zeros((7,), 'double') cdef double t0 = nu[3,0] + nu[1,2] cdef double t1 = nu[2,1] + nu[0,3] cdef double q0 = t0 * t0 diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index d285d453..72d1e2f4 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -440,18 +440,19 @@ def perimeter(image, neighbourhood=4): strel = STREL_4 else: strel = STREL_8 - eroded_image = ndimage.binary_erosion(image, strel) + 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) + 1: (5, 7, 15, 17, 25, 27), + sqrt(2): (21, 33), + (1 + sqrt(2)) / 2: (13, 23) } perimeter_image = ndimage.convolve(border_image, np.array([[10, 2, 10], [ 2, 1, 2], - [10, 2, 10]])) + [10, 2, 10]]), + mode='constant', cval=0) total_perimeter = 0 for weight, values in perimeter_weights.items(): num_values = 0 diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index bd23d8b3..8185bf67 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -246,10 +246,10 @@ def test_orientation(): def test_perimeter(): per = regionprops(SAMPLE, ['Perimeter'])[0]['Perimeter'] - assert_almost_equal(per, 59.2132034355964) + assert_almost_equal(per, 55.2487373415) per = perimeter(SAMPLE.astype('double'), neighbourhood=8) - assert_almost_equal(per, 43.1213203436) + assert_almost_equal(per, 46.8284271247) def test_solidity(): diff --git a/skimage/measure/tests/test_structural_similarity.py b/skimage/measure/tests/test_structural_similarity.py index 84bd7cc6..ec5ce7ef 100644 --- a/skimage/measure/tests/test_structural_similarity.py +++ b/skimage/measure/tests/test_structural_similarity.py @@ -4,6 +4,9 @@ from numpy.testing import assert_equal, assert_raises from skimage.measure import structural_similarity as ssim +np.random.seed(1234) + + def test_ssim_patch_range(): N = 51 X = (np.random.random((N, N)) * 255).astype(np.uint8) @@ -25,18 +28,18 @@ def test_ssim_image(): assert(S1 < 0.3) -## # NOTE: This test is known to randomly fail on some systems (Mac OS X 10.6) -## def test_ssim_grad(): -## N = 30 -## X = np.random.random((N, N)) * 255 -## Y = np.random.random((N, N)) * 255 +# NOTE: This test is known to randomly fail on some systems (Mac OS X 10.6) +def test_ssim_grad(): + N = 30 + X = np.random.random((N, N)) * 255 + Y = np.random.random((N, N)) * 255 -## f = ssim(X, Y, dynamic_range=255) -## g = ssim(X, Y, dynamic_range=255, gradient=True) + f = ssim(X, Y, dynamic_range=255) + g = ssim(X, Y, dynamic_range=255, gradient=True) -## assert f < 0.05 -## assert g[0] < 0.05 -## assert np.all(g[1] < 0.05) + assert f < 0.05 + assert g[0] < 0.05 + assert np.all(g[1] < 0.05) def test_ssim_dtype(): diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index d4c775eb..044fcf89 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -7,3 +7,4 @@ from .watershed import watershed, is_local_maximum from ._skeletonize import skeletonize, medial_axis from .convex_hull import convex_hull_image from .greyreconstruct import reconstruction +from .misc import remove_small_objects diff --git a/skimage/morphology/_convex_hull.pyx b/skimage/morphology/_convex_hull.pyx index dc426900..e4b6470c 100644 --- a/skimage/morphology/_convex_hull.pyx +++ b/skimage/morphology/_convex_hull.pyx @@ -1,9 +1,13 @@ -# -*- python -*- - -cimport numpy as np +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -def possible_hull(np.ndarray[dtype=np.uint8_t, ndim=2, mode="c"] img): +cimport numpy as cnp + + +def possible_hull(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] img): """Return positions of pixels that possibly belong to the convex hull. Parameters @@ -13,47 +17,44 @@ def possible_hull(np.ndarray[dtype=np.uint8_t, ndim=2, mode="c"] img): Returns ------- - coords : ndarray (N, 2) + coords : ndarray (cols, 2) The ``(row, column)`` coordinates of all pixels that possibly belong to the convex hull. """ - cdef int i, j, k - cdef unsigned int M, N - - M = img.shape[0] - N = img.shape[1] + cdef Py_ssize_t r, c + cdef Py_ssize_t rows = img.shape[0] + cdef Py_ssize_t cols = img.shape[1] - # Output: M storage slots for left boundary pixels - # N storage slots for top boundary pixels - # M storage slots for right boundary pixels - # N storage slots for bottom boundary pixels - cdef np.ndarray[dtype=np.int_t, ndim=2] nonzero = \ - np.ones((2 * (M + N), 2), dtype=np.int) - nonzero *= -1 + # Output: rows storage slots for left boundary pixels + # cols storage slots for top boundary pixels + # rows storage slots for right boundary pixels + # cols storage slots for bottom boundary pixels + cdef cnp.ndarray[dtype=cnp.intp_t, ndim=2] nonzero = \ + np.ones((2 * (rows + cols), 2), dtype=np.int) + nonzero *= -1 - k = 0 - for i in range(M): - for j in range(N): - if img[i, j] != 0: + for r in range(rows): + for c in range(cols): + if img[r, c] != 0: # Left check - if nonzero[i, 1] == -1: - nonzero[i, 0] = i - nonzero[i, 1] = j + if nonzero[r, 1] == -1: + nonzero[r, 0] = r + nonzero[r, 1] = c # Right check - elif nonzero[M + N + i, 1] < j: - nonzero[M + N + i, 0] = i - nonzero[M + N + i, 1] = j + elif nonzero[rows + cols + r, 1] < c: + nonzero[rows + cols + r, 0] = r + nonzero[rows + cols + r, 1] = c # Top check - if nonzero[M + j, 1] == -1: - nonzero[M + j, 0] = i - nonzero[M + j, 1] = j + if nonzero[rows + c, 1] == -1: + nonzero[rows + c, 0] = r + nonzero[rows + c, 1] = c # Bottom check - elif nonzero[2 * M + N + j, 0] < i: - nonzero[2 * M + N + j, 0] = i - nonzero[2 * M + N + j, 1] = j - + elif nonzero[2 * rows + cols + c, 0] < r: + nonzero[2 * rows + cols + c, 0] = r + nonzero[2 * rows + cols + c, 1] = c + return nonzero[nonzero[:, 0] != -1] diff --git a/skimage/morphology/_pnpoly.pyx b/skimage/morphology/_pnpoly.pyx index 7deb6a50..f32778cc 100644 --- a/skimage/morphology/_pnpoly.pyx +++ b/skimage/morphology/_pnpoly.pyx @@ -1,7 +1,10 @@ -# -*- python -*- - -cimport numpy as np +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np + +cimport numpy as cnp from skimage._shared.geometry cimport point_in_polygon, points_in_polygon @@ -26,23 +29,24 @@ def grid_points_inside_poly(shape, verts): True where the grid falls inside the polygon. """ - cdef np.ndarray[np.double_t, ndim=1, mode="c"] vx, vy + cdef cnp.ndarray[cnp.double_t, ndim=1, mode="c"] vx, vy verts = np.asarray(verts) vx = verts[:, 0].astype(np.double) vy = verts[:, 1].astype(np.double) - cdef int V = vx.shape[0] + cdef Py_ssize_t V = vx.shape[0] - cdef int M = shape[0] - cdef int N = shape[1] - cdef int m, n + cdef Py_ssize_t M = shape[0] + cdef Py_ssize_t N = shape[1] + cdef Py_ssize_t m, n - cdef np.ndarray[dtype=np.uint8_t, ndim=2, mode="c"] out = \ + cdef cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] out = \ np.zeros((M, N), dtype=np.uint8) 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.data, vy.data, + m, n) return out.view(bool) @@ -64,7 +68,7 @@ def points_inside_poly(points, verts): True if corresponding point is inside the polygon. """ - cdef np.ndarray[np.double_t, ndim=1, mode="c"] x, y, vx, vy + cdef cnp.ndarray[cnp.double_t, ndim=1, mode="c"] x, y, vx, vy points = np.asarray(points) verts = np.asarray(verts) @@ -75,12 +79,12 @@ def points_inside_poly(points, verts): vx = verts[:, 0].astype(np.double) vy = verts[:, 1].astype(np.double) - cdef np.ndarray[np.uint8_t, ndim=1] out = \ - np.zeros(x.shape[0], dtype=np.uint8) + 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, - out.data) + x.shape[0], x.data, y.data, + out.data) return out.astype(bool) diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py index 04a65da6..b48beb86 100644 --- a/skimage/morphology/_skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -277,8 +277,8 @@ def medial_axis(image, mask=None, return_distance=False): i, j = np.mgrid[0:image.shape[0], 0:image.shape[1]] result = masked_image.copy() distance = distance[result] - i = np.ascontiguousarray(i[result], np.int32) - j = np.ascontiguousarray(j[result], np.int32) + i = np.ascontiguousarray(i[result], np.intp) + j = np.ascontiguousarray(j[result], np.intp) result = np.ascontiguousarray(result, np.uint8) # Determine the order in which pixels are processed. diff --git a/skimage/morphology/_skeletonize_cy.pyx b/skimage/morphology/_skeletonize_cy.pyx index ff5fcdf2..13e303d4 100644 --- a/skimage/morphology/_skeletonize_cy.pyx +++ b/skimage/morphology/_skeletonize_cy.pyx @@ -1,3 +1,8 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + ''' Originally part of CellProfiler, code licensed under both GPL and BSD licenses. Website: http://www.cellprofiler.org @@ -10,21 +15,20 @@ Original author: Lee Kamentsky ''' import numpy as np -cimport numpy as np -cimport cython + +cimport numpy as cnp -@cython.boundscheck(False) -def _skeletonize_loop(np.ndarray[dtype=np.uint8_t, ndim=2, - negative_indices=False, mode='c'] result, - np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices=False, mode='c'] i, - np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices=False, mode='c'] j, - np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices=False, mode='c'] order, - np.ndarray[dtype=np.uint8_t, ndim=1, - negative_indices=False, mode='c'] table): +def _skeletonize_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] result, + cnp.ndarray[dtype=cnp.intp_t, ndim=1, + negative_indices=False, mode='c'] i, + cnp.ndarray[dtype=cnp.intp_t, ndim=1, + negative_indices=False, mode='c'] j, + cnp.ndarray[dtype=cnp.int32_t, ndim=1, + negative_indices=False, mode='c'] order, + cnp.ndarray[dtype=cnp.uint8_t, ndim=1, + negative_indices=False, mode='c'] table): """ Inner loop of skeletonize function @@ -37,13 +41,13 @@ def _skeletonize_loop(np.ndarray[dtype=np.uint8_t, ndim=2, i, j : ndarrays The coordinates of each foreground pixel in the image - + order : ndarray The index of each pixel, in the order of processing (order[0] is the first pixel to process, etc.) - + table : ndarray - The 512-element lookup table of values after transformation + The 512-element lookup table of values after transformation (whether to keep or not each configuration in a binary 3x3 array) Notes @@ -55,15 +59,15 @@ def _skeletonize_loop(np.ndarray[dtype=np.uint8_t, ndim=2, the quench-line of the brushfire will be evaluated later than a point closer to the edge. - Note that the neighbourhood of a pixel may evolve before the loop - arrives at this pixel. This is why it is possible to compute the + Note that the neighbourhood of a pixel may evolve before the loop + arrives at this pixel. This is why it is possible to compute the skeleton in only one pass, thanks to an adapted ordering of the pixels. """ cdef: - np.int32_t accumulator - np.int32_t index, order_index - np.int32_t ii, jj + cnp.int32_t accumulator + Py_ssize_t index, order_index + Py_ssize_t ii, jj for index in range(order.shape[0]): accumulator = 16 @@ -92,9 +96,10 @@ def _skeletonize_loop(np.ndarray[dtype=np.uint8_t, ndim=2, # Assign the value of table corresponding to the configuration result[ii, jj] = table[accumulator] -@cython.boundscheck(False) -def _table_lookup_index(np.ndarray[dtype=np.uint8_t, ndim=2, - negative_indices=False, mode='c'] image): + + +def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, + negative_indices=False, mode='c'] image): """ Return an index into a table per pixel of a binary image @@ -110,27 +115,27 @@ def _table_lookup_index(np.ndarray[dtype=np.uint8_t, ndim=2, 256 128 64 32 16 8 4 2 1 - + but this runs about twice as fast because of inlining and the hardwired kernel. """ cdef: - np.ndarray[dtype=np.int32_t, ndim=2, - negative_indices=False, mode='c'] indexer - np.int32_t *p_indexer - np.uint8_t *p_image - np.int32_t i_stride - np.int32_t i_shape - np.int32_t j_shape - np.int32_t i - np.int32_t j - np.int32_t offset + cnp.ndarray[dtype=cnp.int32_t, ndim=2, + negative_indices=False, mode='c'] indexer + cnp.int32_t *p_indexer + cnp.uint8_t *p_image + Py_ssize_t i_stride + Py_ssize_t i_shape + Py_ssize_t j_shape + Py_ssize_t i + Py_ssize_t j + Py_ssize_t offset i_shape = image.shape[0] j_shape = image.shape[1] indexer = np.zeros((i_shape, j_shape), np.int32) - p_indexer = indexer.data - p_image = image.data + p_indexer = indexer.data + p_image = image.data i_stride = image.strides[0] assert i_shape >= 3 and j_shape >= 3, \ "Please use the slow method for arrays < 3x3" diff --git a/skimage/morphology/_watershed.pyx b/skimage/morphology/_watershed.pyx index c86d8744..122f0262 100644 --- a/skimage/morphology/_watershed.pyx +++ b/skimage/morphology/_watershed.pyx @@ -9,39 +9,33 @@ All rights reserved. Original author: Lee Kamentsky """ - -cdef extern from "numpy/arrayobject.h": - cdef void import_array() -import_array() - import numpy as np cimport numpy as np cimport cython -DTYPE_INT32 = np.int32 + ctypedef np.int32_t DTYPE_INT32_t DTYPE_BOOL = np.bool ctypedef np.int8_t DTYPE_BOOL_t + include "heap_watershed.pxi" + @cython.boundscheck(False) -def watershed(np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, - mode='c'] image, - np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, - mode='c'] pq, - DTYPE_INT32_t age, - np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, - mode='c'] structure, - DTYPE_INT32_t ndim, - np.ndarray[DTYPE_BOOL_t, ndim=1, negative_indices=False, - mode='c'] mask, - np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, - mode='c'] image_shape, - np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, - mode='c'] output): +def watershed(np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, + mode='c'] image, + np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, + mode='c'] pq, + Py_ssize_t age, + np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, + mode='c'] structure, + np.ndarray[DTYPE_BOOL_t, ndim=1, negative_indices=False, + mode='c'] mask, + np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, + mode='c'] output): """Do heavy lifting of watershed algorithm - + Parameters ---------- @@ -58,20 +52,17 @@ def watershed(np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, in a flattened array. The remaining elements are the offsets from the point to its neighbor in the various dimensions - ndim - # of dimensions in the image mask - numpy boolean (char) array indicating which pixels to consider and which to ignore. Also flattened. - image_shape - the dimensions of the image, for boundary checking, - a numpy array of np.int32 output - put the image labels in here """ cdef Heapitem elem cdef Heapitem new_elem - cdef DTYPE_INT32_t nneighbors = structure.shape[0] - cdef DTYPE_INT32_t i = 0 - cdef DTYPE_INT32_t index = 0 - cdef DTYPE_INT32_t old_index = 0 - cdef DTYPE_INT32_t max_index = image.shape[0] + cdef Py_ssize_t nneighbors = structure.shape[0] + cdef Py_ssize_t i = 0 + cdef Py_ssize_t index = 0 + cdef Py_ssize_t old_index = 0 + cdef Py_ssize_t max_index = image.shape[0] cdef Heap *hp = heap_from_numpy2() diff --git a/skimage/morphology/ccomp.pxd b/skimage/morphology/ccomp.pxd index 0b431832..569921fa 100644 --- a/skimage/morphology/ccomp.pxd +++ b/skimage/morphology/ccomp.pxd @@ -1,10 +1,10 @@ """Export fast union find in Cython""" -cimport numpy as np +cimport numpy as cnp -DTYPE = np.int -ctypedef np.int_t DTYPE_t +DTYPE = cnp.intp +ctypedef cnp.intp_t DTYPE_t -cdef DTYPE_t find_root(np.int_t *forest, np.int_t n) -cdef set_root(np.int_t *forest, np.int_t n, np.int_t root) -cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m) -cdef link_bg(np.int_t *forest, np.int_t n, np.int_t *background_node) +cdef DTYPE_t find_root(DTYPE_t *forest, DTYPE_t n) +cdef set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root) +cdef join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m) +cdef link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node) diff --git a/skimage/morphology/ccomp.pyx b/skimage/morphology/ccomp.pyx index 6a4fb1f2..1db78a6d 100644 --- a/skimage/morphology/ccomp.pyx +++ b/skimage/morphology/ccomp.pyx @@ -1,8 +1,11 @@ -# -*- python -*- #cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np + +cimport numpy as cnp """ See also: @@ -23,23 +26,25 @@ See also: # Tree operations implemented by an array as described in Wu et al. # The term "forest" is used to indicate an array that stores one or more trees -DTYPE = np.int +DTYPE = np.intp -cdef DTYPE_t find_root(np.int_t *forest, np.int_t n): + +cdef DTYPE_t find_root(DTYPE_t *forest, DTYPE_t n): """Find the root of node n. """ - cdef np.int_t root = n + cdef DTYPE_t root = n while (forest[root] < root): root = forest[root] return root -cdef set_root(np.int_t *forest, np.int_t n, np.int_t root): + +cdef set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root): """ Set all nodes on a path to point to new_root. """ - cdef np.int_t j + cdef DTYPE_t j while (forest[n] < n): j = forest[n] forest[n] = root @@ -48,12 +53,12 @@ cdef set_root(np.int_t *forest, np.int_t n, np.int_t root): forest[n] = root -cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m): +cdef join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m): """Join two trees containing nodes n and m. """ - cdef np.int_t root = find_root(forest, n) - cdef np.int_t root_m + cdef DTYPE_t root = find_root(forest, n) + cdef DTYPE_t root_m if (n != m): root_m = find_root(forest, m) @@ -64,7 +69,8 @@ cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m): set_root(forest, n, root) set_root(forest, m, root) -cdef link_bg(np.int_t *forest, np.int_t n, np.int_t *background_node): + +cdef link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node): """ Link a node to the background node. @@ -76,7 +82,7 @@ cdef link_bg(np.int_t *forest, np.int_t n, np.int_t *background_node): # Connected components search as described in Fiorio et al. -def label(input, np.int_t neighbors=8, np.int_t background=-1): +def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1): """Label connected regions of an integer array. Two pixels are connected when they are neighbors and have the same value. @@ -134,21 +140,21 @@ def label(input, np.int_t neighbors=8, np.int_t background=-1): [-1 -1 -1]] """ - cdef np.int_t rows = input.shape[0] - cdef np.int_t cols = input.shape[1] + cdef DTYPE_t rows = input.shape[0] + cdef DTYPE_t cols = input.shape[1] - cdef np.ndarray[DTYPE_t, ndim=2] data = np.array(input, copy=True, - dtype=DTYPE) - cdef np.ndarray[DTYPE_t, ndim=2] forest + cdef cnp.ndarray[DTYPE_t, ndim=2] data = np.array(input, copy=True, + dtype=DTYPE) + cdef cnp.ndarray[DTYPE_t, ndim=2] forest forest = np.arange(data.size, dtype=DTYPE).reshape((rows, cols)) - cdef np.int_t *forest_p = forest.data - cdef np.int_t *data_p = data.data + cdef DTYPE_t *forest_p = forest.data + cdef DTYPE_t *data_p = data.data - cdef np.int_t i, j + cdef DTYPE_t i, j - cdef np.int_t background_node = -999 + cdef DTYPE_t background_node = -999 if neighbors != 4 and neighbors != 8: raise ValueError('Neighbors must be either 4 or 8.') @@ -197,7 +203,7 @@ def label(input, np.int_t neighbors=8, np.int_t background=-1): # Label output - cdef np.int_t ctr = 0 + cdef DTYPE_t ctr = 0 for i in range(rows): for j in range(cols): if (i*cols + j) == background_node: @@ -208,4 +214,8 @@ def label(input, np.int_t neighbors=8, np.int_t background=-1): else: data[i, j] = data_p[forest[i, j]] - return data + # Work around a bug in ndimage's type checking on 32-bit platforms + if data.dtype == np.int32: + return data.view(np.int32) + else: + return data diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 9b8b3a27..a09a39a3 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -13,13 +13,13 @@ def dilate(np.ndarray[np.uint8_t, ndim=2] image, np.ndarray[np.uint8_t, ndim=2] out=None, char shift_x=0, char shift_y=0): - cdef int rows = image.shape[0] - cdef int cols = image.shape[1] - cdef int srows = selem.shape[0] - cdef int scols = selem.shape[1] + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + cdef Py_ssize_t srows = selem.shape[0] + cdef Py_ssize_t scols = selem.shape[1] - cdef int centre_r = int(selem.shape[0] / 2) - shift_y - cdef int centre_c = int(selem.shape[1] / 2) - shift_x + cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) - shift_y + cdef Py_ssize_t centre_c = int(selem.shape[1] / 2) - shift_x image = np.ascontiguousarray(image) if out is None: @@ -30,11 +30,11 @@ def dilate(np.ndarray[np.uint8_t, ndim=2] image, 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_max + cdef Py_ssize_t r, c, rr, cc, s, value, local_max - cdef int selem_num = np.sum(selem != 0) - cdef int* sr = malloc(selem_num * sizeof(int)) - cdef int* sc = malloc(selem_num * sizeof(int)) + cdef Py_ssize_t selem_num = np.sum(selem != 0) + cdef Py_ssize_t* sr = malloc(selem_num * sizeof(Py_ssize_t)) + cdef Py_ssize_t* sc = malloc(selem_num * sizeof(Py_ssize_t)) s = 0 for r in range(srows): @@ -68,13 +68,13 @@ def erode(np.ndarray[np.uint8_t, ndim=2] image, np.ndarray[np.uint8_t, ndim=2] out=None, char shift_x=0, char shift_y=0): - cdef int rows = image.shape[0] - cdef int cols = image.shape[1] - cdef int srows = selem.shape[0] - cdef int scols = selem.shape[1] + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + cdef Py_ssize_t srows = selem.shape[0] + cdef Py_ssize_t scols = selem.shape[1] - cdef int centre_r = int(selem.shape[0] / 2) - shift_y - cdef int centre_c = int(selem.shape[1] / 2) - shift_x + cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) - shift_y + cdef Py_ssize_t centre_c = int(selem.shape[1] / 2) - shift_x image = np.ascontiguousarray(image) if out is None: @@ -87,9 +87,9 @@ def erode(np.ndarray[np.uint8_t, ndim=2] image, cdef int r, c, rr, cc, s, value, local_min - cdef int selem_num = np.sum(selem != 0) - cdef int* sr = malloc(selem_num * sizeof(int)) - cdef int* sc = malloc(selem_num * sizeof(int)) + cdef Py_ssize_t selem_num = np.sum(selem != 0) + cdef Py_ssize_t* sr = malloc(selem_num * sizeof(Py_ssize_t)) + cdef Py_ssize_t* sc = malloc(selem_num * sizeof(Py_ssize_t)) s = 0 for r in range(srows): diff --git a/skimage/morphology/heap_general.pxi b/skimage/morphology/heap_general.pxi index a113b98e..67b21fa6 100644 --- a/skimage/morphology/heap_general.pxi +++ b/skimage/morphology/heap_general.pxi @@ -10,21 +10,18 @@ All rights reserved. Original author: Lee Kamentsky """ -cdef extern from "stdlib.h": - ctypedef unsigned long size_t - void free(void *ptr) - void *malloc(size_t size) - void *realloc(void *ptr, size_t size) +from libc.stdlib cimport free, malloc, realloc + cdef struct Heap: - unsigned int items - unsigned int space + Py_ssize_t items + Py_ssize_t space Heapitem *data Heapitem **ptrs cdef inline Heap *heap_from_numpy2(): - cdef unsigned int k - cdef Heap *heap + cdef Py_ssize_t k + cdef Heap *heap heap = malloc(sizeof (Heap)) heap.items = 0 heap.space = 1000 @@ -39,7 +36,7 @@ cdef inline void heap_done(Heap *heap): free(heap.ptrs) free(heap) -cdef inline void swap(unsigned int a, unsigned int b, Heap *h): +cdef inline void swap(Py_ssize_t a, Py_ssize_t b, Heap *h): h.ptrs[a], h.ptrs[b] = h.ptrs[b], h.ptrs[a] @@ -47,13 +44,13 @@ cdef inline void swap(unsigned int a, unsigned int b, Heap *h): # heappop - inlined # # pop an element off the heap, maintaining heap invariant -# +# # Note: heap ordering is the same as python heapq, i.e., smallest first. ###################################################### -cdef inline void heappop(Heap *heap, - Heapitem *dest): - cdef unsigned int i, smallest, l, r # heap indices - +cdef inline void heappop(Heap *heap, Heapitem *dest): + + cdef Py_ssize_t i, smallest, l, r # heap indices + # # Start by copying the first element to the destination # @@ -76,10 +73,10 @@ cdef inline void heappop(Heap *heap, smallest = i while True: # loop invariant here: smallest == i - + # find smallest of (i, l, r), and swap it to i's position if necessary - l = i*2+1 #__left(i) - r = i*2+2 #__right(i) + l = i * 2 + 1 #__left(i) + r = i * 2 + 2 #__right(i) if l < heap.items: if smaller(heap.ptrs[l], heap.ptrs[i]): smallest = l @@ -88,13 +85,14 @@ cdef inline void heappop(Heap *heap, else: # this is unnecessary, but trims 0.04 out of 0.85 seconds... break - # the element at i is smaller than either of its children, heap invariant restored. + # the element at i is smaller than either of its children, heap + # invariant restored. if smallest == i: break # swap swap(i, smallest, heap) i = smallest - + ################################################## # heappush - inlined # @@ -102,34 +100,36 @@ cdef inline void heappop(Heap *heap, # # Note: heap ordering is the same as python heapq, i.e., smallest first. ################################################## -cdef inline void heappush(Heap *heap, - Heapitem *new_elem): - cdef unsigned int child = heap.items - cdef unsigned int parent - cdef unsigned int k - cdef Heapitem *new_data +cdef inline void heappush(Heap *heap, Heapitem *new_elem): - # grow if necessary - if heap.items == heap.space: + cdef Py_ssize_t child = heap.items + cdef Py_ssize_t parent + cdef Py_ssize_t k + cdef Heapitem *new_data + + # grow if necessary + if heap.items == heap.space: heap.space = heap.space * 2 - new_data = realloc( heap.data, (heap.space * sizeof(Heapitem))) - heap.ptrs = realloc( heap.ptrs, (heap.space * sizeof(Heapitem *))) + new_data = realloc(heap.data, + (heap.space * sizeof(Heapitem))) + heap.ptrs = realloc(heap.ptrs, + (heap.space * sizeof(Heapitem *))) for k in range(heap.items): heap.ptrs[k] = new_data + (heap.ptrs[k] - heap.data) for k in range(heap.items, heap.space): heap.ptrs[k] = new_data + k heap.data = new_data - # insert new data at child - heap.ptrs[child][0] = new_elem[0] - heap.items += 1 + # insert new data at child + heap.ptrs[child][0] = new_elem[0] + heap.items += 1 - # restore heap invariant, all parents <= children - while child>0: - parent = (child + 1) / 2 - 1 # __parent(i) - - if smaller(heap.ptrs[child], heap.ptrs[parent]): - swap(parent, child, heap) - child = parent - else: - break + # restore heap invariant, all parents <= children + while child > 0: + parent = (child + 1) / 2 - 1 # __parent(i) + + if smaller(heap.ptrs[child], heap.ptrs[parent]): + swap(parent, child, heap) + child = parent + else: + break diff --git a/skimage/morphology/heap_watershed.pxi b/skimage/morphology/heap_watershed.pxi index ea66da26..07b29f5c 100644 --- a/skimage/morphology/heap_watershed.pxi +++ b/skimage/morphology/heap_watershed.pxi @@ -9,18 +9,19 @@ All rights reserved. Original author: Lee Kamentsky """ -import numpy as np -cimport numpy as np -cimport cython +cimport numpy as cnp + cdef struct Heapitem: - np.int32_t value - np.int32_t age - np.int32_t index + cnp.int32_t value + cnp.int32_t age + Py_ssize_t index + cdef inline int smaller(Heapitem *a, Heapitem *b): if a.value <> b.value: - return a.value < b.value + return a.value < b.value return a.age < b.age + include "heap_general.pxi" diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py new file mode 100644 index 00000000..6274df53 --- /dev/null +++ b/skimage/morphology/misc.py @@ -0,0 +1,83 @@ +import numpy as np +import scipy.ndimage as nd + + +def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): + """Remove connected components smaller than the specified size. + + Parameters + ---------- + ar : ndarray (arbitrary shape, int or bool type) + The array containing the connected components of interest. If the array + type is int, it is assumed that it contains already-labeled objects. + The ints must be non-negative. + min_size : int, optional (default: 64) + The smallest allowable connected component size. + connectivity : int, {1, 2, ..., ar.ndim}, optional (default: 1) + The connectivity defining the neighborhood of a pixel. + in_place : bool, optional (default: False) + If `True`, remove the connected components in the input array itself. + Otherwise, make a copy. + + Raises + ------ + ValueError + If the input array is of an invalid type, such as float or string. + + Returns + ------- + out : ndarray, same shape and type as input `ar` + The input array with small connected components removed. + + Examples + -------- + >>> from skimage import morphology + >>> from scipy import ndimage as nd + >>> a = np.array([[0, 0, 0, 1, 0], + ... [1, 1, 1, 0, 0], + ... [1, 1, 1, 0, 1]], bool) + >>> b = morphology.remove_small_connected_components(a, 6) + >>> b + array([[False, False, False, False, False], + [ True, True, True, False, False], + [ True, True, True, False, False]], dtype=bool) + >>> c = morphology.remove_small_connected_components(a, 7, connectivity=2) + >>> c + array([[False, False, False, True, False], + [ True, True, True, False, False], + [ True, True, True, False, False]], dtype=bool) + >>> d = morphology.remove_small_connected_components(a, 6, in_place=True) + >>> d is a + True + """ + # Should use `issubdtype` below, but there's a bug in numpy 1.7 + if not (ar.dtype == bool or np.issubdtype(ar.dtype, int)): + raise ValueError("Only bool or integer image types are supported. " + "Got %s." % ar.dtype) + + if in_place: + out = ar + else: + out = ar.copy() + + if min_size == 0: # shortcut for efficiency + return out + + if out.dtype == bool: + selem = nd.generate_binary_structure(ar.ndim, connectivity) + ccs = nd.label(ar, selem)[0] + else: + ccs = out + + try: + component_sizes = np.bincount(ccs.ravel()) + except ValueError: + raise ValueError("Negative value labels are not supported. Try " + "relabeling the input with `scipy.ndimage.label` or " + "`skimage.morphology.label`.") + + too_small = component_sizes < min_size + too_small_mask = too_small[ccs] + out[too_small_mask] = 0 + + return out diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index b8564ce4..1936377b 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -29,7 +29,7 @@ def configuration(parent_package='', top_path=None): config.add_extension('_skeletonize_cy', sources=['_skeletonize_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_pnpoly', sources=['_pnpoly.c'], - include_dirs=[get_numpy_include_dirs(), '../shared']) + include_dirs=[get_numpy_include_dirs(), '../_shared']) config.add_extension('_convex_hull', sources=['_convex_hull.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_greyreconstruct', sources=['_greyreconstruct.c'], diff --git a/skimage/morphology/tests/test_misc.py b/skimage/morphology/tests/test_misc.py new file mode 100644 index 00000000..abc718c2 --- /dev/null +++ b/skimage/morphology/tests/test_misc.py @@ -0,0 +1,56 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_equal, assert_raises +from skimage.morphology import remove_small_objects + +test_image = np.array([[0, 0, 0, 1, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 1]], bool) + + +def test_one_connectivity(): + expected = np.array([[0, 0, 0, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0]], bool) + observed = remove_small_objects(test_image, min_size=6) + assert_array_equal(observed, expected) + + +def test_two_connectivity(): + expected = np.array([[0, 0, 0, 1, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0]], bool) + observed = remove_small_objects(test_image, min_size=7, connectivity=2) + assert_array_equal(observed, expected) + + +def test_in_place(): + observed = remove_small_objects(test_image, min_size=6, in_place=True) + assert_equal(observed is test_image, True, + "remove_small_objects in_place argument failed.") + + +def test_labeled_image(): + labeled_image = np.array([[2, 2, 2, 0, 1], + [2, 2, 2, 0, 1], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=int) + expected = np.array([[2, 2, 2, 0, 0], + [2, 2, 2, 0, 0], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=int) + observed = remove_small_objects(labeled_image, min_size=3) + assert_array_equal(observed, expected) + + +def test_float_input(): + float_test = np.random.rand(5, 5) + assert_raises(ValueError, remove_small_objects, float_test) + + +def test_negative_input(): + negative_int = np.random.randint(-4, -1, size=(5, 5)) + assert_raises(ValueError, remove_small_objects, negative_int) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py index 8a08fafc..fbd63281 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -28,6 +28,7 @@ from _heapq import heappush, heappop import numpy as np import scipy.ndimage from ..filter import rank_order +from .._shared.utils import deprecated from . import _watershed @@ -213,9 +214,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): c_mask = c_mask.astype(np.int8).flatten() _watershed.watershed(c_image.flatten(), pq, age, c, - c_image.ndim, c_mask, - np.array(c_image.shape, np.int32), c_output) c_output = c_output.reshape(c_image.shape)[[slice(1, -1, None)] * image.ndim] @@ -225,6 +224,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): return c_output +@deprecated('feature.peak_local_max') def is_local_maximum(image, labels=None, footprint=None): """ Return a boolean array of points that are local maxima @@ -233,10 +233,8 @@ def is_local_maximum(image, labels=None, footprint=None): ---------- image: ndarray (2-D, 3-D, ...) intensity image - labels: ndarray, optional find maxima only within labels. Zero is reserved for background. - footprint: ndarray of bools, optional binary mask indicating the neighborhood to be examined `footprint` must be a matrix with odd dimensions, the center is taken @@ -247,6 +245,16 @@ def is_local_maximum(image, labels=None, footprint=None): result: ndarray of bools mask that is True for pixels that are local maxima of `image` + See also + -------- + skimage.feature.peak_local_max: Unified peak finding backend. + The more capable backend for finding local maxima. + + Notes + ----- + This function is now a wrapper for skimage.feature.peak_local_max() and is + retained only for convenience and backward compatibility. + Examples -------- >>> image = np.zeros((4, 4)) @@ -280,63 +288,13 @@ def is_local_maximum(image, labels=None, footprint=None): [False, True, False, True], [False, False, False, False], [False, True, False, True]], dtype=bool) + """ - if labels is None: - labels = np.ones(image.shape, dtype=np.uint8) - if footprint is None: - footprint = np.ones([3] * image.ndim, dtype=np.uint8) - assert((np.all(footprint.shape) & 1) == 1) - footprint = (footprint != 0) - footprint_extent = (np.array(footprint.shape) - 1) // 2 - if np.all(footprint_extent == 0): - return labels > 0 - result = (labels > 0).copy() - # - # Create a labels matrix with zeros at the borders that might be - # hit by the footprint. - # - big_labels = np.zeros(np.array(labels.shape) + footprint_extent * 2, - labels.dtype) - big_labels[[slice(fe, -fe) for fe in footprint_extent]] = labels - # - # Find the relative indexes of each footprint element - # - image_strides = np.array(image.strides) // image.dtype.itemsize - big_strides = np.array(big_labels.strides) // big_labels.dtype.itemsize - result_strides = np.array(result.strides) // result.dtype.itemsize - footprint_offsets = np.mgrid[[slice(-fe, fe + 1) for fe in footprint_extent]] - - fp_image_offsets = np.sum(image_strides[:, np.newaxis] * - footprint_offsets[:, footprint], 0) - fp_big_offsets = np.sum(big_strides[:, np.newaxis] * - footprint_offsets[:, footprint], 0) - # - # Get the index of each labeled pixel in the image and big_labels arrays - # - indexes = np.mgrid[[slice(0, x) for x in labels.shape]][:, labels > 0] - image_indexes = np.sum(image_strides[:, np.newaxis] * indexes, 0) - big_indexes = np.sum(big_strides[:, np.newaxis] * - (indexes + footprint_extent[:, np.newaxis]), 0) - result_indexes = np.sum(result_strides[:, np.newaxis] * indexes, 0) - # - # Now operate on the raveled images - # - big_labels_raveled = big_labels.ravel() - image_raveled = image.ravel() - result_raveled = result.ravel() - # - # A hit is a hit if the label at the offset matches the label at the pixel - # and if the intensity at the pixel is greater or equal to the intensity - # at the offset. - # - for fp_image_offset, fp_big_offset in zip(fp_image_offsets, fp_big_offsets): - same_label = (big_labels_raveled[big_indexes + fp_big_offset] == - big_labels_raveled[big_indexes]) - less_than = (image_raveled[image_indexes[same_label]] < - image_raveled[image_indexes[same_label] + fp_image_offset]) - result_raveled[result_indexes[same_label][less_than]] = False - - return result + # call import here to prevent circular imports + from ..feature import peak_local_max + return peak_local_max(image, labels=labels, min_distance=1, + threshold_rel=0, footprint=footprint, + indices=False, exclude_border=False) # ---------------------- deprecated ------------------------------ diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 9dca2bc3..a0fab77a 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -2,5 +2,6 @@ from .random_walker_segmentation import random_walker from ._felzenszwalb import felzenszwalb from ._slic import slic from ._quickshift import quickshift -from .boundaries import find_boundaries, visualize_boundaries +from .boundaries import find_boundaries, visualize_boundaries, mark_boundaries from ._clear_border import clear_border +from ._join import join_segmentations, relabel_from_one diff --git a/skimage/segmentation/_felzenszwalb_cy.pyx b/skimage/segmentation/_felzenszwalb_cy.pyx index efae45a4..f285e3db 100644 --- a/skimage/segmentation/_felzenszwalb_cy.pyx +++ b/skimage/segmentation/_felzenszwalb_cy.pyx @@ -1,16 +1,18 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np import scipy -cimport cython +cimport cython +cimport numpy as cnp from skimage.morphology.ccomp cimport find_root, join_trees from ..util import img_as_float -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.cdivision(True) -def _felzenszwalb_grey(image, double scale=1, sigma=0.8, int 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 @@ -49,31 +51,31 @@ def _felzenszwalb_grey(image, double scale=1, sigma=0.8, int min_size=20): down_cost = np.abs((image[:, 1:] - image[:, :-1])) dright_cost = np.abs((image[1:, 1:] - image[:-1, :-1])) uright_cost = np.abs((image[1:, :-1] - image[:-1, 1:])) - cdef np.ndarray[np.float_t, ndim=1] costs = np.hstack([right_cost.ravel(), + cdef cnp.ndarray[cnp.float_t, ndim=1] costs = np.hstack([right_cost.ravel(), down_cost.ravel(), dright_cost.ravel(), uright_cost.ravel()]).astype(np.float) # compute edges between pixels: height, width = image.shape[:2] - cdef np.ndarray[np.int_t, ndim=2] segments \ - = np.arange(width * height, dtype=np.int).reshape(height, width) + cdef cnp.ndarray[cnp.intp_t, ndim=2] segments \ + = np.arange(width * height, dtype=np.intp).reshape(height, width) right_edges = np.c_[segments[1:, :].ravel(), segments[:-1, :].ravel()] down_edges = np.c_[segments[:, 1:].ravel(), segments[:, :-1].ravel()] dright_edges = np.c_[segments[1:, 1:].ravel(), segments[:-1, :-1].ravel()] uright_edges = np.c_[segments[:-1, 1:].ravel(), segments[1:, :-1].ravel()] - cdef np.ndarray[np.int_t, ndim=2] edges \ + cdef cnp.ndarray[cnp.intp_t, ndim=2] edges \ = np.vstack([right_edges, down_edges, dright_edges, uright_edges]) # initialize data structures for segment size # and inner cost, then start greedy iteration over edges. edge_queue = np.argsort(costs) edges = np.ascontiguousarray(edges[edge_queue]) costs = np.ascontiguousarray(costs[edge_queue]) - cdef np.int_t *segments_p = segments.data - cdef np.int_t *edges_p = edges.data - cdef np.float_t *costs_p = costs.data - cdef np.ndarray[np.int_t, ndim=1] segment_size \ + cdef cnp.intp_t *segments_p = segments.data + cdef cnp.intp_t *edges_p = edges.data + cdef cnp.float_t *costs_p = costs.data + cdef cnp.ndarray[cnp.intp_t, ndim=1] segment_size \ = np.ones(width * height, dtype=np.int) # inner cost of segments - cdef np.ndarray[np.float_t, ndim=1] cint = np.zeros(width * height) + cdef cnp.ndarray[cnp.float_t, ndim=1] cint = np.zeros(width * height) cdef int seg0, seg1, seg_new, e cdef float cost, inner_cost0, inner_cost1 # set costs_p back one. we increase it before we use it @@ -96,7 +98,7 @@ def _felzenszwalb_grey(image, double scale=1, sigma=0.8, int min_size=20): cint[seg_new] = costs_p[0] # postprocessing to remove small segments - edges_p = edges.data + edges_p = edges.data for e in range(costs.size): seg0 = find_root(segments_p, edges_p[0]) seg1 = find_root(segments_p, edges_p[1]) diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py new file mode 100644 index 00000000..454da71e --- /dev/null +++ b/skimage/segmentation/_join.py @@ -0,0 +1,93 @@ +import numpy as np + +def join_segmentations(s1, s2): + """Return the join of the two input segmentations. + + The join J of S1 and S2 is defined as the segmentation in which two voxels + are in the same segment in J if and only if they are in the same segment + in *both* S1 and S2. + + Parameters + ---------- + s1, s2 : numpy arrays + s1 and s2 are label fields of the same shape. + + Returns + ------- + j : numpy array + The join segmentation of s1 and s2. + + Examples + -------- + >>> import numpy as np + >>> from skimage.segmentation import join_segmentations + >>> s1 = np.array([[0, 0, 1, 1], + ... [0, 2, 1, 1], + ... [2, 2, 2, 1]]) + >>> s2 = np.array([[0, 1, 1, 0], + ... [0, 1, 1, 0], + ... [0, 1, 1, 1]]) + >>> join_segmentations(s1, s2) + array([[0, 1, 3, 2], + [0, 5, 3, 2], + [4, 5, 5, 3]]) + """ + if s1.shape != s2.shape: + raise ValueError("Cannot join segmentations of different shape. " + + "s1.shape: %s, s2.shape: %s" % (s1.shape, s2.shape)) + s1 = relabel_from_one(s1)[0] + s2 = relabel_from_one(s2)[0] + j = (s2.max() + 1) * s1 + s2 + j = relabel_from_one(j)[0] + return j + +def relabel_from_one(label_field): + """Convert labels in an arbitrary label field to {1, ... number_of_labels}. + + This function also returns the forward map (mapping the original labels to + the reduced labels) and the inverse map (mapping the reduced labels back + to the original ones). + + Parameters + ---------- + label_field : numpy ndarray (integer type) + + Returns + ------- + relabeled : numpy array of same shape as ar + forward_map : 1d numpy array of length np.unique(ar) + 1 + inverse_map : 1d numpy array of length len(np.unique(ar)) + The length is len(np.unique(ar)) + 1 if 0 is not in np.unique(ar) + + Examples + -------- + >>> import numpy as np + >>> from skimage.segmentation import relabel_from_one + >>> label_field = array([1, 1, 5, 5, 8, 99, 42]) + >>> relab, fw, inv = relabel_from_one(label_field) + >>> relab + array([1, 1, 2, 2, 3, 5, 4]) + >>> fw + array([0, 1, 0, 0, 0, 2, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 5]) + >>> inv + array([ 0, 1, 5, 8, 42, 99]) + >>> (fw[label_field] == relab).all() + True + >>> (inv[relab] == label_field).all() + True + """ + labels = np.unique(label_field) + labels0 = labels[labels != 0] + m = labels.max() + if m == len(labels0): # nothing to do, already 1...n labels + return label_field, labels, labels + forward_map = np.zeros(m+1, int) + forward_map[labels0] = np.arange(1, len(labels0) + 1) + if not (labels == 0).any(): + labels = np.concatenate(([0], labels)) + inverse_map = labels + return forward_map[label_field], forward_map, inverse_map diff --git a/skimage/segmentation/_quickshift.pyx b/skimage/segmentation/_quickshift.pyx index b465eb08..cc649e8f 100644 --- a/skimage/segmentation/_quickshift.pyx +++ b/skimage/segmentation/_quickshift.pyx @@ -1,18 +1,18 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np -cimport cython -from libc.math cimport exp, sqrt - -from itertools import product from scipy import ndimage +from itertools import product + +cimport numpy as cnp +from libc.math cimport exp, sqrt from ..util import img_as_float from ..color import rgb2lab -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.cdivision(True) def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=False, sigma=0, convert2lab=True, random_seed=None): """Segments image using quickshift clustering in Color-(x,y) space. @@ -69,7 +69,7 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, image = rgb2lab(image) image = ndimage.gaussian_filter(img_as_float(image), [sigma, sigma, 0]) - cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c \ + cdef cnp.ndarray[dtype=cnp.float_t, ndim=3, mode="c"] image_c \ = np.ascontiguousarray(image) * ratio random_state = np.random.RandomState(random_seed) @@ -85,18 +85,19 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, raise ValueError("Sigma should be >= 1") cdef int w = int(3 * kernel_size) - cdef int height = image_c.shape[0] - cdef int width = image_c.shape[1] - cdef int channels = image_c.shape[2] + cdef Py_ssize_t height = image_c.shape[0] + cdef Py_ssize_t width = image_c.shape[1] + cdef Py_ssize_t channels = image_c.shape[2] cdef double current_density, closest, dist - cdef int r, c, r_, c_, channel + cdef Py_ssize_t r, c, r_, c_, channel, r_min, c_min - cdef np.float_t* image_p = image_c.data - cdef np.float_t* current_pixel_p = image_p + cdef cnp.float_t* image_p = image_c.data + cdef cnp.float_t* current_pixel_p = image_p - cdef np.ndarray[dtype=np.float_t, ndim=2] densities \ + cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] densities \ = np.zeros((height, width)) + # compute densities for r in range(height): for c in range(width): @@ -116,10 +117,11 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, densities += random_state.normal(scale=0.00001, size=(height, width)) # default parent to self: - cdef np.ndarray[dtype=np.int_t, ndim=2] parent \ + cdef cnp.ndarray[dtype=cnp.int_t, ndim=2] parent \ = np.arange(width * height).reshape(height, width) - cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent \ + cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] dist_parent \ = np.zeros((height, width)) + # find nearest node with higher density current_pixel_p = image_p for r in range(height): diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index d0e4c2a3..7db072c0 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -1,18 +1,24 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np from time import time from scipy import ndimage + +cimport numpy as cnp + from ..util import img_as_float -from ..color import rgb2lab +from ..color import rgb2lab, gray2rgb def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, - convert2lab=True): + convert2lab=True): """Segments image using k-means clustering in Color-(x,y) space. Parameters ---------- - image : (width, height, 3) ndarray + image : (width, height [, 3]) ndarray Input image. n_segments : int The (approximate) number of labels in the segmented output image. @@ -53,49 +59,50 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, >>> # Increasing the ratio parameter yields more square regions >>> segments = slic(img, n_segments=100, ratio=20) """ - image = np.atleast_3d(image) - if image.shape[2] != 3: - ValueError("Only 3-channel 2D images are supported.") + 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 int height, width + cdef Py_ssize_t height, width height, width = image.shape[:2] # approximate grid size for desired n_segments - cdef int step = np.ceil(np.sqrt(height * width / n_segments)) + cdef Py_ssize_t step = int(np.ceil(np.sqrt(height * width / n_segments))) grid_y, grid_x = np.mgrid[:height, :width] means_y = grid_y[::step, ::step] means_x = grid_x[::step, ::step] means_color = np.zeros((means_y.shape[0], means_y.shape[1], 3)) - cdef np.ndarray[dtype=np.float_t, ndim=2] means \ + cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] means \ = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) - cdef np.float_t* current_mean - cdef np.float_t* mean_entry + 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 np.ndarray[dtype=np.float_t, ndim=3] image_yx \ + cdef cnp.ndarray[dtype=cnp.float_t, ndim=3] image_yx \ = np.dstack([grid_y, grid_x, image / ratio]).copy("C") - cdef int i, k, x, y, x_min, x_max, y_min, y_max, changes + cdef Py_ssize_t i, k, x, y, x_min, x_max, y_min, y_max, changes cdef double dist_mean - cdef np.ndarray[dtype=np.int_t, ndim=2] nearest_mean \ - = np.zeros((height, width), dtype=np.int) - cdef np.ndarray[dtype=np.float_t, ndim=2] distance \ + 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 np.float_t* image_p = image_yx.data - cdef np.float_t* distance_p = distance.data - cdef np.float_t* current_distance - cdef np.float_t* current_pixel + 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 + current_mean = means.data # assign pixels to means for k in range(n_means): # compute windows: diff --git a/skimage/segmentation/boundaries.py b/skimage/segmentation/boundaries.py index b40ecb01..c5bf985b 100644 --- a/skimage/segmentation/boundaries.py +++ b/skimage/segmentation/boundaries.py @@ -1,19 +1,45 @@ import numpy as np from ..morphology import dilation, square from ..util import img_as_float +from ..color import gray2rgb +from .._shared.utils import deprecated def find_boundaries(label_img): + """Return bool array where boundaries between labeled regions are True.""" boundaries = np.zeros(label_img.shape, dtype=np.bool) boundaries[1:, :] += label_img[1:, :] != label_img[:-1, :] boundaries[:, 1:] += label_img[:, 1:] != label_img[:, :-1] return boundaries -def visualize_boundaries(img, label_img): - img = img_as_float(img, force_copy=True) +def mark_boundaries(image, label_img, color=(1, 1, 0), outline_color=(0, 0, 0)): + """Return image with boundaries between labeled regions highlighted. + + Parameters + ---------- + image : (M, N[, 3]) array + Grayscale or RGB image. + label_img : (M, N) array + Label array where regions are marked by different integer values. + color : length-3 sequence + RGB color of boundaries in the output image. + outline_color : length-3 sequence + RGB color surrounding boundaries in the output image. If None, no + outline is drawn. + """ + if image.ndim == 2: + image = gray2rgb(image) + image = img_as_float(image, force_copy=True) + boundaries = find_boundaries(label_img) - outer_boundaries = dilation(boundaries.astype(np.uint8), square(2)) - img[outer_boundaries != 0, :] = np.array([0, 0, 0]) # black - img[boundaries, :] = np.array([1, 1, 0]) # yellow - return img + if outline_color is not None: + outer_boundaries = dilation(boundaries.astype(np.uint8), square(2)) + image[outer_boundaries != 0, :] = np.array(outline_color) + image[boundaries, :] = np.array(color) + return image + + +@deprecated('mark_boundaries') +def visualize_boundaries(*args, **kwargs): + return mark_boundaries(*args, **kwargs) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 6eb7def8..6df714d2 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -14,13 +14,9 @@ import numpy as np from scipy import sparse, ndimage try: from scipy.sparse.linalg.dsolve import umfpack - u = umfpack.UmfpackContext() + UmfpackContext = umfpack.UmfpackContext() except: - warnings.warn("""Scipy was built without UMFPACK. Consider rebuilding - Scipy with UMFPACK, this will greatly speed up the random walker - functions. You may also install pyamg and run the random walker function - in cg_mg mode (see the docstrings) - """) + UmfpackContext = None try: from pyamg import ruge_stuben_solver amg_loaded = True @@ -34,8 +30,7 @@ from ..filter import rank_order def _make_graph_edges_3d(n_x, n_y, n_z): - """ - Returns a list of edges for a 3D image. + """Returns a list of edges for a 3D image. Parameters ---------- @@ -49,9 +44,12 @@ def _make_graph_edges_3d(n_x, n_y, n_z): Returns ------- edges : (2, N) ndarray - with the total number of edges N = n_x * n_y * (nz - 1) + - n_x * (n_y - 1) * nz + - (n_x - 1) * n_y * nz + with the total number of edges:: + + N = n_x * n_y * (nz - 1) + + n_x * (n_y - 1) * nz + + (n_x - 1) * n_y * nz + Graph edges with each column describing a node-id pair. """ vertices = np.arange(n_x * n_y * n_z).reshape((n_x, n_y, n_z)) @@ -176,20 +174,19 @@ 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.): - """ - Random walker algorithm for segmentation from markers, for gray-level or - multichannel images. + """Random walker algorithm for segmentation from markers. + + Random walker algorithm is implemented for gray-level or multichannel + images. Parameters ---------- - data : array_like Image to be segmented in phases. Gray-level `data` can be two- or three-dimensional; multichannel data can be three- or four- dimensional (multichannel=True) with the highest dimension denoting channels. Data spacing is assumed isotropic unless depth keyword argument is used. - labels : array of ints, of same shape as `data` without channels dimension Array of seed markers labeled with different positive integers for different phases. Zero-labeled pixels are unlabeled pixels. @@ -199,11 +196,9 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, labels are consecutive. In the multichannel case, `labels` should have the same shape as a single channel of `data`, i.e. without the final dimension denoting channels. - beta : float Penalization coefficient for the random walker motion (the greater `beta`, the more difficult the diffusion). - mode : {'bf', 'cg_mg', 'cg'} (default: 'bf') Mode for solving the linear system in the random walker algorithm. @@ -212,12 +207,10 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, computed. This is fast for small images (<1024x1024), but very slow (due to the memory cost) and memory-consuming for big images (in 3-D for example). - - 'cg' (conjugate gradient): the linear system is solved iteratively using the Conjugate Gradient method from scipy.sparse.linalg. This is less memory-consuming than the brute force method for large images, but it is quite slow. - - 'cg_mg' (conjugate gradient with multigrid preconditioner): a preconditioner is computed using a multigrid solver, then the solution is computed with the Conjugate Gradient method. This mode @@ -228,20 +221,16 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, tol : float tolerance to achieve when solving the linear system, in cg' and 'cg_mg' modes. - copy : bool If copy is False, the `labels` array will be overwritten with the result of the segmentation. Use copy=False if you want to save on memory. - multichannel : bool, default False If True, input data is parsed as multichannel data (see 'data' above for proper input format in this case) - return_full_prob : bool, default False If True, the probability that a pixel belongs to each of the labels will be returned, instead of only the most likely label. - depth : float, default 1. Correction for non-isotropic voxel depths in 3D volumes. Default (1.) implies isotropy. This factor is derived as follows: @@ -251,25 +240,22 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, Returns ------- - output : ndarray - If `return_full_prob` is False, array of ints of same shape as `data`, - in which each pixel has been labeled according to the marker that - reached the pixel first by anisotropic diffusion. - If `return_full_prob` is True, array of floats of shape - `(nlabels, data.shape)`. `output[label_nb, i, j]` is the probability - that label `label_nb` reaches the pixel `(i, j)` first. + * If `return_full_prob` is False, array of ints of same shape as + `data`, in which each pixel has been labeled according to the marker + that reached the pixel first by anisotropic diffusion. + * If `return_full_prob` is True, array of floats of shape + `(nlabels, data.shape)`. `output[label_nb, i, j]` is the probability + that label `label_nb` reaches the pixel `(i, j)` first. See also -------- - skimage.morphology.watershed: watershed segmentation A segmentation algorithm based on mathematical morphology and "flooding" of regions from markers. Notes ----- - Multichannel inputs are scaled with all channel data combined. Ensure all channels are separately normalized prior to running this algorithm. @@ -319,7 +305,6 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, Examples -------- - >>> a = np.zeros((10, 10)) + 0.2*np.random.random((10, 10)) >>> a[5:8, 5:8] += 1 >>> b = np.zeros_like(a) @@ -338,6 +323,14 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], dtype=int32) """ + + if UmfpackContext is None: + warnings.warn('SciPy was built without UMFPACK. Consider rebuilding ' + 'SciPy with UMFPACK, this will greatly speed up the ' + 'random walker functions. You may also install pyamg ' + 'and run the random walker function in cg_mg mode ' + '(see the docstrings)') + # Parse input data if not multichannel: # We work with 4-D arrays of floats diff --git a/skimage/segmentation/tests/test_join.py b/skimage/segmentation/tests/test_join.py new file mode 100644 index 00000000..f03244e9 --- /dev/null +++ b/skimage/segmentation/tests/test_join.py @@ -0,0 +1,40 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage.segmentation import join_segmentations, relabel_from_one + +def test_join_segmentations(): + s1 = np.array([[0, 0, 1, 1], + [0, 2, 1, 1], + [2, 2, 2, 1]]) + s2 = np.array([[0, 1, 1, 0], + [0, 1, 1, 0], + [0, 1, 1, 1]]) + + # test correct join + # NOTE: technically, equality to j_ref is not required, only that there + # is a one-to-one mapping between j and j_ref. I don't know of an easy way + # to check this (i.e. not as error-prone as the function being tested) + j = join_segmentations(s1, s2) + j_ref = np.array([[0, 1, 3, 2], + [0, 5, 3, 2], + [4, 5, 5, 3]]) + assert_array_equal(j, j_ref) + + # test correct exception when arrays are different shapes + s3 = np.array([[0, 0, 1, 1], [0, 2, 2, 1]]) + assert_raises(ValueError, join_segmentations, s1, s3) + +def test_relabel_from_one(): + ar = np.array([1, 1, 5, 5, 8, 99, 42]) + ar_relab, fw, inv = relabel_from_one(ar) + ar_relab_ref = np.array([1, 1, 2, 2, 3, 5, 4]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 1; fw_ref[5] = 2; fw_ref[8] = 3; fw_ref[42] = 4; fw_ref[99] = 5 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/segmentation/tests/test_quickshift.py b/skimage/segmentation/tests/test_quickshift.py index d43d7559..a940f859 100644 --- a/skimage/segmentation/tests/test_quickshift.py +++ b/skimage/segmentation/tests/test_quickshift.py @@ -34,10 +34,10 @@ def test_color(): seg = quickshift(img, random_seed=0, max_dist=30, kernel_size=10, sigma=0) # we expect 4 segments: assert_equal(len(np.unique(seg)), 4) - assert_array_equal(seg[:10, :10], 0) - assert_array_equal(seg[10:, :10], 3) - assert_array_equal(seg[:10, 10:], 1) - assert_array_equal(seg[10:, 10:], 2) + assert_array_equal(seg[:10, :10], 1) + assert_array_equal(seg[10:, :10], 2) + assert_array_equal(seg[:10, 10:], 0) + assert_array_equal(seg[10:, 10:], 3) seg2 = quickshift(img, kernel_size=1, max_dist=2, random_seed=0, convert2lab=False, sigma=0) diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index f2d6698d..89dee59b 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -13,14 +13,30 @@ def test_color(): img[img > 1] = 1 img[img < 0] = 0 seg = slic(img, sigma=0, n_segments=4) - # we expect 4 segments: - print(seg) + + # we expect 4 segments assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) assert_array_equal(seg[10:, :10], 2) assert_array_equal(seg[:10, 10:], 1) assert_array_equal(seg[10:, 10:], 3) +def test_gray(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21)) + img[:10, :10] = 0.33 + img[10:, :10] = 0.67 + img[10:, 10:] = 1.00 + img += 0.0033 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, sigma=0, n_segments=4, ratio=50.0) + + assert_equal(len(np.unique(seg)), 4) + assert_array_equal(seg[:10, :10], 0) + assert_array_equal(seg[10:, :10], 2) + assert_array_equal(seg[:10, 10:], 1) + assert_array_equal(seg[10:, 10:], 3) if __name__ == '__main__': from numpy import testing diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index a55d5df4..089487ee 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -6,6 +6,6 @@ from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) -from ._warps import swirl, homography, resize, rotate +from ._warps import swirl, resize, rotate, rescale from .pyramids import (pyramid_reduce, pyramid_expand, pyramid_gaussian, pyramid_laplacian) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 1179811d..448829de 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -604,7 +604,7 @@ class PolynomialTransform(GeometricTransform): raise ValueError("invalid shape of transformation parameters") self._params = params - def estimate(self, src, dst, order): + def estimate(self, src, dst, order=2): """Set the transformation matrix with the explicit transformation parameters. @@ -645,7 +645,7 @@ class PolynomialTransform(GeometricTransform): Source coordinates. dst : (N, 2) array Destination coordinates. - order : int + order : int, optional Polynomial order (number of coefficients is order + 1). """ @@ -750,7 +750,8 @@ def estimate_transform(ttype, src, dst, **kwargs): 'affine' `src, `dst` 'piecewise-affine' `src, `dst` 'projective' `src, `dst` - 'polynomial' `src, `dst`, `order` (polynomial order) + 'polynomial' `src, `dst`, `order` (polynomial order, + default order is 2) Also see examples below. @@ -848,6 +849,8 @@ def warp_coords(coord_map, shape, dtype=np.float64): ---------- coord_map : callable like GeometricTransform.inverse Return input coordinates for given output coordinates. + Coordinates are in the shape (P, 2), where P is the number + of coordinates and each element is a ``(x, y)`` pair. shape : tuple Shape of output image ``(rows, cols[, bands])``. dtype : np.dtype or string @@ -874,17 +877,16 @@ def warp_coords(coord_map, shape, dtype=np.float64): Examples -------- - Produce a coordinate map that Shifts an image to the right: + Produce a coordinate map that Shifts an image up and to the right: >>> from skimage import data >>> from scipy.ndimage import map_coordinates >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 - ... return xy + >>> def shift_up10_left20(xy): + ... return xy - np.array([-20, 10])[None, :] >>> >>> image = data.lena().astype(np.float32) - >>> coords = warp_coords(shift_right, image.shape) + >>> coords = warp_coords(shift_up10_left20, image.shape) >>> warped_image = map_coordinates(image, coords) """ diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index ef1cf700..2b1acc7c 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -1,31 +1,101 @@ -cimport cython +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np -cimport numpy as np -from random import randint -from libc.math cimport abs, fabs, sqrt, ceil, floor + +cimport numpy as cnp +cimport cython + +from libc.math cimport abs, fabs, sqrt, ceil from libc.stdlib cimport rand - -np.import_array() - +from skimage.draw import circle_perimeter cdef double PI_2 = 1.5707963267948966 cdef double NEG_PI_2 = -PI_2 -cdef inline int round(double r): - return ((r + 0.5) if (r > 0.0) else (r - 0.5)) +cdef inline Py_ssize_t round(double r): + return ((r + 0.5) if (r > 0.0) else (r - 0.5)) -@cython.boundscheck(False) -def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): +def _hough_circle(cnp.ndarray img, + cnp.ndarray[ndim=1, dtype=cnp.intp_t] radius, + char normalize=True): + """Perform a circular Hough transform. + + Parameters + ---------- + img : (M, N) ndarray + Input image with nonzero values representing edges. + radius : ndarray + Radii at which to compute the Hough transform. + normalize : boolean, optional + Normalize the accumulator with the number + of pixels used to draw the radius + + Returns + ------- + H : 3D ndarray (radius index, (M, N) ndarray) + Hough transform accumulator for each radius + + """ + if img.ndim != 2: + raise ValueError('The input image must be 2D.') + + # compute the nonzero indexes + cdef cnp.ndarray[ndim=1, dtype=cnp.intp_t] x, y + x, y = np.nonzero(img) + + cdef Py_ssize_t num_pixels = x.size + + # Offset the image + cdef Py_ssize_t max_radius = radius.max() + x = x + max_radius + y = y + max_radius + + cdef Py_ssize_t i, p, c, num_circle_pixels, tx, ty + cdef double incr + cdef cnp.ndarray[ndim=1, dtype=cnp.intp_t] circle_x, circle_y + + cdef cnp.ndarray[ndim=3, dtype=cnp.double_t] acc = \ + np.zeros((radius.size, + img.shape[0] + 2 * max_radius, + img.shape[1] + 2 * max_radius), dtype=np.double) + + for i, rad in enumerate(radius): + # Store in memory the circle of given radius + # centered at (0,0) + circle_x, circle_y = circle_perimeter(0, 0, rad) + + num_circle_pixels = circle_x.size + + if normalize: + incr = 1.0 / num_circle_pixels + else: + incr = 1 + + # For each non zero pixel + for p in range(num_pixels): + # Plug the circle at (px, py), + # its coordinates are (tx, ty) + for c in range(num_circle_pixels): + tx = circle_x[c] + x[p] + ty = circle_y[c] + y[p] + acc[i, tx, ty] += incr + + return acc + + +def _hough(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): if img.ndim != 2: raise ValueError('The input image must be 2D.') # Compute the array of angles and their sine and cosine - cdef np.ndarray[ndim=1, dtype=np.double_t] ctheta - cdef np.ndarray[ndim=1, dtype=np.double_t] stheta + cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] ctheta + cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] stheta if theta is None: theta = np.linspace(PI_2, NEG_PI_2, 180) @@ -34,23 +104,22 @@ def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): stheta = np.sin(theta) # compute the bins and allocate the accumulator array - cdef np.ndarray[ndim=2, dtype=np.uint64_t] accum - cdef np.ndarray[ndim=1, dtype=np.double_t] bins - cdef int max_distance, offset + cdef cnp.ndarray[ndim=2, dtype=cnp.uint64_t] accum + cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] bins + cdef Py_ssize_t max_distance, offset - max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + - img.shape[1] * img.shape[1]))) + max_distance = 2 * ceil(sqrt(img.shape[0] * img.shape[0] + + img.shape[1] * img.shape[1])) accum = np.zeros((max_distance, theta.shape[0]), dtype=np.uint64) bins = np.linspace(-max_distance / 2.0, max_distance / 2.0, max_distance) offset = max_distance / 2 # compute the nonzero indexes - cdef np.ndarray[ndim=1, dtype=np.npy_intp] x_idxs, y_idxs - y_idxs, x_idxs = np.PyArray_Nonzero(img) - + cdef cnp.ndarray[ndim=1, dtype=cnp.npy_intp] x_idxs, y_idxs + y_idxs, x_idxs = np.nonzero(img) # finally, run the transform - cdef int nidxs, nthetas, i, j, x, y, accum_idx + cdef Py_ssize_t nidxs, nthetas, i, j, x, y, accum_idx nidxs = y_idxs.shape[0] # x and y are the same shape nthetas = theta.shape[0] for i in range(nidxs): @@ -61,67 +130,75 @@ def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): accum[accum_idx, j] += 1 return accum, theta, bins -import math -@cython.cdivision(True) -@cython.boundscheck(False) -def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ - int line_gap, np.ndarray[ndim=1, dtype=np.double_t] theta=None): +def _probabilistic_hough(cnp.ndarray img, int value_threshold, + int line_length, int line_gap, + cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): + if img.ndim != 2: raise ValueError('The input image must be 2D.') - # compute the array of angles and their sine and cosine - cdef np.ndarray[ndim=1, dtype=np.double_t] ctheta - cdef np.ndarray[ndim=1, dtype=np.double_t] stheta - # calculate thetas if none specified + if theta is None: - theta = np.linspace(math.pi/2, -math.pi/2, 180) - theta = math.pi/2-np.arange(180)/180.0* math.pi - ctheta = np.cos(theta) - stheta = np.sin(theta) - cdef int height = img.shape[0] - cdef int width = img.shape[1] + theta = PI_2 - np.arange(180) / 180.0 * 2 * PI_2 + + cdef Py_ssize_t height = img.shape[0] + cdef Py_ssize_t width = img.shape[1] + # compute the bins and allocate the accumulator array - cdef np.ndarray[ndim=2, dtype=np.int64_t] accum - cdef np.ndarray[ndim=2, dtype=np.uint8_t] mask = np.zeros((height, width), dtype=np.uint8) - cdef np.ndarray[ndim=2, dtype=np.int32_t] line_end = np.zeros((2, 2), dtype=np.int32) - cdef int max_distance, offset, num_indexes, index + cdef cnp.ndarray[ndim=2, dtype=cnp.int64_t] accum + cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] ctheta, stheta + cdef cnp.ndarray[ndim=2, dtype=cnp.uint8_t] mask = \ + np.zeros((height, width), dtype=np.uint8) + cdef cnp.ndarray[ndim=2, dtype=cnp.int32_t] line_end = \ + np.zeros((2, 2), dtype=np.int32) + cdef Py_ssize_t max_distance, offset, num_indexes, index cdef double a, b - cdef int nidxs, nthetas, i, j, x, y, px, py, accum_idx, value, max_value, max_theta + cdef Py_ssize_t nidxs, i, j, x, y, px, py, accum_idx + cdef int value, max_value, max_theta cdef int shift = 16 # maximum line number cutoff - cdef int lines_max = 2 ** 15 - cdef int xflag, x0, y0, dx0, dy0, dx, dy, gap, x1, y1, good_line, count + cdef Py_ssize_t lines_max = 2 ** 15 + cdef Py_ssize_t xflag, x0, y0, dx0, dy0, dx, dy, gap, x1, y1, \ + good_line, count + cdef list lines = list() + max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + img.shape[1] * img.shape[1]))) accum = np.zeros((max_distance, theta.shape[0]), dtype=np.int64) offset = max_distance / 2 - # find the nonzero indexes - cdef np.ndarray[ndim=1, dtype=np.npy_intp] x_idxs, y_idxs - y_idxs, x_idxs = np.nonzero(img) - num_indexes = y_idxs.shape[0] # x and y are the same shape nthetas = theta.shape[0] - points = [] - for i in range(num_indexes): - points.append((x_idxs[i], y_idxs[i])) - lines = [] - # create mask of all non-zero indexes - for i in range(num_indexes): - mask[y_idxs[i], x_idxs[i]] = 1 + + # compute sine and cosine of angles + ctheta = np.cos(theta) + stheta = np.sin(theta) + + # find the nonzero indexes + y_idxs, x_idxs = np.nonzero(img) + points = list(zip(x_idxs, y_idxs)) + # mask all non-zero indexes + mask[y_idxs, x_idxs] = 1 + while 1: - # select random non-zero point + + # quit if no remaining points count = len(points) if count == 0: break - index = rand() % (count) + + # select random non-zero point + index = rand() % count x = points[index][0] y = points[index][1] del points[index] + # if previously eliminated, skip if not mask[y, x]: continue + value = 0 - max_value = value_threshold-1 + max_value = value_threshold - 1 max_theta = -1 + # apply hough transform on point for j in range(nthetas): accum_idx = round((ctheta[j] * x + stheta[j] * y)) + offset @@ -132,7 +209,9 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ max_theta = j if max_value < value_threshold: continue - # from the random point walk in opposite directions and find line beginning and end + + # from the random point walk in opposite directions and find line + # beginning and end a = -stheta[max_theta] b = ctheta[max_theta] x0 = x @@ -188,6 +267,7 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ # confirm line length is sufficient good_line = abs(line_end[1, 1] - line_end[0, 1]) >= line_length or \ abs(line_end[1, 0] - line_end[0, 0]) >= line_length + # pass 2: walk the line again and reset accumulator and mask for k in range(2): px = x0 @@ -207,7 +287,8 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ # if non-zero point found, continue the line if mask[y1, x1]: if good_line: - accum_idx = round((ctheta[j] * x1 + stheta[j] * y1)) + offset + accum_idx = round((ctheta[j] * x1 \ + + stheta[j] * y1)) + offset accum[accum_idx, max_theta] -= 1 mask[y1, x1] = 0 # exit when the point is the line end @@ -218,9 +299,9 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ # add line to the result if good_line: - lines.append(((line_end[0, 0], line_end[0, 1]), (line_end[1, 0], line_end[1, 1]))) + lines.append(((line_end[0, 0], line_end[0, 1]), + (line_end[1, 0], line_end[1, 1]))) if len(lines) > lines_max: return lines + return lines - - diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 72f90050..7ac3e63f 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,17 +1,21 @@ import numpy as np +from scipy import ndimage from ._geometric import (warp, SimilarityTransform, AffineTransform, ProjectiveTransform) def resize(image, output_shape, order=1, mode='constant', cval=0.): - """Resize image. + """Resize image to match a certain size. Parameters ---------- image : ndarray Input image. output_shape : tuple or ndarray - Size of the generated output image `(rows, cols)`. + Size of the generated output image `(rows, cols[, dim])`. If `dim` is + not provided, the number of channels are preserved. In case the number + of input channels does not equal the number of output channels a + 3-dimensional interpolation is applied. Returns ------- @@ -32,24 +36,88 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): """ - rows, cols = output_shape + rows, cols = output_shape[0], output_shape[1] orig_rows, orig_cols = image.shape[0], image.shape[1] - rscale = float(orig_rows) / rows - cscale = float(orig_cols) / cols + row_scale = float(orig_rows) / rows + col_scale = float(orig_cols) / cols - # 3 control points necessary to estimate exact AffineTransform - src_corners = np.array([[1, 1], [1, rows], [cols, rows]]) - 1 - dst_corners = np.zeros(src_corners.shape, dtype=np.double) - # take into account that 0th pixel is at position (0.5, 0.5) - dst_corners[:, 0] = cscale * (src_corners[:, 0] + 0.5) - 0.5 - dst_corners[:, 1] = rscale * (src_corners[:, 1] + 0.5) - 0.5 + # 3-dimensional interpolation + if len(output_shape) == 3 and (image.ndim == 2 + or output_shape[2] != image.shape[2]): + dim = output_shape[2] + orig_dim = 1 if image.ndim == 2 else image.shape[2] + dim_scale = float(orig_dim) / dim - tform = AffineTransform() - tform.estimate(src_corners, dst_corners) + map_rows, map_cols, map_dims = np.mgrid[:rows, :cols, :dim] + map_rows = row_scale * (map_rows + 0.5) - 0.5 + map_cols = col_scale * (map_cols + 0.5) - 0.5 + map_dims = dim_scale * (map_dims + 0.5) - 0.5 - return warp(image, tform, output_shape=output_shape, order=order, - mode=mode, cval=cval) + coord_map = np.array([map_rows, map_cols, map_dims]) + + out = ndimage.map_coordinates(image, coord_map, order=order, mode=mode, + cval=cval) + + else: # 2-dimensional interpolation + + # 3 control points necessary to estimate exact AffineTransform + src_corners = np.array([[1, 1], [1, rows], [cols, rows]]) - 1 + dst_corners = np.zeros(src_corners.shape, dtype=np.double) + # take into account that 0th pixel is at position (0.5, 0.5) + dst_corners[:, 0] = col_scale * (src_corners[:, 0] + 0.5) - 0.5 + dst_corners[:, 1] = row_scale * (src_corners[:, 1] + 0.5) - 0.5 + + tform = AffineTransform() + tform.estimate(src_corners, dst_corners) + + out = warp(image, tform, output_shape=output_shape, order=order, + mode=mode, cval=cval) + + return out + + +def rescale(image, scale, order=1, mode='constant', cval=0.): + """Scale image by a certain factor. + + Parameters + ---------- + image : ndarray + Input image. + scale : {float, tuple of floats} + Scale factors. Separate scale factors can be defined as + `(row_scale, col_scale)`. + + Returns + ------- + scaled : ndarray + Scaled version of the input. + + Other parameters + ---------------- + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + """ + + try: + row_scale, col_scale = scale + except TypeError: + row_scale = col_scale = scale + + orig_rows, orig_cols = image.shape[0], image.shape[1] + rows = np.round(row_scale * orig_rows) + cols = np.round(col_scale * orig_cols) + output_shape = (rows, cols) + + return resize(image, output_shape, order=order, mode=mode, cval=cval) def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): @@ -95,7 +163,7 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): tform = tform1 + tform2 + tform3 output_shape = None - if not resize: + if resize: # determine shape of output image corners = np.array([[1, 1], [1, rows], [cols, rows], [cols, 1]]) corners = tform(corners - 1) @@ -185,98 +253,3 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, return warp(image, _swirl_mapping, map_args=warp_args, output_shape=output_shape, order=order, mode=mode, cval=cval) - - -def homography(image, H, output_shape=None, order=1, - mode='constant', cval=0.): - """ - .. note:: Deprecated in skimage 0.7 - `homography` will be removed in skimage 0.8, it is replaced by - `warp` because the latter provides the same functionality:: - - warp(image, ProjectiveTransform(H)) - - Perform a projective transformation (homography) on an image. - - For each pixel, given its homogeneous coordinate :math:`\mathbf{x} - = [x, y, 1]^T`, its target position is calculated by multiplying - with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. - E.g., to rotate by theta degrees clockwise, the matrix should be - - :: - - [[cos(theta) -sin(theta) 0] - [sin(theta) cos(theta) 0] - [0 0 1]] - - or, to translate x by 10 and y by 20, - - :: - - [[1 0 10] - [0 1 20] - [0 0 1 ]]. - - Parameters - ---------- - image : 2-D array - Input image. - H : array of shape ``(3, 3)`` - Transformation matrix H that defines the homography. - output_shape : tuple (rows, cols) - Shape of the output image generated. - order : int - Order of splines used in interpolation. - mode : string - How to handle values outside the image borders. Passed as-is - to ndimage. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Examples - -------- - >>> # rotate by 90 degrees around origin and shift down by 2 - >>> x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 - >>> x - array([[1, 2, 3], - [4, 5, 6], - [7, 8, 9]], dtype=uint8) - >>> theta = -np.pi/2 - >>> M = np.array([[np.cos(theta),-np.sin(theta),0], - ... [np.sin(theta), np.cos(theta),2], - ... [0, 0, 1]]) - >>> x90 = homography(x, M, order=1) - >>> x90 - array([[3, 6, 9], - [2, 5, 8], - [1, 4, 7]], dtype=uint8) - >>> # translate right by 2 and down by 1 - >>> y = np.zeros((5,5), dtype=np.uint8) - >>> y[1, 1] = 255 - >>> y - array([[ 0, 0, 0, 0, 0], - [ 0, 255, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0]], dtype=uint8) - >>> M = np.array([[ 1., 0., 2.], - ... [ 0., 1., 1.], - ... [ 0., 0., 1.]]) - >>> y21 = homography(y, M, order=1) - >>> y21 - array([[ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 255, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0]], dtype=uint8) - - """ - import warnings - warnings.warn('the homography function is deprecated; ' - 'use the `warp` and `ProjectiveTransform` class instead', - category=DeprecationWarning) - - tform = ProjectiveTransform(H) - return warp(image, inverse_map=tform.inverse, output_shape=output_shape, - order=order, mode=mode, cval=cval) diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index ce400ed6..968f643a 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -2,9 +2,9 @@ #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False - -cimport numpy as np import numpy as np + +cimport numpy as cnp from skimage._shared.interpolation cimport (nearest_neighbour_interpolation, bilinear_interpolation, biquadratic_interpolation, @@ -35,7 +35,7 @@ cdef inline void _matrix_transform(double x, double y, double* H, double *x_, y_[0] = yy / zz -def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, +def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, int order=1, mode='constant', double cval=0): """Projective transformation (homography). @@ -83,9 +83,9 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, """ - cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] img = \ + cdef cnp.ndarray[dtype=cnp.double_t, ndim=2, mode="c"] img = \ np.ascontiguousarray(image, dtype=np.double) - cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] M = \ + cdef cnp.ndarray[dtype=cnp.double_t, ndim=2, mode="c"] M = \ np.ascontiguousarray(H) if mode not in ('constant', 'wrap', 'reflect', 'nearest'): @@ -93,7 +93,7 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, "`constant`, `nearest`, `wrap` or `reflect`.") cdef char mode_c = ord(mode[0].upper()) - cdef int out_r, out_c + cdef Py_ssize_t out_r, out_c if output_shape is None: out_r = img.shape[0] out_c = img.shape[1] @@ -101,15 +101,15 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, out_r = output_shape[0] out_c = output_shape[1] - cdef np.ndarray[dtype=np.double_t, ndim=2] out = \ + cdef cnp.ndarray[dtype=cnp.double_t, ndim=2] out = \ np.zeros((out_r, out_c), dtype=np.double) - cdef int tfr, tfc + cdef Py_ssize_t tfr, tfc cdef double r, c - cdef int rows = img.shape[0] - cdef int cols = img.shape[1] + cdef Py_ssize_t rows = img.shape[0] + cdef Py_ssize_t cols = img.shape[1] - cdef double (*interp_func)(double*, int, int, double, double, + cdef double (*interp_func)(double*, Py_ssize_t, Py_ssize_t, double, double, char, double) if order == 0: interp_func = nearest_neighbour_interpolation diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 4e3acd6e..17b76826 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -1,9 +1,11 @@ -__all__ = ['hough', 'probabilistic_hough'] +__all__ = ['hough', 'hough_line', 'hough_circle', 'hough_peaks', 'probabilistic_hough'] from itertools import izip as zip import numpy as np +from scipy import ndimage from ._hough_transform import _probabilistic_hough +from skimage import measure, morphology def _hough(img, theta=None): @@ -94,8 +96,15 @@ def probabilistic_hough(img, threshold=10, line_length=50, line_gap=10, """ return _probabilistic_hough(img, threshold, line_length, line_gap, theta) +from skimage._shared.utils import deprecated +@deprecated('hough_line') def hough(img, theta=None): + return hough_line(img, theta) + +from ._hough_transform import _hough_circle + +def hough_line(img, theta=None): """Perform a straight line Hough transform. Parameters @@ -110,10 +119,10 @@ def hough(img, theta=None): ------- H : 2-D ndarray of uint64 Hough transform accumulator. - distances : ndarray - Distance values. theta : ndarray Angles at which the transform was computed. + distances : ndarray + Distance values. Examples -------- @@ -135,3 +144,147 @@ def hough(img, theta=None): """ return _hough(img, theta) + +def hough_circle(img, radius, normalize=True): + """Perform a circular Hough transform. + + Parameters + ---------- + img : (M, N) ndarray + Input image with nonzero values representing edges. + radius : ndarray + Radii at which to compute the Hough transform. + normalize : boolean, optional + Normalize the accumulator with the number + of pixels used to draw the radius + + Returns + ------- + H : 3D ndarray (radius index, (M, N) ndarray) + Hough transform accumulator for each radius + + """ + return _hough_circle(img, radius, normalize) + +def hough_peaks(hspace, angles, dists, min_distance=10, min_angle=10, + threshold=None, num_peaks=np.inf): + """Return peaks in hough transform. + + Identifies most prominent lines separated by a certain angle and distance in + a hough transform. Non-maximum suppression with different sizes is applied + separately in the first (distances) and second (angles) dimension of the + hough space to identify peaks. + + Parameters + ---------- + hspace : (N, M) array + Hough space returned by the `hough` function. + angles : (M,) array + Angles returned by the `hough` function. Assumed to be continuous + (`angles[-1] - angles[0] == PI`). + dists : (N, ) array + Distances returned by the `hough` function. + min_distance : int + Minimum distance separating lines (maximum filter size for first + dimension of hough space). + min_angle : int + Minimum angle separating lines (maximum filter size for second + dimension of hough space). + threshold : float + Minimum intensity of peaks. Default is `0.5 * max(hspace)`. + num_peaks : int + Maximum number of peaks. When the number of peaks exceeds `num_peaks`, + return `num_peaks` coordinates based on peak intensity. + + Returns + ------- + hspace, angles, dists : tuple of array + Peak values in hough space, angles and distances. + + Examples + -------- + >>> import numpy as np + >>> from skimage.transform import hough, hough_peaks + >>> from skimage.draw import line + >>> img = np.zeros((15, 15), dtype=np.bool_) + >>> rr, cc = line(0, 0, 14, 14) + >>> img[rr, cc] = 1 + >>> rr, cc = line(0, 14, 14, 0) + >>> img[cc, rr] = 1 + >>> hspace, angles, dists = hough(img) + >>> hspace, angles, dists = hough_peaks(hspace, angles, dists) + >>> angles + array([ 0.74590887, -0.79856126]) + >>> dists + array([ 10.74418605, 0.51162791]) + + """ + + hspace = hspace.copy() + rows, cols = hspace.shape + + if threshold is None: + threshold = 0.5 * np.max(hspace) + + distance_size = 2 * min_distance + 1 + angle_size = 2 * min_angle + 1 + hspace_max = ndimage.maximum_filter1d(hspace, size=distance_size, axis=0, + mode='constant', cval=0) + hspace_max = ndimage.maximum_filter1d(hspace_max, size=angle_size, axis=1, + mode='constant', cval=0) + mask = (hspace == hspace_max) + hspace *= mask + hspace_t = hspace > threshold + + label_hspace = morphology.label(hspace_t) + props = measure.regionprops(label_hspace, ['Centroid']) + coords = np.array([np.round(p['Centroid']) for p in props], dtype=int) + + hspace_peaks = [] + dist_peaks = [] + angle_peaks = [] + + # relative coordinate grid for local neighbourhood suppression + dist_ext, angle_ext = np.mgrid[-min_distance:min_distance + 1, + -min_angle:min_angle + 1] + + for dist_idx, angle_idx in coords: + accum = hspace[dist_idx, angle_idx] + if accum > threshold: + # absolute coordinate grid for local neighbourhood suppression + dist_nh = dist_idx + dist_ext + angle_nh = angle_idx + angle_ext + + # no reflection for distance neighbourhood + dist_in = np.logical_and(dist_nh > 0, dist_nh < rows) + dist_nh = dist_nh[dist_in] + angle_nh = angle_nh[dist_in] + + # reflect angles and assume angles are continuous, e.g. + # (..., 88, 89, -90, -89, ..., 89, -90, -89, ...) + angle_low = angle_nh < 0 + dist_nh[angle_low] = rows - dist_nh[angle_low] + angle_nh[angle_low] += cols + angle_high = angle_nh >= cols + dist_nh[angle_high] = rows - dist_nh[angle_high] + angle_nh[angle_high] -= cols + + # suppress neighbourhood + hspace[dist_nh, angle_nh] = 0 + + # add current line to peaks + hspace_peaks.append(accum) + dist_peaks.append(dists[dist_idx]) + angle_peaks.append(angles[angle_idx]) + + hspace_peaks = np.array(hspace_peaks) + dist_peaks = np.array(dist_peaks) + angle_peaks = np.array(angle_peaks) + + if num_peaks < len(hspace_peaks): + idx_maxsort = np.argsort(hspace_peaks)[::-1][:num_peaks] + hspace_peaks = hspace_peaks[idx_maxsort] + dist_peaks = dist_peaks[idx_maxsort] + angle_peaks = angle_peaks[idx_maxsort] + + return hspace_peaks, angle_peaks, dist_peaks diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index d3a75f7b..c7f9f832 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -159,6 +159,12 @@ def test_polynomial_init(): assert_array_almost_equal(tform2._params, tform._params) +def test_polynomial_default_order(): + tform = estimate_transform('polynomial', SRC, DST) + tform2 = estimate_transform('polynomial', SRC, DST, order=2) + assert_array_almost_equal(tform2._params, tform._params) + + def test_union(): tform1 = SimilarityTransform(scale=0.1, rotation=0.3) tform2 = SimilarityTransform(scale=0.1, rotation=0.9) diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 03b63d07..00427332 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -4,6 +4,7 @@ from numpy.testing import * import skimage.transform as tf import skimage.transform.hough_transform as ht from skimage.transform import probabilistic_hough +from skimage.draw import circle_perimeter def append_desc(func, description): @@ -14,8 +15,6 @@ def append_desc(func, description): return func -from skimage.transform import * - def test_hough(): # Generate a test image @@ -23,7 +22,7 @@ def test_hough(): for i in range(25, 75): img[100 - i, i] = 1 - out, angles, d = tf.hough(img) + out, angles, d = tf.hough_line(img) y, x = np.where(out == out.max()) dist = d[y[0]] @@ -37,7 +36,7 @@ def test_hough_angles(): img = np.zeros((10, 10)) img[0, 0] = 1 - out, angles, d = tf.hough(img, np.linspace(0, 360, 10)) + out, angles, d = tf.hough_line(img, np.linspace(0, 360, 10)) assert_equal(len(angles), 10) @@ -72,5 +71,58 @@ def test_probabilistic_hough(): assert([(25, 25), (74, 74)] in sorted_lines) +def test_hough_peaks_dist(): + img = np.zeros((100, 100), dtype=np.bool_) + img[:, 30] = True + img[:, 40] = True + hspace, angles, dists = tf.hough_line(img) + assert len(tf.hough_peaks(hspace, angles, dists, min_distance=5)[0]) == 2 + assert len(tf.hough_peaks(hspace, angles, dists, min_distance=15)[0]) == 1 + + +def test_hough_peaks_angle(): + img = np.zeros((100, 100), dtype=np.bool_) + img[:, 0] = True + img[0, :] = True + + hspace, angles, dists = tf.hough_line(img) + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + + theta = np.linspace(0, np.pi, 100) + hspace, angles, dists = tf.hough_line(img, theta) + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + + theta = np.linspace(np.pi / 3, 4. / 3 * np.pi, 100) + hspace, angles, dists = tf.hough_line(img, theta) + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 + assert len(tf.hough_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + + +def test_hough_peaks_num(): + img = np.zeros((100, 100), dtype=np.bool_) + img[:, 30] = True + img[:, 40] = True + hspace, angles, dists = tf.hough_line(img) + assert len(tf.hough_peaks(hspace, angles, dists, min_distance=0, + min_angle=0, num_peaks=1)[0]) == 1 + + +def test_houghcircle(): + # Prepare picture + img = np.zeros((120, 100), dtype=int) + radius = 20 + x_0, y_0 = (99, 50) + x, y = circle_perimeter(y_0, x_0, radius) + img[y, x] = 1 + + out = tf.hough_circle(img, np.array([radius])) + + x, y = np.where(out[0] == out[0].max()) + # Offset for x_0, y_0 + assert_equal(x[0], x_0 + radius) + assert_equal(y[0], y_0 + radius) + if __name__ == "__main__": run_module_suite() diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index b705ac47..93f87320 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -2,10 +2,10 @@ from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np from scipy.ndimage import map_coordinates -from skimage.transform import (warp, warp_coords, rotate, resize, +from skimage.transform import (warp, warp_coords, rotate, resize, rescale, AffineTransform, ProjectiveTransform, - SimilarityTransform, homography) + SimilarityTransform) from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray @@ -39,10 +39,6 @@ def test_homography(): assert_array_almost_equal(x90, np.rot90(x)) -def test_homography_basic(): - homography(np.random.random((25, 25)), np.eye(3)) - - def test_fast_homography(): img = rgb2gray(data.lena()).astype(np.uint8) img = img[:, :100] @@ -85,7 +81,36 @@ def test_rotate(): assert_array_almost_equal(x90, np.rot90(x)) -def test_resize(): +def test_rotate_resize(): + x = np.zeros((10, 10), dtype=np.double) + + x45 = rotate(x, 45, resize=False) + assert x45.shape == (10, 10) + + x45 = rotate(x, 45, resize=True) + # new dimension should be d = sqrt(2 * (10/2)^2) + assert x45.shape == (14, 14) + + +def test_rescale(): + # same scale factor + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 + scaled = rescale(x, 2, order=0) + ref = np.zeros((10, 10)) + ref[2:4, 2:4] = 1 + assert_array_almost_equal(scaled, ref) + + # different scale factors + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 + scaled = rescale(x, (2, 1), order=0) + ref = np.zeros((10, 5)) + ref[2:4, 1] = 1 + assert_array_almost_equal(scaled, ref) + + +def test_resize2d(): x = np.zeros((5, 5), dtype=np.double) x[1, 1] = 1 resized = resize(x, (10, 10), order=0) @@ -94,6 +119,42 @@ def test_resize(): assert_array_almost_equal(resized, ref) +def test_resize3d_keep(): + # keep 3rd dimension + x = np.zeros((5, 5, 3), dtype=np.double) + x[1, 1, :] = 1 + resized = resize(x, (10, 10), order=0) + ref = np.zeros((10, 10, 3)) + ref[2:4, 2:4, :] = 1 + assert_array_almost_equal(resized, ref) + resized = resize(x, (10, 10, 3), order=0) + assert_array_almost_equal(resized, ref) + + +def test_resize3d_resize(): + # resize 3rd dimension + x = np.zeros((5, 5, 3), dtype=np.double) + x[1, 1, :] = 1 + resized = resize(x, (10, 10, 1), order=0) + ref = np.zeros((10, 10, 1)) + ref[2:4, 2:4] = 1 + assert_array_almost_equal(resized, ref) + + +def test_resize3d_bilinear(): + # bilinear 3rd dimension + x = np.zeros((5, 5, 2), dtype=np.double) + x[1, 1, 0] = 0 + x[1, 1, 1] = 1 + resized = resize(x, (10, 10, 1), order=1) + ref = np.zeros((10, 10, 1)) + ref[1:5, 1:5, :] = 0.03125 + ref[1:5, 2:4, :] = 0.09375 + ref[2:4, 1:5, :] = 0.09375 + ref[2:4, 2:4, :] = 0.28125 + assert_array_almost_equal(resized, ref) + + def test_swirl(): image = img_as_float(data.checkerboard())