diff --git a/.coveragerc b/.coveragerc index f7ea687a..5a372d61 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,3 +7,7 @@ include = */skimage/* omit = */setup.py +[report] +exclude_lines = + def __repr__ + if __name__ == .__main__.: diff --git a/.travis.yml b/.travis.yml index 2972fd9d..0e038f4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,8 +63,12 @@ script: - "echo 'backend.qt4 : PyQt4' >> $HOME/.matplotlib/matplotlibrc" # Run all tests - - nosetests --exe -v --with-doctest --with-cov --cov skimage --cov-config .coveragerc skimage - + - if [[ $PYVER == '3.x' ]]; then + - nosetests --exe -v --with-doctest --with-cov --cov skimage --cov-config .coveragerc skimage + - fi + - if [[ $PYVER == '2.x' ]]; then + - nosetests --exe -v --with-doctest skimage + - fi # Run all doc examples - export PYTHONPATH=$(pwd):$PYTHONPATH - for f in doc/examples/*.py; do $PYTHON "$f"; if [ $? -ne 0 ]; then exit 1; fi done @@ -74,4 +78,7 @@ script: - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples after_success: - - coveralls + - if [[ $PYVER == '3.x' ]]; then + - coveralls + - fi + diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 39a2f988..140842ab 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -144,14 +144,14 @@ Color separation (color deconvolution) for several stainings. - Jostein Bø Fløystad - Reconstruction circle mode for Radon transform - Simultaneous Algebraic Reconstruction Technique for inverse Radon transform + Tomography: radon/iradon improvements and SART implementation + Phase unwrapping integration - Matt Terry Color difference functions - Eugene Dvoretsky - Yen threshold implementation. + Yen, Ridler-Calvard (ISODATA) threshold implementations. - Riaan van den Dool skimage.io plugin: GDAL @@ -161,3 +161,24 @@ - Michael Hansen novice submodule + +- Munther Gdeisat + Phase unwrapping implementation + +- Miguel Arevallilo Herraez + Phase unwrapping implementation + +- Hussein Abdul-Rahman + Phase unwrapping implementation + +- Gregor Thalhammer + Phase unwrapping integration + +- François Orieux + Image deconvolution http://research.orieux.fr + +- Vighnesh Birodkar + Blob Detection + +- Axel Donath + Blob Detection diff --git a/TODO.txt b/TODO.txt index b867bc73..10dfe117 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,15 @@ +Version 0.11 +------------ +* Remove deprecated `reverse_map` parameter of `skimage.transform.warp` +* Change depecrated `enforce_connectivity=False`on skimage.segmentation.slic + and set it to True as default +* Remove deprecated `skimage.measure.fit.BaseModel._params` attribute +* Remove deprecated `skimage.measure.fit.BaseModel._params`, + `skimage.transform.ProjectiveTransform._matrix`, + `skimage.transform.PolynomialTransform._params`, + `skimage.transform.PiecewiseAffineTransform.affines_*` attributes +* Remove deprecated functions `skimage.filter.denoise_*` + Version 0.10 ------------ * Remove deprecated functions in `skimage.filter.rank.*` @@ -14,3 +26,6 @@ Version 0.10 * Remove deprecated `skimage.color.is_gray` and `skimage.color.is_rgb` functions * Enable doctests of experimental `skimage.feature.brief` +* Remove deprecated `skimage.segmentation.visualize_boundaries` +* Remove deprecated `skimage.morphology.greyscale_*` +* Remove deprecated `skimage.exposure.equalize` diff --git a/bento.info b/bento.info index 84ddf4a8..45f57a36 100644 --- a/bento.info +++ b/bento.info @@ -35,13 +35,11 @@ Library: skimage, skimage.color, skimage.data, skimage.draw, skimage.exposure, skimage.feature, skimage.filter, skimage.graph, skimage.io, skimage.io._plugins, skimage.measure, skimage.morphology, - skimage.scripts, skimage.segmentation, skimage.transform, skimage.util + skimage.scripts, skimage.restoration, skimage.segmentation, + skimage.transform, skimage.util Extension: skimage.morphology._pnpoly Sources: skimage/morphology/_pnpoly.pyx - Extension: skimage.feature._template - Sources: - skimage/feature/_template.pyx Extension: skimage.io._plugins._colormixer Sources: skimage/io/_plugins/_colormixer.pyx @@ -66,9 +64,6 @@ 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 @@ -96,9 +91,12 @@ Library: Extension: skimage.feature.censure_cy Sources: skimage/feature/censure_cy.pyx - Extension: skimage.feature._brief_cy + Extension: skimage.feature.orb_cy Sources: - skimage/feature/_brief_cy.pyx + skimage/feature/orb_cy.pyx + Extension: skimage.feature.brief_cy + Sources: + skimage/feature/brief_cy.pyx Extension: skimage.feature.corner_cy Sources: skimage/feature/corner_cy.pyx @@ -123,9 +121,6 @@ Library: Extension: skimage.transform._warps_cy Sources: skimage/transform/_warps_cy.pyx - Extension: skimage._shared.interpolation - Sources: - skimage/_shared/interpolation.pyx Extension: skimage.segmentation._felzenszwalb_cy Sources: skimage/segmentation/_felzenszwalb_cy.pyx @@ -144,6 +139,18 @@ Library: Extension: skimage.filter.rank.bilateral_cy Sources: skimage/filter/rank/bilateral_cy.pyx + Extension: skimage.restoration._unwrap_1d + Sources: + skimage/restoration/_unwrap_1d.pyx + Extension: skimage.restoration._unwrap_2d + Sources: + skimage/restoration/_unwrap_2d.pyx skimage/exposure/unwrap_2d_ljmu.c + Extension: skimage.restoration._unwrap_3d + Sources: + skimage/restoration/_unwrap_3d.pyx skimage/exposure/unwrap_3d_ljmu.c + Extension: skimage.restoration._denoise_cy + Sources: + skimage/restoration/_denoise_cy.pyx Executable: skivi Module: skimage.scripts.skivi diff --git a/doc/examples/applications/plot_geometric.py b/doc/examples/applications/plot_geometric.py index 141ef51f..86dd5aaa 100644 --- a/doc/examples/applications/plot_geometric.py +++ b/doc/examples/applications/plot_geometric.py @@ -33,14 +33,14 @@ First we create a transformation using explicit parameters: tform = tf.SimilarityTransform(scale=1, rotation=math.pi / 2, translation=(0, 1)) -print(tform._matrix) +print(tform.params) """ Alternatively you can define a transformation by the transformation matrix itself: """ -matrix = tform._matrix.copy() +matrix = tform.params.copy() matrix[1, 2] = 2 tform2 = tf.SimilarityTransform(matrix) diff --git a/doc/examples/plot_brief.py b/doc/examples/plot_brief.py new file mode 100644 index 00000000..47c9ad5f --- /dev/null +++ b/doc/examples/plot_brief.py @@ -0,0 +1,61 @@ +""" +======================= +BRIEF binary descriptor +======================= + +This example demonstrates the BRIEF binary description algorithm. + +The descriptor consists of relatively few bits and can be computed using +a set of intensity difference tests. The short binary descriptor results +in low memory footprint and very efficient matching based on the Hamming +distance metric. + +BRIEF does not provide rotation-invariance. Scale-invariance can be achieved by +detecting and extracting features at different scales. + +""" +from skimage import data +from skimage import transform as tf +from skimage.feature import (match_descriptors, corner_peaks, corner_harris, + plot_matches, BRIEF) +from skimage.color import rgb2gray +import matplotlib.pyplot as plt + + +img1 = rgb2gray(data.lena()) +tform = tf.AffineTransform(scale=(1.2, 1.2), translation=(0, -100)) +img2 = tf.warp(img1, tform) +img3 = tf.rotate(img1, 25) + +keypoints1 = corner_peaks(corner_harris(img1), min_distance=5) +keypoints2 = corner_peaks(corner_harris(img2), min_distance=5) +keypoints3 = corner_peaks(corner_harris(img3), min_distance=5) + +extractor = BRIEF() + +extractor.extract(img1, keypoints1) +keypoints1 = keypoints1[extractor.mask] +descriptors1 = extractor.descriptors + +extractor.extract(img2, keypoints2) +keypoints2 = keypoints2[extractor.mask] +descriptors2 = extractor.descriptors + +extractor.extract(img3, keypoints3) +keypoints3 = keypoints3[extractor.mask] +descriptors3 = extractor.descriptors + +matches12 = match_descriptors(descriptors1, descriptors2, cross_check=True) +matches13 = match_descriptors(descriptors1, descriptors3, cross_check=True) + +fig, ax = plt.subplots(nrows=2, ncols=1) + +plt.gray() + +plot_matches(ax[0], img1, img2, keypoints1, keypoints2, matches12) +ax[0].axis('off') + +plot_matches(ax[1], img1, img3, keypoints1, keypoints3, matches13) +ax[1].axis('off') + +plt.show() diff --git a/doc/examples/plot_censure.py b/doc/examples/plot_censure.py new file mode 100644 index 00000000..c7d70ea5 --- /dev/null +++ b/doc/examples/plot_censure.py @@ -0,0 +1,43 @@ +""" +======================== +CENSURE feature detector +======================== + +The CENSURE feature detector is a scale-invariant center-surround detector +(CENSURE) that claims to outperform other detectors and is capable of real-time +implementation. + +""" +from skimage import data +from skimage import transform as tf +from skimage.feature import CENSURE +from skimage.color import rgb2gray +import matplotlib.pyplot as plt + + +img1 = rgb2gray(data.lena()) +tform = tf.AffineTransform(scale=(1.5, 1.5), rotation=0.5, + translation=(150, -200)) +img2 = tf.warp(img1, tform) + +detector = CENSURE() + +fig, ax = plt.subplots(nrows=1, ncols=2) + +plt.gray() + +detector.detect(img1) + +ax[0].imshow(img1) +ax[0].axis('off') +ax[0].scatter(detector.keypoints[:, 1], detector.keypoints[:, 0], + 2 ** detector.scales, facecolors='none', edgecolors='r') + +detector.detect(img2) + +ax[1].imshow(img2) +ax[1].axis('off') +ax[1].scatter(detector.keypoints[:, 1], detector.keypoints[:, 0], + 2 ** detector.scales, facecolors='none', edgecolors='r') + +plt.show() diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index 7fb67046..fbdd4f2c 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -48,7 +48,7 @@ from skimage.util import img_as_ubyte image = img_as_ubyte(data.coins()[0:95, 70:370]) edges = filter.canny(image, sigma=3, low_threshold=10, high_threshold=50) -fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) +fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(5, 2)) # Detect two radii hough_radii = np.arange(15, 30, 2) @@ -77,6 +77,8 @@ ax.imshow(image, cmap=plt.cm.gray) """ +.. image:: PLOT2RST.current_figure + Ellipse detection ================= @@ -137,7 +139,7 @@ image_rgb[cy, cx] = (0, 0, 255) edges = color.gray2rgb(edges) edges[cy, cx] = (250, 0, 0) -fig2, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(10, 6)) +fig2, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(8, 4)) ax1.set_title('Original picture') ax1.imshow(image_rgb) @@ -146,3 +148,8 @@ ax2.set_title('Edge (white) and result (red)') ax2.imshow(edges) plt.show() + +""" +.. image:: PLOT2RST.current_figure + +""" diff --git a/doc/examples/plot_denoise.py b/doc/examples/plot_denoise.py index 200036ae..3a05c99f 100644 --- a/doc/examples/plot_denoise.py +++ b/doc/examples/plot_denoise.py @@ -29,7 +29,7 @@ import numpy as np import matplotlib.pyplot as plt from skimage import data, img_as_float -from skimage.filter import denoise_tv_chambolle, denoise_bilateral +from skimage.restoration import denoise_tv_chambolle, denoise_bilateral lena = img_as_float(data.lena()) diff --git a/doc/examples/plot_equalize.py b/doc/examples/plot_equalize.py index 52121e3a..3b3f3788 100644 --- a/doc/examples/plot_equalize.py +++ b/doc/examples/plot_equalize.py @@ -17,6 +17,8 @@ that fall within the 2nd and 98th percentiles [2]_. .. [2] http://homepages.inf.ed.ac.uk/rbf/HIPR2/stretch.htm """ + +import matplotlib import matplotlib.pyplot as plt import numpy as np @@ -24,6 +26,9 @@ from skimage import data, img_as_float from skimage import exposure +matplotlib.rcParams['font.size'] = 8 + + def plot_img_and_hist(img, axes, bins=256): """Plot an image along with its histogram and cumulative histogram. @@ -55,8 +60,7 @@ def plot_img_and_hist(img, axes, bins=256): img = data.moon() # Contrast stretching -p2 = np.percentile(img, 2) -p98 = np.percentile(img, 98) +p2, p98 = np.percentile(img, (2, 98)) img_rescale = exposure.rescale_intensity(img, in_range=(p2, p98)) # Equalization @@ -66,7 +70,7 @@ img_eq = exposure.equalize_hist(img) img_adapteq = exposure.equalize_adapthist(img, clip_limit=0.03) # Display results -f, axes = plt.subplots(2, 4, figsize=(8, 4)) +f, axes = plt.subplots(nrows=2, ncols=4, figsize=(8, 5)) ax_img, ax_hist, ax_cdf = plot_img_and_hist(img, axes[:, 0]) ax_img.set_title('Low contrast image') diff --git a/doc/examples/plot_gabor.py b/doc/examples/plot_gabor.py index db1ff7d6..87d99589 100644 --- a/doc/examples/plot_gabor.py +++ b/doc/examples/plot_gabor.py @@ -24,9 +24,6 @@ from skimage.util import img_as_float from skimage.filter import gabor_kernel -matplotlib.rcParams['font.size'] = 9 - - def compute_feats(image, kernels): feats = np.zeros((len(kernels), 2), dtype=np.double) for k, kernel in enumerate(kernels): @@ -104,24 +101,24 @@ for theta in (0, 1): # Save kernel and the power image for each image results.append((kernel, [power(img, kernel) for img in images])) -fig, axes = plt.subplots(nrows=5, ncols=4, figsize=(9, 6)) +fig, axes = plt.subplots(nrows=5, ncols=4, figsize=(5, 6)) plt.gray() -fig.suptitle('Image responses for Gabor filter kernels', fontsize=15) +fig.suptitle('Image responses for Gabor filter kernels', fontsize=12) axes[0][0].axis('off') # Plot original images for label, img, ax in zip(image_names, images, axes[0][1:]): ax.imshow(img) - ax.set_title(label) + ax.set_title(label, fontsize=9) ax.axis('off') for label, (kernel, powers), ax_row in zip(kernel_params, results, axes[1:]): # Plot Gabor kernel ax = ax_row[0] ax.imshow(np.real(kernel), interpolation='nearest') - ax.set_ylabel(label) + ax.set_ylabel(label, fontsize=7) ax.set_xticks([]) ax.set_yticks([]) diff --git a/doc/examples/plot_label.py b/doc/examples/plot_label.py index 0542d68c..6e5773dd 100644 --- a/doc/examples/plot_label.py +++ b/doc/examples/plot_label.py @@ -43,7 +43,7 @@ image_label_overlay = label2rgb(label_image, image=image) fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) ax.imshow(image_label_overlay) -for region in regionprops(label_image, ['Area', 'BoundingBox']): +for region in regionprops(label_image): # skip small images if region['Area'] < 100: diff --git a/doc/examples/plot_local_equalize.py b/doc/examples/plot_local_equalize.py index 1fd8325f..309d1e43 100644 --- a/doc/examples/plot_local_equalize.py +++ b/doc/examples/plot_local_equalize.py @@ -20,6 +20,7 @@ References """ import numpy as np +import matplotlib import matplotlib.pyplot as plt from skimage import data @@ -30,6 +31,9 @@ from skimage.morphology import disk from skimage.filter import rank +matplotlib.rcParams['font.size'] = 9 + + def plot_img_and_hist(img, axes, bins=256): """Plot an image along with its histogram and cumulative histogram. @@ -59,9 +63,7 @@ def plot_img_and_hist(img, axes, bins=256): # Load an example image img = img_as_ubyte(data.moon()) -# Contrast stretching -p2 = np.percentile(img, 2) -p98 = np.percentile(img, 98) +# Global equalize img_rescale = exposure.equalize_hist(img) # Equalization @@ -70,7 +72,7 @@ img_eq = rank.equalize(img, selem=selem) # Display results -f, axes = plt.subplots(2, 3, figsize=(8, 4)) +f, axes = plt.subplots(2, 3, figsize=(8, 5)) ax_img, ax_hist, ax_cdf = plot_img_and_hist(img, axes[:, 0]) ax_img.set_title('Low contrast image') diff --git a/doc/examples/plot_local_otsu.py b/doc/examples/plot_local_otsu.py index 3a321780..aff7db62 100644 --- a/doc/examples/plot_local_otsu.py +++ b/doc/examples/plot_local_otsu.py @@ -15,6 +15,7 @@ The example compares the local threshold with the global threshold. .. [1] http://en.wikipedia.org/wiki/Otsu's_method """ +import matplotlib import matplotlib.pyplot as plt from skimage import data @@ -23,29 +24,41 @@ from skimage.filter import threshold_otsu, rank from skimage.util import img_as_ubyte -p8 = img_as_ubyte(data.page()) +matplotlib.rcParams['font.size'] = 9 -radius = 10 + +img = img_as_ubyte(data.page()) + +radius = 15 selem = disk(radius) -loc_otsu = rank.otsu(p8, selem) -t_glob_otsu = threshold_otsu(p8) -glob_otsu = p8 >= t_glob_otsu +local_otsu = rank.otsu(img, selem) +threshold_global_otsu = threshold_otsu(img) +global_otsu = img >= threshold_global_otsu -plt.figure() +plt.figure(figsize=(8, 5)) + plt.subplot(2, 2, 1) -plt.imshow(p8, cmap=plt.cm.gray) -plt.xlabel('original') -plt.colorbar() +plt.imshow(img, cmap=plt.cm.gray) +plt.title('Original') +plt.colorbar(orientation='horizontal') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(loc_otsu, cmap=plt.cm.gray) -plt.xlabel('local Otsu ($radius=%d$)' % radius) -plt.colorbar() +plt.imshow(local_otsu, cmap=plt.cm.gray) +plt.title('Local Otsu (radius=%d)' % radius) +plt.colorbar(orientation='horizontal') +plt.axis('off') + plt.subplot(2, 2, 3) -plt.imshow(p8 >= loc_otsu, cmap=plt.cm.gray) -plt.xlabel('original >= local Otsu' % t_glob_otsu) +plt.imshow(img >= local_otsu, cmap=plt.cm.gray) +plt.title('Original >= Local Otsu' % threshold_global_otsu) +plt.axis('off') + plt.subplot(2, 2, 4) -plt.imshow(glob_otsu, cmap=plt.cm.gray) -plt.xlabel('global Otsu ($t = %d$)' % t_glob_otsu) +plt.imshow(global_otsu, cmap=plt.cm.gray) +plt.title('Global Otsu (threshold = %d)' % threshold_global_otsu) +plt.axis('off') + plt.show() diff --git a/doc/examples/plot_matching.py b/doc/examples/plot_matching.py index bae6d6e2..5812fa37 100644 --- a/doc/examples/plot_matching.py +++ b/doc/examples/plot_matching.py @@ -27,7 +27,8 @@ from matplotlib import pyplot as plt from skimage import data from skimage.util import img_as_float -from skimage.feature import corner_harris, corner_subpix, corner_peaks +from skimage.feature import (corner_harris, corner_subpix, corner_peaks, + plot_matches) from skimage.transform import warp, AffineTransform from skimage.exposure import rescale_intensity from skimage.color import rgb2gray @@ -117,28 +118,21 @@ print(tform.scale, tform.translation, tform.rotation) print(model.scale, model.translation, model.rotation) print(model_robust.scale, model_robust.translation, model_robust.rotation) - -# visualize correspondences -img_combined = np.concatenate((img_orig_gray, img_warped_gray), axis=1) - +# visualize correspondence fig, ax = plt.subplots(nrows=2, ncols=1) + plt.gray() -ax[0].imshow(img_combined, interpolation='nearest') +inlier_idxs = np.nonzero(inliers)[0] +plot_matches(ax[0], img_orig_gray, img_warped_gray, src, dst, + np.column_stack((inlier_idxs, inlier_idxs)), matches_color='b') ax[0].axis('off') -ax[0].axis((0, 400, 200, 0)) ax[0].set_title('Correct correspondences') -ax[1].imshow(img_combined, interpolation='nearest') + +outlier_idxs = np.nonzero(outliers)[0] +plot_matches(ax[1], img_orig_gray, img_warped_gray, src, dst, + np.column_stack((outlier_idxs, outlier_idxs)), matches_color='r') ax[1].axis('off') -ax[1].axis((0, 400, 200, 0)) ax[1].set_title('Faulty correspondences') - -for ax_idx, (m, color) in enumerate(((inliers, 'g'), (outliers, 'r'))): - ax[ax_idx].plot((src[m, 1], dst[m, 1] + 200), (src[m, 0], dst[m, 0]), '-', - color=color) - ax[ax_idx].plot(src[m, 1], src[m, 0], '.', markersize=10, color=color) - ax[ax_idx].plot(dst[m, 1] + 200, dst[m, 0], '.', markersize=10, - color=color) - plt.show() diff --git a/doc/examples/plot_orb.py b/doc/examples/plot_orb.py new file mode 100644 index 00000000..1a73fc7f --- /dev/null +++ b/doc/examples/plot_orb.py @@ -0,0 +1,56 @@ +""" +========================================== +ORB feature detector and binary descriptor +========================================== + +This example demonstrates the ORB feature detection and binary description +algorithm. It uses an oriented FAST detection method and the rotated BRIEF +descriptors. + +Unlike BRIEF, ORB is comparatively scale- and rotation-invariant while still +employing the very efficient Hamming distance metric for matching. As such, it +is preferred for real-time applications. + +""" +from skimage import data +from skimage import transform as tf +from skimage.feature import (match_descriptors, corner_harris, + corner_peaks, ORB, plot_matches) +from skimage.color import rgb2gray +import matplotlib.pyplot as plt + + +img1 = rgb2gray(data.lena()) +img2 = tf.rotate(img1, 180) +tform = tf.AffineTransform(scale=(1.3, 1.1), rotation=0.5, + translation=(0, -200)) +img3 = tf.warp(img1, tform) + +descriptor_extractor = ORB(n_keypoints=200) + +descriptor_extractor.detect_and_extract(img1) +keypoints1 = descriptor_extractor.keypoints +descriptors1 = descriptor_extractor.descriptors + +descriptor_extractor.detect_and_extract(img2) +keypoints2 = descriptor_extractor.keypoints +descriptors2 = descriptor_extractor.descriptors + +descriptor_extractor.detect_and_extract(img3) +keypoints3 = descriptor_extractor.keypoints +descriptors3 = descriptor_extractor.descriptors + +matches12 = match_descriptors(descriptors1, descriptors2, cross_check=True) +matches13 = match_descriptors(descriptors1, descriptors3, cross_check=True) + +fig, ax = plt.subplots(nrows=2, ncols=1) + +plt.gray() + +plot_matches(ax[0], img1, img2, keypoints1, keypoints2, matches12) +ax[0].axis('off') + +plot_matches(ax[1], img1, img3, keypoints1, keypoints3, matches13) +ax[1].axis('off') + +plt.show() diff --git a/doc/examples/plot_otsu.py b/doc/examples/plot_otsu.py index 5221e934..b3b8e279 100644 --- a/doc/examples/plot_otsu.py +++ b/doc/examples/plot_otsu.py @@ -14,12 +14,16 @@ the intra-class variance. .. [1] http://en.wikipedia.org/wiki/Otsu's_method """ +import matplotlib import matplotlib.pyplot as plt from skimage.data import camera from skimage.filter import threshold_otsu +matplotlib.rcParams['font.size'] = 9 + + image = camera() thresh = threshold_otsu(image) binary = image > thresh diff --git a/doc/examples/plot_phase_unwrap.py b/doc/examples/plot_phase_unwrap.py new file mode 100644 index 00000000..22e3bc89 --- /dev/null +++ b/doc/examples/plot_phase_unwrap.py @@ -0,0 +1,116 @@ +""" +================ +Phase Unwrapping +================ + +Some signals can only be observed modulo 2*pi, and this can also apply to +two- and three dimensional images. In these cases phase unwrapping is +needed to recover the underlying, unwrapped signal. In this example we will +demonstrate an algorithm [1]_ implemented in ``skimage`` at work for such a +problem. One-, two- and three dimensional images can all be unwrapped using +skimage. Here we will demonstrate phase unwrapping in the two dimensional case. +""" + +import numpy as np +from matplotlib import pyplot as plt +from skimage import data, img_as_float, color, exposure +from skimage.restoration import unwrap_phase + + +# Load an image as a floating-point grayscale +image = color.rgb2gray(img_as_float(data.chelsea())) +# Scale the image to [0, 4*pi] +image = exposure.rescale_intensity(image, out_range=(0, 4 * np.pi)) +# Create a phase-wrapped image in the interval [-pi, pi) +image_wrapped = np.angle(np.exp(1j * image)) +# Perform phase unwrapping +image_unwrapped = unwrap_phase(image_wrapped) + +plt.figure() +plt.subplot(221) +plt.title('Original') +plt.imshow(image, cmap='gray', vmin=0, vmax=4 * np.pi) +plt.colorbar() + +plt.subplot(222) +plt.title('Wrapped phase') +plt.imshow(image_wrapped, cmap='gray', vmin=-np.pi, vmax=np.pi) +plt.colorbar() + +plt.subplot(223) +plt.title('After phase unwrapping') +plt.imshow(image_unwrapped, cmap='gray') +plt.colorbar() + +plt.subplot(224) +plt.title('Unwrapped minus original') +plt.imshow(image_unwrapped - image, cmap='gray') +plt.colorbar() + +""" +.. image:: PLOT2RST.current_figure + +The unwrapping procedure accepts masked arrays, and can also optionally +assume cyclic boundaries to connect edges of an image. In the example below, +we study a simple phase ramp which has been split in two by masking +a row of the image. +""" + +# Create a simple ramp +image = np.ones((100, 100)) * np.linspace(0, 8 * np.pi, 100).reshape((-1, 1)) +# Mask the image to split it in two horizontally +mask = np.zeros_like(image, dtype=np.bool) +mask[image.shape[0] // 2, :] = True + +image_wrapped = np.ma.array(np.angle(np.exp(1j * image)), mask=mask) +# Unwrap image without wrap around +image_unwrapped_no_wrap_around = unwrap_phase(image_wrapped, + wrap_around=(False, False)) +# Unwrap with wrap around enabled for the 0th dimension +image_unwrapped_wrap_around = unwrap_phase(image_wrapped, + wrap_around=(True, False)) + +plt.figure() +plt.subplot(221) +plt.title('Original') +plt.imshow(np.ma.array(image, mask=mask), cmap='jet') +plt.colorbar() + +plt.subplot(222) +plt.title('Wrapped phase') +plt.imshow(image_wrapped, cmap='jet', vmin=-np.pi, vmax=np.pi) +plt.colorbar() + +plt.subplot(223) +plt.title('Unwrapped without wrap_around') +plt.imshow(image_unwrapped_no_wrap_around, cmap='jet') +plt.colorbar() + +plt.subplot(224) +plt.title('Unwrapped with wrap_around') +plt.imshow(image_unwrapped_wrap_around, cmap='jet') +plt.colorbar() + +plt.show() + +""" +.. image:: PLOT2RST.current_figure + +In the figures above, the masked row can be seen as a white line across +the image. The difference between the two unwrapped images in the bottom row +is clear: Without unwrapping (lower left), the regions above and below the +masked boundary do not interact at all, resulting in an offset between the +two regions of an arbitrary integer times two pi. We could just as well have +unwrapped the regions as two separate images. With wrap around enabled for the +vertical direction (lower rigth), the situation changes: Unwrapping paths are +now allowed to pass from the bottom to the top of the image and vice versa, in +effect providing a way to determine the offset between the two regions. + +References +---------- + +.. [1] Miguel Arevallilo Herraez, David R. Burton, Michael J. Lalor, + and Munther A. Gdeisat, "Fast two-dimensional phase-unwrapping + algorithm based on sorting by reliability following a noncontinuous + path", Journal Applied Optics, Vol. 41, No. 35, pp. 7437, 2002 +""" diff --git a/doc/examples/plot_restoration.py b/doc/examples/plot_restoration.py new file mode 100644 index 00000000..fdffd953 --- /dev/null +++ b/doc/examples/plot_restoration.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +""" +===================== +Deconvolution of Lena +===================== + +In this example, we deconvolve a noisy version of Lena using Wiener +and unsupervised Wiener algorithms. This algorithms are based on +linear models that can't restore sharp edge as much as non-linear +methods (like TV restoration) but are much faster. + +Wiener filter +------------- +The inverse filter based on the PSF (Point Spread Function), +the prior regularisation (penalisation of high frequency) and the +tradeoff between the data and prior adequacy. The regularization +parameter must be hand tuned. + +Unsupervised Wiener +------------------- +This algorithm has a self-tuned regularisation parameters based on +data learning. This is not common and based on the following +publication. The algorithm is based on a iterative Gibbs sampler that +draw alternatively samples of posterior conditionnal law of the image, +the noise power and the image frequency power. + +.. [1] François Orieux, Jean-François Giovannelli, and Thomas + Rodet, "Bayesian estimation of regularization and point + spread function parameters for Wiener-Hunt deconvolution", + J. Opt. Soc. Am. A 27, 1593-1607 (2010) +""" +import numpy as np +import matplotlib.pyplot as plt + +from skimage import color, data, restoration + +lena = color.rgb2gray(data.lena()) +from scipy.signal import convolve2d as conv2 +psf = np.ones((5, 5)) / 25 +lena = conv2(lena, psf, 'same') +lena += 0.1 * lena.std() * np.random.standard_normal(lena.shape) + +deconvolved, _ = restoration.unsupervised_wiener(lena, psf) + +fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(8, 5)) + +plt.gray() + +ax[0].imshow(lena, vmin=deconvolved.min(), vmax=deconvolved.max()) +ax[0].axis('off') +ax[0].set_title('Data') + +ax[1].imshow(deconvolved) +ax[1].axis('off') +ax[1].set_title('Self tuned restoration') + +fig.subplots_adjust(wspace=0.02, hspace=0.2, + top=0.9, bottom=0.05, left=0, right=1) + +plt.show() diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 2ae842a5..34d1108c 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -4,11 +4,17 @@ Shapes ====== This example shows how to draw several different shapes: -* line -* Bezier curve -* polygon -* circle -* ellipse + + - line + - Bezier curve + - polygon + - circle + - ellipse + +Anti-aliased drawing for: + + - line + - circle """ import math @@ -69,13 +75,6 @@ ax1.imshow(img) ax1.set_title('No anti-aliasing') ax1.axis('off') -""" - -Anti-aliased drawing for: -* line -* circle - -""" from skimage.draw import line_aa, circle_perimeter_aa diff --git a/doc/examples/plot_ssim.py b/doc/examples/plot_ssim.py index 9fe0c930..4e657150 100644 --- a/doc/examples/plot_ssim.py +++ b/doc/examples/plot_ssim.py @@ -22,12 +22,16 @@ but with very different mean structural similarity indices. """ import numpy as np +import matplotlib import matplotlib.pyplot as plt from skimage import data, img_as_float from skimage.measure import structural_similarity as ssim +matplotlib.rcParams['font.size'] = 9 + + img = img_as_float(data.camera()) rows, cols = img.shape @@ -41,7 +45,7 @@ def mse(x, y): img_noise = img + noise img_const = img + abs(noise) -f, (ax0, ax1, ax2) = plt.subplots(1, 3) +f, (ax0, ax1, ax2) = plt.subplots(nrows=1, ncols=3, figsize=(8, 4)) mse_none = mse(img, img) ssim_none = ssim(img, img, dynamic_range=img.max() - img.min()) diff --git a/doc/gh-pages.py b/doc/gh-pages.py old mode 100644 new mode 100755 index 8fe4ee01..75c8d9e3 --- a/doc/gh-pages.py +++ b/doc/gh-pages.py @@ -125,7 +125,7 @@ if __name__ == '__main__': sh("touch .nojekyll") sh('git add .nojekyll') sh('git add index.html') - sh('git add %s' % tag) + sh('git add --all %s' % tag) sh2('git commit -m"Updated doc release: %s"' % tag) print('Most recent commit:') diff --git a/doc/source/user_guide/data_types.txt b/doc/source/user_guide/data_types.txt index 43ea8e9b..258314af 100644 --- a/doc/source/user_guide/data_types.txt +++ b/doc/source/user_guide/data_types.txt @@ -134,7 +134,11 @@ dtype range:: Here, the ``in_range`` argument is set to the maximum range for a 10-bit image. By default, ``rescale_intensity`` stretches the values of ``in_range`` to match -the range of the dtype. +the range of the dtype. ``rescale_intensity`` also accepts strings as inputs +to ``in_range`` and ``out_range``, so the example above could also be written +as:: + + >>> image = exposure.rescale_intensity(img10bit, in_range='uint10') Note about negative values diff --git a/doc/source/user_guide/viewer.txt b/doc/source/user_guide/viewer.txt index 82bb5949..da7f0c50 100644 --- a/doc/source/user_guide/viewer.txt +++ b/doc/source/user_guide/viewer.txt @@ -24,26 +24,26 @@ alternatively, ``skimage.io.imshow`` which adds support for multiple io-plugins) to display images. The advantage of ``ImageViewer`` is that you can easily add plugins for manipulating images. Currently, only a few plugins are implemented, but it is easy to write your own. Before going into the details, -let's see an example of how a plugin is added to the viewer: +let's see an example of how a pre-defined plugin is added to the viewer: .. code-block:: python - from skimage.viewer.plugins import Canny + from skimage.viewer.plugins.lineprofile import LineProfile viewer = ImageViewer(image) - viewer += Canny(view) + viewer += LineProfile(viewer) viewer.show() -At the moment, there aren't very many plugins pre-defined, but there's a really -simple interface for creating your own plugin. First, let's create a plugin to -call the total-variation denoising function, ``tv_denoise``: +At the moment, there are not many plugins pre-defined, but there is a really +simple interface for creating your own plugin. First, let us create a plugin to +call the total-variation denoising function, ``denoise_tv_bregman``: .. code-block:: python - from skimage.filter import tv_denoise + from skimage.filter import denoise_tv_bregman from skimage.viewer.plugins.base import Plugin - denoise_plugin = Plugin(image_filter=tv_denoise) + denoise_plugin = Plugin(image_filter=denoise_tv_bregman) .. note:: diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 8c1ecb54..6a476c21 --- a/setup.py +++ b/setup.py @@ -21,10 +21,14 @@ VERSION = '0.10dev' PYTHON_VERSION = (2, 5) DEPENDENCIES = { 'numpy': (1, 6), - 'Cython': (0, 17), 'six': (1, 3), } +# Only require Cython if we have a developer checkout +if VERSION.endswith('dev'): + DEPENDENCIES['Cython'] = (0, 17) + + import os import sys diff --git a/skimage/__init__.py b/skimage/__init__.py index caec81fe..14dcf341 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -29,12 +29,16 @@ measure Measurement of image properties, e.g., similarity and contours. morphology Morphological operations, e.g. opening or skeletonization. +restoration + Restoration algorithms. segmentation Splitting an image into self-similar regions. transform Geometric and other transforms, e.g. rotation or the Radon transform. util Generic utilities. +viewer + Interactive image viewer and plugin framework. Utility Functions ----------------- diff --git a/skimage/_build.py b/skimage/_build.py index 38239e4b..b2ea94fc 100644 --- a/skimage/_build.py +++ b/skimage/_build.py @@ -3,7 +3,6 @@ import os import hashlib import subprocess - # WindowsError is not defined on unix systems try: WindowsError @@ -26,7 +25,7 @@ def cython(pyx_files, working_path=''): return try: - import Cython + from Cython.Build import cythonize except ImportError: # If cython is not found, we do nothing -- the build will make use of # the distributed .c files @@ -39,24 +38,7 @@ def cython(pyx_files, working_path=''): if not _changed(pyxfile): continue - c_file = pyxfile[:-4] + '.c' - - # run cython compiler - cmd = 'cython -o %s %s' % (c_file, pyxfile) - print(cmd) - - try: - subprocess.call(['cython', '-o', c_file, pyxfile]) - except WindowsError: - # On Windows cython.exe may be missing if Cython was installed - # via distutils. Run the cython.py script instead. - subprocess.call( - [sys.executable, - os.path.join(os.path.dirname(sys.executable), - 'Scripts', 'cython.py'), - '-o', c_file, pyxfile], - shell=True) - + cythonize(pyxfile) def _md5sum(f): m = hashlib.new('md5') @@ -86,4 +68,4 @@ def _changed(filename): with open(filename_cache, 'wb') as cf: cf.write(md5_new.encode('utf-8')) - return md5_cached != md5_new + return md5_cached != md5_new.encode('utf-8') diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd index c5f32b6a..a8b96014 100644 --- a/skimage/_shared/interpolation.pxd +++ b/skimage/_shared/interpolation.pxd @@ -1,27 +1,331 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +from libc.math cimport ceil, floor -cdef double nearest_neighbour_interpolation(double* image, Py_ssize_t rows, - Py_ssize_t cols, double r, - double c, char mode, - double cval) -cdef double bilinear_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, - double r, double c, char mode, - double cval) +cdef inline Py_ssize_t round(double r): + return ((r + 0.5) if (r > 0.0) else (r - 0.5)) -cdef double quadratic_interpolation(double x, double[3] f) -cdef double biquadratic_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, - double r, double c, char mode, - double cval) -cdef double cubic_interpolation(double x, double[4] f) -cdef double bicubic_interpolation(double* image, Py_ssize_t rows, Py_ssize_t cols, - double r, double c, char mode, - double cval) +cdef inline double nearest_neighbour_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, + double c, char mode, + double cval): + """Nearest neighbour interpolation at a given position in the image. -cdef double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, Py_ssize_t r, - Py_ssize_t c, char mode, double cval) + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. -cdef double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, Py_ssize_t dims, - Py_ssize_t r, Py_ssize_t c, Py_ssize_t d, char mode, double cval) + Returns + ------- + value : double + Interpolated value. -cdef Py_ssize_t coord_map(Py_ssize_t dim, Py_ssize_t coord, char mode) + """ + + return get_pixel2d(image, rows, cols, round(r), round(c), mode, cval) + + +cdef inline double bilinear_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, + char mode, double cval): + """Bilinear interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + cdef double dr, dc + cdef Py_ssize_t minr, minc, maxr, maxc + + minr = floor(r) + minc = floor(c) + maxr = ceil(r) + maxc = ceil(c) + dr = r - minr + dc = c - minc + top = (1 - dc) * get_pixel2d(image, rows, cols, minr, minc, mode, cval) \ + + dc * get_pixel2d(image, rows, cols, minr, maxc, mode, cval) + bottom = (1 - dc) * get_pixel2d(image, rows, cols, maxr, minc, mode, cval) \ + + dc * get_pixel2d(image, rows, cols, maxr, maxc, mode, cval) + return (1 - dr) * top + dr * bottom + + +cdef inline double quadratic_interpolation(double x, double[3] f): + """Quadratic interpolation. + + Parameters + ---------- + x : double + Position in the interval [-1, 1]. + f : double[4] + Function values at positions [-1, 0, 1]. + + Returns + ------- + value : double + Interpolated value. + + """ + return f[1] - 0.25 * (f[0] - f[2]) * x + + +cdef inline double biquadratic_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, + char mode, double cval): + """Biquadratic interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + + cdef Py_ssize_t r0 = round(r) + cdef Py_ssize_t c0 = round(c) + if r < 0: + r0 -= 1 + if c < 0: + c0 -= 1 + # scale position to range [-1, 1] + cdef double xr = (r - r0) - 1 + cdef double xc = (c - c0) - 1 + if r == r0: + xr += 1 + if c == c0: + xc += 1 + + cdef double fc[3], fr[3] + + cdef Py_ssize_t pr, pc + + # row-wise cubic interpolation + for pr in range(r0, r0 + 3): + for pc in range(c0, c0 + 3): + fc[pc - c0] = get_pixel2d(image, rows, cols, pr, pc, mode, cval) + fr[pr - r0] = quadratic_interpolation(xc, fc) + + # cubic interpolation for interpolated values of each row + return quadratic_interpolation(xr, fr) + + +cdef inline double cubic_interpolation(double x, double[4] f): + """Cubic interpolation. + + Parameters + ---------- + x : double + Position in the interval [0, 1]. + f : double[4] + Function values at positions [0, 1/3, 2/3, 1]. + + Returns + ------- + value : double + Interpolated value. + + """ + return \ + f[1] + 0.5 * x * \ + (f[2] - f[0] + x * \ + (2.0 * f[0] - 5.0 * f[1] + 4.0 * f[2] - f[3] + x * \ + (3.0 * (f[1] - f[2]) + f[3] - f[0]))) + + +cdef inline double bicubic_interpolation(double* image, Py_ssize_t rows, + Py_ssize_t cols, double r, double c, + char mode, double cval): + """Bicubic interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : double + Position at which to interpolate. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + + cdef Py_ssize_t r0 = r - 1 + cdef Py_ssize_t c0 = c - 1 + if r < 0: + r0 -= 1 + if c < 0: + c0 -= 1 + # scale position to range [0, 1] + cdef double xr = (r - r0) / 3 + cdef double xc = (c - c0) / 3 + + cdef double fc[4], fr[4] + + cdef Py_ssize_t pr, pc + + # row-wise cubic interpolation + for pr in range(r0, r0 + 4): + for pc in range(c0, c0 + 4): + fc[pc - c0] = get_pixel2d(image, rows, cols, pr, pc, mode, cval) + fr[pr - r0] = cubic_interpolation(xc, fc) + + # cubic interpolation for interpolated values of each row + return cubic_interpolation(xr, fr) + + +cdef inline double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t c, char mode, double cval): + """Get a pixel from the image, taking wrapping mode into consideration. + + Parameters + ---------- + image : double array + Input image. + rows, cols : int + Shape of image. + r, c : int + Position at which to get the pixel. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Pixel value at given position. + + """ + if mode == 'C': + if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): + return cval + else: + return image[r * cols + c] + else: + return image[coord_map(rows, r, mode) * cols + coord_map(cols, c, mode)] + + +cdef inline double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t dims, Py_ssize_t r, Py_ssize_t c, Py_ssize_t d, + char mode, double cval): + """Get a pixel from the image, taking wrapping mode into consideration. + + Parameters + ---------- + image : double array + Input image. + rows, cols, dims : int + Shape of image. + r, c, d : int + Position at which to get the pixel. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Pixel value at given position. + + """ + if mode == 'C': + if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): + return cval + else: + return image[r * cols * dims + c * dims + d] + else: + return image[coord_map(rows, r, mode) * cols * dims + + coord_map(cols, c, mode) * dims + + d] + + +cdef inline Py_ssize_t coord_map(Py_ssize_t dim, Py_ssize_t coord, char mode): + """ + Wrap a coordinate, according to a given mode. + + Parameters + ---------- + dim : int + Maximum coordinate. + coord : int + Coord provided by user. May be < 0 or > dim. + mode : {'W', 'R', 'N'} + Whether to wrap or reflect the coordinate if it + falls outside [0, dim). + + """ + dim = dim - 1 + if mode == 'R': # reflect + if coord < 0: + # How many times times does the coordinate wrap? + if (-coord / dim) % 2 != 0: + return dim - (-coord % dim) + else: + return (-coord % dim) + elif coord > dim: + if (coord / dim) % 2 != 0: + return (dim - (coord % dim)) + else: + return (coord % dim) + elif mode == 'W': # wrap + if coord < 0: + return (dim - (-coord % dim)) + elif coord > dim: + return (coord % dim) + elif mode == 'N': # nearest + if coord < 0: + return 0 + elif coord > dim: + return dim + + return coord diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx deleted file mode 100644 index a8b96014..00000000 --- a/skimage/_shared/interpolation.pyx +++ /dev/null @@ -1,331 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False -from libc.math cimport ceil, floor - - -cdef inline Py_ssize_t round(double r): - return ((r + 0.5) if (r > 0.0) else (r - 0.5)) - - -cdef inline double nearest_neighbour_interpolation(double* image, Py_ssize_t rows, - Py_ssize_t cols, double r, - double c, char mode, - double cval): - """Nearest neighbour interpolation at a given position in the image. - - Parameters - ---------- - image : double array - Input image. - rows, cols : int - Shape of image. - r, c : double - Position at which to interpolate. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Interpolated value. - - """ - - return get_pixel2d(image, rows, cols, round(r), round(c), mode, cval) - - -cdef inline double bilinear_interpolation(double* image, Py_ssize_t rows, - Py_ssize_t cols, double r, double c, - char mode, double cval): - """Bilinear interpolation at a given position in the image. - - Parameters - ---------- - image : double array - Input image. - rows, cols : int - Shape of image. - r, c : double - Position at which to interpolate. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Interpolated value. - - """ - cdef double dr, dc - cdef Py_ssize_t minr, minc, maxr, maxc - - minr = floor(r) - minc = floor(c) - maxr = ceil(r) - maxc = ceil(c) - dr = r - minr - dc = c - minc - top = (1 - dc) * get_pixel2d(image, rows, cols, minr, minc, mode, cval) \ - + dc * get_pixel2d(image, rows, cols, minr, maxc, mode, cval) - bottom = (1 - dc) * get_pixel2d(image, rows, cols, maxr, minc, mode, cval) \ - + dc * get_pixel2d(image, rows, cols, maxr, maxc, mode, cval) - return (1 - dr) * top + dr * bottom - - -cdef inline double quadratic_interpolation(double x, double[3] f): - """Quadratic interpolation. - - Parameters - ---------- - x : double - Position in the interval [-1, 1]. - f : double[4] - Function values at positions [-1, 0, 1]. - - Returns - ------- - value : double - Interpolated value. - - """ - return f[1] - 0.25 * (f[0] - f[2]) * x - - -cdef inline double biquadratic_interpolation(double* image, Py_ssize_t rows, - Py_ssize_t cols, double r, double c, - char mode, double cval): - """Biquadratic interpolation at a given position in the image. - - Parameters - ---------- - image : double array - Input image. - rows, cols : int - Shape of image. - r, c : double - Position at which to interpolate. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Interpolated value. - - """ - - cdef Py_ssize_t r0 = round(r) - cdef Py_ssize_t c0 = round(c) - if r < 0: - r0 -= 1 - if c < 0: - c0 -= 1 - # scale position to range [-1, 1] - cdef double xr = (r - r0) - 1 - cdef double xc = (c - c0) - 1 - if r == r0: - xr += 1 - if c == c0: - xc += 1 - - cdef double fc[3], fr[3] - - cdef Py_ssize_t pr, pc - - # row-wise cubic interpolation - for pr in range(r0, r0 + 3): - for pc in range(c0, c0 + 3): - fc[pc - c0] = get_pixel2d(image, rows, cols, pr, pc, mode, cval) - fr[pr - r0] = quadratic_interpolation(xc, fc) - - # cubic interpolation for interpolated values of each row - return quadratic_interpolation(xr, fr) - - -cdef inline double cubic_interpolation(double x, double[4] f): - """Cubic interpolation. - - Parameters - ---------- - x : double - Position in the interval [0, 1]. - f : double[4] - Function values at positions [0, 1/3, 2/3, 1]. - - Returns - ------- - value : double - Interpolated value. - - """ - return \ - f[1] + 0.5 * x * \ - (f[2] - f[0] + x * \ - (2.0 * f[0] - 5.0 * f[1] + 4.0 * f[2] - f[3] + x * \ - (3.0 * (f[1] - f[2]) + f[3] - f[0]))) - - -cdef inline double bicubic_interpolation(double* image, Py_ssize_t rows, - Py_ssize_t cols, double r, double c, - char mode, double cval): - """Bicubic interpolation at a given position in the image. - - Parameters - ---------- - image : double array - Input image. - rows, cols : int - Shape of image. - r, c : double - Position at which to interpolate. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Interpolated value. - - """ - - cdef Py_ssize_t r0 = r - 1 - cdef Py_ssize_t c0 = c - 1 - if r < 0: - r0 -= 1 - if c < 0: - c0 -= 1 - # scale position to range [0, 1] - cdef double xr = (r - r0) / 3 - cdef double xc = (c - c0) / 3 - - cdef double fc[4], fr[4] - - cdef Py_ssize_t pr, pc - - # row-wise cubic interpolation - for pr in range(r0, r0 + 4): - for pc in range(c0, c0 + 4): - fc[pc - c0] = get_pixel2d(image, rows, cols, pr, pc, mode, cval) - fr[pr - r0] = cubic_interpolation(xc, fc) - - # cubic interpolation for interpolated values of each row - return cubic_interpolation(xr, fr) - - -cdef inline double get_pixel2d(double* image, Py_ssize_t rows, Py_ssize_t cols, - Py_ssize_t r, Py_ssize_t c, char mode, double cval): - """Get a pixel from the image, taking wrapping mode into consideration. - - Parameters - ---------- - image : double array - Input image. - rows, cols : int - Shape of image. - r, c : int - Position at which to get the pixel. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Pixel value at given position. - - """ - if mode == 'C': - if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): - return cval - else: - return image[r * cols + c] - else: - return image[coord_map(rows, r, mode) * cols + coord_map(cols, c, mode)] - - -cdef inline double get_pixel3d(double* image, Py_ssize_t rows, Py_ssize_t cols, - Py_ssize_t dims, Py_ssize_t r, Py_ssize_t c, Py_ssize_t d, - char mode, double cval): - """Get a pixel from the image, taking wrapping mode into consideration. - - Parameters - ---------- - image : double array - Input image. - rows, cols, dims : int - Shape of image. - r, c, d : int - Position at which to get the pixel. - mode : {'C', 'W', 'R', 'N'} - Wrapping mode. Constant, Wrap, Reflect or Nearest. - cval : double - Constant value to use for constant mode. - - Returns - ------- - value : double - Pixel value at given position. - - """ - if mode == 'C': - if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): - return cval - else: - return image[r * cols * dims + c * dims + d] - else: - return image[coord_map(rows, r, mode) * cols * dims - + coord_map(cols, c, mode) * dims - + d] - - -cdef inline Py_ssize_t coord_map(Py_ssize_t dim, Py_ssize_t coord, char mode): - """ - Wrap a coordinate, according to a given mode. - - Parameters - ---------- - dim : int - Maximum coordinate. - coord : int - Coord provided by user. May be < 0 or > dim. - mode : {'W', 'R', 'N'} - Whether to wrap or reflect the coordinate if it - falls outside [0, dim). - - """ - dim = dim - 1 - if mode == 'R': # reflect - if coord < 0: - # How many times times does the coordinate wrap? - if (-coord / dim) % 2 != 0: - return dim - (-coord % dim) - else: - return (-coord % dim) - elif coord > dim: - if (coord / dim) % 2 != 0: - return (dim - (coord % dim)) - else: - return (coord % dim) - elif mode == 'W': # wrap - if coord < 0: - return (dim - (-coord % dim)) - elif coord > dim: - return (coord % dim) - elif mode == 'N': # nearest - if coord < 0: - return 0 - elif coord > dim: - return dim - - return coord diff --git a/skimage/_shared/setup.py b/skimage/_shared/setup.py index 765c30de..4c36b97e 100644 --- a/skimage/_shared/setup.py +++ b/skimage/_shared/setup.py @@ -14,12 +14,9 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['geometry.pyx'], working_path=base_path) - cython(['interpolation.pyx'], working_path=base_path) cython(['transform.pyx'], working_path=base_path) config.add_extension('geometry', sources=['geometry.c']) - config.add_extension('interpolation', sources=['interpolation.c'], - include_dirs=[get_numpy_include_dirs()]) config.add_extension('transform', sources=['transform.c'], include_dirs=[get_numpy_include_dirs()]) diff --git a/skimage/_shared/six.py b/skimage/_shared/six.py deleted file mode 100644 index 8a877b17..00000000 --- a/skimage/_shared/six.py +++ /dev/null @@ -1,423 +0,0 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -# Copyright (c) 2010-2013 Benjamin Peterson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import operator -import sys -import types - -__author__ = "Benjamin Peterson " -__version__ = "1.3.0" - - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - - if sys.platform.startswith("java"): - # Jython always uses 32 bits. - MAXSIZE = int((1 << 31) - 1) - else: - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit - MAXSIZE = int((1 << 31) - 1) - else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X - - -def _add_doc(func, doc): - """Add documentation to a function.""" - func.__doc__ = doc - - -def _import_module(name): - """Import module, returning the module after the last dot.""" - __import__(name) - return sys.modules[name] - - -class _LazyDescr(object): - - def __init__(self, name): - self.name = name - - def __get__(self, obj, tp): - result = self._resolve() - setattr(obj, self.name, result) - # This is a bit ugly, but it avoids running this again. - delattr(tp, self.name) - return result - - -class MovedModule(_LazyDescr): - - def __init__(self, name, old, new=None): - super(MovedModule, self).__init__(name) - if PY3: - if new is None: - new = name - self.mod = new - else: - self.mod = old - - def _resolve(self): - return _import_module(self.mod) - - -class MovedAttribute(_LazyDescr): - - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): - super(MovedAttribute, self).__init__(name) - if PY3: - if new_mod is None: - new_mod = name - self.mod = new_mod - if new_attr is None: - if old_attr is None: - new_attr = name - else: - new_attr = old_attr - self.attr = new_attr - else: - self.mod = old_mod - if old_attr is None: - old_attr = name - self.attr = old_attr - - def _resolve(self): - module = _import_module(self.mod) - return getattr(module, self.attr) - - - -class _MovedItems(types.ModuleType): - """Lazy loading of moved objects""" - - -_moved_attributes = [ - MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), - MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), - MovedAttribute("map", "itertools", "builtins", "imap", "map"), - MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "imp", "reload"), - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("copyreg", "copy_reg"), - MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), - MovedModule("http_cookies", "Cookie", "http.cookies"), - MovedModule("html_entities", "htmlentitydefs", "html.entities"), - MovedModule("html_parser", "HTMLParser", "html.parser"), - MovedModule("http_client", "httplib", "http.client"), - MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), - MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), - MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), - MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), - MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), - MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), - MovedModule("cPickle", "cPickle", "pickle"), - MovedModule("queue", "Queue"), - MovedModule("reprlib", "repr"), - MovedModule("socketserver", "SocketServer"), - MovedModule("tkinter", "Tkinter"), - MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), - MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), - MovedModule("tkinter_tix", "Tix", "tkinter.tix"), - MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), - MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), - MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), - MovedModule("tkinter_font", "tkFont", "tkinter.font"), - MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), - MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("winreg", "_winreg"), -] -for attr in _moved_attributes: - setattr(_MovedItems, attr.name, attr) -del attr - -moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") - - -def add_move(move): - """Add an item to six.moves.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from six.moves.""" - try: - delattr(_MovedItems, name) - except AttributeError: - try: - del moves.__dict__[name] - except KeyError: - raise AttributeError("no such move, %r" % (name,)) - - -if PY3: - _meth_func = "__func__" - _meth_self = "__self__" - - _func_closure = "__closure__" - _func_code = "__code__" - _func_defaults = "__defaults__" - _func_globals = "__globals__" - - _iterkeys = "keys" - _itervalues = "values" - _iteritems = "items" - _iterlists = "lists" -else: - _meth_func = "im_func" - _meth_self = "im_self" - - _func_closure = "func_closure" - _func_code = "func_code" - _func_defaults = "func_defaults" - _func_globals = "func_globals" - - _iterkeys = "iterkeys" - _itervalues = "itervalues" - _iteritems = "iteritems" - _iterlists = "iterlists" - - -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() -next = advance_iterator - - -try: - callable = callable -except NameError: - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) - - -if PY3: - def get_unbound_function(unbound): - return unbound - - create_bound_method = types.MethodType - - Iterator = object -else: - def get_unbound_function(unbound): - return unbound.im_func - - def create_bound_method(func, obj): - return types.MethodType(func, obj, obj.__class__) - - class Iterator(object): - - def next(self): - return type(self).__next__(self) - - callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") - - -get_method_function = operator.attrgetter(_meth_func) -get_method_self = operator.attrgetter(_meth_self) -get_function_closure = operator.attrgetter(_func_closure) -get_function_code = operator.attrgetter(_func_code) -get_function_defaults = operator.attrgetter(_func_defaults) -get_function_globals = operator.attrgetter(_func_globals) - - -def iterkeys(d, **kw): - """Return an iterator over the keys of a dictionary.""" - return iter(getattr(d, _iterkeys)(**kw)) - -def itervalues(d, **kw): - """Return an iterator over the values of a dictionary.""" - return iter(getattr(d, _itervalues)(**kw)) - -def iteritems(d, **kw): - """Return an iterator over the (key, value) pairs of a dictionary.""" - return iter(getattr(d, _iteritems)(**kw)) - -def iterlists(d, **kw): - """Return an iterator over the (key, [values]) pairs of a dictionary.""" - return iter(getattr(d, _iterlists)(**kw)) - - -if PY3: - def b(s): - return s.encode("latin-1") - def u(s): - return s - unichr = chr - if sys.version_info[1] <= 1: - def int2byte(i): - return bytes((i,)) - else: - # This is about 2x faster than the implementation above on 3.2+ - int2byte = operator.methodcaller("to_bytes", 1, "big") - byte2int = operator.itemgetter(0) - indexbytes = operator.getitem - iterbytes = iter - import io - StringIO = io.StringIO - BytesIO = io.BytesIO -else: - def b(s): - return s - def u(s): - return unicode(s, "unicode_escape") - unichr = unichr - int2byte = chr - def byte2int(bs): - return ord(bs[0]) - def indexbytes(buf, i): - return ord(buf[i]) - def iterbytes(buf): - return (ord(byte) for byte in buf) - import StringIO - StringIO = BytesIO = StringIO.StringIO -_add_doc(b, """Byte literal""") -_add_doc(u, """Text literal""") - - -if PY3: - import builtins - exec_ = getattr(builtins, "exec") - - - def reraise(tp, value, tb=None): - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - - - print_ = getattr(builtins, "print") - del builtins - -else: - def exec_(_code_, _globs_=None, _locs_=None): - """Execute code in a namespace.""" - if _globs_ is None: - frame = sys._getframe(1) - _globs_ = frame.f_globals - if _locs_ is None: - _locs_ = frame.f_locals - del frame - elif _locs_ is None: - _locs_ = _globs_ - exec("""exec _code_ in _globs_, _locs_""") - - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - - - def print_(*args, **kwargs): - """The new-style print function.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - def write(data): - if not isinstance(data, basestring): - data = str(data) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) - -_add_doc(reraise, """Reraise an exception.""") - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - return meta("NewBase", bases, {}) diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py index eab83a56..9b42c582 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -1,6 +1,12 @@ """Testing utilities.""" +import re + + +SKIP_RE = re.compile("(\s*>>>.*?)(\s*)#\s*skip\s+if\s+(.*)$") + + def _assert_less(a, b, msg=None): message = "%r is not lower than %r" % (a, b) if msg is not None: @@ -24,3 +30,48 @@ try: from nose.tools import assert_greater except ImportError: assert_greater = _assert_greater + + +def doctest_skip_parser(func): + """ Decorator replaces custom skip test markup in doctests + + Say a function has a docstring:: + + >>> something # skip if not HAVE_AMODULE + >>> something + else + >>> something # skip if HAVE_BMODULE + + This decorator will evaluate the expresssion after ``skip if``. If this + evaluates to True, then the comment is replaced by ``# doctest: +SKIP``. If + False, then the comment is just removed. The expression is evaluated in the + ``globals`` scope of `func`. + + For example, if the module global ``HAVE_AMODULE`` is False, and module + global ``HAVE_BMODULE`` is False, the returned function will have docstring:: + + >>> something # doctest: +SKIP + >>> something + else + >>> something + + """ + lines = func.__doc__.split('\n') + new_lines = [] + for line in lines: + match = SKIP_RE.match(line) + if match is None: + new_lines.append(line) + continue + code, space, expr = match.groups() + + try: + # Works as a function decorator + if eval(expr, func.__globals__): + code = code + space + "# doctest: +SKIP" + except AttributeError: + # Works as a class decorator + if eval(expr, func.__init__.__globals__): + code = code + space + "# doctest: +SKIP" + + new_lines.append(code) + func.__doc__ = "\n".join(new_lines) + return func diff --git a/skimage/_shared/tests/test_testing.py b/skimage/_shared/tests/test_testing.py new file mode 100644 index 00000000..f563caad --- /dev/null +++ b/skimage/_shared/tests/test_testing.py @@ -0,0 +1,87 @@ +""" Testing decorators module +""" + +import numpy as np +from nose.tools import (assert_true, assert_raises, assert_equal) +from skimage._shared.testing import doctest_skip_parser + + +def test_skipper(): + def f(): + pass + + class c(): + def __init__(self): + self.me = "I think, therefore..." + + docstring = \ + """ Header + + >>> something # skip if not HAVE_AMODULE + >>> something + else + >>> a = 1 # skip if not HAVE_BMODULE + >>> something2 # skip if HAVE_AMODULE + """ + f.__doc__ = docstring + c.__doc__ = docstring + + global HAVE_AMODULE, HAVE_BMODULE + HAVE_AMODULE = False + HAVE_BMODULE = True + + f2 = doctest_skip_parser(f) + c2 = doctest_skip_parser(c) + assert_true(f is f2) + assert_true(c is c2) + + assert_equal(f2.__doc__, + """ Header + + >>> something # doctest: +SKIP + >>> something + else + >>> a = 1 + >>> something2 + """) + assert_equal(c2.__doc__, + """ Header + + >>> something # doctest: +SKIP + >>> something + else + >>> a = 1 + >>> something2 + """) + + HAVE_AMODULE = True + HAVE_BMODULE = False + f.__doc__ = docstring + c.__doc__ = docstring + f2 = doctest_skip_parser(f) + c2 = doctest_skip_parser(c) + + assert_true(f is f2) + assert_equal(f2.__doc__, + """ Header + + >>> something + >>> something + else + >>> a = 1 # doctest: +SKIP + >>> something2 # doctest: +SKIP + """) + assert_equal(c2.__doc__, + """ Header + + >>> something + >>> something + else + >>> a = 1 # doctest: +SKIP + >>> something2 # doctest: +SKIP + """) + + del HAVE_AMODULE + f.__doc__ = docstring + c.__doc__ = docstring + assert_raises(NameError, doctest_skip_parser, f) + assert_raises(NameError, doctest_skip_parser, c) + + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/_shared/transform.pyx b/skimage/_shared/transform.pyx index 9bdc6824..d77ec583 100644 --- a/skimage/_shared/transform.pyx +++ b/skimage/_shared/transform.pyx @@ -41,4 +41,5 @@ cdef float integrate(float[:, ::1] sat, Py_ssize_t r0, Py_ssize_t c0, if (c0 - 1 >= 0): S -= sat[r1, c0 - 1] + return S diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index 1c61020d..a471fd82 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -13,6 +13,10 @@ from .colorconv import (convert_colorspace, lab2xyz, lab2rgb, rgb2lab, + xyz2luv, + luv2xyz, + luv2rgb, + rgb2luv, rgb2hed, hed2rgb, lab2lch, diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 7562b650..e2c7af17 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -29,9 +29,12 @@ Supported color spaces * LAB CIE : Lightness, a, b Colorspace derived from XYZ CIE that is intended to be more perceptually uniform +* LUV CIE : Lightness, u, v + Colorspace derived from XYZ CIE that is intended to be more + perceptually uniform * LCH CIE : Lightness, Chroma, Hue Defined in terms of LAB CIE. C and H are the polar representation of - a and b. The polar angle C is defined to be on (0, 2*pi) + a and b. The polar angle C is defined to be on ``(0, 2*pi)`` :author: Nicolas Pinto (rgb2hsv) :author: Ralf Gommers (hsv2rgb) @@ -68,7 +71,7 @@ def guess_spatial_dimensions(image): ------- spatial_dims : int or None The number of spatial dimensions of `image`. If ambiguous, the value - is `None`. + is ``None``. Raises ------ @@ -122,12 +125,12 @@ def convert_colorspace(arr, fromspace, tospace): The image to convert. fromspace : str The color space to convert from. Valid color space strings are - ['RGB', 'HSV', 'RGB CIE', 'XYZ']. Value may also be specified as lower - case. + ``['RGB', 'HSV', 'RGB CIE', 'XYZ']``. Value may also be specified as + lower case. tospace : str The color space to convert to. Valid color space strings are - ['RGB', 'HSV', 'RGB CIE', 'XYZ']. Value may also be specified as lower - case. + ``['RGB', 'HSV', 'RGB CIE', 'XYZ']``. Value may also be specified as + lower case. Returns ------- @@ -137,7 +140,7 @@ def convert_colorspace(arr, fromspace, tospace): Notes ----- Conversion occurs through the "central" RGB color space, i.e. conversion - from XYZ to HSV is implemented as XYZ -> RGB -> HSV instead of directly. + from XYZ to HSV is implemented as ``XYZ -> RGB -> HSV`` instead of directly. Examples -------- @@ -181,17 +184,17 @@ def rgb2hsv(rgb): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in HSV format, in a 3-D array of shape (.., .., 3). + The image in HSV format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- @@ -259,21 +262,21 @@ def hsv2rgb(hsv): Parameters ---------- hsv : array_like - The image in HSV format, in a 3-D array of shape (.., .., 3). + The image in HSV format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `hsv` is not a 3-D array of shape (.., .., 3). + If `hsv` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- - The conversion assumes an input data range of [0, 1] for all + The conversion assumes an input data range of ``[0, 1]`` for all color components. Conversion between RGB and HSV color spaces results in some loss of @@ -468,17 +471,17 @@ def xyz2rgb(xyz): Parameters ---------- xyz : array_like - The image in XYZ format, in a 3-D array of shape (.., .., 3). + The image in XYZ format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `xyz` is not a 3-D array of shape (.., .., 3). + If `xyz` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- @@ -513,18 +516,18 @@ def rgb2xyz(rgb): ---------- rgb : array_like The image in RGB format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Returns ------- out : ndarray The image in XYZ format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Raises ------ ValueError - If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). + If `rgb` is not a 3- or 4-D array of shape ``(.., ..,[ ..,] 3)``. Notes ----- @@ -556,17 +559,17 @@ def rgb2rgbcie(rgb): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB CIE format, in a 3-D array of shape (.., .., 3). + The image in RGB CIE format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3-D array of shape ``(.., .., 3)``. References ---------- @@ -588,17 +591,17 @@ def rgbcie2rgb(rgbcie): Parameters ---------- rgbcie : array_like - The image in RGB CIE format, in a 3-D array of shape (.., .., 3). + The image in RGB CIE format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `rgbcie` is not a 3-D array of shape (.., .., 3). + If `rgbcie` is not a 3-D array of shape ``(.., .., 3)``. References ---------- @@ -621,8 +624,8 @@ def rgb2gray(rgb): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3), - or in RGBA format with shape (.., .., 4). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``, + or in RGBA format with shape ``(.., .., 4)``. Returns ------- @@ -632,8 +635,8 @@ def rgb2gray(rgb): Raises ------ ValueError - If `rgb2gray` is not a 3-D array of shape (.., .., 3) or - (.., .., 4). + If `rgb2gray` is not a 3-D array of shape ``(.., .., 3)`` or + ``(.., .., 4)``. References ---------- @@ -698,18 +701,18 @@ def xyz2lab(xyz): ---------- xyz : array_like The image in XYZ format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Returns ------- out : ndarray The image in CIE-LAB format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Raises ------ ValueError - If `xyz` is not a 3-D array of shape (.., ..,[ ..,] 3). + If `xyz` is not a 3-D array of shape ``(.., ..,[ ..,] 3)``. Notes ----- @@ -755,21 +758,21 @@ def lab2xyz(lab): Parameters ---------- lab : array_like - The image in lab format, in a 3-D array of shape (.., .., 3). + The image in lab format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in XYZ format, in a 3-D array of shape (.., .., 3). + The image in XYZ format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `lab` is not a 3-D array of shape (.., .., 3). + If `lab` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- - Observer= 2A, Illuminant= D65 + Observer = 2A, Illuminant = D65 CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 References @@ -804,18 +807,18 @@ def rgb2lab(rgb): ---------- rgb : array_like The image in RGB format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Returns ------- out : ndarray The image in Lab format, in a 3- or 4-D array of shape - (.., ..,[ ..,] 3). + ``(.., ..,[ ..,] 3)``. Raises ------ ValueError - If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). + If `rgb` is not a 3- or 4-D array of shape ``(.., ..,[ ..,] 3)``. Notes ----- @@ -830,17 +833,17 @@ def lab2rgb(lab): Parameters ---------- rgb : array_like - The image in Lab format, in a 3-D array of shape (.., .., 3). + The image in Lab format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `lab` is not a 3-D array of shape (.., .., 3). + If `lab` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- @@ -849,23 +852,203 @@ def lab2rgb(lab): return xyz2rgb(lab2xyz(lab)) +def xyz2luv(xyz): + """XYZ to CIE-Luv color space conversion. + + Parameters + ---------- + xyz : (M, N, [P,] 3) array_like + The 3 or 4 dimensional image in XYZ format. Final dimension denotes + channels. + + Returns + ------- + out : (M, N, [P,] 3) ndarray + The image in CIE-Luv format. Same dimensions as input. + + Raises + ------ + ValueError + If `xyz` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + XYZ conversion weights use Observer = 2A. Reference whitepoint for D65 + Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. + + References + ---------- + .. [1] http://www.easyrgb.com/index.php?X=MATH&H=16#text16 + .. [2] http://en.wikipedia.org/wiki/CIELUV + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2xyz, xyz2luv + >>> lena = data.lena() + >>> lena_xyz = rgb2xyz(lena) + >>> lena_luv = xyz2luv(lena_xyz) + """ + arr = _prepare_colorarray(xyz) + + # extract channels + x, y, z = arr[..., 0], arr[..., 1], arr[..., 2] + + eps = np.finfo(np.float).eps + + # compute y_r and L + L = y / lab_ref_white[1] + mask = L > 0.008856 + L[mask] = 116. * np.power(L[mask], 1. / 3.) - 16. + L[~mask] = 903.3 * L[~mask] + + u0 = 4*lab_ref_white[0] / np.dot([1, 15, 3], lab_ref_white) + v0 = 9*lab_ref_white[1] / np.dot([1, 15, 3], lab_ref_white) + + # u' and v' helper functions + def fu(X, Y, Z): + return (4.*X) / (X + 15.*Y + 3.*Z + eps) + + def fv(X, Y, Z): + return (9.*Y) / (X + 15.*Y + 3.*Z + eps) + + # compute u and v using helper functions + u = 13.*L * (fu(x, y, z) - u0) + v = 13.*L * (fv(x, y, z) - v0) + + return np.concatenate([q[..., np.newaxis] for q in [L, u, v]], axis=-1) + + +def luv2xyz(luv): + """CIE-Luv to XYZ color space conversion. + + Parameters + ---------- + luv : (M, N, [P,] 3) array_like + The 3 or 4 dimensional image in CIE-Luv format. Final dimension denotes + channels. + + Returns + ------- + out : (M, N, [P,] 3) ndarray + The image in XYZ format. Same dimensions as input. + + Raises + ------ + ValueError + If `luv` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + XYZ conversion weights use Observer = 2A. Reference whitepoint for D65 + Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. + + References + ---------- + .. [1] http://www.easyrgb.com/index.php?X=MATH&H=16#text16 + .. [2] http://en.wikipedia.org/wiki/CIELUV + + """ + + arr = _prepare_colorarray(luv).copy() + + L, u, v = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + + eps = np.finfo(np.float).eps + + # compute y + y = L.copy() + mask = y > 7.999625 + y[mask] = np.power((y[mask]+16.) / 116., 3.) + y[~mask] = y[~mask] / 903.3 + y *= lab_ref_white[1] + + # reference white x,z + uv_weights = [1, 15, 3] + u0 = 4*lab_ref_white[0] / np.dot(uv_weights, lab_ref_white) + v0 = 9*lab_ref_white[1] / np.dot(uv_weights, lab_ref_white) + + # compute intermediate values + a = u0 + u / (13.*L + eps) + b = v0 + v / (13.*L + eps) + c = 3*y * (5*b-3) + + # compute x and z + z = ((a-4)*c - 15*a*b*y) / (12*b) + x = -(c/b + 3.*z) + + return np.concatenate([q[..., np.newaxis] for q in [x, y, z]], axis=-1) + + +def rgb2luv(rgb): + """RGB to CIE-Luv color space conversion. + + Parameters + ---------- + rgb : (M, N, [P,] 3) array_like + The 3 or 4 dimensional image in RGB format. Final dimension denotes + channels. + + Returns + ------- + out : (M, N, [P,] 3) ndarray + The image in CIE Luv format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + This function uses rgb2xyz and xyz2luv. + """ + return xyz2luv(rgb2xyz(rgb)) + + +def luv2rgb(luv): + """Luv to RGB color space conversion. + + Parameters + ---------- + luv : (M, N, [P,] 3) array_like + The 3 or 4 dimensional image in CIE Luv format. Final dimension denotes + channels. + + Returns + ------- + out : (M, N, [P,] 3) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `luv` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + This function uses luv2xyz and xyz2rgb. + """ + return xyz2rgb(luv2xyz(luv)) + + def rgb2hed(rgb): """RGB to Haematoxylin-Eosin-DAB (HED) color space conversion. Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in HED format, in a 3-D array of shape (.., .., 3). + The image in HED format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3-D array of shape ``(.., .., 3)``. References @@ -891,17 +1074,17 @@ def hed2rgb(hed): Parameters ---------- hed : array_like - The image in the HED color space, in a 3-D array of shape (.., .., 3). + The image in the HED color space, in a 3-D array of shape ``(.., .., 3)``. Returns ------- out : ndarray - The image in RGB, in a 3-D array of shape (.., .., 3). + The image in RGB, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `hed` is not a 3-D array of shape (.., .., 3). + If `hed` is not a 3-D array of shape ``(.., .., 3)``. References ---------- @@ -927,19 +1110,19 @@ def separate_stains(rgb, conv_matrix): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. conv_matrix: ndarray The stain separation matrix as described by G. Landini [1]_. Returns ------- out : ndarray - The image in stain color space, in a 3-D array of shape (.., .., 3). + The image in stain color space, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- @@ -981,19 +1164,19 @@ def combine_stains(stains, conv_matrix): Parameters ---------- stains : array_like - The image in stain color space, in a 3-D array of shape (.., .., 3). + The image in stain color space, in a 3-D array of shape ``(.., .., 3)``. conv_matrix: ndarray The stain separation matrix as described by G. Landini [1]_. Returns ------- out : ndarray - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3-D array of shape ``(.., .., 3)``. Raises ------ ValueError - If `stains` is not a 3-D array of shape (.., .., 3). + If `stains` is not a 3-D array of shape ``(.., .., 3)``. Notes ----- @@ -1043,9 +1226,9 @@ def lab2lch(lab): Parameters ---------- lab : array_like - The N-D image in CIE-LAB format. The last (`N+1`th) dimension must have - at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` color - channels. Subsequent elements are copied. + The N-D image in CIE-LAB format. The last (``N+1``-th) dimension must + have at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` + color channels. Subsequent elements are copied. Returns ------- @@ -1059,7 +1242,7 @@ def lab2lch(lab): Notes ----- - The Hue is expressed as an angle between (0, 2*pi) + The Hue is expressed as an angle between ``(0, 2*pi)`` Examples -------- @@ -1079,7 +1262,7 @@ def lab2lch(lab): def _cart2polar_2pi(x, y): """convert cartesian coordiantes to polar (uses non-standard theta range!) - NON-STANDARD RANGE! Maps to (0, 2*pi) rather than usual (-pi, +pi) + NON-STANDARD RANGE! Maps to ``(0, 2*pi)`` rather than usual ``(-pi, +pi)`` """ r, t = np.hypot(x, y), np.arctan2(y, x) t += np.where(t < 0., 2 * np.pi, 0) @@ -1094,9 +1277,9 @@ def lch2lab(lch): Parameters ---------- lch : array_like - The N-D image in CIE-LCH format. The last (`N+1`th) dimension must have - at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` color - channels. Subsequent elements are copied. + The N-D image in CIE-LCH format. The last (``N+1``-th) dimension must + have at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` + color channels. Subsequent elements are copied. Returns ------- diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index fbec9ba6..c7393355 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -33,6 +33,8 @@ from skimage.color import (rgb2hsv, hsv2rgb, rgb2grey, gray2rgb, xyz2lab, lab2xyz, lab2rgb, rgb2lab, + xyz2luv, luv2xyz, + luv2rgb, rgb2luv, is_rgb, is_gray, lab2lch, lch2lab, guess_spatial_dimensions @@ -69,17 +71,24 @@ class TestColorconv(TestCase): colbars_point75_array = np.swapaxes(colbars_point75.reshape(3, 4, 2), 0, 2) xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], # red - [[0, 0, 0]], # black - [[.9505, 1., 1.089]], # white - [[.1805, .0722, .9505]], # blue - [[.07719, .15438, .02573]], # green - ]) + [[0, 0, 0]], # black + [[.9505, 1., 1.089]], # white + [[.1805, .0722, .9505]], # blue + [[.07719, .15438, .02573]], # green + ]) lab_array = np.array([[[53.233, 80.109, 67.220]], # red - [[0., 0., 0.]], # black - [[100.0, 0.005, -0.010]], # white - [[32.303, 79.197, -107.864]], # blue - [[46.229, -51.7, 49.898]], # green - ]) + [[0., 0., 0.]], # black + [[100.0, 0.005, -0.010]], # white + [[32.303, 79.197, -107.864]], # blue + [[46.229, -51.7, 49.898]], # green + ]) + + luv_array = np.array([[[53.233, 175.053, 37.751]], # red + [[0., 0., 0.]], # black + [[100., 0.001, -0.017]], # white + [[32.303, -9.400, -130.358]], # blue + [[46.228, -43.774, 56.589]], # green + ]) # RGB to HSV def test_rgb2hsv_conversion(self): @@ -250,6 +259,41 @@ class TestColorconv(TestCase): img_rgb = img_as_float(self.img_rgb) assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb) + # test matrices for xyz2luv and luv2xyz generated using + # http://www.easyrgb.com/index.php?X=CALC + # Note: easyrgb website displays xyz*100 + def test_xyz2luv(self): + assert_array_almost_equal(xyz2luv(self.xyz_array), + self.luv_array, decimal=3) + + def test_luv2xyz(self): + assert_array_almost_equal(luv2xyz(self.luv_array), + self.xyz_array, decimal=3) + + def test_rgb2luv_brucelindbloom(self): + """ + Test the RGB->Lab conversion by comparing to the calculator on the + authoritative Bruce Lindbloom + [website](http://brucelindbloom.com/index.html?ColorCalculator.html). + """ + # Obtained with D65 white point, sRGB model and gamma + gt_for_colbars = np.array([ + [100, 0, 0], + [97.1393, 7.7056, 106.7866], + [91.1132, -70.4773, -15.2042], + [87.7347, -83.0776, 107.3985], + [60.3242, 84.0714, -108.6834], + [53.2408, 175.0151, 37.7564], + [32.2970, -9.4054, -130.3423], + [0, 0, 0]]).T + gt_array = np.swapaxes(gt_for_colbars.reshape(3, 4, 2), 0, 2) + assert_array_almost_equal(rgb2luv(self.colbars_array), + gt_array, decimal=2) + + def test_luv_rgb_roundtrip(self): + img_rgb = img_as_float(self.img_rgb) + assert_array_almost_equal(luv2rgb(rgb2luv(img_rgb)), img_rgb) + def test_lab_lch_roundtrip(self): rgb = img_as_float(self.img_rgb) lab = rgb2lab(rgb) diff --git a/skimage/data/orb_descriptor_positions.txt b/skimage/data/orb_descriptor_positions.txt new file mode 100644 index 00000000..541f972f --- /dev/null +++ b/skimage/data/orb_descriptor_positions.txt @@ -0,0 +1,256 @@ +8.000000000000000000e+00 -3.000000000000000000e+00 9.000000000000000000e+00 5.000000000000000000e+00 +4.000000000000000000e+00 2.000000000000000000e+00 7.000000000000000000e+00 -1.200000000000000000e+01 +-1.100000000000000000e+01 9.000000000000000000e+00 -8.000000000000000000e+00 2.000000000000000000e+00 +7.000000000000000000e+00 -1.200000000000000000e+01 1.200000000000000000e+01 -1.300000000000000000e+01 +2.000000000000000000e+00 -1.300000000000000000e+01 2.000000000000000000e+00 1.200000000000000000e+01 +1.000000000000000000e+00 -7.000000000000000000e+00 1.000000000000000000e+00 6.000000000000000000e+00 +-2.000000000000000000e+00 -1.000000000000000000e+01 -2.000000000000000000e+00 -4.000000000000000000e+00 +-1.300000000000000000e+01 -1.300000000000000000e+01 -1.100000000000000000e+01 -8.000000000000000000e+00 +-1.300000000000000000e+01 -3.000000000000000000e+00 -1.200000000000000000e+01 -9.000000000000000000e+00 +1.000000000000000000e+01 4.000000000000000000e+00 1.100000000000000000e+01 9.000000000000000000e+00 +-1.300000000000000000e+01 -8.000000000000000000e+00 -8.000000000000000000e+00 -9.000000000000000000e+00 +-1.100000000000000000e+01 7.000000000000000000e+00 -9.000000000000000000e+00 1.200000000000000000e+01 +7.000000000000000000e+00 7.000000000000000000e+00 1.200000000000000000e+01 6.000000000000000000e+00 +-4.000000000000000000e+00 -5.000000000000000000e+00 -3.000000000000000000e+00 0.000000000000000000e+00 +-1.300000000000000000e+01 2.000000000000000000e+00 -1.200000000000000000e+01 -3.000000000000000000e+00 +-9.000000000000000000e+00 0.000000000000000000e+00 -7.000000000000000000e+00 5.000000000000000000e+00 +1.200000000000000000e+01 -6.000000000000000000e+00 1.200000000000000000e+01 -1.000000000000000000e+00 +-3.000000000000000000e+00 6.000000000000000000e+00 -2.000000000000000000e+00 1.200000000000000000e+01 +-6.000000000000000000e+00 -1.300000000000000000e+01 -4.000000000000000000e+00 -8.000000000000000000e+00 +1.100000000000000000e+01 -1.300000000000000000e+01 1.200000000000000000e+01 -8.000000000000000000e+00 +4.000000000000000000e+00 7.000000000000000000e+00 5.000000000000000000e+00 1.000000000000000000e+00 +5.000000000000000000e+00 -3.000000000000000000e+00 1.000000000000000000e+01 -3.000000000000000000e+00 +3.000000000000000000e+00 -7.000000000000000000e+00 6.000000000000000000e+00 1.200000000000000000e+01 +-8.000000000000000000e+00 -7.000000000000000000e+00 -6.000000000000000000e+00 -2.000000000000000000e+00 +-2.000000000000000000e+00 1.100000000000000000e+01 -1.000000000000000000e+00 -1.000000000000000000e+01 +-1.300000000000000000e+01 1.200000000000000000e+01 -8.000000000000000000e+00 1.000000000000000000e+01 +-7.000000000000000000e+00 3.000000000000000000e+00 -5.000000000000000000e+00 -3.000000000000000000e+00 +-4.000000000000000000e+00 2.000000000000000000e+00 -3.000000000000000000e+00 7.000000000000000000e+00 +-1.000000000000000000e+01 -1.200000000000000000e+01 -6.000000000000000000e+00 1.100000000000000000e+01 +5.000000000000000000e+00 -1.200000000000000000e+01 6.000000000000000000e+00 -7.000000000000000000e+00 +5.000000000000000000e+00 -6.000000000000000000e+00 7.000000000000000000e+00 -1.000000000000000000e+00 +1.000000000000000000e+00 0.000000000000000000e+00 4.000000000000000000e+00 -5.000000000000000000e+00 +9.000000000000000000e+00 1.100000000000000000e+01 1.100000000000000000e+01 -1.300000000000000000e+01 +4.000000000000000000e+00 7.000000000000000000e+00 4.000000000000000000e+00 1.200000000000000000e+01 +2.000000000000000000e+00 -1.000000000000000000e+00 4.000000000000000000e+00 4.000000000000000000e+00 +-4.000000000000000000e+00 -1.200000000000000000e+01 -2.000000000000000000e+00 7.000000000000000000e+00 +-8.000000000000000000e+00 -5.000000000000000000e+00 -7.000000000000000000e+00 -1.000000000000000000e+01 +4.000000000000000000e+00 1.100000000000000000e+01 9.000000000000000000e+00 1.200000000000000000e+01 +0.000000000000000000e+00 -8.000000000000000000e+00 1.000000000000000000e+00 -1.300000000000000000e+01 +-1.300000000000000000e+01 -2.000000000000000000e+00 -8.000000000000000000e+00 2.000000000000000000e+00 +-3.000000000000000000e+00 -2.000000000000000000e+00 -2.000000000000000000e+00 3.000000000000000000e+00 +-6.000000000000000000e+00 9.000000000000000000e+00 -4.000000000000000000e+00 -9.000000000000000000e+00 +8.000000000000000000e+00 1.200000000000000000e+01 1.000000000000000000e+01 7.000000000000000000e+00 +0.000000000000000000e+00 9.000000000000000000e+00 1.000000000000000000e+00 3.000000000000000000e+00 +7.000000000000000000e+00 -5.000000000000000000e+00 1.100000000000000000e+01 -1.000000000000000000e+01 +-1.300000000000000000e+01 -6.000000000000000000e+00 -1.100000000000000000e+01 0.000000000000000000e+00 +1.000000000000000000e+01 7.000000000000000000e+00 1.200000000000000000e+01 1.000000000000000000e+00 +-6.000000000000000000e+00 -3.000000000000000000e+00 -6.000000000000000000e+00 1.200000000000000000e+01 +1.000000000000000000e+01 -9.000000000000000000e+00 1.200000000000000000e+01 -4.000000000000000000e+00 +-1.300000000000000000e+01 8.000000000000000000e+00 -8.000000000000000000e+00 -1.200000000000000000e+01 +-1.300000000000000000e+01 0.000000000000000000e+00 -8.000000000000000000e+00 -4.000000000000000000e+00 +3.000000000000000000e+00 3.000000000000000000e+00 7.000000000000000000e+00 8.000000000000000000e+00 +5.000000000000000000e+00 7.000000000000000000e+00 1.000000000000000000e+01 -7.000000000000000000e+00 +-1.000000000000000000e+00 7.000000000000000000e+00 1.000000000000000000e+00 -1.200000000000000000e+01 +3.000000000000000000e+00 -1.000000000000000000e+01 5.000000000000000000e+00 6.000000000000000000e+00 +2.000000000000000000e+00 -4.000000000000000000e+00 3.000000000000000000e+00 -1.000000000000000000e+01 +-1.300000000000000000e+01 0.000000000000000000e+00 -1.300000000000000000e+01 5.000000000000000000e+00 +-1.300000000000000000e+01 -7.000000000000000000e+00 -1.200000000000000000e+01 1.200000000000000000e+01 +-1.300000000000000000e+01 3.000000000000000000e+00 -1.100000000000000000e+01 8.000000000000000000e+00 +-7.000000000000000000e+00 1.200000000000000000e+01 -4.000000000000000000e+00 7.000000000000000000e+00 +6.000000000000000000e+00 -1.000000000000000000e+01 1.200000000000000000e+01 8.000000000000000000e+00 +-9.000000000000000000e+00 -1.000000000000000000e+00 -7.000000000000000000e+00 -6.000000000000000000e+00 +-2.000000000000000000e+00 -5.000000000000000000e+00 0.000000000000000000e+00 1.200000000000000000e+01 +-1.200000000000000000e+01 5.000000000000000000e+00 -7.000000000000000000e+00 5.000000000000000000e+00 +3.000000000000000000e+00 -1.000000000000000000e+01 8.000000000000000000e+00 -1.300000000000000000e+01 +-7.000000000000000000e+00 -7.000000000000000000e+00 -4.000000000000000000e+00 5.000000000000000000e+00 +-3.000000000000000000e+00 -2.000000000000000000e+00 -1.000000000000000000e+00 -7.000000000000000000e+00 +2.000000000000000000e+00 9.000000000000000000e+00 5.000000000000000000e+00 -1.100000000000000000e+01 +-1.100000000000000000e+01 -1.300000000000000000e+01 -5.000000000000000000e+00 -1.300000000000000000e+01 +-1.000000000000000000e+00 6.000000000000000000e+00 0.000000000000000000e+00 -1.000000000000000000e+00 +5.000000000000000000e+00 -3.000000000000000000e+00 5.000000000000000000e+00 2.000000000000000000e+00 +-4.000000000000000000e+00 -1.300000000000000000e+01 -4.000000000000000000e+00 1.200000000000000000e+01 +-9.000000000000000000e+00 -6.000000000000000000e+00 -9.000000000000000000e+00 6.000000000000000000e+00 +-1.200000000000000000e+01 -1.000000000000000000e+01 -8.000000000000000000e+00 -4.000000000000000000e+00 +1.000000000000000000e+01 2.000000000000000000e+00 1.200000000000000000e+01 -3.000000000000000000e+00 +7.000000000000000000e+00 1.200000000000000000e+01 1.200000000000000000e+01 1.200000000000000000e+01 +-7.000000000000000000e+00 -1.300000000000000000e+01 -6.000000000000000000e+00 5.000000000000000000e+00 +-4.000000000000000000e+00 9.000000000000000000e+00 -3.000000000000000000e+00 4.000000000000000000e+00 +7.000000000000000000e+00 -1.000000000000000000e+00 1.200000000000000000e+01 2.000000000000000000e+00 +-7.000000000000000000e+00 6.000000000000000000e+00 -5.000000000000000000e+00 1.000000000000000000e+00 +-1.300000000000000000e+01 1.100000000000000000e+01 -1.200000000000000000e+01 5.000000000000000000e+00 +-3.000000000000000000e+00 7.000000000000000000e+00 -2.000000000000000000e+00 -6.000000000000000000e+00 +7.000000000000000000e+00 -8.000000000000000000e+00 1.200000000000000000e+01 -7.000000000000000000e+00 +-1.300000000000000000e+01 -7.000000000000000000e+00 -1.100000000000000000e+01 -1.200000000000000000e+01 +1.000000000000000000e+00 -3.000000000000000000e+00 1.200000000000000000e+01 1.200000000000000000e+01 +2.000000000000000000e+00 -6.000000000000000000e+00 3.000000000000000000e+00 0.000000000000000000e+00 +-4.000000000000000000e+00 3.000000000000000000e+00 -2.000000000000000000e+00 -1.300000000000000000e+01 +-1.000000000000000000e+00 -1.300000000000000000e+01 1.000000000000000000e+00 9.000000000000000000e+00 +7.000000000000000000e+00 1.000000000000000000e+00 8.000000000000000000e+00 -6.000000000000000000e+00 +1.000000000000000000e+00 -1.000000000000000000e+00 3.000000000000000000e+00 1.200000000000000000e+01 +9.000000000000000000e+00 1.000000000000000000e+00 1.200000000000000000e+01 6.000000000000000000e+00 +-1.000000000000000000e+00 -9.000000000000000000e+00 -1.000000000000000000e+00 3.000000000000000000e+00 +-1.300000000000000000e+01 -1.300000000000000000e+01 -1.000000000000000000e+01 5.000000000000000000e+00 +7.000000000000000000e+00 7.000000000000000000e+00 1.000000000000000000e+01 1.200000000000000000e+01 +1.200000000000000000e+01 -5.000000000000000000e+00 1.200000000000000000e+01 9.000000000000000000e+00 +6.000000000000000000e+00 3.000000000000000000e+00 7.000000000000000000e+00 1.100000000000000000e+01 +5.000000000000000000e+00 -1.300000000000000000e+01 6.000000000000000000e+00 1.000000000000000000e+01 +2.000000000000000000e+00 -1.200000000000000000e+01 2.000000000000000000e+00 3.000000000000000000e+00 +3.000000000000000000e+00 8.000000000000000000e+00 4.000000000000000000e+00 -6.000000000000000000e+00 +2.000000000000000000e+00 6.000000000000000000e+00 1.200000000000000000e+01 -1.300000000000000000e+01 +9.000000000000000000e+00 -1.200000000000000000e+01 1.000000000000000000e+01 3.000000000000000000e+00 +-8.000000000000000000e+00 4.000000000000000000e+00 -7.000000000000000000e+00 9.000000000000000000e+00 +-1.100000000000000000e+01 1.200000000000000000e+01 -4.000000000000000000e+00 -6.000000000000000000e+00 +1.000000000000000000e+00 1.200000000000000000e+01 2.000000000000000000e+00 -8.000000000000000000e+00 +6.000000000000000000e+00 -9.000000000000000000e+00 7.000000000000000000e+00 -4.000000000000000000e+00 +2.000000000000000000e+00 3.000000000000000000e+00 3.000000000000000000e+00 -2.000000000000000000e+00 +6.000000000000000000e+00 3.000000000000000000e+00 1.100000000000000000e+01 0.000000000000000000e+00 +3.000000000000000000e+00 -3.000000000000000000e+00 8.000000000000000000e+00 -8.000000000000000000e+00 +7.000000000000000000e+00 8.000000000000000000e+00 9.000000000000000000e+00 3.000000000000000000e+00 +-1.100000000000000000e+01 -5.000000000000000000e+00 -6.000000000000000000e+00 -4.000000000000000000e+00 +-1.000000000000000000e+01 1.100000000000000000e+01 -5.000000000000000000e+00 1.000000000000000000e+01 +-5.000000000000000000e+00 -8.000000000000000000e+00 -3.000000000000000000e+00 1.200000000000000000e+01 +-1.000000000000000000e+01 5.000000000000000000e+00 -9.000000000000000000e+00 0.000000000000000000e+00 +8.000000000000000000e+00 -1.000000000000000000e+00 1.200000000000000000e+01 -6.000000000000000000e+00 +4.000000000000000000e+00 -6.000000000000000000e+00 6.000000000000000000e+00 -1.100000000000000000e+01 +-1.000000000000000000e+01 1.200000000000000000e+01 -8.000000000000000000e+00 7.000000000000000000e+00 +4.000000000000000000e+00 -2.000000000000000000e+00 6.000000000000000000e+00 7.000000000000000000e+00 +-2.000000000000000000e+00 0.000000000000000000e+00 -2.000000000000000000e+00 1.200000000000000000e+01 +-5.000000000000000000e+00 -8.000000000000000000e+00 -5.000000000000000000e+00 2.000000000000000000e+00 +7.000000000000000000e+00 -6.000000000000000000e+00 1.000000000000000000e+01 1.200000000000000000e+01 +-9.000000000000000000e+00 -1.300000000000000000e+01 -8.000000000000000000e+00 -8.000000000000000000e+00 +-5.000000000000000000e+00 -1.300000000000000000e+01 -5.000000000000000000e+00 -2.000000000000000000e+00 +8.000000000000000000e+00 -8.000000000000000000e+00 9.000000000000000000e+00 -1.300000000000000000e+01 +-9.000000000000000000e+00 -1.100000000000000000e+01 -9.000000000000000000e+00 0.000000000000000000e+00 +1.000000000000000000e+00 -8.000000000000000000e+00 1.000000000000000000e+00 -2.000000000000000000e+00 +7.000000000000000000e+00 -4.000000000000000000e+00 9.000000000000000000e+00 1.000000000000000000e+00 +-2.000000000000000000e+00 1.000000000000000000e+00 -1.000000000000000000e+00 -4.000000000000000000e+00 +1.100000000000000000e+01 -6.000000000000000000e+00 1.200000000000000000e+01 -1.100000000000000000e+01 +-1.200000000000000000e+01 -9.000000000000000000e+00 -6.000000000000000000e+00 4.000000000000000000e+00 +3.000000000000000000e+00 7.000000000000000000e+00 7.000000000000000000e+00 1.200000000000000000e+01 +5.000000000000000000e+00 5.000000000000000000e+00 1.000000000000000000e+01 8.000000000000000000e+00 +0.000000000000000000e+00 -4.000000000000000000e+00 2.000000000000000000e+00 8.000000000000000000e+00 +-9.000000000000000000e+00 1.200000000000000000e+01 -5.000000000000000000e+00 -1.300000000000000000e+01 +0.000000000000000000e+00 7.000000000000000000e+00 2.000000000000000000e+00 1.200000000000000000e+01 +-1.000000000000000000e+00 2.000000000000000000e+00 1.000000000000000000e+00 7.000000000000000000e+00 +5.000000000000000000e+00 1.100000000000000000e+01 7.000000000000000000e+00 -9.000000000000000000e+00 +3.000000000000000000e+00 5.000000000000000000e+00 6.000000000000000000e+00 -8.000000000000000000e+00 +-1.300000000000000000e+01 -4.000000000000000000e+00 -8.000000000000000000e+00 9.000000000000000000e+00 +-5.000000000000000000e+00 9.000000000000000000e+00 -3.000000000000000000e+00 -3.000000000000000000e+00 +-4.000000000000000000e+00 -7.000000000000000000e+00 -3.000000000000000000e+00 -1.200000000000000000e+01 +6.000000000000000000e+00 5.000000000000000000e+00 8.000000000000000000e+00 0.000000000000000000e+00 +-7.000000000000000000e+00 6.000000000000000000e+00 -6.000000000000000000e+00 1.200000000000000000e+01 +-1.300000000000000000e+01 6.000000000000000000e+00 -5.000000000000000000e+00 -2.000000000000000000e+00 +1.000000000000000000e+00 -1.000000000000000000e+01 3.000000000000000000e+00 1.000000000000000000e+01 +4.000000000000000000e+00 1.000000000000000000e+00 8.000000000000000000e+00 -4.000000000000000000e+00 +-2.000000000000000000e+00 -2.000000000000000000e+00 2.000000000000000000e+00 -1.300000000000000000e+01 +2.000000000000000000e+00 -1.200000000000000000e+01 1.200000000000000000e+01 1.200000000000000000e+01 +-2.000000000000000000e+00 -1.300000000000000000e+01 0.000000000000000000e+00 -6.000000000000000000e+00 +4.000000000000000000e+00 1.000000000000000000e+00 9.000000000000000000e+00 3.000000000000000000e+00 +-6.000000000000000000e+00 -1.000000000000000000e+01 -3.000000000000000000e+00 -5.000000000000000000e+00 +-3.000000000000000000e+00 -1.300000000000000000e+01 -1.000000000000000000e+00 1.000000000000000000e+00 +7.000000000000000000e+00 5.000000000000000000e+00 1.200000000000000000e+01 -1.100000000000000000e+01 +4.000000000000000000e+00 -2.000000000000000000e+00 5.000000000000000000e+00 -7.000000000000000000e+00 +-1.300000000000000000e+01 9.000000000000000000e+00 -9.000000000000000000e+00 -5.000000000000000000e+00 +7.000000000000000000e+00 1.000000000000000000e+00 8.000000000000000000e+00 6.000000000000000000e+00 +7.000000000000000000e+00 -8.000000000000000000e+00 7.000000000000000000e+00 6.000000000000000000e+00 +-7.000000000000000000e+00 -4.000000000000000000e+00 -7.000000000000000000e+00 1.000000000000000000e+00 +-8.000000000000000000e+00 1.100000000000000000e+01 -7.000000000000000000e+00 -8.000000000000000000e+00 +-1.300000000000000000e+01 6.000000000000000000e+00 -1.200000000000000000e+01 -8.000000000000000000e+00 +2.000000000000000000e+00 4.000000000000000000e+00 3.000000000000000000e+00 9.000000000000000000e+00 +1.000000000000000000e+01 -5.000000000000000000e+00 1.200000000000000000e+01 3.000000000000000000e+00 +-6.000000000000000000e+00 -5.000000000000000000e+00 -6.000000000000000000e+00 7.000000000000000000e+00 +8.000000000000000000e+00 -3.000000000000000000e+00 9.000000000000000000e+00 -8.000000000000000000e+00 +2.000000000000000000e+00 -1.200000000000000000e+01 2.000000000000000000e+00 8.000000000000000000e+00 +-1.100000000000000000e+01 -2.000000000000000000e+00 -1.000000000000000000e+01 3.000000000000000000e+00 +-1.200000000000000000e+01 -1.300000000000000000e+01 -7.000000000000000000e+00 -9.000000000000000000e+00 +-1.100000000000000000e+01 0.000000000000000000e+00 -1.000000000000000000e+01 -5.000000000000000000e+00 +5.000000000000000000e+00 -3.000000000000000000e+00 1.100000000000000000e+01 8.000000000000000000e+00 +-2.000000000000000000e+00 -1.300000000000000000e+01 -1.000000000000000000e+00 1.200000000000000000e+01 +-1.000000000000000000e+00 -8.000000000000000000e+00 0.000000000000000000e+00 9.000000000000000000e+00 +-1.300000000000000000e+01 -1.100000000000000000e+01 -1.200000000000000000e+01 -5.000000000000000000e+00 +-1.000000000000000000e+01 -2.000000000000000000e+00 -1.000000000000000000e+01 1.100000000000000000e+01 +-3.000000000000000000e+00 9.000000000000000000e+00 -2.000000000000000000e+00 -1.300000000000000000e+01 +2.000000000000000000e+00 -3.000000000000000000e+00 3.000000000000000000e+00 2.000000000000000000e+00 +-9.000000000000000000e+00 -1.300000000000000000e+01 -4.000000000000000000e+00 0.000000000000000000e+00 +-4.000000000000000000e+00 6.000000000000000000e+00 -3.000000000000000000e+00 -1.000000000000000000e+01 +-4.000000000000000000e+00 1.200000000000000000e+01 -2.000000000000000000e+00 -7.000000000000000000e+00 +-6.000000000000000000e+00 -1.100000000000000000e+01 -4.000000000000000000e+00 9.000000000000000000e+00 +6.000000000000000000e+00 -3.000000000000000000e+00 6.000000000000000000e+00 1.100000000000000000e+01 +-1.300000000000000000e+01 1.100000000000000000e+01 -5.000000000000000000e+00 5.000000000000000000e+00 +1.100000000000000000e+01 1.100000000000000000e+01 1.200000000000000000e+01 6.000000000000000000e+00 +7.000000000000000000e+00 -5.000000000000000000e+00 1.200000000000000000e+01 -2.000000000000000000e+00 +-1.000000000000000000e+00 1.200000000000000000e+01 0.000000000000000000e+00 7.000000000000000000e+00 +-4.000000000000000000e+00 -8.000000000000000000e+00 -3.000000000000000000e+00 -2.000000000000000000e+00 +-7.000000000000000000e+00 1.000000000000000000e+00 -6.000000000000000000e+00 7.000000000000000000e+00 +-1.300000000000000000e+01 -1.200000000000000000e+01 -8.000000000000000000e+00 -1.300000000000000000e+01 +-7.000000000000000000e+00 -2.000000000000000000e+00 -6.000000000000000000e+00 -8.000000000000000000e+00 +-8.000000000000000000e+00 5.000000000000000000e+00 -6.000000000000000000e+00 -9.000000000000000000e+00 +-5.000000000000000000e+00 -1.000000000000000000e+00 -4.000000000000000000e+00 5.000000000000000000e+00 +-1.300000000000000000e+01 7.000000000000000000e+00 -8.000000000000000000e+00 1.000000000000000000e+01 +1.000000000000000000e+00 5.000000000000000000e+00 5.000000000000000000e+00 -1.300000000000000000e+01 +1.000000000000000000e+00 0.000000000000000000e+00 1.000000000000000000e+01 -1.300000000000000000e+01 +9.000000000000000000e+00 1.200000000000000000e+01 1.000000000000000000e+01 -1.000000000000000000e+00 +5.000000000000000000e+00 -8.000000000000000000e+00 1.000000000000000000e+01 -9.000000000000000000e+00 +-1.000000000000000000e+00 1.100000000000000000e+01 1.000000000000000000e+00 -1.300000000000000000e+01 +-9.000000000000000000e+00 -3.000000000000000000e+00 -6.000000000000000000e+00 2.000000000000000000e+00 +-1.000000000000000000e+00 -1.000000000000000000e+01 1.000000000000000000e+00 1.200000000000000000e+01 +-1.300000000000000000e+01 1.000000000000000000e+00 -8.000000000000000000e+00 -1.000000000000000000e+01 +8.000000000000000000e+00 -1.100000000000000000e+01 1.000000000000000000e+01 -6.000000000000000000e+00 +2.000000000000000000e+00 -1.300000000000000000e+01 3.000000000000000000e+00 -6.000000000000000000e+00 +7.000000000000000000e+00 -1.300000000000000000e+01 1.200000000000000000e+01 -9.000000000000000000e+00 +-1.000000000000000000e+01 -1.000000000000000000e+01 -5.000000000000000000e+00 -7.000000000000000000e+00 +-1.000000000000000000e+01 -8.000000000000000000e+00 -8.000000000000000000e+00 -1.300000000000000000e+01 +4.000000000000000000e+00 -6.000000000000000000e+00 8.000000000000000000e+00 5.000000000000000000e+00 +3.000000000000000000e+00 1.200000000000000000e+01 8.000000000000000000e+00 -1.300000000000000000e+01 +-4.000000000000000000e+00 2.000000000000000000e+00 -3.000000000000000000e+00 -3.000000000000000000e+00 +5.000000000000000000e+00 -1.300000000000000000e+01 1.000000000000000000e+01 -1.200000000000000000e+01 +4.000000000000000000e+00 -1.300000000000000000e+01 5.000000000000000000e+00 -1.000000000000000000e+00 +-9.000000000000000000e+00 9.000000000000000000e+00 -4.000000000000000000e+00 3.000000000000000000e+00 +0.000000000000000000e+00 3.000000000000000000e+00 3.000000000000000000e+00 -9.000000000000000000e+00 +-1.200000000000000000e+01 1.000000000000000000e+00 -6.000000000000000000e+00 1.000000000000000000e+00 +3.000000000000000000e+00 2.000000000000000000e+00 4.000000000000000000e+00 -8.000000000000000000e+00 +-1.000000000000000000e+01 -1.000000000000000000e+01 -1.000000000000000000e+01 9.000000000000000000e+00 +8.000000000000000000e+00 -1.300000000000000000e+01 1.200000000000000000e+01 1.200000000000000000e+01 +-8.000000000000000000e+00 -1.200000000000000000e+01 -6.000000000000000000e+00 -5.000000000000000000e+00 +2.000000000000000000e+00 2.000000000000000000e+00 3.000000000000000000e+00 7.000000000000000000e+00 +1.000000000000000000e+01 6.000000000000000000e+00 1.100000000000000000e+01 -8.000000000000000000e+00 +6.000000000000000000e+00 8.000000000000000000e+00 8.000000000000000000e+00 -1.200000000000000000e+01 +-7.000000000000000000e+00 1.000000000000000000e+01 -6.000000000000000000e+00 5.000000000000000000e+00 +-3.000000000000000000e+00 -9.000000000000000000e+00 -3.000000000000000000e+00 9.000000000000000000e+00 +-1.000000000000000000e+00 -1.300000000000000000e+01 -1.000000000000000000e+00 5.000000000000000000e+00 +-3.000000000000000000e+00 -7.000000000000000000e+00 -3.000000000000000000e+00 4.000000000000000000e+00 +-8.000000000000000000e+00 -2.000000000000000000e+00 -8.000000000000000000e+00 3.000000000000000000e+00 +4.000000000000000000e+00 2.000000000000000000e+00 1.200000000000000000e+01 1.200000000000000000e+01 +2.000000000000000000e+00 -5.000000000000000000e+00 3.000000000000000000e+00 1.100000000000000000e+01 +6.000000000000000000e+00 -9.000000000000000000e+00 1.100000000000000000e+01 -1.300000000000000000e+01 +3.000000000000000000e+00 -1.000000000000000000e+00 7.000000000000000000e+00 1.200000000000000000e+01 +1.100000000000000000e+01 -1.000000000000000000e+00 1.200000000000000000e+01 4.000000000000000000e+00 +-3.000000000000000000e+00 0.000000000000000000e+00 -3.000000000000000000e+00 6.000000000000000000e+00 +4.000000000000000000e+00 -1.100000000000000000e+01 4.000000000000000000e+00 1.200000000000000000e+01 +2.000000000000000000e+00 -4.000000000000000000e+00 2.000000000000000000e+00 1.000000000000000000e+00 +-1.000000000000000000e+01 -6.000000000000000000e+00 -8.000000000000000000e+00 1.000000000000000000e+00 +-1.300000000000000000e+01 7.000000000000000000e+00 -1.100000000000000000e+01 1.000000000000000000e+00 +-1.300000000000000000e+01 1.200000000000000000e+01 -1.100000000000000000e+01 -1.300000000000000000e+01 +6.000000000000000000e+00 0.000000000000000000e+00 1.100000000000000000e+01 -1.300000000000000000e+01 +0.000000000000000000e+00 -1.000000000000000000e+00 1.000000000000000000e+00 4.000000000000000000e+00 +-1.300000000000000000e+01 3.000000000000000000e+00 -9.000000000000000000e+00 -2.000000000000000000e+00 +-9.000000000000000000e+00 8.000000000000000000e+00 -6.000000000000000000e+00 -3.000000000000000000e+00 +-1.300000000000000000e+01 -6.000000000000000000e+00 -8.000000000000000000e+00 -2.000000000000000000e+00 +5.000000000000000000e+00 -9.000000000000000000e+00 8.000000000000000000e+00 1.000000000000000000e+01 +2.000000000000000000e+00 7.000000000000000000e+00 3.000000000000000000e+00 -9.000000000000000000e+00 +-1.000000000000000000e+00 -6.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 +9.000000000000000000e+00 5.000000000000000000e+00 1.100000000000000000e+01 -2.000000000000000000e+00 +1.100000000000000000e+01 -3.000000000000000000e+00 1.200000000000000000e+01 -8.000000000000000000e+00 +3.000000000000000000e+00 0.000000000000000000e+00 3.000000000000000000e+00 5.000000000000000000e+00 +-1.000000000000000000e+00 4.000000000000000000e+00 0.000000000000000000e+00 1.000000000000000000e+01 +3.000000000000000000e+00 -6.000000000000000000e+00 4.000000000000000000e+00 5.000000000000000000e+00 +-1.300000000000000000e+01 0.000000000000000000e+00 -1.000000000000000000e+01 5.000000000000000000e+00 +5.000000000000000000e+00 8.000000000000000000e+00 1.200000000000000000e+01 1.100000000000000000e+01 +8.000000000000000000e+00 9.000000000000000000e+00 9.000000000000000000e+00 -6.000000000000000000e+00 +7.000000000000000000e+00 -4.000000000000000000e+00 8.000000000000000000e+00 -1.200000000000000000e+01 +-1.000000000000000000e+01 4.000000000000000000e+00 -1.000000000000000000e+01 9.000000000000000000e+00 +7.000000000000000000e+00 3.000000000000000000e+00 1.200000000000000000e+01 4.000000000000000000e+00 +9.000000000000000000e+00 -7.000000000000000000e+00 1.000000000000000000e+01 -2.000000000000000000e+00 +7.000000000000000000e+00 0.000000000000000000e+00 1.200000000000000000e+01 -2.000000000000000000e+00 +-1.000000000000000000e+00 -6.000000000000000000e+00 0.000000000000000000e+00 -1.100000000000000000e+01 diff --git a/skimage/draw/tests/test_draw3d.py b/skimage/draw/tests/test_draw3d.py index 2e1198eb..48caf1a0 100644 --- a/skimage/draw/tests/test_draw3d.py +++ b/skimage/draw/tests/test_draw3d.py @@ -1,9 +1,25 @@ import numpy as np from numpy.testing import assert_array_equal, assert_allclose +from nose.tools import raises from skimage.draw import ellipsoid, ellipsoid_stats +@raises(ValueError) +def test_ellipsoid_sign_parameters1(): + ellipsoid(-1, 2, 2) + + +@raises(ValueError) +def test_ellipsoid_sign_parameters2(): + ellipsoid(0, 2, 2) + + +@raises(ValueError) +def test_ellipsoid_sign_parameters3(): + ellipsoid(-3, -2, 2) + + def test_ellipsoid_bool(): test = ellipsoid(2, 2, 2)[1:-1, 1:-1, 1:-1] test_anisotropic = ellipsoid(2, 2, 4, spacing=(1., 1., 2.)) @@ -86,18 +102,18 @@ def test_ellipsoid_levelset(): def test_ellipsoid_stats(): # Test comparison values generated by Wolfram Alpha vol, surf = ellipsoid_stats(6, 10, 16) - assert(round(1280 * np.pi, 4) == round(vol, 4)) - assert(1383.28 == round(surf, 2)) + assert_allclose(1280 * np.pi, vol, atol=1e-4) + assert_allclose(1383.28, surf, atol=1e-2) # Test when a <= b <= c does not hold vol, surf = ellipsoid_stats(16, 6, 10) - assert(round(1280 * np.pi, 4) == round(vol, 4)) - assert(1383.28 == round(surf, 2)) + assert_allclose(1280 * np.pi, vol, atol=1e-4) + assert_allclose(1383.28, surf, atol=1e-2) # Larger test to ensure reliability over broad range vol, surf = ellipsoid_stats(17, 27, 169) - assert(round(103428 * np.pi, 4) == round(vol, 4)) - assert(37426.3 == round(surf, 1)) + assert_allclose(103428 * np.pi, vol, atol=1e-4) + assert_allclose(37426.3, surf, atol=1e-1) if __name__ == "__main__": diff --git a/skimage/exposure/__init__.py b/skimage/exposure/__init__.py index b873c339..7ceb5fbf 100644 --- a/skimage/exposure/__init__.py +++ b/skimage/exposure/__init__.py @@ -4,6 +4,7 @@ from .exposure import histogram, equalize, equalize_hist, \ from ._adapthist import equalize_adapthist + __all__ = ['histogram', 'equalize', 'equalize_hist', diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index f703f846..ed3f6c4e 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -7,8 +7,16 @@ from skimage._shared.utils import deprecated __all__ = ['histogram', 'cumulative_distribution', 'equalize', - 'rescale_intensity', 'adjust_gamma', - 'adjust_log', 'adjust_sigmoid'] + 'rescale_intensity', 'adjust_gamma', 'adjust_log', 'adjust_sigmoid'] + + +DTYPE_RANGE = dtype_range.copy() +DTYPE_RANGE.update((d.__name__, limits) for d, limits in dtype_range.items()) +DTYPE_RANGE.update({'uint10': (0, 2**10 - 1), + 'uint12': (0, 2**12 - 1), + 'uint14': (0, 2**14 - 1), + 'bool': dtype_range[np.bool_], + 'float': dtype_range[np.float64]}) def histogram(image, nbins=256): @@ -143,14 +151,15 @@ def rescale_intensity(image, in_range=None, out_range=None): ---------- image : array Image array. - in_range : 2-tuple (float, float) + in_range : 2-tuple (float, float) or str Min and max *allowed* intensity values of input image. If None, the *allowed* min/max values are set to the *actual* min/max values in the - input image. - out_range : 2-tuple (float, float) + input image. Intensity values outside this range are clipped. + If string, use data limits of dtype specified by the string. + out_range : 2-tuple (float, float) or str Min and max intensity values of output image. If None, use the min/max intensities of the image data type. See `skimage.util.dtype` for - details. + details. If string, use data limits of dtype specified by the string. Returns ------- @@ -201,11 +210,14 @@ def rescale_intensity(image, in_range=None, out_range=None): if in_range is None: imin = np.min(image) imax = np.max(image) + elif in_range in DTYPE_RANGE: + imin, imax = DTYPE_RANGE[in_range] else: imin, imax = in_range - if out_range is None: - omin, omax = dtype_range[dtype] + if out_range is None or out_range in DTYPE_RANGE: + out_range = dtype if out_range is None else out_range + omin, omax = DTYPE_RANGE[out_range] if imin >= 0: omin = 0 else: @@ -263,7 +275,7 @@ def adjust_gamma(image, gamma=1, gain=1): dtype = image.dtype.type if gamma < 0: - return "Gamma should be a non-negative real number" + raise ValueError("Gamma should be a non-negative real number.") scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0]) diff --git a/skimage/exposure/setup.py b/skimage/exposure/setup.py new file mode 100644 index 00000000..d0c534dd --- /dev/null +++ b/skimage/exposure/setup.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +import os + +from skimage._build import cython + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs + + config = Configuration('exposure', parent_package, top_path) + config.add_data_dir('tests') + + return config + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='scikit-image Developers', + author='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Exposure corrections', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 2b696b32..6471ff59 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -2,7 +2,7 @@ import warnings import numpy as np from numpy.testing import assert_array_almost_equal as assert_close -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_equal, assert_raises import skimage from skimage import data from skimage import exposure @@ -44,6 +44,13 @@ def check_cdf_slope(cdf): # Test rescale intensity # ====================== + +uint10_max = 2**10 - 1 +uint12_max = 2**12 - 1 +uint14_max = 2**14 - 1 +uint16_max = 2**16 - 1 + + def test_rescale_stretch(): image = np.array([51, 102, 153], dtype=np.uint8) out = exposure.rescale_intensity(image) @@ -76,6 +83,30 @@ def test_rescale_out_range(): assert_close(out, [0, 63, 127]) +def test_rescale_named_in_range(): + image = np.array([0, uint10_max, uint10_max + 100], dtype=np.uint16) + out = exposure.rescale_intensity(image, in_range='uint10') + assert_close(out, [0, uint16_max, uint16_max]) + + +def test_rescale_named_out_range(): + image = np.array([0, uint16_max], dtype=np.uint16) + out = exposure.rescale_intensity(image, out_range='uint10') + assert_close(out, [0, uint10_max]) + + +def test_rescale_uint12_limits(): + image = np.array([0, uint16_max], dtype=np.uint16) + out = exposure.rescale_intensity(image, out_range='uint12') + assert_close(out, [0, uint12_max]) + + +def test_rescale_uint14_limits(): + image = np.array([0, uint16_max], dtype=np.uint16) + out = exposure.rescale_intensity(image, out_range='uint14') + assert_close(out, [0, uint14_max]) + + # Test adaptive histogram equalization # ==================================== @@ -230,6 +261,11 @@ def test_adjust_gamma_greater_one(): assert_array_equal(result, expected) +def test_adjust_gamma_neggative(): + image = np.arange(0, 255, 4, np.uint8).reshape(8,8) + assert_raises(ValueError, exposure.adjust_gamma, image, -1) + + # Test Logarithmic Correction # =========================== @@ -336,3 +372,8 @@ def test_adjust_inv_sigmoid_cutoff_half(): result = exposure.adjust_sigmoid(image, 0.5, 10, True) assert_array_equal(result, expected) + + +def test_neggative(): + image = np.arange(-10, 245, 4).reshape(8, 8).astype(np.double) + assert_raises(ValueError, exposure.adjust_gamma, image) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 4a6518d6..54b7bac3 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -4,9 +4,17 @@ from .texture import greycomatrix, greycoprops, local_binary_pattern from .peak import peak_local_max from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, corner_foerstner, corner_subpix, - corner_peaks) -from .corner_cy import corner_moravec + corner_peaks, corner_fast, structure_tensor, + structure_tensor_eigvals, hessian_matrix, + hessian_matrix_eigvals) +from .corner_cy import corner_moravec, corner_orientations from .template import match_template +from .brief import BRIEF +from .censure import CENSURE +from .orb import ORB +from .match import match_descriptors +from .util import plot_matches +from .blob import blob_dog, blob_log __all__ = ['daisy', @@ -15,6 +23,10 @@ __all__ = ['daisy', 'greycoprops', 'local_binary_pattern', 'peak_local_max', + 'structure_tensor', + 'structure_tensor_eigvals', + 'hessian_matrix', + 'hessian_matrix_eigvals', 'corner_kitchen_rosenfeld', 'corner_harris', 'corner_shi_tomasi', @@ -22,4 +34,13 @@ __all__ = ['daisy', 'corner_subpix', 'corner_peaks', 'corner_moravec', - 'match_template'] + 'corner_fast', + 'corner_orientations', + 'match_template', + 'BRIEF', + 'CENSURE', + 'ORB', + 'match_descriptors', + 'plot_matches', + 'blob_dog', + 'blob_log'] diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py deleted file mode 100644 index 8a8d78db..00000000 --- a/skimage/feature/_brief.py +++ /dev/null @@ -1,229 +0,0 @@ -import numpy as np -from scipy.ndimage.filters import gaussian_filter - -from ..util import img_as_float -from .util import _mask_border_keypoints, pairwise_hamming_distance - -from ._brief_cy import _brief_loop - - -def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, - sample_seed=1, variance=2): - """**Experimental function**. - - Extract BRIEF Descriptor about given keypoints for a given image. - - Parameters - ---------- - image : 2D ndarray - Input image. - keypoints : (P, 2) ndarray - Array of keypoint locations in the format (row, col). - descriptor_size : int - Size of BRIEF descriptor about each keypoint. Sizes 128, 256 and 512 - preferred by the authors. Default is 256. - mode : string - Probability distribution for sampling location of decision pixel-pairs - around keypoints. Default is 'normal' otherwise uniform. - patch_size : int - Length of the two dimensional square patch sampling region around - the keypoints. Default is 49. - sample_seed : int - Seed for sampling the decision pixel-pairs. From a square window with - length patch_size, pixel pairs are sampled using the `mode` parameter - to build the descriptors using intensity comparison. The value of - `sample_seed` should be the same for the images to be matched while - building the descriptors. Default is 1. - variance : float - Variance of the Gaussian Low Pass filter applied on the image to - alleviate noise sensitivity. Default is 2. - - Returns - ------- - descriptors : (Q, `descriptor_size`) ndarray of dtype bool - 2D ndarray of binary descriptors of size `descriptor_size` about Q - keypoints after filtering out border keypoints with value at an index - (i, j) either being True or False representing the outcome - of Intensity comparison about ith keypoint on jth decision pixel-pair. - keypoints : (Q, 2) ndarray - Location i.e. (row, col) of keypoints after removing out those that - are near border. - - References - ---------- - .. [1] Michael Calonder, Vincent Lepetit, Christoph Strecha and Pascal Fua - "BRIEF : Binary robust independent elementary features", - http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf - - Examples - -------- - >> from skimage.feature import corner_peaks, corner_harris, \\ - .. pairwise_hamming_distance, brief, match_keypoints_brief - >> square1 = np.zeros([8, 8], dtype=np.int32) - >> square1[2:6, 2:6] = 1 - >> square1 - array([[0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) - >> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) - >> keypoints1 - array([[2, 2], - [2, 5], - [5, 2], - [5, 5]]) - >> descriptors1, keypoints1 = brief(square1, keypoints1, patch_size=5) - >> keypoints1 - array([[2, 2], - [2, 5], - [5, 2], - [5, 5]]) - >> square2 = np.zeros([9, 9], dtype=np.int32) - >> square2[2:7, 2:7] = 1 - >> square2 - array([[0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) - >> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) - >> keypoints2 - array([[2, 2], - [2, 6], - [6, 2], - [6, 6]]) - >> descriptors2, keypoints2 = brief(square2, keypoints2, patch_size=5) - >> keypoints2 - array([[2, 2], - [2, 6], - [6, 2], - [6, 6]]) - >> pairwise_hamming_distance(descriptors1, descriptors2) - array([[ 0.03125 , 0.3203125, 0.3671875, 0.6171875], - [ 0.3203125, 0.03125 , 0.640625 , 0.375 ], - [ 0.375 , 0.6328125, 0.0390625, 0.328125 ], - [ 0.625 , 0.3671875, 0.34375 , 0.0234375]]) - >> match_keypoints_brief(keypoints1, descriptors1, - .. keypoints2, descriptors2) - array([[[ 2, 2], - [ 2, 2]], - - [[ 2, 5], - [ 2, 6]], - - [[ 5, 2], - [ 6, 2]], - - [[ 5, 5], - [ 6, 6]]]) - - """ - - np.random.seed(sample_seed) - - image = np.squeeze(image) - if image.ndim != 2: - raise ValueError("Only 2-D gray-scale images supported.") - - image = img_as_float(image) - - # Gaussian Low pass filtering to alleviate noise - # sensitivity - image = gaussian_filter(image, variance) - - image = np.ascontiguousarray(image) - - keypoints = np.array(keypoints + 0.5, dtype=np.intp, order='C') - - # Removing keypoints that are within (patch_size / 2) distance from the - # image border - keypoints = keypoints[_mask_border_keypoints(image, keypoints, patch_size // 2)] - keypoints = np.ascontiguousarray(keypoints) - - descriptors = np.zeros((keypoints.shape[0], descriptor_size), dtype=bool, - order='C') - - # Sampling pairs of decision pixels in patch_size x patch_size window - if mode == 'normal': - - samples = (patch_size / 5.0) * np.random.randn(descriptor_size * 8) - samples = np.array(samples, dtype=np.int32) - samples = samples[(samples < (patch_size // 2)) - & (samples > - (patch_size - 2) // 2)] - - pos1 = samples[:descriptor_size * 2] - pos1 = pos1.reshape(descriptor_size, 2) - pos2 = samples[descriptor_size * 2:descriptor_size * 4] - pos2 = pos2.reshape(descriptor_size, 2) - - else: - - samples = np.random.randint(-(patch_size - 2) // 2, - (patch_size // 2) + 1, - (descriptor_size * 2, 2)) - pos1, pos2 = np.split(samples, 2) - - pos1 = np.ascontiguousarray(pos1) - pos2 = np.ascontiguousarray(pos2) - - _brief_loop(image, descriptors.view(np.uint8), keypoints, pos1, pos2) - - return descriptors, keypoints - - -def match_keypoints_brief(keypoints1, descriptors1, keypoints2, - descriptors2, threshold=0.15): - """**Experimental function**. - - Match keypoints described using BRIEF descriptors in one image to - those in second image. - - Parameters - ---------- - keypoints1 : (M, 2) ndarray - M Keypoints from the first image described using skimage.feature.brief - descriptors1 : (M, P) ndarray - BRIEF descriptors of size P about M keypoints in the first image. - keypoints2 : (N, 2) ndarray - N Keypoints from the second image described using skimage.feature.brief - descriptors2 : (N, P) ndarray - BRIEF descriptors of size P about N keypoints in the second image. - threshold : float in range [0, 1] - Maximum allowable hamming distance between descriptors of two keypoints - in separate images to be regarded as a match. Default is 0.15. - - Returns - ------- - match_keypoints_brief : (Q, 2, 2) ndarray - Location of Q matched keypoint pairs from two images. - - """ - if (keypoints1.shape[0] != descriptors1.shape[0] - or keypoints2.shape[0] != descriptors2.shape[0]): - raise ValueError("The number of keypoints and number of described " - "keypoints do not match. Make the optional parameter " - "return_keypoints True to get described keypoints.") - - if descriptors1.shape[1] != descriptors2.shape[1]: - raise ValueError("Descriptor sizes for matching keypoints in both " - "the images should be equal.") - - # Get hamming distances between keeypoints1 and keypoints2 - distance = pairwise_hamming_distance(descriptors1, descriptors2) - - temp = distance > threshold - row_check = np.any(~temp, axis=1) - matched_keypoints2 = keypoints2[np.argmin(distance, axis=1)] - matched_keypoint_pairs = np.zeros((np.sum(row_check), 2, 2), dtype=np.intp) - matched_keypoint_pairs[:, 0, :] = keypoints1[row_check] - matched_keypoint_pairs[:, 1, :] = matched_keypoints2[row_check] - - return matched_keypoint_pairs diff --git a/skimage/feature/_daisy.py b/skimage/feature/_daisy.py index 1a97de8f..3a55faf1 100644 --- a/skimage/feature/_daisy.py +++ b/skimage/feature/_daisy.py @@ -94,15 +94,15 @@ def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8, ''' # Validate image format. - if img.ndim > 2: + if img.ndim != 2: raise ValueError('Only grey-level images are supported.') - if img.dtype.kind != 'f': - img = img_as_float(img) + + img = img_as_float(img) # Validate parameters. if sigmas is not None and ring_radii is not None \ and len(sigmas) - 1 != len(ring_radii): - raise ValueError('len(sigmas)-1 != len(ring_radii)') + raise ValueError('`len(sigmas)-1 != len(ring_radii)`') if ring_radii is not None: rings = len(ring_radii) radius = ring_radii[-1] diff --git a/skimage/feature/_greycomatrix.py b/skimage/feature/_greycomatrix.py deleted file mode 100644 index 45476d33..00000000 --- a/skimage/feature/_greycomatrix.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -Compute grey level co-occurrence matrices (GLCMs) and associated -properties to characterize image textures. -""" - -import numpy as np - -from ._texture import _glcm_loop - - -def greycomatrix(image, distances, angles, levels=256, symmetric=False, - normed=False): - """Calculate the grey-level co-occurrence matrix. - - A grey level co-occurence matrix is a histogram of co-occuring - greyscale values at a given offset over an image. - - Parameters - ---------- - image : array_like of uint8 - Integer typed input image. The image will be cast to uint8, so - the maximum value must be less than 256. - distances : array_like - List of pixel pair distance offsets. - angles : array_like - List of pixel pair angles in radians. - levels : int, optional - The input image should contain integers in [0, levels-1], - where levels indicate the number of grey-levels counted - (typically 256 for an 8-bit image). The maximum value is - 256. - symmetric : bool, optional - If True, the output matrix `P[:, :, d, theta]` is symmetric. This - is accomplished by ignoring the order of value pairs, so both - (i, j) and (j, i) are accumulated when (i, j) is encountered - for a given offset. The default is False. - normed : bool, optional - If True, normalize each matrix `P[:, :, d, theta]` by dividing - by the total number of accumulated co-occurrences for the given - offset. The elements of the resulting matrix sum to 1. The - default is False. - - Returns - ------- - P : 4-D ndarray - The grey-level co-occurrence histogram. The value - `P[i,j,d,theta]` is the number of times that grey-level `j` - occurs at a distance `d` and at an angle `theta` from - grey-level `i`. If `normed` is `False`, the output is of - type uint32, otherwise it is float64. - - References - ---------- - .. [1] The GLCM Tutorial Home Page, - http://www.fp.ucalgary.ca/mhallbey/tutorial.htm - .. [2] Pattern Recognition Engineering, Morton Nadler & Eric P. - Smith - .. [3] Wikipedia, http://en.wikipedia.org/wiki/Co-occurrence_matrix - - - Examples - -------- - Compute 2 GLCMs: One for a 1-pixel offset to the right, and one - for a 1-pixel offset upwards. - - >>> image = np.array([[0, 0, 1, 1], - ... [0, 0, 1, 1], - ... [0, 2, 2, 2], - ... [2, 2, 3, 3]], dtype=np.uint8) - >>> result = greycomatrix(image, [1], [0, np.pi/2], levels=4) - >>> result[:, :, 0, 0] - array([[2, 2, 1, 0], - [0, 2, 0, 0], - [0, 0, 3, 1], - [0, 0, 0, 1]], dtype=uint32) - >>> result[:, :, 0, 1] - array([[3, 0, 2, 0], - [0, 2, 2, 0], - [0, 0, 1, 2], - [0, 0, 0, 0]], dtype=uint32) - - """ - - assert levels <= 256 - image = np.ascontiguousarray(image) - assert image.ndim == 2 - assert image.min() >= 0 - assert image.max() < levels - image = image.astype(np.uint8) - distances = np.ascontiguousarray(distances, dtype=np.float64) - angles = np.ascontiguousarray(angles, dtype=np.float64) - assert distances.ndim == 1 - assert angles.ndim == 1 - - P = np.zeros((levels, levels, len(distances), len(angles)), - dtype=np.uint32, order='C') - - # count co-occurences - _glcm_loop(image, distances, angles, levels, P) - - # make each GLMC symmetric - if symmetric: - Pt = np.transpose(P, (1, 0, 2, 3)) - P = P + Pt - - # normalize each GLMC - if normed: - P = P.astype(np.float64) - glcm_sums = np.apply_over_axes(np.sum, P, axes=(0, 1)) - glcm_sums[glcm_sums == 0] = 1 - P /= glcm_sums - - return P - - -def greycoprops(P, prop='contrast'): - """Calculate texture properties of a GLCM. - - Compute a feature of a grey level co-occurrence matrix to serve as - a compact summary of the matrix. The properties are computed as - follows: - - - 'contrast': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}(i-j)^2` - - 'dissimilarity': :math:`\\sum_{i,j=0}^{levels-1}P_{i,j}|i-j|` - - 'homogeneity': :math:`\\sum_{i,j=0}^{levels-1}\\frac{P_{i,j}}{1+(i-j)^2}` - - 'ASM': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}^2` - - 'energy': :math:`\\sqrt{ASM}` - - 'correlation': - .. math:: \\sum_{i,j=0}^{levels-1} P_{i,j}\\left[\\frac{(i-\\mu_i) \\ - (j-\\mu_j)}{\\sqrt{(\\sigma_i^2)(\\sigma_j^2)}}\\right] - - - Parameters - ---------- - P : ndarray - Input array. `P` is the grey-level co-occurrence histogram - for which to compute the specified property. The value - `P[i,j,d,theta]` is the number of times that grey-level j - occurs at a distance d and at an angle theta from - grey-level i. - - prop : {'contrast', 'dissimilarity', 'homogeneity', 'energy', \ - 'correlation', 'ASM'}, optional - The property of the GLCM to compute. The default is 'contrast'. - - Returns - ------- - results : 2-D ndarray - 2-dimensional array. `results[d, a]` is the property 'prop' for - the d'th distance and the a'th angle. - - References - ---------- - .. [1] The GLCM Tutorial Home Page, - http://www.fp.ucalgary.ca/mhallbey/tutorial.htm - - Examples - -------- - Compute the contrast for GLCMs with distances [1, 2] and angles - [0 degrees, 90 degrees] - - >>> image = np.array([[0, 0, 1, 1], - ... [0, 0, 1, 1], - ... [0, 2, 2, 2], - ... [2, 2, 3, 3]], dtype=np.uint8) - >>> g = greycomatrix(image, [1, 2], [0, np.pi/2], levels=4, - ... normed=True, symmetric=True) - >>> contrast = greycoprops(g, 'contrast') - >>> contrast - array([[ 0.58333333, 1. ], - [ 1.25 , 2.75 ]]) - - """ - - assert P.ndim == 4 - (num_level, num_level2, num_dist, num_angle) = P.shape - assert num_level == num_level2 - assert num_dist > 0 - assert num_angle > 0 - - # create weights for specified property - I, J = np.ogrid[0:num_level, 0:num_level] - if prop == 'contrast': - weights = (I - J)**2 - elif prop == 'dissimilarity': - weights = np.abs(I - J) - elif prop == 'homogeneity': - weights = 1. / (1. + (I - J)**2) - elif prop in ['ASM', 'energy', 'correlation']: - pass - else: - raise ValueError('%s is an invalid property' % (prop)) - - # compute property for each GLCM - if prop == 'energy': - asm = np.apply_over_axes(np.sum, (P**2), axes=(0, 1))[0, 0] - results = np.sqrt(asm) - elif prop == 'ASM': - results = np.apply_over_axes(np.sum, (P**2), axes=(0, 1))[0, 0] - elif prop == 'correlation': - results = np.zeros((num_dist, num_angle), dtype=np.float64) - I = np.array(range(num_level)).reshape((num_level, 1, 1, 1)) - J = np.array(range(num_level)).reshape((1, num_level, 1, 1)) - diff_i = I - np.apply_over_axes(np.sum, (I * P), axes=(0, 1))[0, 0] - diff_j = J - np.apply_over_axes(np.sum, (J * P), axes=(0, 1))[0, 0] - - std_i = np.sqrt(np.apply_over_axes(np.sum, (P * (diff_i)**2), - axes=(0, 1))[0, 0]) - std_j = np.sqrt(np.apply_over_axes(np.sum, (P * (diff_j)**2), - axes=(0, 1))[0, 0]) - cov = np.apply_over_axes(np.sum, (P * (diff_i * diff_j)), - axes=(0, 1))[0, 0] - - # handle the special case of standard deviations near zero - mask_0 = std_i < 1e-15 - mask_0[std_j < 1e-15] = True - results[mask_0] = 1 - - # handle the standard case - mask_1 = mask_0 == False - results[mask_1] = cov[mask_1] / (std_i[mask_1] * std_j[mask_1]) - elif prop in ['contrast', 'dissimilarity', 'homogeneity']: - weights = weights.reshape((num_level, num_level, 1, 1)) - results = np.apply_over_axes(np.sum, (P * weights), axes=(0, 1))[0, 0] - - return results diff --git a/skimage/feature/_template.pyx b/skimage/feature/_template.pyx deleted file mode 100644 index 855ece23..00000000 --- a/skimage/feature/_template.pyx +++ /dev/null @@ -1,97 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -""" -Template matching using normalized cross-correlation. - -We use fast normalized cross-correlation algorithm (see [1]_ and [2]_) to -compute match probability. This algorithm calculates the normalized -cross-correlation of an image, `I`, with a template `T` according to the -following equation:: - - sum{ I(x, y) [T(x, y) - ] } - ------------------------------------------------------- - sqrt(sum{ [I(x, y) - ]^2 } sum{ [T(x, y) - ]^2 }) - -where `` is the average of the template, and `` is the average of the -image *coincident with the template*, and sums are over the template and the -image window coincident with the template. Note that the numerator is simply -the cross-correlation of the image and the zero-mean template. - -To speed up calculations, we use summed-area tables (a.k.a. integral images) to -quickly calculate sums of image windows inside the loop. This step relies on -the following relation (see Eq. 10 of [1]):: - - sum{ [I(x, y) - ]^2 } = - sum{ I^2(x, y) } - [sum{ I(x, y) }]^2 / N_x N_y - -(Without this relation, you would need to subtract each image-window mean from -the image window *before* squaring.) - -.. [1] Briechle and Hanebeck, "Template Matching using Fast Normalized - Cross Correlation", Proceedings of the SPIE (2001). -.. [2] J. P. Lewis, "Fast Normalized Cross-Correlation", Industrial Light and - Magic. -""" - -import numpy as np -from scipy.signal import fftconvolve - -cimport numpy as cnp -from libc.math cimport sqrt, fabs -from skimage._shared.transform cimport integrate - - -from skimage.transform import integral - - -def match_template(cnp.ndarray[float, ndim=2, mode="c"] image, - cnp.ndarray[float, ndim=2, mode="c"] template): - - cdef float[:, ::1] corr - cdef float[:, ::1] image_sat - cdef float[:, ::1] image_sqr_sat - cdef float template_mean = np.mean(template) - cdef float template_ssd - cdef float inv_area - cdef Py_ssize_t r, c, r_end, c_end - cdef Py_ssize_t template_rows = template.shape[0] - cdef Py_ssize_t template_cols = template.shape[1] - cdef float den, window_sqr_sum, window_mean_sqr, window_sum - - image_sat = integral.integral_image(image) - image_sqr_sat = integral.integral_image(image**2) - - template -= template_mean - template_ssd = np.sum(template**2) - # use inversed area for accuracy - inv_area = 1.0 / (template.shape[0] * template.shape[1]) - - # when `dtype=float` is used, ascontiguousarray returns ``double``. - corr = np.ascontiguousarray(fftconvolve(image, - template[::-1, ::-1], - mode="valid"), - dtype=np.float32) - - - # move window through convolution results, normalizing in the process - for r in range(corr.shape[0]): - for c in range(corr.shape[1]): - # subtract 1 because `i_end` and `c_end` are used for indexing into - # summed-area table, instead of slicing windows of the image. - r_end = r + template_rows - 1 - c_end = c + template_cols - 1 - - window_sum = integrate(image_sat, r, c, r_end, c_end) - window_mean_sqr = window_sum * window_sum * inv_area - window_sqr_sum = integrate(image_sqr_sat, r, c, r_end, c_end) - if window_sqr_sum <= window_mean_sqr: - corr[r, c] = 0 - continue - - den = sqrt((window_sqr_sum - window_mean_sqr) * template_ssd) - corr[r, c] /= den - - return np.asarray(corr) diff --git a/skimage/feature/blob.py b/skimage/feature/blob.py new file mode 100644 index 00000000..1b465c5f --- /dev/null +++ b/skimage/feature/blob.py @@ -0,0 +1,300 @@ +import numpy as np +from scipy.ndimage.filters import gaussian_filter, gaussian_laplace +import itertools as itt +import math +from math import sqrt, hypot, log +from numpy import arccos +from skimage.util import img_as_float +from .peak import peak_local_max + + +# This basic blob detection algorithm is based on: +# http://www.cs.utah.edu/~jfishbau/advimproc/project1/ (04.04.2013) +# Theory behind: http://en.wikipedia.org/wiki/Blob_detection (04.04.2013) + + +def _blob_overlap(blob1, blob2): + """Finds the overlapping area fraction between two blobs. + + Returns a float representing fraction of overlapped area. + + Parameters + ---------- + blob1 : sequence + A sequence of ``(y,x,sigma)``, where ``x,y`` are coordinates of blob + and sigma is the standard deviation of the Gaussian kernel which + detected the blob. + blob2 : sequence + A sequence of ``(y,x,sigma)``, where ``x,y`` are coordinates of blob + and sigma is the standard deviation of the Gaussian kernel which + detected the blob. + + Returns + ------- + f : float + Fraction of overlapped area. + + """ + root2 = sqrt(2) + + # extent of the blob is given by sqrt(2)*scale + r1 = blob1[2] * root2 + r2 = blob2[2] * root2 + + d = hypot(blob1[0] - blob2[0], blob1[1] - blob2[1]) + + if d > r1 + r2: + return 0 + + # one blob is inside the other, the smaller blob must die + if d <= abs(r1 - r2): + return 1 + + acos1 = arccos((d ** 2 + r1 ** 2 - r2 ** 2) / (2 * d * r1)) + acos2 = arccos((d ** 2 + r2 ** 2 - r1 ** 2) / (2 * d * r2)) + a = -d + r2 + r1 + b = d - r2 + r1 + c = d + r2 - r1 + d = d + r2 + r1 + area = r1 ** 2 * acos1 + r2 ** 2 * acos2 - 0.5 * sqrt(abs(a * b * c * d)) + + return area / (math.pi * (min(r1, r2) ** 2)) + + +def _prune_blobs(blobs_array, overlap): + """Eliminated blobs with area overlap. + + Parameters + ---------- + blobs_array : ndarray + A 2d array with each row representing 3 values, ``(y,x,sigma)`` + where ``(y,x)`` are coordinates of the blob and ``sigma`` is the + standard deviation of the Gaussian kernel which detected the blob. + overlap : float + A value between 0 and 1. If the fraction of area overlapping for 2 + blobs is greater than `overlap` the smaller blob is eliminated. + + Returns + ------- + A : ndarray + `array` with overlapping blobs removed. + + """ + + # iterating again might eliminate more blobs, but one iteration suffices + # for most cases + for blob1, blob2 in itt.combinations(blobs_array, 2): + if _blob_overlap(blob1, blob2) > overlap: + if blob1[2] > blob2[2]: + blob2[2] = -1 + else: + blob1[2] = -1 + + # return blobs_array[blobs_array[:, 2] > 0] + return np.array([b for b in blobs_array if b[2] > 0]) + + +def blob_dog(image, min_sigma=1, max_sigma=50, sigma_ratio=1.6, threshold=2.0, + overlap=.5,): + """Finds blobs in the given grayscale image. + + Blobs are found using the Difference of Gaussian (DoG) method [1]_. + For each blob found, the method returns its coordinates and the standard + deviation of the Gaussian kernel that detected the blob. + + Parameters + ---------- + image : ndarray + Input grayscale image, blobs are assumed to be light on dark + background (white on black). + min_sigma : float, optional + The minimum standard deviation for Gaussian Kernel. Keep this low to + detect smaller blobs. + max_sigma : float, optional + The maximum standard deviation for Gaussian Kernel. Keep this high to + detect larger blobs. + sigma_ratio : float, optional + The ratio between the standard deviation of Gaussian Kernels used for + computing the Difference of Gaussians + threshold : float, optional. + The absolute lower bound for scale space maxima. Local maxima smaller + than thresh are ignored. Reduce this to detect blobs with less + intensities. + overlap : float, optional + A value between 0 and 1. If the area of two blobs overlaps by a + fraction greater than `threshold`, the smaller blob is eliminated. + + Returns + ------- + A : (n, 3) ndarray + A 2d array with each row representing 3 values, ``(y,x,sigma)`` + where ``(y,x)`` are coordinates of the blob and ``sigma`` is the + standard deviation of the Gaussian kernel which detected the blob. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Blob_detection#The_difference_of_Gaussians_approach + + Examples + -------- + >>> from skimage import data, feature + >>> feature.blob_dog(data.coins(), threshold=.5, max_sigma=40) + array([[ 45, 336, 16], + [ 52, 155, 16], + [ 52, 216, 16], + [ 54, 42, 16], + [ 54, 276, 10], + [ 58, 100, 10], + [120, 272, 16], + [124, 337, 10], + [125, 45, 16], + [125, 208, 10], + [127, 102, 10], + [128, 154, 10], + [185, 347, 16], + [193, 213, 16], + [194, 277, 16], + [195, 102, 16], + [196, 43, 10], + [198, 155, 10], + [260, 46, 16], + [261, 173, 16], + [263, 245, 16], + [263, 302, 16], + [267, 115, 10], + [267, 359, 16]]) + + Notes + ----- + The radius of each blob is approximately :math:`\sqrt{2}sigma`. + """ + + if image.ndim != 2: + raise ValueError("'image' must be a grayscale ") + + image = img_as_float(image) + + # k such that min_sigma*(sigma_ratio**k) > max_sigma + k = int(log(float(max_sigma) / min_sigma, sigma_ratio)) + 1 + + # a geometric progression of standard deviations for gaussian kernels + sigma_list = np.array([min_sigma * (sigma_ratio ** i) + for i in range(k + 1)]) + + gaussian_images = [gaussian_filter(image, s) for s in sigma_list] + + # computing difference between two successive Gaussian blurred images + # multiplying with standard deviation provides scale invariance + dog_images = [(gaussian_images[i] - gaussian_images[i + 1]) + * sigma_list[i] for i in range(k)] + image_cube = np.dstack(dog_images) + + # local_maxima = get_local_maxima(image_cube, threshold) + local_maxima = peak_local_max(image_cube, threshold_abs=threshold, + footprint=np.ones((3, 3, 3)), + threshold_rel=0.0, + exclude_border=False) + + # Convert the last index to its corresponding scale value + local_maxima[:, 2] = sigma_list[local_maxima[:, 2]] + return _prune_blobs(local_maxima, overlap) + + +def blob_log(image, min_sigma=1, max_sigma=50, num_sigma=10, threshold=.2, + overlap=.5, log_scale=False): + """Finds blobs in the given grayscale image. + + Blobs are found using the Laplacian of Gaussian (LoG) method [1]_. + For each blob found, the method returns its coordinates and the standard + deviation of the Gaussian kernel that detected the blob. + + Parameters + ---------- + image : ndarray + Input grayscale image, blobs are assumed to be light on dark + background (white on black). + min_sigma : float, optional + The minimum standard deviation for Gaussian Kernel. Keep this low to + detect smaller blobs. + max_sigma : float, optional + The maximum standard deviation for Gaussian Kernel. Keep this high to + detect larger blobs. + num_sigma : int, optional + The number of intermediate values of standard deviations to consider + between `min_sigma` and `max_sigma`. + threshold : float, optional. + The absolute lower bound for scale space maxima. Local maxima smaller + than thresh are ignored. Reduce this to detect blobs with less + intensities. + overlap : float, optional + A value between 0 and 1. If the area of two blobs overlaps by a + fraction greater than `threshold`, the smaller blob is eliminated. + log_scale : bool, optional + If set intermediate values of standard deviations are interpolated + using a logarithmic scale to the base `10`. If not, linear + interpolation is used. + + Returns + ------- + A : (n, 3) ndarray + A 2d array with each row representing 3 values, ``(y,x,sigma)`` + where ``(y,x)`` are coordinates of the blob and ``sigma`` is the + standard deviation of the Gaussian kernel which detected the blob. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Blob_detection#The_Laplacian_of_Gaussian + + Examples + -------- + >>> from skimage import data, feature, exposure + >>> img = data.coins() + >>> img = exposure.equalize_hist(img) # improves detection + >>> feature.blob_log(img, threshold = .3) + array([[113, 323, 1], + [121, 272, 17], + [124, 336, 11], + [126, 46, 11], + [126, 208, 11], + [127, 102, 11], + [128, 154, 11], + [185, 344, 17], + [194, 213, 17], + [194, 276, 17], + [197, 44, 11], + [198, 103, 11], + [198, 155, 11], + [260, 174, 17], + [263, 244, 17], + [263, 302, 17], + [266, 115, 11]]) + + Notes + ----- + The radius of each blob is approximately :math:`\sqrt{2}sigma`. + """ + + if image.ndim != 2: + raise ValueError("'image' must be a grayscale ") + + image = img_as_float(image) + + if log_scale: + start, stop = log(min_sigma, 10), log(max_sigma, 10) + sigma_list = np.logspace(start, stop, num_sigma) + else: + sigma_list = np.linspace(min_sigma, max_sigma, num_sigma) + + #computing gaussian laplace + #s**2 provides scale invariance + gl_images = [-gaussian_laplace(image, s) * s ** 2 for s in sigma_list] + image_cube = np.dstack(gl_images) + + local_maxima = peak_local_max(image_cube, threshold_abs=threshold, + footprint=np.ones((3, 3, 3)), + threshold_rel=0.0, + exclude_border=False) + + # Convert the last index to its corresponding scale value + local_maxima[:, 2] = sigma_list[local_maxima[:, 2]] + return _prune_blobs(local_maxima, overlap) diff --git a/skimage/feature/brief.py b/skimage/feature/brief.py new file mode 100644 index 00000000..d1626f17 --- /dev/null +++ b/skimage/feature/brief.py @@ -0,0 +1,181 @@ +import numpy as np +from scipy.ndimage.filters import gaussian_filter + +from .util import (DescriptorExtractor, _mask_border_keypoints, + _prepare_grayscale_input_2D) + +from .brief_cy import _brief_loop + + +class BRIEF(DescriptorExtractor): + + """BRIEF binary descriptor extractor. + + BRIEF (Binary Robust Independent Elementary Features) is an efficient + feature point descriptor. It is highly discriminative even when using + relatively few bits and is computed using simple intensity difference + tests. + + For each keypoint, intensity comparisons are carried out for a specifically + distributed number N of pixel-pairs resulting in a binary descriptor of + length N. For binary descriptors the Hamming distance can be used for + feature matching, which leads to lower computational cost in comparison to + the L2 norm. + + Parameters + ---------- + descriptor_size : int, optional + Size of BRIEF descriptor for each keypoint. Sizes 128, 256 and 512 + recommended by the authors. Default is 256. + patch_size : int, optional + Length of the two dimensional square patch sampling region around + the keypoints. Default is 49. + mode : {'normal', 'uniform'}, optional + Probability distribution for sampling location of decision pixel-pairs + around keypoints. + sample_seed : int, optional + Seed for the random sampling of the decision pixel-pairs. From a square + window with length `patch_size`, pixel pairs are sampled using the + `mode` parameter to build the descriptors using intensity comparison. + The value of `sample_seed` must be the same for the images to be + matched while building the descriptors. + sigma : float, optional + Standard deviation of the Gaussian low-pass filter applied to the image + to alleviate noise sensitivity, which is strongly recommended to obtain + discriminative and good descriptors. + + Attributes + ---------- + descriptors : (Q, `descriptor_size`) array of dtype bool + 2D ndarray of binary descriptors of size `descriptor_size` for Q + keypoints after filtering out border keypoints with value at an + index ``(i, j)`` either being ``True`` or ``False`` representing + the outcome of the intensity comparison for i-th keypoint on j-th + decision pixel-pair. It is ``Q == np.sum(mask)``. + mask : (N, ) array of dtype bool + Mask indicating whether a keypoint has been filtered out + (``False``) or is described in the `descriptors` array (``True``). + + Examples + -------- + >>> from skimage.feature import (corner_harris, corner_peaks, BRIEF, + ... match_descriptors) + >>> import numpy as np + >>> square1 = np.zeros((8, 8), dtype=np.int32) + >>> square1[2:6, 2:6] = 1 + >>> square1 + array([[0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) + >>> square2 = np.zeros((9, 9), dtype=np.int32) + >>> square2[2:7, 2:7] = 1 + >>> square2 + array([[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) + >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) + >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) + >>> extractor = BRIEF(patch_size=5) + >>> extractor.extract(square1, keypoints1) + >>> descriptors1 = extractor.descriptors + >>> extractor.extract(square2, keypoints2) + >>> descriptors2 = extractor.descriptors + >>> matches = match_descriptors(descriptors1, descriptors2) + >>> matches + array([[0, 0], + [1, 1], + [2, 2], + [3, 3]]) + >>> keypoints1[matches[:, 0]] + array([[2, 2], + [2, 5], + [5, 2], + [5, 5]]) + >>> keypoints2[matches[:, 1]] + array([[2, 2], + [2, 6], + [6, 2], + [6, 6]]) + + """ + + def __init__(self, descriptor_size=256, patch_size=49, + mode='normal', sigma=1, sample_seed=1): + + mode = mode.lower() + if mode not in ('normal', 'uniform'): + raise ValueError("`mode` must be 'normal' or 'uniform'.") + + self.descriptor_size = descriptor_size + self.patch_size = patch_size + self.mode = mode + self.sigma = sigma + self.sample_seed = sample_seed + + self.descriptors = None + self.mask = None + + def extract(self, image, keypoints): + """Extract BRIEF binary descriptors for given keypoints in image. + + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + + """ + + np.random.seed(self.sample_seed) + + image = _prepare_grayscale_input_2D(image) + + # Gaussian low-pass filtering to alleviate noise sensitivity + image = np.ascontiguousarray(gaussian_filter(image, self.sigma)) + + # Sampling pairs of decision pixels in patch_size x patch_size window + desc_size = self.descriptor_size + patch_size = self.patch_size + if self.mode == 'normal': + samples = (patch_size / 5.0) * np.random.randn(desc_size * 8) + samples = np.array(samples, dtype=np.int32) + samples = samples[(samples < (patch_size // 2)) + & (samples > - (patch_size - 2) // 2)] + + pos1 = samples[:desc_size * 2].reshape(desc_size, 2) + pos2 = samples[desc_size * 2:desc_size * 4].reshape(desc_size, 2) + elif self.mode == 'uniform': + samples = np.random.randint(-(patch_size - 2) // 2, + (patch_size // 2) + 1, + (desc_size * 2, 2)) + samples = np.array(samples, dtype=np.int32) + pos1, pos2 = np.split(samples, 2) + + pos1 = np.ascontiguousarray(pos1) + pos2 = np.ascontiguousarray(pos2) + + # Removing keypoints that are within (patch_size / 2) distance from the + # image border + self.mask = _mask_border_keypoints(image.shape, keypoints, + patch_size // 2) + + keypoints = np.array(keypoints[self.mask, :], dtype=np.intp, + order='C', copy=False) + + self.descriptors = np.zeros((keypoints.shape[0], desc_size), + dtype=bool, order='C') + + _brief_loop(image, self.descriptors.view(np.uint8), keypoints, + pos1, pos2) diff --git a/skimage/feature/_brief_cy.pyx b/skimage/feature/brief_cy.pyx similarity index 89% rename from skimage/feature/_brief_cy.pyx rename to skimage/feature/brief_cy.pyx index c53d85fc..8cd1afa7 100644 --- a/skimage/feature/_brief_cy.pyx +++ b/skimage/feature/brief_cy.pyx @@ -6,7 +6,7 @@ cimport numpy as cnp -def _brief_loop(double[:, ::1] image, char[:, ::1] descriptors, +def _brief_loop(double[:, ::1] image, unsigned char[:, ::1] descriptors, Py_ssize_t[:, ::1] keypoints, int[:, ::1] pos0, int[:, ::1] pos1): diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 4bb7fdda..eb69f115 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -1,9 +1,10 @@ import numpy as np from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve +from skimage.feature.util import FeatureDetector, _prepare_grayscale_input_2D + from skimage.transform import integral_image -from skimage.feature.corner import _compute_auto_correlation -from skimage.util import img_as_float +from skimage.feature import structure_tensor from skimage.morphology import octagon, star from skimage.feature.util import _mask_border_keypoints @@ -36,7 +37,7 @@ def _filter_image(image, min_scale, max_scale, mode): # make response[:, :, i] contiguous memory block item_size = response.itemsize - response.strides = (item_size * response.shape[0], item_size, + response.strides = (item_size * response.shape[1], item_size, item_size * response.shape[0] * response.shape[1]) integral_img = integral_image(image) @@ -65,19 +66,19 @@ def _filter_image(image, min_scale, max_scale, mode): mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1] mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1] response[:, :, i] = convolve(image, - _octagon_filter_kernel(mo, no, mi, ni)) + _octagon_kernel(mo, no, mi, ni)) elif mode == 'star': for i in range(max_scale - min_scale + 1): m = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][0]] n = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][1]] - response[:, :, i] = convolve(image, _star_filter_kernel(m, n)) + response[:, :, i] = convolve(image, _star_kernel(m, n)) return response -def _octagon_filter_kernel(mo, no, mi, ni): +def _octagon_kernel(mo, no, mi, ni): outer = (mo + 2 * no)**2 - 2 * no * (no + 1) inner = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) outer_weight = 1.0 / (outer - inner) @@ -91,7 +92,7 @@ def _octagon_filter_kernel(mo, no, mi, ni): return bfilter -def _star_filter_kernel(m, n): +def _star_kernel(m, n): c = m + m // 2 - n - n // 2 outer_star = star(m) inner_star = np.zeros_like(outer_star) @@ -104,29 +105,25 @@ def _star_filter_kernel(m, n): def _suppress_lines(feature_mask, image, sigma, line_threshold): - Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) - feature_mask[(Axx + Ayy) * (Axx + Ayy) - > line_threshold * (Axx * Ayy - Axy * Axy)] = False + Axx, Axy, Ayy = structure_tensor(image, sigma) + feature_mask[(Axx + Ayy) ** 2 + > line_threshold * (Axx * Ayy - Axy ** 2)] = False -def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', - non_max_threshold=0.15, line_threshold=10): - """**Experimental function**. - Extracts CenSurE keypoints along with the corresponding scale using - either Difference of Boxes, Octagon or STAR bi-level filter. +class CENSURE(FeatureDetector): - Parameters - ---------- - image : 2D ndarray - Input image. - min_scale : int + """CENSURE keypoint detector. + + min_scale : int, optional Minimum scale to extract keypoints from. - max_scale : int + max_scale : int, optional Maximum scale to extract keypoints from. The keypoints will be extracted from all the scales except the first and the last i.e. - from the scales in the range [min_scale + 1, max_scale - 1]. - mode : {'DoB', 'Octagon', 'STAR'} + from the scales in the range [min_scale + 1, max_scale - 1]. The filter + sizes for different scales is such that the two adjacent scales + comprise of an octave. + mode : {'DoB', 'Octagon', 'STAR'}, optional Type of bi-level filter used to get the scales of the input image. Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes represent the shape of the bi-level filters i.e. box(square), octagon @@ -135,24 +132,24 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', weights being uniformly negative in both the inner octagon while uniformly positive in the difference region. Use STAR and Octagon for better features and DoB for better performance. - non_max_threshold : float + non_max_threshold : float, optional Threshold value used to suppress maximas and minimas with a weak magnitude response obtained after Non-Maximal Suppression. - line_threshold : float + line_threshold : float, optional Threshold for rejecting interest points which have ratio of principal curvatures greater than this value. - Returns - ------- + Attributes + ---------- keypoints : (N, 2) array - Location of the extracted keypoints in the ``(row, col)`` format. - scales : (N, 1) array - The corresponding scale of the N extracted keypoints. + Keypoint coordinates as ``(row, col)``. + scales : (N, ) array + Corresponding scales. References ---------- .. [1] Motilal Agrawal, Kurt Konolige and Morten Rufus Blas - "CenSurE: Center Surround Extremas for Realtime Feature + "CENSURE: Center Surround Extremas for Realtime Feature Detection and Matching", http://link.springer.com/content/pdf/10.1007%2F978-3-540-88693-8_8.pdf @@ -161,74 +158,129 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', Descriptors in the Context of Robot Navigation" http://www.jamris.org/01_2013/saveas.php?QUEST=JAMRIS_No01_2013_P_11-20.pdf + Examples + -------- + >>> from skimage.data import lena + >>> from skimage.color import rgb2gray + >>> from skimage.feature import CENSURE + >>> img = rgb2gray(lena()[100:300, 100:300]) + >>> censure = CENSURE() + >>> censure.detect(img) + >>> censure.keypoints + array([[ 71, 148], + [ 77, 186], + [ 78, 189], + [ 89, 174], + [127, 134], + [131, 133], + [134, 125], + [137, 125], + [149, 36], + [162, 165], + [168, 167], + [170, 5], + [171, 29], + [179, 20], + [194, 65]]) + >>> censure.scales + array([2, 4, 2, 3, 4, 2, 2, 3, 4, 6, 3, 2, 3, 4, 2]) + """ - # (1) First we generate the required scales on the input grayscale image - # using a bi-level filter and stack them up in `filter_response`. - # (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on the - # filter_response to suppress points that are neither minima or maxima in - # 3 x 3 x 3 neighbourhood. We obtain a boolean ndarray `feature_mask` - # containing all the minimas and maximas in `filter_response` as True. - # (3) Then we suppress all the points in the `feature_mask` for which the - # corresponding point in the image at a particular scale has the ratio of - # principal curvatures greater than `line_threshold`. - # (4) Finally, we remove the border keypoints and return the keypoints - # along with its corresponding scale. + def __init__(self, min_scale=1, max_scale=7, mode='DoB', + non_max_threshold=0.15, line_threshold=10): - image = np.squeeze(image) - if image.ndim != 2: - raise ValueError("Only 2-D gray-scale images supported.") + mode = mode.lower() + if mode not in ('dob', 'octagon', 'star'): + raise ValueError("`mode` must be one of 'DoB', 'Octagon', 'STAR'.") - mode = mode.lower() - if mode not in ('dob', 'octagon', 'star'): - raise ValueError('Mode must be one of "DoB", "Octagon", "STAR".') + if min_scale < 1 or max_scale < 1 or max_scale - min_scale < 2: + raise ValueError('The scales must be >= 1 and the number of ' + 'scales should be >= 3.') - if min_scale < 1 or max_scale < 1 or max_scale - min_scale < 2: - raise ValueError('The scales must be >= 1 and the number of scales ' - 'should be >= 3.') + self.min_scale = min_scale + self.max_scale = max_scale + self.mode = mode + self.non_max_threshold = non_max_threshold + self.line_threshold = line_threshold - image = img_as_float(image) - image = np.ascontiguousarray(image) + self.keypoints = None + self.scales = None - # Generating all the scales - filter_response = _filter_image(image, min_scale, max_scale, mode) + def detect(self, image): + """Detect CENSURE keypoints along with the corresponding scale. - # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 - # neighbourhood to zero - minimas = minimum_filter(filter_response, (3, 3, 3)) == filter_response - maximas = maximum_filter(filter_response, (3, 3, 3)) == filter_response + Parameters + ---------- + image : 2D ndarray + Input image. - feature_mask = minimas | maximas - feature_mask[filter_response < non_max_threshold] = False + """ - for i in range(1, max_scale - min_scale): - # sigma = (window_size - 1) / 6.0, so the window covers > 99% of the - # kernel's distribution - # window_size = 7 + 2 * (min_scale - 1 + i) - # Hence sigma = 1 + (min_scale - 1 + i)/ 3.0 - _suppress_lines(feature_mask[:, :, i], image, - (1 + (min_scale + i - 1) / 3.0), line_threshold) + # (1) First we generate the required scales on the input grayscale + # image using a bi-level filter and stack them up in `filter_response`. - rows, cols, scales = np.nonzero(feature_mask[..., 1:max_scale - min_scale]) - keypoints = np.column_stack([rows, cols]) - scales = scales + min_scale + 1 + # (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on + # the filter_response to suppress points that are neither minima or + # maxima in 3 x 3 x 3 neighbourhood. We obtain a boolean ndarray + # `feature_mask` containing all the minimas and maximas in + # `filter_response` as True. + # (3) Then we suppress all the points in the `feature_mask` for which + # the corresponding point in the image at a particular scale has the + # ratio of principal curvatures greater than `line_threshold`. + # (4) Finally, we remove the border keypoints and return the keypoints + # along with its corresponding scale. - if mode == 'dob': - return keypoints, scales + num_scales = self.max_scale - self.min_scale - cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool) + image = np.ascontiguousarray(_prepare_grayscale_input_2D(image)) - if mode == 'octagon': - for i in range(min_scale + 1, max_scale): - c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 \ - + OCTAGON_OUTER_SHAPE[i - 1][1] - cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ - & (scales == i) - elif mode == 'star': - for i in range(min_scale + 1, max_scale): - c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] \ - + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 - cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ - & (scales == i) + # Generating all the scales + filter_response = _filter_image(image, self.min_scale, self.max_scale, + self.mode) - return keypoints[cumulative_mask], scales[cumulative_mask] + # Suppressing points that are neither minima or maxima in their + # 3 x 3 x 3 neighborhood to zero + minimas = minimum_filter(filter_response, (3, 3, 3)) == filter_response + maximas = maximum_filter(filter_response, (3, 3, 3)) == filter_response + + feature_mask = minimas | maximas + feature_mask[filter_response < self.non_max_threshold] = False + + for i in range(1, num_scales): + # sigma = (window_size - 1) / 6.0, so the window covers > 99% of + # the kernel's distribution + # window_size = 7 + 2 * (min_scale - 1 + i) + # Hence sigma = 1 + (min_scale - 1 + i)/ 3.0 + _suppress_lines(feature_mask[:, :, i], image, + (1 + (self.min_scale + i - 1) / 3.0), + self.line_threshold) + + rows, cols, scales = np.nonzero(feature_mask[..., 1:num_scales]) + keypoints = np.column_stack([rows, cols]) + scales = scales + self.min_scale + 1 + + if self.mode == 'dob': + self.keypoints = keypoints + self.scales = scales + return + + cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool) + + if self.mode == 'octagon': + for i in range(self.min_scale + 1, self.max_scale): + c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 \ + + OCTAGON_OUTER_SHAPE[i - 1][1] + cumulative_mask |= ( + _mask_border_keypoints(image.shape, keypoints, c) + & (scales == i)) + elif self.mode == 'star': + for i in range(self.min_scale + 1, self.max_scale): + c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] \ + + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 + cumulative_mask |= ( + _mask_border_keypoints(image.shape, keypoints, c) + & (scales == i)) + + self.keypoints = keypoints[cumulative_mask] + self.scales = scales[cumulative_mask] diff --git a/skimage/feature/corner.py b/skimage/feature/corner.py index a025a7e4..48f74c27 100644 --- a/skimage/feature/corner.py +++ b/skimage/feature/corner.py @@ -1,18 +1,26 @@ import numpy as np from scipy import ndimage from scipy import stats + from skimage.color import rgb2grey from skimage.util import img_as_float, pad from skimage.feature import peak_local_max +from skimage.feature.util import _prepare_grayscale_input_2D +from skimage.feature.corner_cy import _corner_fast -def _compute_derivatives(image): +def _compute_derivatives(image, mode='constant', cval=0): """Compute derivatives in x and y direction using the Sobel operator. Parameters ---------- image : ndarray Input image. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. Returns ------- @@ -23,14 +31,82 @@ def _compute_derivatives(image): """ - imy = ndimage.sobel(image, axis=0, mode='constant', cval=0) - imx = ndimage.sobel(image, axis=1, mode='constant', cval=0) + imy = ndimage.sobel(image, axis=0, mode=mode, cval=cval) + imx = ndimage.sobel(image, axis=1, mode=mode, cval=cval) return imx, imy -def _compute_auto_correlation(image, sigma): - """Compute auto-correlation matrix using sum of squared differences. +def structure_tensor(image, sigma=1, mode='constant', cval=0): + """Compute structure tensor using sum of squared differences. + + The structure tensor A is defined as:: + + A = [Axx Axy] + [Axy Ayy] + + which is approximated by the weighted sum of squared differences in a local + window around each pixel in the image. + + Parameters + ---------- + image : ndarray + Input image. + sigma : float + Standard deviation used for the Gaussian kernel, which is used as a + weighting function for the local summation of squared differences. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + Axx : ndarray + Element of the structure tensor for each pixel in the input image. + Axy : ndarray + Element of the structure tensor for each pixel in the input image. + Ayy : ndarray + Element of the structure tensor for each pixel in the input image. + + Examples + -------- + >>> from skimage.feature import structure_tensor + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 1 + >>> Axx, Axy, Ayy = structure_tensor(square, sigma=0.1) + >>> Axx + array([[ 0., 0., 0., 0., 0.], + [ 0., 1., 0., 1., 0.], + [ 0., 4., 0., 4., 0.], + [ 0., 1., 0., 1., 0.], + [ 0., 0., 0., 0., 0.]]) + + """ + + image = _prepare_grayscale_input_2D(image) + + imx, imy = _compute_derivatives(image, mode=mode, cval=cval) + + # structure tensore + Axx = ndimage.gaussian_filter(imx * imx, sigma, mode=mode, cval=cval) + Axy = ndimage.gaussian_filter(imx * imy, sigma, mode=mode, cval=cval) + Ayy = ndimage.gaussian_filter(imy * imy, sigma, mode=mode, cval=cval) + + return Axx, Axy, Ayy + + +def hessian_matrix(image, sigma=1, mode='constant', cval=0): + """Compute Hessian matrix. + + The Hessian matrix is defined as:: + + H = [Hxx Hxy] + [Hxy Hyy] + + which is computed by convolving the image with the second derivatives + of the Gaussian kernel in the respective x- and y-directions. Parameters ---------- @@ -39,32 +115,142 @@ def _compute_auto_correlation(image, sigma): sigma : float Standard deviation used for the Gaussian kernel, which is used as weighting function for the auto-correlation matrix. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. Returns ------- - Axx : ndarray - Element of the auto-correlation matrix for each pixel in input image. - Axy : ndarray - Element of the auto-correlation matrix for each pixel in input image. - Ayy : ndarray - Element of the auto-correlation matrix for each pixel in input image. + Hxx : ndarray + Element of the Hessian matrix for each pixel in the input image. + Hxy : ndarray + Element of the Hessian matrix for each pixel in the input image. + Hyy : ndarray + Element of the Hessian matrix for each pixel in the input image. + + Examples + -------- + >>> from skimage.feature import hessian_matrix, hessian_matrix_eigvals + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 1 + >>> Hxx, Hxy, Hyy = hessian_matrix(square, sigma=0.1) + >>> Hxx + array([[ 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0.], + [ 0., 0., 1., 0., 0.], + [ 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0.]]) """ - if image.ndim == 3: - image = img_as_float(rgb2grey(image)) + image = _prepare_grayscale_input_2D(image) - imx, imy = _compute_derivatives(image) + # window extent to the left and right, which covers > 99% of the normal + # distribution + window_ext = max(1, np.ceil(3 * sigma)) - # structure tensore - Axx = ndimage.gaussian_filter(imx * imx, sigma, mode='constant', cval=0) - Axy = ndimage.gaussian_filter(imx * imy, sigma, mode='constant', cval=0) - Ayy = ndimage.gaussian_filter(imy * imy, sigma, mode='constant', cval=0) + ky, kx = np.mgrid[-window_ext:window_ext + 1, -window_ext:window_ext + 1] - return Axx, Axy, Ayy + # second derivative Gaussian kernels + gaussian_exp = np.exp(-(kx ** 2 + ky ** 2) / (2 * sigma ** 2)) + kernel_xx = 1 / (2 * np.pi * sigma ** 4) * (kx ** 2 / sigma ** 2 - 1) + kernel_xx *= gaussian_exp + kernel_xx /= kernel_xx.sum() + kernel_xy = 1 / (2 * np.pi * sigma ** 6) * (kx * ky) + kernel_xy *= gaussian_exp + kernel_xy /= kernel_xx.sum() + kernel_yy = kernel_xx.transpose() + + Hxx = ndimage.convolve(image, kernel_xx, mode=mode, cval=cval) + Hxy = ndimage.convolve(image, kernel_xy, mode=mode, cval=cval) + Hyy = ndimage.convolve(image, kernel_yy, mode=mode, cval=cval) + + return Hxx, Hxy, Hyy -def corner_kitchen_rosenfeld(image): +def _image_orthogonal_matrix22_eigvals(M00, M01, M11): + l1 = (M00 + M11) / 2 + np.sqrt(4 * M01 ** 2 + (M00 - M11) ** 2) / 2 + l2 = (M00 + M11) / 2 - np.sqrt(4 * M01 ** 2 + (M00 - M11) ** 2) / 2 + return l1, l2 + + +def structure_tensor_eigvals(Axx, Axy, Ayy): + """Compute Eigen values of structure tensor. + + Parameters + ---------- + Axx : ndarray + Element of the structure tensor for each pixel in the input image. + Axy : ndarray + Element of the structure tensor for each pixel in the input image. + Ayy : ndarray + Element of the structure tensor for each pixel in the input image. + + Returns + ------- + l1 : ndarray + Larger eigen value for each input matrix. + l2 : ndarray + Smaller eigen value for each input matrix. + + Examples + -------- + >>> from skimage.feature import structure_tensor, structure_tensor_eigvals + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 1 + >>> Axx, Axy, Ayy = structure_tensor(square, sigma=0.1) + >>> structure_tensor_eigvals(Axx, Axy, Ayy)[0] + array([[ 0., 0., 0., 0., 0.], + [ 0., 2., 4., 2., 0.], + [ 0., 4., 0., 4., 0.], + [ 0., 2., 4., 2., 0.], + [ 0., 0., 0., 0., 0.]]) + + """ + + return _image_orthogonal_matrix22_eigvals(Axx, Axy, Ayy) + + +def hessian_matrix_eigvals(Hxx, Hxy, Hyy): + """Compute Eigen values of Hessian matrix. + + Parameters + ---------- + Hxx : ndarray + Element of the Hessian matrix for each pixel in the input image. + Hxy : ndarray + Element of the Hessian matrix for each pixel in the input image. + Hyy : ndarray + Element of the Hessian matrix for each pixel in the input image. + + Returns + ------- + l1 : ndarray + Larger eigen value for each input matrix. + l2 : ndarray + Smaller eigen value for each input matrix. + + Examples + -------- + >>> from skimage.feature import hessian_matrix, hessian_matrix_eigvals + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 1 + >>> Hxx, Hxy, Hyy = hessian_matrix(square, sigma=0.1) + >>> hessian_matrix_eigvals(Hxx, Hxy, Hyy)[0] + array([[ 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0.], + [ 0., 0., 1., 0., 0.], + [ 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0.]]) + + """ + + return _image_orthogonal_matrix22_eigvals(Hyy, Hxy, Hyy) + + +def corner_kitchen_rosenfeld(image, mode='constant', cval=0): """Compute Kitchen and Rosenfeld corner measure response image. The corner measure is calculated as follows:: @@ -79,6 +265,11 @@ def corner_kitchen_rosenfeld(image): ---------- image : ndarray Input image. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. Returns ------- @@ -87,9 +278,9 @@ def corner_kitchen_rosenfeld(image): """ - imx, imy = _compute_derivatives(image) - imxx, imxy = _compute_derivatives(imx) - imyx, imyy = _compute_derivatives(imy) + imx, imy = _compute_derivatives(image, mode=mode, cval=cval) + imxx, imxy = _compute_derivatives(imx, mode=mode, cval=cval) + imyx, imyy = _compute_derivatives(imy, mode=mode, cval=cval) numerator = (imxx * imy**2 + imyy * imx**2 - 2 * imxy * imx * imy) denominator = (imx**2 + imy**2) @@ -147,9 +338,9 @@ def corner_harris(image, method='k', k=0.05, eps=1e-6, sigma=1): Examples -------- >>> from skimage.feature import corner_harris, corner_peaks - >>> square = np.zeros([10, 10], dtype=int) + >>> square = np.zeros([10, 10]) >>> square[2:8, 2:8] = 1 - >>> square + >>> square.astype(int) array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], @@ -168,7 +359,7 @@ def corner_harris(image, method='k', k=0.05, eps=1e-6, sigma=1): """ - Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + Axx, Axy, Ayy = structure_tensor(image, sigma) # determinant detA = Axx * Ayy - Axy**2 @@ -217,9 +408,9 @@ def corner_shi_tomasi(image, sigma=1): Examples -------- >>> from skimage.feature import corner_shi_tomasi, corner_peaks - >>> square = np.zeros([10, 10], dtype=int) + >>> square = np.zeros([10, 10]) >>> square[2:8, 2:8] = 1 - >>> square + >>> square.astype(int) array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], @@ -238,7 +429,7 @@ def corner_shi_tomasi(image, sigma=1): """ - Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + Axx, Axy, Ayy = structure_tensor(image, sigma) # minimum eigenvalue of A response = ((Axx + Ayy) - np.sqrt((Axx - Ayy)**2 + 4 * Axy**2)) / 2 @@ -308,7 +499,7 @@ def corner_foerstner(image, sigma=1): """ - Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + Axx, Axy, Ayy = structure_tensor(image, sigma) # determinant detA = Axx * Ayy - Axy**2 @@ -326,6 +517,69 @@ def corner_foerstner(image, sigma=1): return w, q +def corner_fast(image, n=12, threshold=0.15): + """Extract FAST corners for a given image. + + Parameters + ---------- + image : 2D ndarray + Input image. + n : int + Minimum number of consecutive pixels out of 16 pixels on the circle + that should all be either brighter or darker w.r.t testpixel. + A point c on the circle is darker w.r.t test pixel p if + `Ic < Ip - threshold` and brighter if `Ic > Ip + threshold`. Also + stands for the n in `FAST-n` corner detector. + threshold : float + Threshold used in deciding whether the pixels on the circle are + brighter, darker or similar w.r.t. the test pixel. Decrease the + threshold when more corners are desired and vice-versa. + + Returns + ------- + response : ndarray + FAST corner response image. + + References + ---------- + .. [1] Edward Rosten and Tom Drummond + "Machine Learning for high-speed corner detection", + http://www.edwardrosten.com/work/rosten_2006_machine.pdf + .. [2] Wikipedia, "Features from accelerated segment test", + https://en.wikipedia.org/wiki/Features_from_accelerated_segment_test + + Examples + -------- + >>> from skimage.feature import corner_fast, corner_peaks + >>> square = np.zeros((12, 12)) + >>> square[3:9, 3:9] = 1 + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> corner_peaks(corner_fast(square, 9), min_distance=1) + array([[3, 3], + [3, 8], + [8, 3], + [8, 8]]) + + """ + image = _prepare_grayscale_input_2D(image) + + image = np.ascontiguousarray(image) + response = _corner_fast(image, n, threshold) + return response + + def corner_subpix(image, corners, window_size=11, alpha=0.99): """Determine subpixel position of corners. @@ -354,10 +608,10 @@ def corner_subpix(image, corners, window_size=11, alpha=0.99): Examples -------- >>> from skimage.feature import corner_harris, corner_peaks, corner_subpix - >>> img = np.zeros((10, 10), dtype=int) + >>> img = np.zeros((10, 10)) >>> img[:5, :5] = 1 >>> img[5:, 5:] = 1 - >>> img + >>> img.astype(int) array([[1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], @@ -408,7 +662,7 @@ def corner_subpix(image, corners, window_size=11, alpha=0.99): maxx = x0 + wext + 2 window = image[miny:maxy, minx:maxx] - winx, winy = _compute_derivatives(window) + winx, winy = _compute_derivatives(window, mode='constant', cval=0) # compute gradient suares and remove border winx_winx = (winx * winx)[1:-1, 1:-1] diff --git a/skimage/feature/corner_cy.pyx b/skimage/feature/corner_cy.pyx index c459ee92..86ad3c43 100644 --- a/skimage/feature/corner_cy.pyx +++ b/skimage/feature/corner_cy.pyx @@ -5,9 +5,12 @@ import numpy as np cimport numpy as cnp from libc.float cimport DBL_MAX +from libc.math cimport atan2 +from skimage.util import img_as_float, pad from skimage.color import rgb2grey -from skimage.util import img_as_float + +from .util import _prepare_grayscale_input_2D def corner_moravec(image, Py_ssize_t window_size=1): @@ -30,30 +33,30 @@ def corner_moravec(image, Py_ssize_t window_size=1): References ---------- - ..[1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/moravec.htm - ..[2] http://en.wikipedia.org/wiki/Corner_detection + .. [1] http://kiwi.cs.dal.ca/~dparks/CornerDetection/moravec.htm + .. [2] http://en.wikipedia.org/wiki/Corner_detection Examples -------- - >>> from skimage.feature import corner_moravec, peak_local_max + >>> from skimage.feature import corner_moravec >>> square = np.zeros([7, 7]) >>> square[3, 3] = 1 - >>> square - array([[ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 1., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.]]) - >>> corner_moravec(square) - array([[ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 2., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0.]]) + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]]) + >>> corner_moravec(square).astype(int) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 2, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]]) """ cdef Py_ssize_t rows = image.shape[0] @@ -80,3 +83,193 @@ def corner_moravec(image, Py_ssize_t window_size=1): out[r, c] = min_msum return np.asarray(out) + + +cdef inline double _corner_fast_response(double curr_pixel, + double* circle_intensities, + char* bins, char state, char n): + cdef char consecutive_count = 0 + cdef double curr_response + cdef Py_ssize_t l, m + for l in range(15 + n): + if bins[l % 16] == state: + consecutive_count += 1 + if consecutive_count == n: + curr_response = 0 + for m in range(16): + curr_response += abs(circle_intensities[m] - curr_pixel) + return curr_response + else: + consecutive_count = 0 + return 0 + + +def _corner_fast(double[:, ::1] image, char n, double threshold): + + cdef Py_ssize_t rows = image.shape[0] + cdef Py_ssize_t cols = image.shape[1] + + cdef Py_ssize_t i, j, k + + cdef char speed_sum_b, speed_sum_d + cdef double curr_pixel + cdef double lower_threshold, upper_threshold + cdef double[:, ::1] corner_response = np.zeros((rows, cols), + dtype=np.double) + + cdef char *rp = [0, 1, 2, 3, 3, 3, 2, 1, 0, -1, -2, -3, -3, -3, -2, -1] + cdef char *cp = [3, 3, 2, 1, 0, -1, -2, -3, -3, -3, -2, -1, 0, 1, 2, 3] + cdef char bins[16] + cdef double circle_intensities[16] + + cdef double curr_response + + for i in range(3, rows - 3): + for j in range(3, cols - 3): + + curr_pixel = image[i, j] + lower_threshold = curr_pixel - threshold + upper_threshold = curr_pixel + threshold + + for k in range(16): + circle_intensities[k] = image[i + rp[k], j + cp[k]] + if circle_intensities[k] > upper_threshold: + # Brighter pixel + bins[k] = 'b' + elif circle_intensities[k] < lower_threshold: + # Darker pixel + bins[k] = 'd' + else: + # Similar pixel + bins[k] = 's' + + # High speed test for n >= 12 + if n >= 12: + speed_sum_b = 0 + speed_sum_d = 0 + for k in range(0, 16, 4): + if bins[k] == 'b': + speed_sum_b += 1 + elif bins[k] == 'd': + speed_sum_d += 1 + if speed_sum_d < 3 and speed_sum_b < 3: + continue + + # Test for bright pixels + curr_response = \ + _corner_fast_response(curr_pixel, circle_intensities, + bins, 'b', n) + + # Test for dark pixels + if curr_response == 0: + curr_response = \ + _corner_fast_response(curr_pixel, circle_intensities, + bins, 'd', n) + + corner_response[i, j] = curr_response + + return np.asarray(corner_response) + + +def corner_orientations(image, Py_ssize_t[:, :] corners, mask): + """Compute the orientation of corners. + + The orientation of corners is computed using the first order central moment + i.e. the center of mass approach. The corner orientation is the angle of + the vector from the corner coordinate to the intensity centroid in the + local neighborhood around the corner calculated using first order central + moment. + + Parameters + ---------- + image : 2D array + Input grayscale image. + corners : (N, 2) array + Corner coordinates as ``(row, col)``. + mask : 2D array + Mask defining the local neighborhood of the corner used for the + calculation of the central moment. + + Returns + ------- + orientations : (N, 1) array + Orientations of corners in the range [-pi, pi]. + + References + ---------- + .. [1] Ethan Rublee, Vincent Rabaud, Kurt Konolige and Gary Bradski + "ORB : An efficient alternative to SIFT and SURF" + http://www.vision.cs.chubu.ac.jp/CV-R/pdf/Rublee_iccv2011.pdf + .. [2] Paul L. Rosin, "Measuring Corner Properties" + http://users.cs.cf.ac.uk/Paul.Rosin/corner2.pdf + + Examples + -------- + >>> from skimage.morphology import octagon + >>> from skimage.feature import (corner_fast, corner_peaks, + ... corner_orientations) + >>> square = np.zeros((12, 12)) + >>> square[3:9, 3:9] = 1 + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> corners = corner_peaks(corner_fast(square, 9), min_distance=1) + >>> corners + array([[3, 3], + [3, 8], + [8, 3], + [8, 8]]) + >>> orientations = corner_orientations(square, corners, octagon(3, 2)) + >>> np.rad2deg(orientations) + array([ 45., 135., -45., -135.]) + + """ + + image = _prepare_grayscale_input_2D(image) + + if mask.shape[0] % 2 != 1 or mask.shape[1] % 2 != 1: + raise ValueError("Size of mask must be uneven.") + + cdef unsigned char[:, ::1] cmask = np.ascontiguousarray(mask != 0, + dtype=np.uint8) + + cdef Py_ssize_t i, r, c, r0, c0 + cdef Py_ssize_t mrows = mask.shape[0] + cdef Py_ssize_t mcols = mask.shape[1] + cdef Py_ssize_t mrows2 = (mrows - 1) / 2 + cdef Py_ssize_t mcols2 = (mcols - 1) / 2 + cdef double[:, :] cimage = pad(image, (mrows2, mcols2), mode='constant', + constant_values=0) + cdef double[:] orientations = np.zeros(corners.shape[0], dtype=np.double) + cdef double curr_pixel + cdef double m01, m10, m01_tmp + + for i in range(corners.shape[0]): + r0 = corners[i, 0] + c0 = corners[i, 1] + + m01 = 0 + m10 = 0 + + for r in range(mrows): + m01_tmp = 0 + for c in range(mcols): + if cmask[r, c]: + curr_pixel = cimage[r0 + r, c0 + c] + m10 += curr_pixel * (c - mcols2) + m01_tmp += curr_pixel + m01 += m01_tmp * (r - mrows2) + + orientations[i] = atan2(m01, m10) + + return np.asarray(orientations) diff --git a/skimage/feature/match.py b/skimage/feature/match.py new file mode 100644 index 00000000..5cb1a5ed --- /dev/null +++ b/skimage/feature/match.py @@ -0,0 +1,70 @@ +import numpy as np +from scipy.spatial.distance import cdist + + +def match_descriptors(descriptors1, descriptors2, metric=None, p=2, + max_distance=np.inf, cross_check=True): + """Brute-force matching of descriptors. + + For each descriptor in the first set this matcher finds the closest + descriptor in the second set (and vice-versa in the case of enabled + cross-checking). + + Parameters + ---------- + descriptors1 : (M, P) array + Binary descriptors of size P about M keypoints in the first image. + descriptors2 : (N, P) array + Binary descriptors of size P about N keypoints in the second image. + metric : {'euclidean', 'cityblock', 'minkowski', 'hamming', ...} + The metric to compute the distance between two descriptors. See + `scipy.spatial.distance.cdist` for all possible types. The hamming + distance should be used for binary descriptors. By default the L2-norm + is used for all descriptors of dtype float or double and the Hamming + distance is used for binary descriptors automatically. + p : int + The p-norm to apply for ``metric='minkowski'``. + max_distance : float + Maximum allowed distance between descriptors of two keypoints + in separate images to be regarded as a match. + cross_check : bool + If True, the matched keypoints are returned after cross checking i.e. a + matched pair (keypoint1, keypoint2) is returned if keypoint2 is the + best match for keypoint1 in second image and keypoint1 is the best + match for keypoint2 in first image. + + Returns + ------- + matches : (Q, 2) array + Indices of corresponding matches in first and second set of + descriptors, where ``matches[:, 0]`` denote the indices in the first + and ``matches[:, 1]`` the indices in the second set of descriptors. + + """ + + if descriptors1.shape[1] != descriptors2.shape[1]: + raise ValueError("Descriptor length must equal.") + + if metric is None: + if np.issubdtype(descriptors1.dtype, np.bool): + metric = 'hamming' + else: + metric = 'euclidean' + + distances = cdist(descriptors1, descriptors2, metric=metric, p=p) + + indices1 = np.arange(descriptors1.shape[0]) + indices2 = np.argmin(distances, axis=1) + + if cross_check: + matches1 = np.argmin(distances, axis=0) + mask = indices1 == matches1[indices2] + indices1 = indices1[mask] + indices2 = indices2[mask] + + matches = np.column_stack((indices1, indices2)) + + if max_distance < np.inf: + matches = matches[distances[indices1, indices2] < max_distance] + + return matches diff --git a/skimage/feature/orb.py b/skimage/feature/orb.py new file mode 100644 index 00000000..2ddcf4f3 --- /dev/null +++ b/skimage/feature/orb.py @@ -0,0 +1,336 @@ +import numpy as np + +from skimage.feature.util import (FeatureDetector, DescriptorExtractor, + _mask_border_keypoints, + _prepare_grayscale_input_2D) + +from skimage.feature import (corner_fast, corner_orientations, corner_peaks, + corner_harris) +from skimage.transform import pyramid_gaussian + +from .orb_cy import _orb_loop + + +OFAST_MASK = np.zeros((31, 31)) +OFAST_UMAX = [15, 15, 15, 15, 14, 14, 14, 13, 13, 12, 11, 10, 9, 8, 6, 3] +for i in range(-15, 16): + for j in range(-OFAST_UMAX[abs(i)], OFAST_UMAX[abs(i)] + 1): + OFAST_MASK[15 + j, 15 + i] = 1 + + +class ORB(FeatureDetector, DescriptorExtractor): + + """Oriented FAST and rotated BRIEF feature detector and binary descriptor + extractor. + + Parameters + ---------- + n_keypoints : int, optional + Number of keypoints to be returned. The function will return the best + `n_keypoints` according to the Harris corner response if more than + `n_keypoints` are detected. If not, then all the detected keypoints + are returned. + fast_n : int, optional + The `n` parameter in `skimage.feature.corner_fast`. Minimum number of + consecutive pixels out of 16 pixels on the circle that should all be + either brighter or darker w.r.t test-pixel. A point c on the circle is + darker w.r.t test pixel p if ``Ic < Ip - threshold`` and brighter if + ``Ic > Ip + threshold``. Also stands for the n in ``FAST-n`` corner + detector. + fast_threshold : float, optional + The ``threshold`` parameter in ``feature.corner_fast``. Threshold used + to decide whether the pixels on the circle are brighter, darker or + similar w.r.t. the test pixel. Decrease the threshold when more + corners are desired and vice-versa. + harris_k : float, optional + The `k` parameter in `skimage.feature.corner_harris`. Sensitivity + factor to separate corners from edges, typically in range ``[0, 0.2]``. + Small values of `k` result in detection of sharp corners. + downscale : float, optional + Downscale factor for the image pyramid. Default value 1.2 is chosen so + that there are more dense scales which enable robust scale invariance + for a subsequent feature description. + n_scales : int, optional + Maximum number of scales from the bottom of the image pyramid to + extract the features from. + + Attributes + ---------- + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + scales : (N, ) array + Corresponding scales. + orientations : (N, ) array + Corresponding orientations in radians. + responses : (N, ) array + Corresponding Harris corner responses. + descriptors : (Q, `descriptor_size`) array of dtype bool + 2D array of binary descriptors of size `descriptor_size` for Q + keypoints after filtering out border keypoints with value at an + index ``(i, j)`` either being ``True`` or ``False`` representing + the outcome of the intensity comparison for i-th keypoint on j-th + decision pixel-pair. It is ``Q == np.sum(mask)``. + + References + ---------- + .. [1] Ethan Rublee, Vincent Rabaud, Kurt Konolige and Gary Bradski + "ORB: An efficient alternative to SIFT and SURF" + http://www.vision.cs.chubu.ac.jp/CV-R/pdf/Rublee_iccv2011.pdf + + Examples + -------- + >>> from skimage.feature import ORB, match_descriptors + >>> img1 = np.zeros((100, 100)) + >>> img2 = np.zeros_like(img1) + >>> np.random.seed(1) + >>> square = np.random.rand(20, 20) + >>> img1[40:60, 40:60] = square + >>> img2[53:73, 53:73] = square + >>> detector_extractor1 = ORB(n_keypoints=5) + >>> detector_extractor2 = ORB(n_keypoints=5) + >>> detector_extractor1.detect_and_extract(img1) + >>> detector_extractor2.detect_and_extract(img2) + >>> matches = match_descriptors(detector_extractor1.descriptors, + ... detector_extractor2.descriptors) + >>> matches + array([[0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4]]) + >>> detector_extractor1.keypoints[matches[:, 0]] + array([[ 42., 40.], + [ 47., 58.], + [ 44., 40.], + [ 59., 42.], + [ 45., 44.]]) + >>> detector_extractor2.keypoints[matches[:, 1]] + array([[ 55., 53.], + [ 60., 71.], + [ 57., 53.], + [ 72., 55.], + [ 58., 57.]]) + + """ + + def __init__(self, downscale=1.2, n_scales=8, + n_keypoints=500, fast_n=9, fast_threshold=0.08, + harris_k=0.04): + self.downscale = downscale + self.n_scales = n_scales + self.n_keypoints = n_keypoints + self.fast_n = fast_n + self.fast_threshold = fast_threshold + self.harris_k = harris_k + + self.keypoints = None + self.scales = None + self.responses = None + self.orientations = None + self.descriptors = None + + def _build_pyramid(self, image): + image = _prepare_grayscale_input_2D(image) + return list(pyramid_gaussian(image, self.n_scales - 1, self.downscale)) + + def _detect_octave(self, octave_image): + # Extract keypoints for current octave + fast_response = corner_fast(octave_image, self.fast_n, + self.fast_threshold) + keypoints = corner_peaks(fast_response, min_distance=1) + + if len(keypoints) == 0: + return (np.zeros((0, 2), dtype=np.double), + np.zeros((0, ), dtype=np.double), + np.zeros((0, ), dtype=np.double)) + + mask = _mask_border_keypoints(octave_image.shape, keypoints, + distance=16) + keypoints = keypoints[mask] + + orientations = corner_orientations(octave_image, keypoints, + OFAST_MASK) + + harris_response = corner_harris(octave_image, method='k', + k=self.harris_k) + responses = harris_response[keypoints[:, 0], keypoints[:, 1]] + + return keypoints, orientations, responses + + def detect(self, image): + """Detect oriented FAST keypoints along with the corresponding scale. + + Parameters + ---------- + image : 2D array + Input image. + + """ + + pyramid = self._build_pyramid(image) + + keypoints_list = [] + orientations_list = [] + scales_list = [] + responses_list = [] + + for octave in range(len(pyramid)): + + octave_image = np.ascontiguousarray(pyramid[octave]) + + keypoints, orientations, responses = \ + self._detect_octave(octave_image) + + keypoints_list.append(keypoints * self.downscale ** octave) + orientations_list.append(orientations) + scales_list.append(self.downscale ** octave + * np.ones(keypoints.shape[0], dtype=np.intp)) + responses_list.append(responses) + + keypoints = np.vstack(keypoints_list) + orientations = np.hstack(orientations_list) + scales = np.hstack(scales_list) + responses = np.hstack(responses_list) + + if keypoints.shape[0] < self.n_keypoints: + self.keypoints = keypoints + self.scales = scales + self.orientations = orientations + self.responses = responses + else: + # Choose best n_keypoints according to Harris corner response + best_indices = responses.argsort()[::-1][:self.n_keypoints] + self.keypoints = keypoints[best_indices] + self.scales = scales[best_indices] + self.orientations = orientations[best_indices] + self.responses = responses[best_indices] + + def _extract_octave(self, octave_image, keypoints, orientations): + mask = _mask_border_keypoints(octave_image.shape, keypoints, + distance=20) + keypoints = np.array(keypoints[mask], dtype=np.intp, order='C', + copy=False) + orientations = np.array(orientations[mask], dtype=np.double, order='C', + copy=False) + + descriptors = _orb_loop(octave_image, keypoints, orientations) + + return descriptors, mask + + def extract(self, image, keypoints, scales, orientations): + """Extract rBRIEF binary descriptors for given keypoints in image. + + Note that the keypoints must be extracted using the same `downscale` + and `n_scales` parameters. Additionally, if you want to extract both + keypoints and descriptors you should use the faster + `detect_and_extract`. + + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + scales : (N, ) array + Corresponding scales. + orientations : (N, ) array + Corresponding orientations in radians. + + """ + + pyramid = self._build_pyramid(image) + + descriptors_list = [] + mask_list = [] + + # Determine octaves from scales + octaves = (np.log(scales) / np.log(self.downscale)).astype(np.intp) + + for octave in range(len(pyramid)): + + # Mask for all keypoints in current octave + octave_mask = octaves == octave + + if np.sum(octave_mask) > 0: + + octave_image = np.ascontiguousarray(pyramid[octave]) + + octave_keypoints = keypoints[octave_mask] + octave_keypoints /= self.downscale ** octave + + octave_orientations = orientations[octave_mask] + + descriptors, mask = self._extract_octave(octave_image, + octave_keypoints, + octave_orientations) + + descriptors_list.append(descriptors) + mask_list.append(mask) + + self.descriptors = np.vstack(descriptors_list).view(np.bool) + self.mask_ = np.hstack(mask_list) + + def detect_and_extract(self, image): + """Detect oriented FAST keypoints and extract rBRIEF descriptors. + + Note that this is faster than first calling `detect` and then + `extract`. + + Parameters + ---------- + image : 2D array + Input image. + + """ + + pyramid = self._build_pyramid(image) + + keypoints_list = [] + responses_list = [] + scales_list = [] + orientations_list = [] + descriptors_list = [] + + for octave in range(len(pyramid)): + + octave_image = np.ascontiguousarray(pyramid[octave]) + + keypoints, orientations, responses = \ + self._detect_octave(octave_image) + + if len(keypoints) == 0: + keypoints_list.append(keypoints) + responses_list.append(responses) + descriptors_list.append(np.zeros((0, 256), dtype=np.bool)) + continue + + descriptors, mask = self._extract_octave(octave_image, keypoints, + orientations) + + keypoints_list.append(keypoints[mask] * self.downscale ** octave) + responses_list.append(responses[mask]) + orientations_list.append(orientations[mask]) + scales_list.append(self.downscale ** octave + * np.ones(keypoints.shape[0], dtype=np.intp)) + descriptors_list.append(descriptors) + + keypoints = np.vstack(keypoints_list) + responses = np.hstack(responses_list) + scales = np.hstack(scales_list) + orientations = np.hstack(orientations_list) + descriptors = np.vstack(descriptors_list).view(np.bool) + + if keypoints.shape[0] < self.n_keypoints: + self.keypoints = keypoints + self.scales = scales + self.orientations = orientations + self.responses = responses + self.descriptors = descriptors + else: + # Choose best n_keypoints according to Harris corner response + best_indices = responses.argsort()[::-1][:self.n_keypoints] + self.keypoints = keypoints[best_indices] + self.scales = scales[best_indices] + self.orientations = orientations[best_indices] + self.responses = responses[best_indices] + self.descriptors = descriptors[best_indices] diff --git a/skimage/feature/orb_cy.pyx b/skimage/feature/orb_cy.pyx new file mode 100644 index 00000000..6e0801f8 --- /dev/null +++ b/skimage/feature/orb_cy.pyx @@ -0,0 +1,56 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +import os +import numpy as np + +from skimage import data_dir + +cimport numpy as cnp +from libc.math cimport sin, cos + +from skimage._shared.interpolation cimport round + +POS = np.loadtxt(os.path.join(data_dir, "orb_descriptor_positions.txt"), + dtype=np.int8) +POS0 = np.ascontiguousarray(POS[:, :2]) +POS1 = np.ascontiguousarray(POS[:, 2:]) + + +def _orb_loop(double[:, ::1] image, Py_ssize_t[:, ::1] keypoints, + double[:] orientations): + + cdef Py_ssize_t i, d, kr, kc, pr0, pr1, pc0, pc1, spr0, spc0, spr1, spc1 + cdef int[:, ::1] steered_pos0, steered_pos1 + cdef double angle + cdef char[:, ::1] descriptors = np.zeros((keypoints.shape[0], + POS.shape[0]), dtype=np.uint8) + cdef char[:, ::1] cpos0 = POS0 + cdef char[:, ::1] cpos1 = POS1 + + for i in range(descriptors.shape[0]): + + angle = orientations[i] + sin_a = sin(angle) + cos_a = cos(angle) + + kr = keypoints[i, 0] + kc = keypoints[i, 1] + + for j in range(descriptors.shape[1]): + pr0 = cpos0[j, 0] + pc0 = cpos0[j, 1] + pr1 = cpos1[j, 0] + pc1 = cpos1[j, 1] + + spr0 = round(sin_a * pr0 + cos_a * pc0) + spc0 = round(cos_a * pr0 - sin_a * pc0) + spr1 = round(sin_a * pr1 + cos_a * pc1) + spc1 = round(cos_a * pr1 - sin_a * pc1) + + if image[kr + spr0, kc + spc0] < image[kr + spr1, kc + spc1]: + descriptors[i, j] = True + + return np.asarray(descriptors) diff --git a/skimage/feature/peak.py b/skimage/feature/peak.py index 3268831a..378d22b4 100644 --- a/skimage/feature/peak.py +++ b/skimage/feature/peak.py @@ -49,9 +49,9 @@ def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, Returns ------- - output : (N, 2) array or ndarray of bools + output : ndarray or ndarray of bools - * If `indices = True` : (row, column) coordinates of peaks. + * If `indices = True` : (row, column, ...) coordinates of peaks. * If `indices = False` : Boolean array shaped like `image`, with peaks represented by True values. @@ -65,10 +65,10 @@ def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, Examples -------- - >>> im = np.zeros((7, 7)) - >>> im[3, 4] = 1 - >>> im[3, 2] = 1.5 - >>> im + >>> img1 = np.zeros((7, 7)) + >>> img1[3, 4] = 1 + >>> img1[3, 2] = 1.5 + >>> img1 array([[ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], @@ -77,13 +77,18 @@ def peak_local_max(image, min_distance=10, threshold_abs=0, threshold_rel=0.1, [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ]]) - >>> peak_local_max(im, min_distance=1) + >>> peak_local_max(img1, min_distance=1) array([[3, 2], [3, 4]]) - >>> peak_local_max(im, min_distance=2) + >>> peak_local_max(img1, min_distance=2) array([[3, 2]]) + >>> img2 = np.zeros((20, 20, 20)) + >>> img2[10, 10, 10] = 1 + >>> peak_local_max(img2, exclude_border=False) + array([[10, 10, 10]]) + """ out = np.zeros_like(image, dtype=np.bool) # In the case of labels, recursively build and return an output diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index 7df64c32..9d8b4fbb 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -14,20 +14,20 @@ def configuration(parent_package='', top_path=None): cython(['corner_cy.pyx'], working_path=base_path) cython(['censure_cy.pyx'], working_path=base_path) - cython(['_brief_cy.pyx'], working_path=base_path) + cython(['orb_cy.pyx'], working_path=base_path) + cython(['brief_cy.pyx'], working_path=base_path) cython(['_texture.pyx'], working_path=base_path) - cython(['_template.pyx'], working_path=base_path) config.add_extension('corner_cy', sources=['corner_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('censure_cy', sources=['censure_cy.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_brief_cy', sources=['_brief_cy.c'], + config.add_extension('orb_cy', sources=['orb_cy.c'], + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('brief_cy', sources=['brief_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_texture', sources=['_texture.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) - config.add_extension('_template', sources=['_template.c'], - include_dirs=[get_numpy_include_dirs(), '../_shared']) return config diff --git a/skimage/feature/template.py b/skimage/feature/template.py index 5cb4d382..fbf95866 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -1,81 +1,171 @@ -"""template.py - Template matching -""" import numpy as np -from . import _template +from scipy.signal import fftconvolve + +from skimage.util import pad -def match_template(image, template, pad_input=False): - """Match a template to an image using normalized correlation. +def _window_sum_2d(image, window_shape): - The output is an array with values between -1.0 and 1.0, which correspond - to the probability that the template is found at that position. + window_sum = np.cumsum(image, axis=0) + window_sum = (window_sum[window_shape[0]:-1] + - window_sum[:-window_shape[0]-1]) + + window_sum = np.cumsum(window_sum, axis=1) + window_sum = (window_sum[:, window_shape[1]:-1] + - window_sum[:, :-window_shape[1]-1]) + + return window_sum + + +def _window_sum_3d(image, window_shape): + + window_sum = _window_sum_2d(image, window_shape) + + window_sum = np.cumsum(window_sum, axis=2) + window_sum = (window_sum[:, :, window_shape[2]:-1] + - window_sum[:, :, :-window_shape[2]-1]) + + return window_sum + + +def match_template(image, template, pad_input=False, mode='constant', + constant_values=0): + """Match a template to a 2-D or 3-D image using normalized correlation. + + The output is an array with values between -1.0 and 1.0. The value at a + given position corresponds to the correlation coefficient between the image + and the template. + + For `pad_input=True` matches correspond to the center and otherwise to the + top-left corner of the template. To find the best match you must search for + peaks in the response (output) image. Parameters ---------- - image : array_like - Image to process. - template : array_like - Template to locate. + image : (M, N[, D]) array + 2-D or 3-D input image. + template : (m, n[, d]) array + Template to locate. It must be `(m <= M, n <= N[, d <= D])`. pad_input : bool - If True, pad `image` with image mean so that output is the same size as - the image, and output values correspond to the template center. - Otherwise, the output is an array with shape `(M - m + 1, N - n + 1)` - for an `(M, N)` image and an `(m, n)` template, and matches correspond - to origin (top-left corner) of the template. + If True, pad `image` so that output is the same size as the image, and + output values correspond to the template center. Otherwise, the output + is an array with shape `(M - m + 1, N - n + 1)` for an `(M, N)` image + and an `(m, n)` template, and matches correspond to origin + (top-left corner) of the template. + mode : see `numpy.pad`, optional + Padding mode. + constant_values : see `numpy.pad`, optional + Constant values used in conjunction with ``mode='constant'``. Returns ------- - output : ndarray - Correlation results between -1.0 and 1.0. For an `(M, N)` image and an - `(m, n)` template, the `output` is `(M - m + 1, N - n + 1)` when - `pad_input = False` and `(M, N)` when `pad_input = True`. + output : array + Response image with correlation coefficients. + + References + ---------- + .. [1] Briechle and Hanebeck, "Template Matching using Fast Normalized + Cross Correlation", Proceedings of the SPIE (2001). + .. [2] J. P. Lewis, "Fast Normalized Cross-Correlation", Industrial Light + and Magic. Examples -------- >>> template = np.zeros((3, 3)) >>> template[1, 1] = 1 - >>> print(template) - [[ 0. 0. 0.] - [ 0. 1. 0.] - [ 0. 0. 0.]] + >>> template + array([[ 0., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 0.]]) >>> image = np.zeros((6, 6)) >>> image[1, 1] = 1 >>> image[4, 4] = -1 - >>> print(image) - [[ 0. 0. 0. 0. 0. 0.] - [ 0. 1. 0. 0. 0. 0.] - [ 0. 0. 0. 0. 0. 0.] - [ 0. 0. 0. 0. 0. 0.] - [ 0. 0. 0. 0. -1. 0.] - [ 0. 0. 0. 0. 0. 0.]] + >>> image + array([[ 0., 0., 0., 0., 0., 0.], + [ 0., 1., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., -1., 0.], + [ 0., 0., 0., 0., 0., 0.]]) >>> result = match_template(image, template) - >>> print(np.round(result, 3)) - [[ 1. -0.125 0. 0. ] - [-0.125 -0.125 0. 0. ] - [ 0. 0. 0.125 0.125] - [ 0. 0. 0.125 -1. ]] + >>> np.round(result, 3) + array([[ 1. , -0.125, 0. , 0. ], + [-0.125, -0.125, 0. , 0. ], + [ 0. , 0. , 0.125, 0.125], + [ 0. , 0. , 0.125, -1. ]], dtype=float32) >>> result = match_template(image, template, pad_input=True) - >>> print(np.round(result, 3)) - [[-0.125 -0.125 -0.125 0. 0. 0. ] - [-0.125 1. -0.125 0. 0. 0. ] - [-0.125 -0.125 -0.125 0. 0. 0. ] - [ 0. 0. 0. 0.125 0.125 0.125] - [ 0. 0. 0. 0.125 -1. 0.125] - [ 0. 0. 0. 0.125 0.125 0.125]] + >>> np.round(result, 3) + array([[-0.125, -0.125, -0.125, 0. , 0. , 0. ], + [-0.125, 1. , -0.125, 0. , 0. , 0. ], + [-0.125, -0.125, -0.125, 0. , 0. , 0. ], + [ 0. , 0. , 0. , 0.125, 0.125, 0.125], + [ 0. , 0. , 0. , 0.125, -1. , 0.125], + [ 0. , 0. , 0. , 0.125, 0.125, 0.125]], dtype=float32) """ + + if image.ndim not in (2, 3) or template.ndim not in (2, 3): + raise ValueError("Only 2- and 3-D images supported.") + if image.ndim < template.ndim: + raise ValueError("Dimensionality of template must be less than or " + "equal to the dimensionality of image.") if np.any(np.less(image.shape, template.shape)): raise ValueError("Image must be larger than template.") - image = np.ascontiguousarray(image, dtype=np.float32) - template = np.ascontiguousarray(template, dtype=np.float32) - if pad_input: - pad_size = tuple(np.array(image.shape) + np.array(template.shape) - 1) - pad_image = np.mean(image) * np.ones(pad_size, dtype=np.float32) - h, w = image.shape - i0, j0 = template.shape - i0 /= 2 - j0 /= 2 - pad_image[i0:i0 + h, j0:j0 + w] = image - image = pad_image - result = _template.match_template(image, template) - return result + image_shape = image.shape + + image = np.array(image, dtype=np.float32, copy=False) + + pad_width = tuple((width, width) for width in template.shape) + if mode == 'constant': + image = pad(image, pad_width=pad_width, mode=mode, + constant_values=constant_values) + else: + image = pad(image, pad_width=pad_width, mode=mode) + + # Use special case for 2-D images for much better performance in + # computation of integral images + if image.ndim == 2: + image_window_sum = _window_sum_2d(image, template.shape) + image_window_sum2 = _window_sum_2d(image**2, template.shape) + elif image.ndim == 3: + image_window_sum = _window_sum_3d(image, template.shape) + image_window_sum2 = _window_sum_3d(image**2, template.shape) + + template_volume = np.prod(template.shape) + template_ssd = np.sum((template - template.mean())**2) + + if image.ndim == 2: + xcorr = fftconvolve(image, template[::-1, ::-1], + mode="valid")[1:-1, 1:-1] + elif image.ndim == 3: + xcorr = fftconvolve(image, template[::-1, ::-1, ::-1], + mode="valid")[1:-1, 1:-1, 1:-1] + + nom = xcorr - image_window_sum * (template.sum() / template_volume) + + denom = image_window_sum2 + np.multiply(image_window_sum, image_window_sum, out=image_window_sum) + np.divide(image_window_sum, template_volume, out=image_window_sum) + denom -= image_window_sum + denom *= template_ssd + np.maximum(denom, 0, out=denom) # sqrt of negative number not allowed + np.sqrt(denom, out=denom) + + response = np.zeros_like(xcorr, dtype=np.float32) + + # avoid zero-division + mask = denom > np.finfo(np.float32).eps + + response[mask] = nom[mask] / denom[mask] + + slices = [] + for i in range(template.ndim): + if pad_input: + d0 = (template.shape[i] - 1) // 2 + d1 = d0 + image_shape[i] + else: + d0 = template.shape[i] - 1 + d1 = d0 + image_shape[i] - template.shape[i] + 1 + slices.append(slice(d0, d1)) + + return response[slices] diff --git a/skimage/feature/tests/_test_brief.py b/skimage/feature/tests/_test_brief.py deleted file mode 100644 index 1d26cbbd..00000000 --- a/skimage/feature/tests/_test_brief.py +++ /dev/null @@ -1,83 +0,0 @@ -import numpy as np -from numpy.testing import assert_array_equal, assert_raises -from skimage import data -from skimage import transform as tf -from skimage.color import rgb2gray -from skimage.feature import (brief, match_keypoints_brief, corner_peaks, - corner_harris) - - -def test_brief_color_image_unsupported_error(): - """Brief descriptors can be evaluated on gray-scale images only.""" - img = np.zeros((20, 20, 3)) - keypoints = [[7, 5], [11, 13]] - assert_raises(ValueError, brief, img, keypoints) - - -def test_match_keypoints_brief_lena_translation(): - """Test matched keypoints between lena image and its translated version.""" - img = data.lena() - img = rgb2gray(img) - img.shape - tform = tf.SimilarityTransform(scale=1, rotation=0, translation=(15, 20)) - translated_img = tf.warp(img, tform) - - keypoints1 = corner_peaks(corner_harris(img), min_distance=5) - descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) - - keypoints2 = corner_peaks(corner_harris(translated_img), min_distance=5) - descriptors2, keypoints2 = brief(translated_img, keypoints2, - descriptor_size=512) - - matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, - keypoints2, descriptors2, - threshold=0.10) - - assert_array_equal(matched_keypoints[:, 0, :], matched_keypoints[:, 1, :] + - [20, 15]) - - -def test_match_keypoints_brief_lena_rotation(): - """Verify matched keypoints result between lena image and its rotated - version with the expected keypoint pairs.""" - img = data.lena() - img = rgb2gray(img) - img.shape - tform = tf.SimilarityTransform(scale=1, rotation=0.10, translation=(0, 0)) - rotated_img = tf.warp(img, tform) - - keypoints1 = corner_peaks(corner_harris(img), min_distance=5) - descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) - - keypoints2 = corner_peaks(corner_harris(rotated_img), min_distance=5) - descriptors2, keypoints2 = brief(rotated_img, keypoints2, - descriptor_size=512) - - matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, - keypoints2, descriptors2, - threshold=0.07) - - expected = np.array([[[263, 272], - [234, 298]], - - [[271, 120], - [258, 146]], - - [[323, 164], - [305, 195]], - - [[414, 70], - [405, 111]], - - [[435, 181], - [415, 223]], - - [[454, 176], - [435, 221]]]) - - assert_array_equal(matched_keypoints, expected) - - -if __name__ == '__main__': - from numpy import testing - testing.run_module_suite() diff --git a/skimage/feature/tests/_test_censure.py b/skimage/feature/tests/_test_censure.py deleted file mode 100644 index 4cd2ad68..00000000 --- a/skimage/feature/tests/_test_censure.py +++ /dev/null @@ -1,89 +0,0 @@ -import numpy as np -from numpy.testing import assert_array_equal, assert_raises -from skimage.data import moon -from skimage.feature import keypoints_censure - - -def test_keypoints_censure_color_image_unsupported_error(): - """Censure keypoints can be extracted from gray-scale images only.""" - img = np.zeros((20, 20, 3)) - assert_raises(ValueError, keypoints_censure, img) - - -def test_keypoints_censure_mode_validity_error(): - """Mode argument in keypoints_censure can be either DoB, Octagon or - STAR.""" - img = np.zeros((20, 20)) - assert_raises(ValueError, keypoints_censure, img, mode='dummy') - - -def test_keypoints_censure_scale_range_error(): - """Difference between the the max_scale and min_scale parameters in - keypoints_censure should be greater than or equal to two.""" - img = np.zeros((20, 20)) - assert_raises(ValueError, keypoints_censure, img, min_scale=1, max_scale=2) - - -def test_keypoints_censure_moon_image_dob(): - """Verify the actual Censure keypoints and their corresponding scale with - the expected values for DoB filter.""" - img = moon() - actual_kp_dob, actual_scale = keypoints_censure(img, 1, 7, 'DoB', 0.15) - expected_kp_dob = np.array([[ 21, 497], - [ 36, 46], - [119, 350], - [185, 177], - [287, 250], - [357, 239], - [463, 116], - [464, 132], - [467, 260]]) - expected_scale = np.array([3, 4, 4, 2, 2, 3, 2, 2, 2]) - - assert_array_equal(expected_kp_dob, actual_kp_dob) - assert_array_equal(expected_scale, actual_scale) - - -def test_keypoints_censure_moon_image_octagon(): - """Verify the actual Censure keypoints and their corresponding scale with - the expected values for Octagon filter.""" - img = moon() - actual_kp_octagon, actual_scale = keypoints_censure(img, 1, 7, 'Octagon', - 0.15) - expected_kp_octagon = np.array([[ 21, 496], - [ 35, 46], - [287, 250], - [356, 239], - [463, 116]]) - - expected_scale = np.array([3, 4, 2, 2, 2]) - - assert_array_equal(expected_kp_octagon, actual_kp_octagon) - assert_array_equal(expected_scale, actual_scale) - - -def test_keypoints_censure_moon_image_star(): - """Verify the actual Censure keypoints and their corresponding scale with - the expected values for STAR filter.""" - img = moon() - actual_kp_star, actual_scale = keypoints_censure(img, 1, 7, 'STAR', 0.15) - expected_kp_star = np.array([[ 21, 497], - [ 36, 46], - [117, 356], - [185, 177], - [260, 227], - [287, 250], - [357, 239], - [451, 281], - [463, 116], - [467, 260]]) - - expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) - - assert_array_equal(expected_kp_star, actual_kp_star) - assert_array_equal(expected_scale, actual_scale) - - -if __name__ == '__main__': - from numpy import testing - testing.run_module_suite() diff --git a/skimage/feature/tests/test_blob.py b/skimage/feature/tests/test_blob.py new file mode 100644 index 00000000..3ae949c2 --- /dev/null +++ b/skimage/feature/tests/test_blob.py @@ -0,0 +1,81 @@ +import numpy as np +from skimage.draw import circle +from skimage.feature import blob_dog, blob_log +import math + + +def test_blob_dog(): + r2 = math.sqrt(2) + img = np.ones((512, 512)) + + xs, ys = circle(400, 130, 5) + img[xs, ys] = 255 + + xs, ys = circle(100, 300, 25) + img[xs, ys] = 255 + + xs, ys = circle(200, 350, 45) + img[xs, ys] = 255 + + blobs = blob_dog(img, min_sigma=5, max_sigma=50) + radius = lambda x: r2*x[2] + s = sorted(blobs, key=radius) + thresh = 5 + + b = s[0] + assert abs(b[0] - 400) <= thresh + assert abs(b[1] - 130) <= thresh + assert abs(radius(b) - 5) <= thresh + + b = s[1] + assert abs(b[0] - 100) <= thresh + assert abs(b[1] - 300) <= thresh + assert abs(radius(b) - 25) <= thresh + + b = s[2] + assert abs(b[0] - 200) <= thresh + assert abs(b[1] - 350) <= thresh + assert abs(radius(b) - 45) <= thresh + + +def test_blob_log(): + r2 = math.sqrt(2) + img = np.ones((512, 512)) + + xs, ys = circle(400, 130, 5) + img[xs, ys] = 255 + + xs, ys = circle(160, 50, 15) + img[xs, ys] = 255 + + xs, ys = circle(100, 300, 25) + img[xs, ys] = 255 + + xs, ys = circle(200, 350, 30) + img[xs, ys] = 255 + + blobs = blob_log(img, min_sigma=5, max_sigma=20, threshold=1) + + radius = lambda x: r2*x[2] + s = sorted(blobs, key=radius) + thresh = 3 + + b = s[0] + assert abs(b[0] - 400) <= thresh + assert abs(b[1] - 130) <= thresh + assert abs(radius(b) - 5) <= thresh + + b = s[1] + assert abs(b[0] - 160) <= thresh + assert abs(b[1] - 50) <= thresh + assert abs(radius(b) - 15) <= thresh + + b = s[2] + assert abs(b[0] - 100) <= thresh + assert abs(b[1] - 300) <= thresh + assert abs(radius(b) - 25) <= thresh + + b = s[3] + assert abs(b[0] - 200) <= thresh + assert abs(b[1] - 350) <= thresh + assert abs(radius(b) - 30) <= thresh diff --git a/skimage/feature/tests/test_brief.py b/skimage/feature/tests/test_brief.py new file mode 100644 index 00000000..554301b2 --- /dev/null +++ b/skimage/feature/tests/test_brief.py @@ -0,0 +1,77 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage import data +from skimage import transform as tf +from skimage.color import rgb2gray +from skimage.feature import BRIEF, corner_peaks, corner_harris + + +def test_color_image_unsupported_error(): + """Brief descriptors can be evaluated on gray-scale images only.""" + img = np.zeros((20, 20, 3)) + keypoints = np.asarray([[7, 5], [11, 13]]) + assert_raises(ValueError, BRIEF().extract, img, keypoints) + + +def test_normal_mode(): + """Verify the computed BRIEF descriptors with expected for normal mode.""" + img = rgb2gray(data.lena()) + + keypoints = corner_peaks(corner_harris(img), min_distance=5) + + extractor = BRIEF(descriptor_size=8, sigma=2) + + extractor.extract(img, keypoints[:8]) + + expected = np.array([[ True, False, True, False, True, True, False, False], + [False, False, False, False, True, False, False, False], + [ True, True, True, True, True, True, True, True], + [ True, False, True, True, False, True, False, True], + [False, True, True, True, True, True, True, True], + [ True, False, False, False, False, True, False, True], + [False, True, True, True, False, False, True, False], + [False, False, False, False, True, False, False, False]], dtype=bool) + + assert_array_equal(extractor.descriptors, expected) + + +def test_uniform_mode(): + """Verify the computed BRIEF descriptors with expected for uniform mode.""" + img = rgb2gray(data.lena()) + + keypoints = corner_peaks(corner_harris(img), min_distance=5) + + extractor = BRIEF(descriptor_size=8, sigma=2, mode='uniform') + + extractor.extract(img, keypoints[:8]) + + expected = np.array([[ True, False, True, False, False, True, False, False], + [False, True, False, False, True, True, True, True], + [ True, False, False, False, False, False, False, False], + [False, True, True, False, False, False, True, False], + [False, False, False, False, False, False, True, False], + [False, True, False, False, True, False, False, False], + [False, False, True, True, False, False, True, True], + [ True, True, False, False, False, False, False, False]], dtype=bool) + + assert_array_equal(extractor.descriptors, expected) + + +def test_unsupported_mode(): + assert_raises(ValueError, BRIEF, mode='foobar') + + +def test_border(): + img = np.zeros((100, 100)) + keypoints = np.array([[1, 1], [20, 20], [50, 50], [80, 80]]) + + extractor = BRIEF(patch_size=41) + extractor.extract(img, keypoints) + + assert extractor.descriptors.shape[0] == 3 + assert_array_equal(extractor.mask, (False, True, True, True)) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py new file mode 100644 index 00000000..53c1c59b --- /dev/null +++ b/skimage/feature/tests/test_censure.py @@ -0,0 +1,97 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage.data import moon +from skimage.feature import CENSURE + + +img = moon() + + +def test_censure_on_rectangular_images(): + """Censure feature detector should work on 2D image of any shape.""" + rect_image = np.random.random((300, 200)) + square_image = np.random.random((200, 200)) + CENSURE().detect((square_image)) + CENSURE().detect((rect_image)) + + +def test_keypoints_censure_color_image_unsupported_error(): + """Censure keypoints can be extracted from gray-scale images only.""" + assert_raises(ValueError, CENSURE().detect, np.zeros((20, 20, 3))) + + +def test_keypoints_censure_mode_validity_error(): + """Mode argument in keypoints_censure can be either DoB, Octagon or + STAR.""" + assert_raises(ValueError, CENSURE, mode='dummy') + + +def test_keypoints_censure_scale_range_error(): + """Difference between the the max_scale and min_scale parameters in + keypoints_censure should be greater than or equal to two.""" + assert_raises(ValueError, CENSURE, min_scale=1, max_scale=2) + + +def test_keypoints_censure_moon_image_dob(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for DoB filter.""" + detector = CENSURE() + detector.detect(img) + expected_keypoints = np.array([[ 21, 497], + [ 36, 46], + [119, 350], + [185, 177], + [287, 250], + [357, 239], + [463, 116], + [464, 132], + [467, 260]]) + expected_scales = np.array([3, 4, 4, 2, 2, 3, 2, 2, 2]) + + assert_array_equal(expected_keypoints, detector.keypoints) + assert_array_equal(expected_scales, detector.scales) + + +def test_keypoints_censure_moon_image_octagon(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for Octagon filter.""" + + detector = CENSURE(mode='octagon') + detector.detect(img) + expected_keypoints = np.array([[ 21, 496], + [ 35, 46], + [287, 250], + [356, 239], + [463, 116]]) + + expected_scales = np.array([3, 4, 2, 2, 2]) + + assert_array_equal(expected_keypoints, detector.keypoints) + assert_array_equal(expected_scales, detector.scales) + + +def test_keypoints_censure_moon_image_star(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for STAR filter.""" + detector = CENSURE(mode='star') + detector.detect(img) + expected_keypoints = np.array([[ 21, 497], + [ 36, 46], + [117, 356], + [185, 177], + [260, 227], + [287, 250], + [357, 239], + [451, 281], + [463, 116], + [467, 260]]) + + expected_scales = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) + + assert_array_equal(expected_keypoints, detector.keypoints) + assert_array_equal(expected_scales, detector.scales) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_corner.py b/skimage/feature/tests/test_corner.py index 9c07c006..21070779 100644 --- a/skimage/feature/tests/test_corner.py +++ b/skimage/feature/tests/test_corner.py @@ -1,12 +1,94 @@ import numpy as np -from numpy.testing import assert_array_equal, assert_almost_equal +from numpy.testing import (assert_array_equal, assert_raises, + assert_almost_equal) from skimage import data from skimage import img_as_float +from skimage.color import rgb2gray +from skimage.morphology import octagon from skimage.feature import (corner_moravec, corner_harris, corner_shi_tomasi, corner_subpix, peak_local_max, corner_peaks, - corner_kitchen_rosenfeld, corner_foerstner) + corner_kitchen_rosenfeld, corner_foerstner, + corner_fast, corner_orientations, + structure_tensor, structure_tensor_eigvals, + hessian_matrix, hessian_matrix_eigvals) + + +def test_structure_tensor(): + square = np.zeros((5, 5)) + square[2, 2] = 1 + Axx, Axy, Ayy = structure_tensor(square, sigma=0.1) + assert_array_equal(Axx, np.array([[ 0, 0, 0, 0, 0], + [ 0, 1, 0, 1, 0], + [ 0, 4, 0, 4, 0], + [ 0, 1, 0, 1, 0], + [ 0, 0, 0, 0, 0]])) + assert_array_equal(Axy, np.array([[ 0, 0, 0, 0, 0], + [ 0, 1, 0, -1, 0], + [ 0, 0, 0, -0, 0], + [ 0, -1, -0, 1, 0], + [ 0, 0, 0, 0, 0]])) + assert_array_equal(Ayy, np.array([[ 0, 0, 0, 0, 0], + [ 0, 1, 4, 1, 0], + [ 0, 0, 0, 0, 0], + [ 0, 1, 4, 1, 0], + [ 0, 0, 0, 0, 0]])) + + +def test_hessian_matrix(): + square = np.zeros((5, 5)) + square[2, 2] = 1 + Hxx, Hxy, Hyy = hessian_matrix(square, sigma=0.1) + assert_array_equal(Hxx, np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]])) + assert_array_equal(Hxy, np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]])) + assert_array_equal(Hyy, np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]])) + + +def test_structure_tensor_eigvals(): + square = np.zeros((5, 5)) + square[2, 2] = 1 + Axx, Axy, Ayy = structure_tensor(square, sigma=0.1) + l1, l2 = structure_tensor_eigvals(Axx, Axy, Ayy) + assert_array_equal(l1, np.array([[0, 0, 0, 0, 0], + [0, 2, 4, 2, 0], + [0, 4, 0, 4, 0], + [0, 2, 4, 2, 0], + [0, 0, 0, 0, 0]])) + assert_array_equal(l2, np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]])) + + +def test_hessian_matrix_eigvals(): + square = np.zeros((5, 5)) + square[2, 2] = 1 + Hxx, Hxy, Hyy = hessian_matrix(square, sigma=0.1) + l1, l2 = hessian_matrix_eigvals(Hxx, Hxy, Hyy) + assert_array_equal(l1, np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]])) + assert_array_equal(l2, np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]])) def test_square_image(): @@ -19,7 +101,11 @@ def test_square_image(): assert len(results) == 57 # Harris - results = peak_local_max(corner_harris(im)) + results = peak_local_max(corner_harris(im, method='k')) + # interest at corner + assert len(results) == 1 + + results = peak_local_max(corner_harris(im, method='eps')) # interest at corner assert len(results) == 1 @@ -41,7 +127,9 @@ def test_noisy_square_image(): assert results.any() # Harris - results = peak_local_max(corner_harris(im, sigma=1.5)) + results = peak_local_max(corner_harris(im, sigma=1.5, method='k')) + assert len(results) == 1 + results = peak_local_max(corner_harris(im, sigma=1.5, method='eps')) assert len(results) == 1 # Shi-Tomasi @@ -94,8 +182,8 @@ def test_rotated_lena(): def test_subpix(): img = np.zeros((50, 50)) - img[:25,:25] = 255 - img[25:,25:] = 255 + img[:25, :25] = 255 + img[25:, 25:] = 255 corner = peak_local_max(corner_harris(img), num_peaks=1) subpix = corner_subpix(img, corner) assert_array_equal(subpix[0], (24.5, 24.5)) @@ -122,7 +210,7 @@ def test_num_peaks(): peak_local_max returns exactly the right amount of peaks. Test is run on Lena in order to produce a sufficient number of corners""" - lena_corners = corner_harris(data.lena()) + lena_corners = corner_harris(rgb2gray(data.lena())) for i in range(20): n = np.random.random_integers(20) @@ -140,6 +228,10 @@ def test_corner_peaks(): corners = corner_peaks(response, exclude_border=False, min_distance=0) assert len(corners) == 4 + corners = corner_peaks(response, exclude_border=False, min_distance=0, + indices=False) + assert np.sum(corners) == 4 + def test_blank_image_nans(): """Some of the corner detectors had a weakness in terms of returning @@ -156,6 +248,59 @@ def test_blank_image_nans(): assert np.all(np.isfinite(response)) +def test_corner_fast_image_unsupported_error(): + img = np.zeros((20, 20, 3)) + assert_raises(ValueError, corner_fast, img) + + +def test_corner_fast_lena(): + img = rgb2gray(data.lena()) + expected = np.array([[ 67, 157], + [204, 261], + [247, 146], + [269, 111], + [318, 158], + [386, 73], + [413, 70], + [435, 180], + [455, 177], + [461, 160]]) + actual = corner_peaks(corner_fast(img, 12, 0.3)) + assert_array_equal(actual, expected) + + +def test_corner_orientations_image_unsupported_error(): + img = np.zeros((20, 20, 3)) + assert_raises(ValueError, corner_orientations, img, + np.asarray([[7, 7]]), np.ones((3, 3))) + + +def test_corner_orientations_even_shape_error(): + img = np.zeros((20, 20)) + assert_raises(ValueError, corner_orientations, img, + np.asarray([[7, 7]]), np.ones((4, 4))) + + +def test_corner_orientations_lena(): + img = rgb2gray(data.lena()) + corners = corner_peaks(corner_fast(img, 11, 0.35)) + expected = np.array([-1.9195897 , -3.03159624, -1.05991162, -2.89573739, + -2.61607644, 2.98660159]) + actual = corner_orientations(img, corners, octagon(3, 2)) + assert_almost_equal(actual, expected) + + +def test_corner_orientations_square(): + square = np.zeros((12, 12)) + square[3:9, 3:9] = 1 + corners = corner_peaks(corner_fast(square, 9), min_distance=1) + actual_orientations = corner_orientations(square, corners, octagon(3, 2)) + actual_orientations_degrees = np.rad2deg(actual_orientations) + expected_orientations_degree = np.array([ 45., 135., -45., -135.]) + assert_array_equal(actual_orientations_degrees, + expected_orientations_degree) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/feature/tests/test_daisy.py b/skimage/feature/tests/test_daisy.py index 7f4e85d7..32a7a5df 100644 --- a/skimage/feature/tests/test_daisy.py +++ b/skimage/feature/tests/test_daisy.py @@ -45,8 +45,15 @@ def test_descs_shape(): assert(descs.shape[1] == ceil((img.shape[1] - radius * 2) / float(step))) +def test_daisy_sigmas_and_radii(): + img = img_as_float(data.lena()[:64, :64].mean(axis=2)) + sigmas = [1, 2, 3] + radii = [1, 2] + daisy(img, sigmas=sigmas, ring_radii=radii) + + def test_daisy_incompatible_sigmas_and_radii(): - img = img_as_float(data.lena()[:128, :128].mean(axis=2)) + img = img_as_float(data.lena()[:64, :64].mean(axis=2)) sigmas = [1, 2] radii = [1, 2] assert_raises(ValueError, daisy, img, sigmas=sigmas, ring_radii=radii) diff --git a/skimage/feature/tests/test_match.py b/skimage/feature/tests/test_match.py new file mode 100644 index 00000000..1b0a622f --- /dev/null +++ b/skimage/feature/tests/test_match.py @@ -0,0 +1,120 @@ +import numpy as np +from numpy.testing import assert_equal, assert_raises +from skimage import data +from skimage import transform as tf +from skimage.color import rgb2gray +from skimage.feature import (BRIEF, match_descriptors, + corner_peaks, corner_harris) + + +def test_binary_descriptors_unequal_descriptor_sizes_error(): + """Sizes of descriptors of keypoints to be matched should be equal.""" + descs1 = np.array([[True, True, False, True], + [False, True, False, True]]) + descs2 = np.array([[True, False, False, True, False], + [False, True, True, True, False]]) + assert_raises(ValueError, match_descriptors, descs1, descs2) + + +def test_binary_descriptors(): + descs1 = np.array([[True, True, False, True, True], + [False, True, False, True, True]]) + descs2 = np.array([[True, False, False, True, False], + [False, False, True, True, True]]) + matches = match_descriptors(descs1, descs2) + assert_equal(matches, [[0, 0], [1, 1]]) + + +def test_binary_descriptors_lena_rotation_crosscheck_false(): + """Verify matched keypoints and their corresponding masks results between + lena image and its rotated version with the expected keypoint pairs with + cross_check disabled.""" + img = data.lena() + img = rgb2gray(img) + tform = tf.SimilarityTransform(scale=1, rotation=0.15, translation=(0, 0)) + rotated_img = tf.warp(img, tform) + + extractor = BRIEF(descriptor_size=512) + + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + extractor.extract(img, keypoints1) + descriptors1 = extractor.descriptors + + keypoints2 = corner_peaks(corner_harris(rotated_img), min_distance=5) + extractor.extract(rotated_img, keypoints2) + descriptors2 = extractor.descriptors + + matches = match_descriptors(descriptors1, descriptors2, cross_check=False) + + exp_matches1 = np.array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46]) + exp_matches2 = np.array([33, 0, 35, 7, 1, 35, 3, 2, 3, 6, 4, 9, + 11, 10, 28, 7, 8, 5, 31, 14, 13, 15, 21, 16, + 16, 13, 17, 18, 19, 21, 22, 23, 0, 24, 1, 24, + 23, 0, 26, 27, 25, 34, 28, 14, 29, 30, 21]) + assert_equal(matches[:, 0], exp_matches1) + assert_equal(matches[:, 1], exp_matches2) + + +def test_binary_descriptors_lena_rotation_crosscheck_true(): + """Verify matched keypoints and their corresponding masks results between + lena image and its rotated version with the expected keypoint pairs with + cross_check enabled.""" + img = data.lena() + img = rgb2gray(img) + tform = tf.SimilarityTransform(scale=1, rotation=0.15, translation=(0, 0)) + rotated_img = tf.warp(img, tform) + + extractor = BRIEF(descriptor_size=512) + + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + extractor.extract(img, keypoints1) + descriptors1 = extractor.descriptors + + keypoints2 = corner_peaks(corner_harris(rotated_img), min_distance=5) + extractor.extract(rotated_img, keypoints2) + descriptors2 = extractor.descriptors + + matches = match_descriptors(descriptors1, descriptors2, cross_check=True) + + exp_matches1 = np.array([ 0, 1, 2, 4, 6, 7, 9, 10, 11, 12, 13, 15, + 16, 17, 19, 20, 21, 24, 26, 27, 28, 29, 30, 35, + 36, 38, 39, 40, 42, 44, 45]) + exp_matches2 = np.array([33, 0, 35, 1, 3, 2, 6, 4, 9, 11, 10, 7, + 8, 5, 14, 13, 15, 16, 17, 18, 19, 21, 22, 24, + 23, 26, 27, 25, 28, 29, 30]) + assert_equal(matches[:, 0], exp_matches1) + assert_equal(matches[:, 1], exp_matches2) + + +def test_max_distance(): + descs1 = np.zeros((10, 128)) + descs2 = np.zeros((15, 128)) + + descs1[0, :] = 1 + + matches = match_descriptors(descs1, descs2, metric='euclidean', + max_distance=0.1, cross_check=False) + assert len(matches) == 9 + + matches = match_descriptors(descs1, descs2, metric='euclidean', + max_distance=np.sqrt(128.1), + cross_check=False) + assert len(matches) == 10 + + matches = match_descriptors(descs1, descs2, metric='euclidean', + max_distance=0.1, + cross_check=True) + assert_equal(matches, [[1, 0]]) + + matches = match_descriptors(descs1, descs2, metric='euclidean', + max_distance=np.sqrt(128.1), + cross_check=True) + assert_equal(matches, [[1, 0]]) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_orb.py b/skimage/feature/tests/test_orb.py new file mode 100644 index 00000000..30394d07 --- /dev/null +++ b/skimage/feature/tests/test_orb.py @@ -0,0 +1,115 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_almost_equal +from skimage.feature import ORB +from skimage.data import lena +from skimage.color import rgb2gray + + +img = rgb2gray(lena()) + + +def test_keypoints_orb_desired_no_of_keypoints(): + detector_extractor = ORB(n_keypoints=10, fast_n=12, fast_threshold=0.20) + detector_extractor.detect(img) + + exp_rows = np.array([ 435. , 435.6 , 376. , 455. , 434.88, 269. , + 375.6 , 310.8 , 413. , 311.04]) + exp_cols = np.array([ 180. , 180. , 156. , 176. , 180. , 111. , + 156. , 172.8, 70. , 172.8]) + + exp_scales = np.array([ 1. , 1.2 , 1. , 1. , 1.44 , 1. , + 1.2 , 1.2 , 1. , 1.728]) + + exp_orientations = np.array([-175.64733392, -167.94842949, -148.98350192, + -142.03599837, -176.08535837, -53.08162354, + -150.89208271, 97.7693776 , -173.4479964 , + 38.66312042]) + exp_response = np.array([ 0.96770745, 0.81027306, 0.72376257, + 0.5626413 , 0.5097993 , 0.44351774, + 0.39154173, 0.39084861, 0.39063076, + 0.37602487]) + + assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0]) + assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1]) + assert_almost_equal(exp_scales, detector_extractor.scales) + assert_almost_equal(exp_response, detector_extractor.responses) + assert_almost_equal(exp_orientations, + np.rad2deg(detector_extractor.orientations), 5) + + detector_extractor.detect_and_extract(img) + assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0]) + assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1]) + + +def test_keypoints_orb_less_than_desired_no_of_keypoints(): + img = rgb2gray(lena()) + detector_extractor = ORB(n_keypoints=15, fast_n=12, + fast_threshold=0.33, downscale=2, n_scales=2) + detector_extractor.detect(img) + + exp_rows = np.array([ 67., 247., 269., 413., 435., 230., 264., + 330., 372.]) + exp_cols = np.array([ 157., 146., 111., 70., 180., 136., 336., + 148., 156.]) + + exp_scales = np.array([ 1., 1., 1., 1., 1., 2., 2., 2., 2.]) + + exp_orientations = np.array([-105.76503839, -96.28973044, -53.08162354, + -173.4479964 , -175.64733392, -106.07927215, + -163.40016243, 75.80865813, -154.73195911]) + + exp_response = np.array([ 0.13197835, 0.24931321, 0.44351774, + 0.39063076, 0.96770745, 0.04935129, + 0.21431068, 0.15826555, 0.42403573]) + + assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0]) + assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1]) + assert_almost_equal(exp_scales, detector_extractor.scales) + assert_almost_equal(exp_response, detector_extractor.responses) + assert_almost_equal(exp_orientations, + np.rad2deg(detector_extractor.orientations), 5) + + detector_extractor.detect_and_extract(img) + assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0]) + assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1]) + + +def test_descriptor_orb(): + detector_extractor = ORB(fast_n=12, fast_threshold=0.20) + + exp_descriptors = np.array([[ True, False, True, True, False, False, False, False, False, False], + [False, False, True, True, False, True, True, False, True, True], + [ True, False, False, False, True, False, True, True, True, False], + [ True, False, False, True, False, True, True, False, False, False], + [False, True, True, True, False, False, False, True, True, False], + [False, False, False, False, False, True, False, True, True, True], + [False, True, True, True, True, False, False, True, False, True], + [ True, True, True, False, True, True, True, True, False, False], + [ True, True, False, True, True, True, True, False, False, False], + [ True, False, False, False, False, True, False, False, True, True], + [ True, False, False, False, True, True, True, False, False, False], + [False, False, True, False, True, False, False, True, False, False], + [False, False, True, True, False, False, False, False, False, True], + [ True, True, False, False, False, True, True, True, True, True], + [ True, True, True, False, False, True, False, True, True, False], + [False, True, True, False, False, True, True, True, True, True], + [ True, True, True, False, False, False, False, True, True, True], + [False, False, False, False, True, False, False, True, True, False], + [False, True, False, False, True, False, False, False, True, True], + [ True, False, True, False, False, False, True, True, False, False]], dtype=bool) + + detector_extractor.detect(img) + detector_extractor.extract(img, detector_extractor.keypoints, + detector_extractor.scales, + detector_extractor.orientations) + assert_array_equal(exp_descriptors, + detector_extractor.descriptors[100:120, 10:20]) + + detector_extractor.detect_and_extract(img) + assert_array_equal(exp_descriptors, + detector_extractor.descriptors[100:120, 10:20]) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_peak.py b/skimage/feature/tests/test_peak.py index 1a3e91f2..98242a4d 100644 --- a/skimage/feature/tests/test_peak.py +++ b/skimage/feature/tests/test_peak.py @@ -1,5 +1,6 @@ import numpy as np -from numpy.testing import assert_array_almost_equal as assert_close +from numpy.testing import (assert_array_almost_equal as assert_close, + assert_equal) import scipy.ndimage from skimage.feature import peak @@ -266,6 +267,30 @@ def test_disk(): assert np.all(result) +def test_3D(): + image = np.zeros((30, 30, 30)) + image[15, 15, 15] = 1 + image[5, 5, 5] = 1 + assert_equal(peak.peak_local_max(image), [[15, 15, 15]]) + assert_equal(peak.peak_local_max(image, min_distance=6), [[15, 15, 15]]) + assert_equal(peak.peak_local_max(image, exclude_border=False), + [[5, 5, 5], [15, 15, 15]]) + assert_equal(peak.peak_local_max(image, min_distance=5), + [[5, 5, 5], [15, 15, 15]]) + + +def test_4D(): + image = np.zeros((30, 30, 30, 30)) + image[15, 15, 15, 15] = 1 + image[5, 5, 5, 5] = 1 + assert_equal(peak.peak_local_max(image), [[15, 15, 15, 15]]) + assert_equal(peak.peak_local_max(image, min_distance=6), [[15, 15, 15, 15]]) + assert_equal(peak.peak_local_max(image, exclude_border=False), + [[5, 5, 5, 5], [15, 15, 15, 15]]) + assert_equal(peak.peak_local_max(image, min_distance=5), + [[5, 5, 5, 5], [15, 15, 15, 15]]) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/feature/tests/test_template.py b/skimage/feature/tests/test_template.py index 1b9ff213..10e6f677 100644 --- a/skimage/feature/tests/test_template.py +++ b/skimage/feature/tests/test_template.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import assert_array_almost_equal as assert_close +from numpy.testing import assert_almost_equal, assert_equal, assert_raises from skimage.morphology import diamond from skimage.feature import match_template, peak_local_max @@ -31,7 +31,7 @@ def test_template(): positions = positions[np.argsort(positions[:, 0])] for xy_target, xy in zip(target_positions, positions): - yield assert_close, xy, xy_target + yield assert_almost_equal, xy, xy_target def test_normalization(): @@ -88,7 +88,7 @@ def test_no_nans(): def test_switched_arguments(): image = np.ones((5, 5)) template = np.ones((3, 3)) - np.testing.assert_raises(ValueError, match_template, template, image) + assert_raises(ValueError, match_template, template, image) def test_pad_input(): @@ -108,14 +108,66 @@ def test_pad_input(): image[mid, -9:-4] -= template # full min template centered at 12 image[mid, -3:] += template[:, :3] # half max template centered at 18 - result = match_template(image, template, pad_input=True) + result = match_template(image, template, pad_input=True, + constant_values=image.mean()) # get the max and min results. sorted_result = np.argsort(result.flat) i, j = np.unravel_index(sorted_result[:2], result.shape) - assert_close(j, (12, 0)) + assert_equal(j, (12, 0)) i, j = np.unravel_index(sorted_result[-2:], result.shape) - assert_close(j, (18, 6)) + assert_equal(j, (18, 6)) + + +def test_3d(): + np.random.seed(1) + template = np.random.rand(3, 3, 3) + image = np.zeros((12, 12, 12)) + + image[3:6, 5:8, 4:7] = template + + result = match_template(image, template) + + assert_equal(result.shape, (10, 10, 10)) + assert_equal(np.unravel_index(result.argmax(), result.shape), (3, 5, 4)) + + +def test_3d_pad_input(): + np.random.seed(1) + template = np.random.rand(3, 3, 3) + image = np.zeros((12, 12, 12)) + + image[3:6, 5:8, 4:7] = template + + result = match_template(image, template, pad_input=True) + + assert_equal(result.shape, (12, 12, 12)) + assert_equal(np.unravel_index(result.argmax(), result.shape), (4, 6, 5)) + + +def test_padding_reflect(): + template = diamond(2) + image = np.zeros((10, 10)) + image[2:7, :3] = template[:, -3:] + + result = match_template(image, template, pad_input=True, + mode='reflect') + + assert_equal(np.unravel_index(result.argmax(), result.shape), (4, 0)) + + +def test_wrong_input(): + image = np.ones((5, 5, 1)) + template = np.ones((3, 3)) + assert_raises(ValueError, match_template, template, image) + + image = np.ones((5, 5)) + template = np.ones((3, 3, 2)) + assert_raises(ValueError, match_template, template, image) + + image = np.ones((5, 5, 3, 3)) + template = np.ones((3, 3, 2)) + assert_raises(ValueError, match_template, template, image) if __name__ == "__main__": diff --git a/skimage/feature/tests/test_util.py b/skimage/feature/tests/test_util.py index 6e25f51a..2b601b94 100644 --- a/skimage/feature/tests/test_util.py +++ b/skimage/feature/tests/test_util.py @@ -1,30 +1,71 @@ import numpy as np -from numpy.testing import assert_array_equal -from skimage.feature.util import pairwise_hamming_distance +import matplotlib.pyplot as plt +from numpy.testing import assert_equal, assert_raises + +from skimage.feature.util import (FeatureDetector, DescriptorExtractor, + _prepare_grayscale_input_2D, + _mask_border_keypoints, plot_matches) -def test_pairwise_hamming_distance_range(): - """Values of all the pairwise hamming distances should be in the range - [0, 1].""" - a = np.random.random_sample((10, 50)) > 0.5 - b = np.random.random_sample((20, 50)) > 0.5 - dist = pairwise_hamming_distance(a, b) - assert np.all((0 <= dist) & (dist <= 1)) +def test_feature_detector(): + assert_raises(NotImplementedError, FeatureDetector().detect, None) -def test_pairwise_hamming_distance_value(): - """The result of pairwise_hamming_distance of two fixed sets of boolean - vectors should be same as expected.""" - np.random.seed(10) - a = np.random.random_sample((4, 100)) > 0.5 - np.random.seed(20) - b = np.random.random_sample((3, 100)) > 0.5 - result = pairwise_hamming_distance(a, b) - expected = np.array([[0.5 , 0.49, 0.44], - [0.44, 0.53, 0.52], - [0.4 , 0.55, 0.5 ], - [0.47, 0.48, 0.57]]) - assert_array_equal(result, expected) +def test_descriptor_extractor(): + assert_raises(NotImplementedError, DescriptorExtractor().extract, + None, None) + + +def test_prepare_grayscale_input_2D(): + assert_raises(ValueError, _prepare_grayscale_input_2D, np.zeros((3, 3, 3))) + assert_raises(ValueError, _prepare_grayscale_input_2D, np.zeros((3, 1))) + assert_raises(ValueError, _prepare_grayscale_input_2D, np.zeros((3, 1, 1))) + img = _prepare_grayscale_input_2D(np.zeros((3, 3))) + img = _prepare_grayscale_input_2D(np.zeros((3, 3, 1))) + img = _prepare_grayscale_input_2D(np.zeros((1, 3, 3))) + + +def test_mask_border_keypoints(): + keypoints = np.array([[0, 0], [1, 1], [2, 2], [3, 3], [4, 4]]) + assert_equal(_mask_border_keypoints((10, 10), keypoints, 0), + [1, 1, 1, 1, 1]) + assert_equal(_mask_border_keypoints((10, 10), keypoints, 2), + [0, 0, 1, 1, 1]) + assert_equal(_mask_border_keypoints((4, 4), keypoints, 2), + [0, 0, 1, 0, 0]) + assert_equal(_mask_border_keypoints((10, 10), keypoints, 5), + [0, 0, 0, 0, 0]) + assert_equal(_mask_border_keypoints((10, 10), keypoints, 4), + [0, 0, 0, 0, 1]) + + +def test_plot_matches(): + fig, ax = plt.subplots(nrows=1, ncols=1) + + shapes = (((10, 10), (10, 10)), + ((10, 10), (12, 10)), + ((10, 10), (10, 12)), + ((10, 10), (12, 12)), + ((12, 10), (10, 10)), + ((10, 12), (10, 10)), + ((12, 12), (10, 10))) + + keypoints1 = 10 * np.random.rand(10, 2) + keypoints2 = 10 * np.random.rand(10, 2) + idxs1 = np.random.randint(10, size=10) + idxs2 = np.random.randint(10, size=10) + matches = np.column_stack((idxs1, idxs2)) + + for shape1, shape2 in shapes: + img1 = np.zeros(shape1) + img2 = np.zeros(shape2) + plot_matches(ax, img1, img2, keypoints1, keypoints2, matches) + plot_matches(ax, img1, img2, keypoints1, keypoints2, matches, + only_matches=True) + plot_matches(ax, img1, img2, keypoints1, keypoints2, matches, + keypoints_color='r') + plot_matches(ax, img1, img2, keypoints1, keypoints2, matches, + matches_color='r') if __name__ == '__main__': diff --git a/skimage/feature/util.py b/skimage/feature/util.py index a5267d44..8ee2baf8 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -1,38 +1,161 @@ +import numpy as np + +from skimage.util import img_as_float -def _mask_border_keypoints(image, keypoints, dist): - """Removes keypoints that are within dist pixels from the image border.""" - width = image.shape[0] - height = image.shape[1] +class FeatureDetector(object): - keypoints_filtering_mask = ((dist - 1 < keypoints[:, 0]) & - (keypoints[:, 0] < width - dist + 1) & - (dist - 1 < keypoints[:, 1]) & - (keypoints[:, 1] < height - dist + 1)) + def __init__(self): + self.keypoints_ = np.array([]) - return keypoints_filtering_mask + def detect(self, image): + """Detect keypoints in image. + + Parameters + ---------- + image : 2D array + Input image. + + """ + raise NotImplementedError() -def pairwise_hamming_distance(array1, array2): - """**Experimental function**. +class DescriptorExtractor(object): - Calculate hamming dissimilarity measure between two sets of - vectors. + def __init__(self): + self.descriptors_ = np.array([]) + + def extract(self, image, keypoints): + """Extract feature descriptors in image for given keypoints. + + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint locations as ``(row, col)``. + + """ + raise NotImplementedError() + + +def plot_matches(ax, image1, image2, keypoints1, keypoints2, matches, + keypoints_color='k', matches_color=None, only_matches=False): + """Plot matched features. Parameters ---------- - array1 : (P1, D) array - P1 vectors of size D. - array2 : (P2, D) array - P2 vectors of size D. + ax : matplotlib.axes.Axes + Matches and image are drawn in this ax. + image1 : (N, M [, 3]) array + First grayscale or color image. + image2 : (N, M [, 3]) array + Second grayscale or color image. + keypoints1 : (K1, 2) array + First keypoint coordinates as ``(row, col)``. + keypoints2 : (K2, 2) array + Second keypoint coordinates as ``(row, col)``. + matches : (Q, 2) array + Indices of corresponding matches in first and second set of + descriptors, where ``matches[:, 0]`` denote the indices in the first + and ``matches[:, 1]`` the indices in the second set of descriptors. + keypoints_color : matplotlib color, optional + Color for keypoint locations. + matches_color : matplotlib color, optional + Color for lines which connect keypoint matches. By default the + color is chosen randomly. + only_matches : bool, optional + Whether to only plot matches and not plot the keypoint locations. + + """ + + image1 = img_as_float(image1) + image2 = img_as_float(image2) + + new_shape1 = list(image1.shape) + new_shape2 = list(image2.shape) + + if image1.shape[0] < image2.shape[0]: + new_shape1[0] = image2.shape[0] + elif image1.shape[0] > image2.shape[0]: + new_shape2[0] = image1.shape[0] + + if image1.shape[1] < image2.shape[1]: + new_shape1[1] = image2.shape[1] + elif image1.shape[1] > image2.shape[1]: + new_shape2[1] = image1.shape[1] + + if new_shape1 != image1.shape: + new_image1 = np.zeros(new_shape1, dtype=image1.dtype) + new_image1[:image1.shape[0], :image1.shape[1]] = image1 + image1 = new_image1 + + if new_shape2 != image2.shape: + new_image2 = np.zeros(new_shape2, dtype=image2.dtype) + new_image2[:image2.shape[0], :image2.shape[1]] = image2 + image2 = new_image2 + + image = np.concatenate([image1, image2], axis=1) + + offset = image1.shape + + if not only_matches: + ax.scatter(keypoints1[:, 1], keypoints1[:, 0], + facecolors='none', edgecolors=keypoints_color) + ax.scatter(keypoints2[:, 1] + offset[1], keypoints2[:, 0], + facecolors='none', edgecolors=keypoints_color) + + ax.imshow(image) + ax.axis((0, 2 * offset[1], offset[0], 0)) + + for i in range(matches.shape[0]): + idx1 = matches[i, 0] + idx2 = matches[i, 1] + + if matches_color is None: + color = np.random.rand(3, 1) + else: + color = matches_color + + ax.plot((keypoints1[idx1, 1], keypoints2[idx2, 1] + offset[1]), + (keypoints1[idx1, 0], keypoints2[idx2, 0]), + '-', color=color) + + +def _prepare_grayscale_input_2D(image): + image = np.squeeze(image) + if image.ndim != 2: + raise ValueError("Only 2-D gray-scale images supported.") + + return img_as_float(image) + + +def _mask_border_keypoints(image_shape, keypoints, distance): + """Mask coordinates that are within certain distance from the image border. + + Parameters + ---------- + image_shape : (2, ) array_like + Shape of the image as ``(rows, cols)``. + keypoints : (N, 2) array + Keypoint coordinates as ``(rows, cols)``. + distance : int + Image border distance. Returns ------- - distance : (P1, P2) array of dtype float - 2D ndarray with value at an index (i, j) representing the hamming - distance in the range [0, 1] between ith vector in array1 and jth - vector in array2. + mask : (N, ) bool array + Mask indicating if pixels are within the image (``True``) or in the + border region of the image (``False``). """ - distance = (array1[:, None] != array2[None]).mean(axis=2) - return distance + + rows = image_shape[0] + cols = image_shape[1] + + mask = (((distance - 1) < keypoints[:, 0]) + & (keypoints[:, 0] < (rows - distance + 1)) + & ((distance - 1) < keypoints[:, 1]) + & (keypoints[:, 1] < (cols - distance + 1))) + + return mask diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index cef9b2e2..c8fca735 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -5,14 +5,23 @@ from ._canny import canny from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, hprewitt, vprewitt, roberts, roberts_positive_diagonal, roberts_negative_diagonal) -from ._denoise import denoise_tv_chambolle -from ._denoise_cy import denoise_bilateral, denoise_tv_bregman from ._rank_order import rank_order from ._gabor import gabor_kernel, gabor_filter -from .thresholding import threshold_otsu, threshold_adaptive +from .thresholding import (threshold_adaptive, threshold_otsu, threshold_yen, + threshold_isodata) from . import rank +from skimage._shared.utils import deprecated +from skimage import restoration +denoise_bilateral = deprecated('skimage.restoration.denoise_bilateral')\ + (restoration.denoise_bilateral) +denoise_tv_bregman = deprecated('skimage.restoration.denoise_tv_bregman')\ + (restoration.denoise_tv_bregman) +denoise_tv_chambolle = deprecated('skimage.restoration.denoise_tv_chambolle')\ + (restoration.denoise_tv_chambolle) + + __all__ = ['inverse', 'wiener', 'LPIFilter2D', @@ -37,6 +46,8 @@ __all__ = ['inverse', 'rank_order', 'gabor_kernel', 'gabor_filter', - 'threshold_otsu', 'threshold_adaptive', + 'threshold_otsu', + 'threshold_yen', + 'threshold_isodata', 'rank'] diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py index 04a4b854..a2804178 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -1,11 +1,11 @@ from .generic import (autolevel, bottomhat, equalize, gradient, maximum, mean, subtract_mean, median, minimum, modal, enhance_contrast, - pop, threshold, tophat, noise_filter, entropy, otsu) + pop, threshold, tophat, noise_filter, entropy, otsu, sum) from ._percentile import (autolevel_percentile, gradient_percentile, mean_percentile, subtract_mean_percentile, enhance_contrast_percentile, percentile, - pop_percentile, threshold_percentile) -from .bilateral import mean_bilateral, pop_bilateral + pop_percentile, sum_percentile, threshold_percentile) +from .bilateral import mean_bilateral, pop_bilateral, sum_bilateral from skimage._shared.utils import deprecated @@ -51,12 +51,15 @@ __all__ = ['autolevel', 'pop', 'pop_percentile', 'pop_bilateral', + 'sum', + 'sum_bilateral', + 'sum_percentile', 'threshold', 'threshold_percentile', 'tophat', 'noise_filter', 'entropy', - 'otsu' + 'otsu', 'percentile', # Deprecated 'percentile_autolevel', diff --git a/skimage/filter/rank/_percentile.py b/skimage/filter/rank/_percentile.py index ff3b1559..f8ace8a4 100644 --- a/skimage/filter/rank/_percentile.py +++ b/skimage/filter/rank/_percentile.py @@ -50,17 +50,19 @@ def autolevel_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): """Return greyscale local autolevel of an image. - Autolevel is computed on the given structuring element. Only levels between - percentiles [p0, p1] are used. + This filter locally stretches the histogram of greyvalues to cover the + entire range of values from "white" to "black". + + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -74,7 +76,7 @@ def autolevel_percentile(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -86,19 +88,18 @@ def autolevel_percentile(image, selem, out=None, mask=None, shift_x=False, def gradient_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): - """Return greyscale local gradient of an image. + """Return local gradient of an image (i.e. local maximum - local minimum). - gradient is computed on the given structuring element. Only - levels between percentiles [p0, p1] are used. + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -112,7 +113,7 @@ def gradient_percentile(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -124,19 +125,18 @@ def gradient_percentile(image, selem, out=None, mask=None, shift_x=False, def mean_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): - """Return greyscale local mean of an image. + """Return local mean of an image. - Mean is computed on the given structuring element. Only levels between - percentiles [p0, p1] are used. + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -150,7 +150,7 @@ def mean_percentile(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -162,19 +162,18 @@ def mean_percentile(image, selem, out=None, mask=None, shift_x=False, def subtract_mean_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): - """Return greyscale local subtract_mean of an image. + """Return image subtracted from its local mean. - subtract_mean is computed on the given structuring element. Only levels - between percentiles [p0, p1] are used. + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -188,7 +187,7 @@ def subtract_mean_percentile(image, selem, out=None, mask=None, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -200,19 +199,22 @@ def subtract_mean_percentile(image, selem, out=None, mask=None, def enhance_contrast_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): - """Return greyscale local enhance_contrast of an image. + """Enhance contrast of an image. - enhance_contrast is computed on the given structuring element. Only levels - between percentiles [p0, p1] are used. + This replaces each pixel by the local maximum if the pixel greyvalue is + closer to the local maximum than the local minimum. Otherwise it is + replaced by the local minimum. + + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -226,7 +228,7 @@ def enhance_contrast_percentile(image, selem, out=None, mask=None, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -238,19 +240,21 @@ def enhance_contrast_percentile(image, selem, out=None, mask=None, def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0): - """Return greyscale local percentile of an image. + """Return local percentile of an image. - percentile is computed on the given structuring element. Returns the value - of the p0 lower percentile of the neighborhood value distribution. + Returns the value of the p0 lower percentile of the local greyvalue + distribution. + + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -263,7 +267,7 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -275,19 +279,21 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, def pop_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): - """Return greyscale local pop of an image. + """Return the local number (population) of pixels. - pop is computed on the given structuring element. Only levels between - percentiles [p0, p1] are used. + The number of pixels is defined as the number of pixels which are included + in the structuring element and the mask. + + Only greyvalues between percentiles [p0, p1] are considered in the filter. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -301,7 +307,7 @@ def pop_percentile(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. """ @@ -311,23 +317,63 @@ def pop_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=shift_y, p0=p0, p1=p1) -def threshold_percentile(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=0): - """Return greyscale local threshold of an image. +def sum_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0, p1=1): + """Return the local sum of pixels. - threshold is computed on the given structuring element. Returns - thresholded image such that pixels having a higher value than the the p0 - percentile of the neighborhood value distribution are set to 2^nbit-1 - (e.g. 255 for 8bit image). + Only greyvalues between percentiles [p0, p1] are considered in the filter. + + Note that the sum may overflow depending on the data type of the input + array. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + p0, p1 : float in [0, ..., 1] + Define the [p0, p1] percentile interval to be considered for computing + the value. + + Returns + ------- + out : 2-D array (same dtype as input image) + Output image. + + """ + + return _apply(percentile_cy._sum, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) + + +def threshold_percentile(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, p0=0): + """Local threshold of an image. + + The resulting binary mask is True if the greyvalue of the center pixel is + greater than the local mean. + + Only greyvalues between percentiles [p0, p1] are considered in the filter. + + Parameters + ---------- + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array + The neighborhood expressed as a 2-D array of 1's and 0's. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -338,7 +384,7 @@ def threshold_percentile(image, selem, out=None, mask=None, shift_x=False, p0 : float in [0, ..., 1] Set the percentile value. - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. local threshold : ndarray (same dtype as input) The result of the local threshold. diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index f1b10fec..d01680db 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -30,7 +30,7 @@ from . import bilateral_cy from .generic import _handle_input -__all__ = ['mean_bilateral', 'pop_bilateral'] +__all__ = ['mean_bilateral', 'pop_bilateral', 'sum_bilateral'] def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1, @@ -53,21 +53,22 @@ def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, pixels based on their spatial closeness and radiometric similarity. Spatial closeness is measured by considering only the local pixel - neighborhood given by a structuring element (selem). + neighborhood given by a structuring element. Radiometric similarity is defined by the greylevel interval [g-s0, g+s1] - where g is the current pixel greylevel. Only pixels belonging to the - structuring element AND having a greylevel inside this interval are - averaged. Return greyscale local bilateral_mean of an image. + where g is the current pixel greylevel. + + Only pixels belonging to the structuring element and having a greylevel + inside this interval are averaged. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -81,22 +82,20 @@ def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. See also -------- - skimage.filter.denoise_bilateral for a gaussian bilateral filter. + skimage.filter.denoise_bilateral for a Gaussian bilateral filter. Examples -------- >>> from skimage import data >>> from skimage.morphology import disk - >>> from skimage.filter.rank import bilateral_mean - >>> # Load test image / cast to uint16 - >>> ima = data.camera().astype(np.uint16) - >>> # bilateral filtering of cameraman image using a flat kernel - >>> bilat_ima = bilateral_mean(ima, disk(20), s0=10,s1=10) + >>> from skimage.filter.rank import mean_bilateral + >>> img = data.camera().astype(np.uint16) + >>> bilat_img = mean_bilateral(img, disk(20), s0=10,s1=10) """ @@ -106,18 +105,22 @@ def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, def pop_bilateral(image, selem, out=None, mask=None, shift_x=False, shift_y=False, s0=10, s1=10): - """Return the number (population) of pixels actually inside the bilateral - neighborhood, i.e. being inside the structuring element AND having a gray - level inside the interval [g-s0, g+s1]. + """Return the local number (population) of pixels. + + + The number of pixels is defined as the number of pixels which are included + in the structuring element and the mask. Additionally the must have a + greylevel inside the interval [g-s0, g+s1] where g is the greyvalue of the + center pixel. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -131,20 +134,19 @@ def pop_bilateral(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples -------- - >>> # Local mean >>> from skimage.morphology import square >>> import skimage.filter.rank as rank - >>> ima16 = 255 * np.array([[0, 0, 0, 0, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 0, 0, 0, 0]], dtype=np.uint16) - >>> rank.bilateral_pop(ima16, square(3), s0=10,s1=10) + >>> img = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint16) + >>> rank.pop_bilateral(img, square(3), s0=10, s1=10) array([[3, 4, 3, 4, 3], [4, 4, 6, 4, 4], [3, 6, 9, 6, 3], @@ -155,3 +157,63 @@ def pop_bilateral(image, selem, out=None, mask=None, shift_x=False, return _apply(bilateral_cy._pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) + +def sum_bilateral(image, selem, out=None, mask=None, shift_x=False, + shift_y=False, s0=10, s1=10): + """Apply a flat kernel bilateral filter. + + This is an edge-preserving and noise reducing denoising filter. It averages + pixels based on their spatial closeness and radiometric similarity. + + Spatial closeness is measured by considering only the local pixel + neighborhood given by a structuring element (selem). + + Radiometric similarity is defined by the greylevel interval [g-s0, g+s1] + where g is the current pixel greylevel. + + Only pixels belonging to the structuring element AND having a greylevel + inside this interval are summed. + + Note that the sum may overflow depending on the data type of the input + array. + + Parameters + ---------- + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array + The neighborhood expressed as a 2-D array of 1's and 0's. + out : 2-D array (same dtype as input) + If None, a new array is allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + s0, s1 : int + Define the [s0, s1] interval around the greyvalue of the center pixel + to be considered for computing the value. + + Returns + ------- + out : 2-D array (same dtype as input image) + Output image. + + See also + -------- + skimage.filter.denoise_bilateral for a Gaussian bilateral filter. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import sum_bilateral + >>> img = data.camera().astype(np.uint16) + >>> bilat_img = sum_bilateral(img, disk(10), s0=10, s1=10) + + """ + + return _apply(bilateral_cy._sum, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) diff --git a/skimage/filter/rank/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx index de2b3e53..25a81766 100644 --- a/skimage/filter/rank/bilateral_cy.pyx +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -47,6 +47,27 @@ cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, else: return 0 +cdef inline double _kernel_sum(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t bilat_pop = 0 + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + sum += histo[i] * i + if bilat_pop: + return sum + else: + return 0 + else: + return 0 + def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, @@ -68,3 +89,13 @@ def _pop(dtype_t[:, ::1] image, _core(_kernel_pop[dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, s0, s1, max_bin) + +def _sum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): + + _core(_kernel_sum[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 34c0787b..ccc1166e 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -77,16 +77,19 @@ def _apply(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Autolevel image using local histogram. + """Auto-level image using local histogram. + + This filter locally stretches the histogram of greyvalues to cover the + entire range of values from "white" to "black". Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -97,7 +100,7 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples @@ -105,10 +108,8 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): >>> from skimage import data >>> from skimage.morphology import disk >>> from skimage.filter.rank import autolevel - >>> # Load test image - >>> ima = data.camera() - >>> # Stretch image contrast locally - >>> auto = autolevel(ima, disk(20)) + >>> img = data.camera() + >>> auto = autolevel(img, disk(5)) """ @@ -117,17 +118,20 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns greyscale local bottomhat of an image. + """Local bottom-hat of an image. + + This filter computes the morphological closing of the image and then + subtracts the result from the original image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. - mask : ndarray + out : 2-D array (same dtype as input) + If None, a new array is allocated. + mask : 2-D array Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -137,8 +141,16 @@ def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - bottomhat : ndarray (same dtype as input image) - The result of the local bottomhat. + out : 2-D array (same dtype as input image) + Output image. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import bottomhat + >>> img = data.camera() + >>> out = bottomhat(img, disk(5)) """ @@ -151,12 +163,12 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -167,7 +179,7 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples @@ -175,10 +187,8 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): >>> from skimage import data >>> from skimage.morphology import disk >>> from skimage.filter.rank import equalize - >>> # Load test image - >>> ima = data.camera() - >>> # Local equalization - >>> equ = equalize(ima, disk(20)) + >>> img = data.camera() + >>> equ = equalize(img, disk(5)) """ @@ -187,18 +197,16 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local gradient of an image (i.e. local maximum - local - minimum). - + """Return local gradient of an image (i.e. local maximum - local minimum). Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -209,9 +217,17 @@ def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import gradient + >>> img = data.camera() + >>> out = gradient(img, disk(5)) + """ return _apply(generic_cy._gradient, image, selem, @@ -219,17 +235,16 @@ def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local maximum of an image. - + """Return local maximum of an image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -240,7 +255,7 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. See also @@ -249,8 +264,16 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Notes ----- - * the lower algorithm complexity makes the rank.maximum() more efficient - for larger images and structuring elements + The lower algorithm complexity makes the `skimage.filter.rank.maximum` + more efficient for larger images and structuring elements. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import maximum + >>> img = data.camera() + >>> out = maximum(img, disk(5)) """ @@ -259,16 +282,16 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local mean of an image. + """Return local mean of an image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -279,7 +302,7 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples @@ -287,10 +310,8 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): >>> from skimage import data >>> from skimage.morphology import disk >>> from skimage.filter.rank import mean - >>> # Load test image - >>> ima = data.camera() - >>> # Local mean - >>> avg = mean(ima, disk(20)) + >>> img = data.camera() + >>> avg = mean(img, disk(5)) """ @@ -304,12 +325,12 @@ def subtract_mean(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -320,9 +341,17 @@ def subtract_mean(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import subtract_mean + >>> img = data.camera() + >>> out = subtract_mean(img, disk(5)) + """ return _apply(generic_cy._subtract_mean, image, selem, @@ -330,16 +359,16 @@ def subtract_mean(image, selem, out=None, mask=None, shift_x=False, def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local median of an image. + """Return local median of an image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -350,7 +379,7 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples @@ -358,10 +387,8 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): >>> from skimage import data >>> from skimage.morphology import disk >>> from skimage.filter.rank import median - >>> # Load test image - >>> ima = data.camera() - >>> # Local mean - >>> avg = median(ima, disk(20)) + >>> img = data.camera() + >>> med = median(img, disk(5)) """ @@ -370,16 +397,16 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local minimum of an image. + """Return local minimum of an image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -390,7 +417,7 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. See also @@ -399,8 +426,16 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Notes ----- - * the lower algorithm complexity makes the rank.minimum() more efficient - for larger images and structuring elements + The lower algorithm complexity makes the `skimage.filter.rank.minimum` more + efficient for larger images and structuring elements. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import minimum + >>> img = data.camera() + >>> out = minimum(img, disk(5)) """ @@ -409,16 +444,18 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local mode of an image. + """Return local mode of an image. + + The mode is the value that appears most often in the local histogram. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -429,9 +466,17 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import modal + >>> img = data.camera() + >>> out = modal(img, disk(5)) + """ return _apply(generic_cy._modal, image, selem, @@ -440,18 +485,20 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Enhance an image replacing each pixel by the local maximum if pixel - greylevel is closest to maximimum than local minimum OR local minimum - otherwise. + """Enhance contrast of an image. + + This replaces each pixel by the local maximum if the pixel greyvalue is + closer to the local maximum than the local minimum. Otherwise it is + replaced by the local minimum. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -462,7 +509,7 @@ def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, Returns Output image. - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) The result of the local enhance_contrast. Examples @@ -470,10 +517,8 @@ def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, >>> from skimage import data >>> from skimage.morphology import disk >>> from skimage.filter.rank import enhance_contrast - >>> # Load test image - >>> ima = data.camera() - >>> # Local mean - >>> avg = enhance_contrast(ima, disk(20)) + >>> img = data.camera() + >>> out = enhance_contrast(img, disk(5)) """ @@ -482,17 +527,19 @@ def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return the number (population) of pixels actually inside the - neighborhood. + """Return the local number (population) of pixels. + + The number of pixels is defined as the number of pixels which are included + in the structuring element and the mask. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -503,20 +550,19 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. Examples -------- - >>> # Local mean >>> from skimage.morphology import square >>> import skimage.filter.rank as rank - >>> ima = 255 * np.array([[0, 0, 0, 0, 0], + >>> img = 255 * np.array([[0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], ... [0, 0, 0, 0, 0]], dtype=np.uint8) - >>> rank.pop(ima, square(3)) + >>> rank.pop(img, square(3)) array([[4, 6, 6, 6, 4], [6, 9, 9, 9, 6], [6, 9, 9, 9, 6], @@ -529,17 +575,20 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): mask=mask, shift_x=shift_x, shift_y=shift_y) -def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local threshold of an image. +def sum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return the local sum of pixels. + + Note that the sum may overflow depending on the data type of the input + array. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -550,20 +599,68 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) + Output image. + + Examples + -------- + >>> from skimage.morphology import square + >>> import skimage.filter.rank as rank + >>> img = np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> rank.sum(img, square(3)) + array([[1, 2, 3, 2, 1], + [2, 4, 6, 4, 2], + [3, 6, 9, 6, 3], + [2, 4, 6, 4, 2], + [1, 2, 3, 2, 1]], dtype=uint8) + + """ + + return _apply(generic_cy._sum, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Local threshold of an image. + + The resulting binary mask is True if the greyvalue of the center pixel is + greater than the local mean. + + Parameters + ---------- + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array + The neighborhood expressed as a 2-D array of 1's and 0's. + out : 2-D array (same dtype as input) + If None, a new array is allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : 2-D array (same dtype as input image) Output image. Examples -------- - >>> # Local threshold >>> from skimage.morphology import square >>> from skimage.filter.rank import threshold - >>> ima = 255 * np.array([[0, 0, 0, 0, 0], + >>> img = 255 * np.array([[0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], ... [0, 0, 0, 0, 0]], dtype=np.uint8) - >>> threshold(ima, square(3)) + >>> threshold(img, square(3)) array([[0, 0, 0, 0, 0], [0, 1, 1, 1, 0], [0, 1, 0, 1, 0], @@ -577,16 +674,19 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local tophat of an image. + """Local top-hat of an image. + + This filter computes the morphological opening of the image and then + subtracts the result from the original image. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -597,9 +697,17 @@ def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import tophat + >>> img = data.camera() + >>> out = tophat(img, disk(5)) + """ return _apply(generic_cy._tophat, image, selem, @@ -608,16 +716,16 @@ def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def noise_filter(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns the noise feature as described in [Hashimoto12]_ + """Noise feature as described in [Hashimoto12]_. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -633,9 +741,17 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import noise_filter + >>> img = data.camera() + >>> out = noise_filter(img, disk(5)) + """ # ensure that the central pixel in the structuring element is empty @@ -650,18 +766,19 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns the entropy [1]_ computed locally. Entropy is computed - using base 2 logarithm i.e. the filter returns the minimum number of - bits needed to encode local greylevel distribution. + """Local entropy [1]_. + + The entropy is computed using base 2 logarithm i.e. the filter returns the + minimum number of bits needed to encode the local greylevel distribution. Parameters ---------- - image : ndarray (uint8, uint16) - Image array. - selem : ndarray + image : 2-D array (uint8, uint16) + Input image. + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray (same dtype as input) - If None, a new array will be allocated. + out : 2-D array (same dtype as input) + If None, a new array is allocated. mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). @@ -677,16 +794,15 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): References ---------- - .. [1] http://en.wikipedia.org/wiki/Entropy_(information_theory)> + .. [1] http://en.wikipedia.org/wiki/Entropy_(information_theory) Examples -------- - >>> # Local entropy >>> from skimage import data >>> from skimage.filter.rank import entropy >>> from skimage.morphology import disk - >>> a8 = data.camera() - >>> ent8 = entropy(a8, disk(5)) + >>> img = data.camera() + >>> ent = entropy(img, disk(5)) """ @@ -696,13 +812,13 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns the Otsu's threshold value for each pixel. + """Local Otsu's threshold value for each pixel. Parameters ---------- image : ndarray Image array (uint8 array). - selem : ndarray + selem : 2-D array The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray If None, a new array will be allocated. @@ -716,7 +832,7 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : 2-D array (same dtype as input image) Output image. References @@ -725,14 +841,12 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Examples -------- - >>> # Local entropy >>> from skimage import data >>> from skimage.filter.rank import otsu >>> from skimage.morphology import disk - >>> # defining a 8-bit test images - >>> a8 = data.camera() - >>> loc_otsu = otsu(a8, disk(5)) - >>> thresh_image = a8 >= loc_otsu + >>> img = data.camera() + >>> local_otsu = otsu(img, disk(5)) + >>> thresh_image = img >= local_otsu """ diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index dcf6e361..1c26cf53 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -222,6 +222,22 @@ cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, return pop +cdef inline double _kernel_sum(Py_ssize_t* histo, double pop,dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + sum += histo[i] * i + return sum + else: + return 0 + + cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -455,6 +471,15 @@ def _pop(dtype_t[:, ::1] image, _core(_kernel_pop[dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) +def _sum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_sum[dtype_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + def _threshold(dtype_t[:, ::1] image, char[:, ::1] selem, diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index e951a76e..38d04b33 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -90,6 +90,29 @@ cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, else: return 0 +cdef inline double _kernel_sum(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, sum, sum_g, n + + if pop: + sum = 0 + sum_g = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + sum_g += histo[i] * i + + if n > 0: + return sum_g + else: + return 0 + else: + return 0 cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, dtype_t g, @@ -245,6 +268,15 @@ def _mean(dtype_t[:, ::1] image, _core(_kernel_mean[dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) +def _sum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): + + _core(_kernel_sum[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) def _subtract_mean(dtype_t[:, ::1] image, char[:, ::1] selem, diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index cedeb92d..72f9afe6 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -498,6 +498,48 @@ def test_percentile_median(): img_max = rank.median(img16, selem=selem) assert_array_equal(img_p0, img_max) +def test_sum(): + # check the number of valid pixels in the neighborhood + + image8 = np.array([[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], dtype=np.uint8) + image16 = 400*np.array([[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], dtype=np.uint16) + elem = np.ones((3, 3), dtype=np.uint8) + out8 = np.empty_like(image8) + out16 = np.empty_like(image16) + mask = np.ones(image8.shape, dtype=np.uint8) + + r = np.array([[1, 2, 3, 2, 1], + [2, 4, 6, 4, 2], + [3, 6, 9, 6, 3], + [2, 4, 6, 4, 2], + [1, 2, 3, 2, 1]], dtype=np.uint8) + rank.sum(image=image8, selem=elem, out=out8, mask=mask) + assert_array_equal(r, out8) + rank.sum_percentile(image=image8, selem=elem, out=out8, mask=mask,p0=.0,p1=1.) + assert_array_equal(r, out8) + rank.sum_bilateral(image=image8, selem=elem, out=out8, mask=mask,s0=255,s1=255) + assert_array_equal(r, out8) + + r = 400* np.array([[1, 2, 3, 2, 1], + [2, 4, 6, 4, 2], + [3, 6, 9, 6, 3], + [2, 4, 6, 4, 2], + [1, 2, 3, 2, 1]], dtype=np.uint16) + rank.sum(image=image16, selem=elem, out=out16, mask=mask) + assert_array_equal(r, out16) + rank.sum_percentile(image=image16, selem=elem, out=out16, mask=mask,p0=.0,p1=1.) + assert_array_equal(r, out16) + rank.sum_bilateral(image=image16, selem=elem, out=out16, mask=mask,s0=1000,s1=1000) + assert_array_equal(r, out16) + if __name__ == "__main__": run_module_suite() diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index 359c3b68..2f5a72e6 100644 --- a/skimage/filter/setup.py +++ b/skimage/filter/setup.py @@ -14,7 +14,6 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('rank/tests') cython(['_ctmf.pyx'], working_path=base_path) - cython(['_denoise_cy.pyx'], working_path=base_path) cython(['rank/core_cy.pyx'], working_path=base_path) cython(['rank/generic_cy.pyx'], working_path=base_path) cython(['rank/percentile_cy.pyx'], working_path=base_path) @@ -22,8 +21,6 @@ def configuration(parent_package='', top_path=None): config.add_extension('_ctmf', sources=['_ctmf.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_denoise_cy', sources=['_denoise_cy.c'], - include_dirs=[get_numpy_include_dirs(), '../_shared']) config.add_extension('rank.core_cy', sources=['rank/core_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('rank.generic_cy', sources=['rank/generic_cy.c'], diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 0edfe4e7..f0a68f7d 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -5,7 +5,8 @@ import skimage from skimage import data from skimage.filter.thresholding import (threshold_adaptive, threshold_otsu, - threshold_yen) + threshold_yen, + threshold_isodata) class TestSimpleImage(): @@ -43,10 +44,29 @@ class TestSimpleImage(): assert threshold_yen(image) == 127 def test_yen_binary(self): - image = np.zeros([2,256], dtype='uint8') + image = np.zeros([2,256], dtype=np.uint8) image[0] = 255 assert threshold_yen(image) < 1 + def test_yen_blank_zero(self): + image = np.zeros((5, 5), dtype=np.uint8) + assert threshold_yen(image) == 0 + + def test_yen_blank_max(self): + image = np.empty((5, 5), dtype=np.uint8) + image.fill(255) + assert threshold_yen(image) == 255 + + def test_isodata(self): + assert threshold_isodata(self.image) == 2 + + def test_isodata_blank_zero(self): + image = np.zeros((5, 5), dtype=np.uint8) + assert threshold_isodata(image) == 0 + + def test_isodata_linspace(self): + assert -63.8 < threshold_isodata(np.linspace(-127, 0, 256)) < -63.6 + def test_threshold_adaptive_generic(self): def func(arr): return arr.sum() / arr.shape[0] @@ -114,6 +134,11 @@ def test_otsu_lena_image(): assert 140 < threshold_otsu(lena) < 142 +def test_yen_camera_image(): + camera = skimage.img_as_ubyte(data.camera()) + assert 197 < threshold_yen(camera) < 199 + + def test_yen_coins_image(): coins = skimage.img_as_ubyte(data.coins()) assert 109 < threshold_yen(coins) < 111 @@ -124,5 +149,32 @@ def test_yen_coins_image_as_float(): assert 0.43 < threshold_yen(coins) < 0.44 +def test_isodata_camera_image(): + camera = skimage.img_as_ubyte(data.camera()) + assert threshold_isodata(camera) == 88 + + +def test_isodata_coins_image(): + coins = skimage.img_as_ubyte(data.coins()) + assert threshold_isodata(coins) == 107 + + +def test_isodata_moon_image(): + moon = skimage.img_as_ubyte(data.moon()) + assert threshold_isodata(moon) == 87 + + +def test_isodata_moon_image_negative_int(): + moon = skimage.img_as_ubyte(data.moon()).astype(np.int32) + moon -= 100 + assert threshold_isodata(moon) == -13 + + +def test_isodata_moon_image_negative_float(): + moon = skimage.img_as_ubyte(data.moon()).astype(np.float64) + moon -= 100 + assert -13 < threshold_isodata(moon) < -12 + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 2ce8a3e1..6ccf8f6b 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -1,4 +1,7 @@ -__all__ = ['threshold_adaptive', 'threshold_otsu', 'threshold_yen'] +__all__ = ['threshold_adaptive', + 'threshold_otsu', + 'threshold_yen', + 'threshold_isodata'] import numpy as np import scipy.ndimage @@ -129,7 +132,7 @@ def threshold_otsu(image, nbins=256): # Clip ends to align class 1 and class 2 variables: # The last value of `weight1`/`mean1` should pair with zero values in # `weight2`/`mean2`, which do not exist. - variance12 = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:])**2 + variance12 = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:]) ** 2 idx = np.argmax(variance12) threshold = bin_centers[:-1][idx] @@ -172,15 +175,84 @@ def threshold_yen(image, nbins=256): >>> binary = image <= thresh """ hist, bin_centers = histogram(image, nbins) - norm_histo = hist.astype(float) / hist.sum() # Probability mass function - P1 = np.cumsum(norm_histo) # Cumulative normalized histogram - P1_sq = np.cumsum(norm_histo ** 2) + # On blank images (e.g. filled with 0) with int dtype, `histogram()` + # returns `bin_centers` containing only one value. Speed up with it. + if bin_centers.size == 1: + return bin_centers[0] + + # Calculate probability mass function + pmf = hist.astype(np.float32) / hist.sum() + P1 = np.cumsum(pmf) # Cumulative normalized histogram + P1_sq = np.cumsum(pmf ** 2) # Get cumsum calculated from end of squared array: - P2_sq = np.cumsum(norm_histo[::-1] ** 2)[::-1] + P2_sq = np.cumsum(pmf[::-1] ** 2)[::-1] # P2_sq indexes is shifted +1. I assume, with P1[:-1] it's help avoid '-inf' # in crit. ImageJ Yen implementation replaces those values by zero. - crit = np.log(((P1_sq[:-1] * P2_sq[1:]) ** -1) * \ + crit = np.log(((P1_sq[:-1] * P2_sq[1:]) ** -1) * (P1[:-1] * (1.0 - P1[:-1])) ** 2) - max_crit = np.argmax(crit) - threshold = bin_centers[:-1][max_crit] + return bin_centers[crit.argmax()] + + +def threshold_isodata(image, nbins=256): + """Return threshold value based on ISODATA method. + + Histogram-based threshold, known as Ridler-Calvard method or intermeans. + + Parameters + ---------- + image : array + Input image. + nbins : int, optional + Number of bins used to calculate histogram. This value is ignored for + integer arrays. + + Returns + ------- + threshold : float or int, corresponding input array dtype. + Upper threshold value. All pixels intensities that less or equal of + this value assumed as background. + + References + ---------- + .. [1] Ridler, TW & Calvard, S (1978), "Picture thresholding using an + iterative selection method" + .. [2] IEEE Transactions on Systems, Man and Cybernetics 8: 630-632, + http://ieeexplore.ieee.org/xpls/abs_all.jsp?arnumber=4310039 + .. [3] Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding + Techniques and Quantitative Performance Evaluation" Journal of + Electronic Imaging, 13(1): 146-165, + http://www.busim.ee.boun.edu.tr/~sankur/SankurFolder/Threshold_survey.pdf + .. [4] ImageJ AutoThresholder code, + http://fiji.sc/wiki/index.php/Auto_Threshold + + Examples + -------- + >>> from skimage.data import coins + >>> image = coins() + >>> thresh = threshold_isodata(image) + >>> binary = image > thresh + """ + hist, bin_centers = histogram(image, nbins) + # On blank images (e.g. filled with 0) with int dtype, `histogram()` + # returns `bin_centers` containing only one value. Speed up with it. + if bin_centers.size == 1: + return bin_centers[0] + # It is not necessary to calculate the probability mass function here, + # because the l and h fractions already include the normalization. + pmf = hist.astype(np.float32) # / hist.sum() + cpmfl = np.cumsum(pmf, dtype=np.float32) + cpmfh = np.cumsum(pmf[::-1], dtype=np.float32)[::-1] + + binnums = np.arange(pmf.size, dtype=np.uint8) + # l and h contain average value of pixels in sum of bins, calculated + # from lower to higher and from higher to lower respectively. + l = np.ma.divide(np.cumsum(pmf * binnums, dtype=np.float32), cpmfl) + h = np.ma.divide( + np.cumsum((pmf[::-1] * binnums[::-1]), dtype=np.float32)[::-1], + cpmfh) + + allmean = (l + h) / 2.0 + threshold = bin_centers[np.nonzero(allmean.round() == binnums)[0][0]] + # This implementation returns threshold where + # `background <= threshold < foreground`. return threshold diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index eb817c77..a335971d 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -1,7 +1,9 @@ from .spath import shortest_path -from .mcp import MCP, MCP_Geometric, route_through_array +from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array __all__ = ['shortest_path', 'MCP', 'MCP_Geometric', + 'MCP_Connect', + 'MCP_Flexible', 'route_through_array'] \ No newline at end of file diff --git a/skimage/graph/_mcp.pxd b/skimage/graph/_mcp.pxd index b2e2a548..1cfdbc9a 100644 --- a/skimage/graph/_mcp.pxd +++ b/skimage/graph/_mcp.pxd @@ -9,22 +9,36 @@ cimport numpy as cnp ctypedef heap.BOOL_T BOOL_T ctypedef unsigned char DIM_T ctypedef cnp.float64_t FLOAT_T +ctypedef cnp.intp_t INDEX_T +ctypedef cnp.int8_t EDGE_T +ctypedef cnp.int8_t OFFSET_T +ctypedef cnp.int16_t OFFSETS_INDEX_T + cdef class MCP: cdef heap.FastUpdateBinaryHeap costs_heap cdef object costs_shape + cdef object _starts + cdef object _ends cdef DIM_T dim - cdef object flat_costs - cdef object flat_cumulative_costs - cdef object traceback_offsets - cdef object flat_pos_edge_map - cdef object flat_neg_edge_map - cdef readonly object offsets - cdef object flat_offsets - cdef object offset_lengths cdef BOOL_T dirty cdef BOOL_T use_start_cost # if use_start_cost is true, the cost of the starting element is added to # the cost of the path. Set to true by default in the base class... - + + # Arrays used during front propagation + cdef FLOAT_T [:] flat_costs + cdef FLOAT_T [:] flat_cumulative_costs + cdef OFFSETS_INDEX_T [:] traceback_offsets + cdef EDGE_T [:,:] flat_pos_edge_map + cdef EDGE_T [:,:] flat_neg_edge_map + cdef OFFSET_T [:,:] offsets + cdef INDEX_T [:] flat_offsets + cdef FLOAT_T [:] offset_lengths + + # Methods + cpdef int goal_reached(self, INDEX_T index, FLOAT_T cumcost) cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, FLOAT_T offset_length) + cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index, FLOAT_T offset_length) + cdef void _update_node(self, INDEX_T index, INDEX_T new_index, FLOAT_T offset_length) + \ No newline at end of file diff --git a/skimage/graph/_mcp.pyx b/skimage/graph/_mcp.pyx index 34904dcc..c65922db 100644 --- a/skimage/graph/_mcp.pyx +++ b/skimage/graph/_mcp.pyx @@ -5,6 +5,7 @@ for use with data on a n-dimensional lattice. Original author: Zachary Pincus Inspired by code from Almar Klein +Later modifications by Almar Klein (Dec 2013) License: BSD @@ -39,13 +40,9 @@ import heap cimport numpy as cnp cimport heap -ctypedef cnp.int8_t OFFSET_T OFFSET_D = np.int8 -ctypedef cnp.int16_t OFFSETS_INDEX_T OFFSETS_INDEX_D = np.int16 -ctypedef cnp.int8_t EDGE_T EDGE_D = np.int8 -ctypedef cnp.intp_t INDEX_T INDEX_D = np.intp FLOAT_D = np.float64 @@ -106,7 +103,7 @@ def _offset_edge_map(shape, offsets): [0, 0, 2, 1]], dtype=int8) """ - indices = np.indices(shape) # indices.shape = (n,)+shape + indices = np.indices(shape) # indices.shape = (n,)+shape #get the distance from each index to the upper or lower edge in each dim pos_edges = (shape - indices.T).T @@ -172,11 +169,12 @@ def make_offsets(d, fully_connected): def _unravel_index_fortran(flat_indices, shape): """_unravel_index_fortran(flat_indices, shape) - Given a flat index into an n-d fortran-strided array, return an index tuple. - + Given a flat index into an n-d fortran-strided array, return an + index tuple. + """ strides = np.multiply.accumulate([1] + list(shape[:-1])) - indices = [tuple(idx/strides % shape) for idx in flat_indices] + indices = [tuple((idx // strides) % shape) for idx in flat_indices] return indices @@ -185,7 +183,8 @@ def _unravel_index_fortran(flat_indices, shape): def _ravel_index_fortran(indices, shape): """_ravel_index_fortran(flat_indices, shape) - Given an index tuple into an n-d fortran-strided array, return a flat index. + Given an index tuple into an n-d fortran-strided array, return a + flat index. """ strides = np.multiply.accumulate([1] + list(shape[:-1])) @@ -209,7 +208,7 @@ def _normalize_indices(indices, shape): for i, s in zip(index, shape): i = int(i) if i < 0: - i = s+i + i = s + i if not (0 <= i < s): return None new_index.append(i) @@ -220,14 +219,15 @@ def _normalize_indices(indices, shape): @cython.boundscheck(True) @cython.wraparound(True) def _reverse(arr): - """Reverse index an array safely, with bounds/wraparound checks on.""" + """Reverse index an array safely, with bounds/wraparound checks on. + """ return arr[::-1] @cython.boundscheck(False) @cython.wraparound(False) cdef class MCP: - """MCP(costs, offsets=None, fully_connected=True) + """MCP(costs, offsets=None, fully_connected=True, sampling=None) A class for finding the minimum cost path through a given n-d costs array. @@ -261,7 +261,10 @@ cdef class MCP: generated neighborhood. If true, the path may go along diagonals between elements of the `costs` array; otherwise only axial moves are permitted. - + sampling : tuple, optional + For each dimension, specifies the distance between two cells/voxels. + If not given or None, the distance is assumed unit. + Attributes ---------- offsets : ndarray @@ -271,15 +274,27 @@ cdef class MCP: returned by the find_costs() method. """ - def __init__(self, costs, offsets=None, fully_connected=True): - """__init__(costs, offsets=None, fully_connected=True) + + def __init__(self, costs, offsets=None, fully_connected=True, + sampling=None): + """__init__(costs, offsets=None, fully_connected=True, sampling=None) See class documentation. """ costs = np.asarray(costs) if not np.can_cast(costs.dtype, FLOAT_D): raise TypeError('cannot cast costs array to ' + str(FLOAT_D)) - + + # Check sampling + if sampling is None: + sampling = np.array([1.0 for s in costs.shape], FLOAT_D) + elif isinstance(sampling, (list, tuple)): + sampling = np.array(sampling, FLOAT_D) + if sampling.ndim != 1 or len(sampling) != costs.ndim: + raise ValueError('Need one sampling element per dimension.') + else: + raise ValueError('Invalid type for sampling: %r.' % type(sampling)) + # We use flat, fortran-style indexing here (could use C-style, # but this is my code and I like fortran-style! Also, it's # faster when working with image arrays, which are often @@ -287,23 +302,21 @@ cdef class MCP: self.flat_costs = costs.astype(FLOAT_D).flatten('F') size = self.flat_costs.shape[0] self.flat_cumulative_costs = np.empty(size, dtype=FLOAT_D) - self.flat_cumulative_costs.fill(np.inf) self.dim = len(costs.shape) self.costs_shape = costs.shape - self.costs_heap = heap.FastUpdateBinaryHeap(initial_capacity=size, + self.costs_heap = heap.FastUpdateBinaryHeap(initial_capacity=128, max_reference=size-1) # This array stores, for each point, the index into the offset # array (see below) that leads to that point from the # predecessor point. self.traceback_offsets = np.empty(size, dtype=OFFSETS_INDEX_D) - self.traceback_offsets.fill(-1) # The offsets are a list of relative offsets from a central # point to each point in the relevant neighborhood. (e.g. (-1, # 0) might be a 2d offset). - # These offsets are raveled to provide flat, 1d offsets that can be used - # in the same way for flat indices to move to neighboring points. + # These offsets are raveled to provide flat, 1d offsets that can be + # used in the same way for flat indices to move to neighboring points. if offsets is None: offsets = make_offsets(self.dim, fully_connected) self.offsets = np.array(offsets, dtype=OFFSET_D) @@ -313,9 +326,9 @@ cdef class MCP: # Instead of unraveling each index during the pathfinding algorithm, we # will use a pre-computed "edge map" that specifies for each dimension - # whether a given index is on a lower or upper boundary (or none at all) - # Flatten this map to get something that can be indexed as by the same - # flat indices as elsewhere. + # whether a given index is on a lower or upper boundary (or none at + # all). Flatten this map to get something that can be indexed as by the + # same flat indices as elsewhere. # The edge map stores more than a boolean "on some edge" flag so as to # allow us to examine the non-out-of-bounds neighbors for a given edge # point while excluding the neighbors which are outside the array. @@ -325,26 +338,80 @@ cdef class MCP: # The offset lengths are the distances traveled along each offset - self.offset_lengths = np.sqrt( - np.sum(self.offsets**2, axis=1)).astype(FLOAT_D) + self.offset_lengths = np.sqrt(np.sum((sampling * self.offsets)**2, + axis=1)).astype(FLOAT_D) self.dirty = 0 self.use_start_cost = 1 - + + def _reset(self): """_reset() - Clears paths found by find_costs(). """ self.costs_heap.reset() - self.traceback_offsets.fill(-1) - self.flat_cumulative_costs.fill(np.inf) + self.traceback_offsets[...] = -2 # -2 is not reached, -1 is start + self.flat_cumulative_costs[...] = np.inf self.dirty = 0 - + + # Get starts and ends + # We do not pass them in as arguments for backwards compat + starts, ends = self._starts, self._ends + + # push each start point into the heap. Note that we use flat indexing! + for start in _ravel_index_fortran(starts, self.costs_shape): + self.traceback_offsets[start] = -1 + if self.use_start_cost: + self.costs_heap.push_fast(self.flat_costs[start], start) + else: + self.costs_heap.push_fast(0, start) + + cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, FLOAT_T offset_length): + """ float _travel_cost(float old_cost, float new_cost, + float offset_length) + The travel cost for going from the current node to the next. + Default is simply the cost of the next node. + """ return new_cost - - def find_costs(self, starts, ends=None, find_all_ends=True): + + + cpdef int goal_reached(self, INDEX_T index, FLOAT_T cumcost): + """ int goal_reached(int index, float cumcost) + This method is called each iteration after popping an index + from the heap, before examining the neighbours. + + This method can be overloaded to modify the behavior of the MCP + algorithm. An example might be to stop the algorithm when a + certain cumulative cost is reached, or when the front is a + certain distance away from the seed point. + + This method should return 1 if the algorithm should not check + the current point's neighbours and 2 if the algorithm is now + done. + """ + return 0 + + + cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ _examine_neighbor(int index, int new_index, float offset_length) + This method is called once for every pair of neighboring nodes, + as soon as both nodes become frozen. + """ + pass + + + cdef void _update_node(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ _update_node(int index, int new_index, float offset_length) + This method is called when a node is updated. + """ + pass + + + def find_costs(self, starts, ends=None, find_all_ends=True, + max_coverage=1.0, max_cumulative_cost=None, max_cost=None): """ Find the minimum-cost path from the given starting points. @@ -392,10 +459,13 @@ cdef class MCP: cdef BOOL_T use_ends = 0 cdef INDEX_T num_ends cdef BOOL_T all_ends = find_all_ends - cdef cnp.ndarray[INDEX_T, ndim=1] flat_ends + cdef INDEX_T [:] flat_ends starts = _normalize_indices(starts, self.costs_shape) if starts is None: raise ValueError('start points must all be within the costs array') + elif not starts: + raise ValueError('no valid start points to start front' + + 'propagation') if ends is not None: ends = _normalize_indices(ends, self.costs_shape) if ends is None: @@ -406,57 +476,66 @@ cdef class MCP: flat_ends = np.array(_ravel_index_fortran( ends, self.costs_shape), dtype=INDEX_D) - if self.dirty: - self._reset() - - # lookup and array-ify object attributes for fast use + # Always perform a reset to (re)initialize our arrays and start + # positions + self._starts, self._ends = starts, ends + self._reset() + + # Get shorter names for arrays + cdef FLOAT_T [:] flat_costs = self.flat_costs + cdef FLOAT_T [:] flat_cumulative_costs = self.flat_cumulative_costs + cdef OFFSETS_INDEX_T [:] traceback_offsets = self.traceback_offsets + cdef EDGE_T [:,:] flat_pos_edge_map = self.flat_pos_edge_map + cdef EDGE_T [:,:] flat_neg_edge_map = self.flat_neg_edge_map + cdef OFFSET_T [:,:] offsets = self.offsets + cdef INDEX_T [:] flat_offsets = self.flat_offsets + cdef FLOAT_T [:] offset_lengths = self.offset_lengths + + # Short names for other attributes cdef heap.FastUpdateBinaryHeap costs_heap = self.costs_heap - cdef cnp.ndarray[FLOAT_T, ndim=1] flat_costs = self.flat_costs - cdef cnp.ndarray[FLOAT_T, ndim=1] flat_cumulative_costs = \ - self.flat_cumulative_costs - cdef cnp.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ - self.traceback_offsets - cdef cnp.ndarray[EDGE_T, ndim=2] flat_pos_edge_map = \ - self.flat_pos_edge_map - cdef cnp.ndarray[EDGE_T, ndim=2] flat_neg_edge_map = \ - self.flat_neg_edge_map - cdef cnp.ndarray[OFFSET_T, ndim=2] offsets = self.offsets - cdef cnp.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets - cdef cnp.ndarray[FLOAT_T, ndim=1] offset_lengths = self.offset_lengths - cdef DIM_T dim = self.dim cdef int num_offsets = len(flat_offsets) - - # push each start point into the heap. Note that we use flat indexing! - for start in _ravel_index_fortran(starts, self.costs_shape): - if self.use_start_cost: - costs_heap.push_fast(flat_costs[start], start) - else: - costs_heap.push_fast(0, start) - - cdef FLOAT_T cost, new_cost + + # Variables used during front propagation + cdef FLOAT_T cost, new_cost, cumcost, new_cumcost, offset_length cdef INDEX_T index, new_index cdef BOOL_T is_at_edge, use_offset - cdef INDEX_T d, i + cdef INDEX_T d, i, iter cdef OFFSET_T offset cdef EDGE_T pos_edge_val, neg_edge_val cdef int num_ends_found = 0 cdef FLOAT_T inf = np.inf - cdef FLOAT_T travel_cost - while 1: + cdef int goal_reached + + cdef INDEX_T maxiter = int(max_coverage * flat_costs.size) + + for iter in range(maxiter): + + # This is rather like a while loop, except we are guaranteed to + # exit, which is nice during developing to prevent eternal loops. + # Find the point with the minimum cost in the heap. Once # popped, this point's minimum cost path has been found. if costs_heap.count == 0: # nothing in the heap: we've found paths to every # point in the array break - - cost = costs_heap.pop_fast() + + # Get current cumulative cost and index from the heap + cumcost = costs_heap.pop_fast() index = costs_heap._popped_ref # Record the cost we found to this point - flat_cumulative_costs[index] = cost - + flat_cumulative_costs[index] = cumcost + + # Check if goal is reached + goal_reached = self.goal_reached(index, cumcost) + if goal_reached > 0: + if goal_reached == 1: + continue # Skip neighbours + else: + break # Done completely + if use_ends: # If we're only tracing out a path to one or more # endpoints, check to see if this is an endpoint, and @@ -470,7 +549,7 @@ cdef class MCP: # if we've found one or all of the end points (as # requested), stop searching break - + # Look into the edge map to see if this point is at an # edge along any axis is_at_edge = 0 @@ -504,58 +583,75 @@ cdef class MCP: # push over the edge, then we go on. if not use_offset: continue - + # using the flat offsets, calculate the new flat index new_index = index + flat_offsets[i] - + + # Get offset length + offset_length = offset_lengths[i] + # If we have already found the best path here then # ignore this point if flat_cumulative_costs[new_index] != inf: + # Give subclass the oportunity to examine these two nodes + # Note that only when both nodes are "frozen" their + # cumulative cost is set. By doing the check here, each + # pair of nodes is checked exactly once. + self._examine_neighbor(index, new_index, offset_length) continue - # If the cost at this point is negative or infinite, ignore it + + # Get cost and new cost + cost = flat_costs[index] new_cost = flat_costs[new_index] + + # If the cost at this point is negative or infinite, ignore it if new_cost < 0 or new_cost == inf: continue - + + # Calculate new cumulative cost + new_cumcost = cumcost + self._travel_cost(cost, new_cost, + offset_length) + # Now we ask the heap to append or update the cost to # this new point, but only if that point isn't already # in the heap, or it is but the new cost is lower. - travel_cost = self._travel_cost(flat_costs[index], - new_cost, - offset_lengths[i]) # don't push infs into the heap though! - new_cost = cost + travel_cost - if new_cost != inf: - costs_heap.push_if_lower_fast(new_cost, new_index) + if new_cumcost != inf: + costs_heap.push_if_lower_fast(new_cumcost, new_index) # If we did perform an append or update, we should # record the offset from the predecessor to this new # point if costs_heap._pushed: traceback_offsets[new_index] = i - + self._update_node(index, new_index, offset_length) + + # Un-flatten the costs and traceback arrays for human consumption. - cumulative_costs = flat_cumulative_costs.reshape(self.costs_shape, - order='F') - traceback = traceback_offsets.reshape(self.costs_shape, order='F') + cumulative_costs = np.asarray(flat_cumulative_costs) + cumulative_costs = cumulative_costs.reshape(self.costs_shape, + order='F') + traceback = np.asarray(traceback_offsets) + traceback = traceback.reshape(self.costs_shape, order='F') self.dirty = 1 return cumulative_costs, traceback - + + def traceback(self, end): """traceback(end) - + Trace a minimum cost path through the pre-calculated traceback array. - + This convenience function reconstructs the the minimum cost path to a given end position from one of the starting indices provided to find_costs(), which must have been called previously. This function can be called as many times as desired after find_costs() has been run. - + Parameters ---------- end : iterable An n-d index into the `costs` array. - + Returns ------- traceback : list of n-d tuples @@ -579,14 +675,14 @@ cdef class MCP: if self.flat_cumulative_costs[flat_position] == np.inf: raise ValueError('no minimum-cost path was found ' 'to the specified end point') - - cdef cnp.ndarray[INDEX_T, ndim=1] position = \ - np.array(ends[0], dtype=INDEX_D) - cdef cnp.ndarray[OFFSETS_INDEX_T, ndim=1] traceback_offsets = \ - self.traceback_offsets - cdef cnp.ndarray[OFFSET_T, ndim=2] offsets = self.offsets - cdef cnp.ndarray[INDEX_T, ndim=1] flat_offsets = self.flat_offsets - + + # Short names for arrays + cdef OFFSETS_INDEX_T [:] traceback_offsets = self.traceback_offsets + cdef OFFSET_T [:,:] offsets = self.offsets + cdef INDEX_T [:] flat_offsets = self.flat_offsets + # New array + cdef INDEX_T [:] position = np.array(ends[0], dtype=INDEX_D) + cdef OFFSETS_INDEX_T offset cdef DIM_T d cdef DIM_T dim = self.dim @@ -602,6 +698,7 @@ cdef class MCP: return _reverse(traceback) + @cython.boundscheck(False) @cython.wraparound(False) cdef class MCP_Geometric(MCP): @@ -626,15 +723,17 @@ cdef class MCP_Geometric(MCP): `(sqrt(2)/2)*costs[1,1] + (sqrt(2)/2)*costs[2,2]`. These calculations don't make a lot of sense with offsets of magnitude - greater than 1. + greater than 1. Use the `sampling` argument in order to deal with + anisotropic data. """ - def __init__(self, costs, offsets=None, fully_connected=True): - """__init__(costs, offsets=None, fully_connected=True) + def __init__(self, costs, offsets=None, fully_connected=True, + sampling=None): + """__init__(costs, offsets=None, fully_connected=True, sampling=None) See class documentation. """ - MCP.__init__(self, costs, offsets, fully_connected) + MCP.__init__(self, costs, offsets, fully_connected, sampling) if np.absolute(self.offsets).max() > 1: raise ValueError('all offset components must be 0, 1, or -1') self.use_start_cost = 0 @@ -642,3 +741,185 @@ cdef class MCP_Geometric(MCP): cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, FLOAT_T offset_length): return offset_length * 0.5 * (old_cost + new_cost) + + + +@cython.boundscheck(True) +@cython.wraparound(True) +cdef class MCP_Connect(MCP): + """MCP_Connect(costs, offsets=None, fully_connected=True) + + Connect source points using the distance-weighted minimum cost function. + + A front is grown from each seed point simultaneously, while the + origin of the front is tracked as well. When two fronts meet, + create_connection() is called. This method must be overloaded to + deal with the found edges in a way that is appropriate for the + application. + """ + + cdef INDEX_T [:] flat_idmap + + + def __init__(self, costs, offsets=None, fully_connected=True, + sampling=None): + MCP.__init__(self, costs, offsets, fully_connected, sampling) + + # Create id map to keep track of origin of nodes + self.flat_idmap = np.zeros(self.costs_shape, INDEX_D).ravel('F') + + + def _reset(self): + """ Reset the id map. + """ + MCP._reset(self) + starts, ends = self._starts, self._ends + + # Reset idmap + self.flat_idmap[...] = -1 + id = 0 + for start in _ravel_index_fortran(starts, self.costs_shape): + self.flat_idmap[start] = id + id += 1 + + + cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, + FLOAT_T offset_length): + """ Equivalent to MCP_Geometric. + """ + return offset_length * 0.5 * (old_cost + new_cost) + + + cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ Check whether two fronts are meeting. If so, the flat_traceback + is obtained and a connection is created. + """ + + # Short names + cdef INDEX_T [:] flat_idmap = self.flat_idmap + cdef FLOAT_T [:] flat_cumulative_costs = self.flat_cumulative_costs + + # Get ids + cdef INDEX_T id1 = flat_idmap[index] + cdef INDEX_T id2 = flat_idmap[new_index] + + if id2 < 0 or id1 < 0: + pass + elif id2 != id1: + # We reached the 'front' of another seed point! + # Get position/coordinates + pos1, pos2 = _unravel_index_fortran([index, new_index], + self.costs_shape) + # Also get the costs, so we can keep the path with the least cost + cost1 = flat_cumulative_costs[index] + cost2 = flat_cumulative_costs[new_index] + # Create connection + self.create_connection(id1, id2, pos1, pos2, cost1, cost2) + + + def create_connection(self, id1, id2, tb1, tb2, cost1, cost2): + """ create_connection id1, id2, pos1, pos2, cost1, cost2) + + Overload this method to keep track of the connections that are + found during MCP processing. Note that a connection with the + same ids can be found multiple times (but with different + positions and costs). + + At the time that this method is called, both points are "frozen" + and will not be visited again by the MCP algorithm. + + Parameters + ---------- + id1 : int + The seed point id where the first neighbor originated from. + id2 : int + The seed point id where the second neighbor originated from. + pos1 : tuple + The index of of the first neighbour in the connection. + pos2 : tuple + The index of of the second neighbour in the connection. + cost1 : float + The cumulative cost at `pos1`. + cost2 : float + The cumulative costs at `pos2`. + """ + pass + + + cdef void _update_node(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ Keep track of the id map so that we know which seed point + a certain front originates from. + """ + self.flat_idmap[new_index] = self.flat_idmap[index] + + + +@cython.boundscheck(False) +@cython.wraparound(False) +cdef class MCP_Flexible(MCP): + """MCP_Flexible(costs, offsets=None, fully_connected=True) + + Find minimum cost paths through an N-d costs array. + + See the documentation for MCP for full details. This class differs from + MCP in that several methods can be overloaded (from pure Python) to + modify the behavior of the algorithm and/or create custom algorithms + based on MCP. Note that goal_reached can also be overloaded in the + MCP class. + + """ + + def travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, + FLOAT_T offset_length): + """ travel_cost(old_cost, new_cost, offset_length) + This method calculates the travel cost for going from the + current node to the next. The default implementation returns + new_cost. Overload this method to adapt the behaviour of the + algorithm. + """ + return new_cost + + + def examine_neighbor(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ examine_neighbor(index, new_index, offset_length) + This method is called once for every pair of neighboring nodes, + as soon as both nodes are frozen. + + This method can be overloaded to obtain information about + neightboring nodes, and/or to modify the behavior of the MCP + algorithm. One example is the MCP_Connect class, which checks + for meeting fronts using this hook. + """ + pass + + + def update_node(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + """ update_node(index, new_index, offset_length) + This method is called when a node is updated, right after + new_index is pushed onto the heap and the traceback map is + updated. + + This method can be overloaded to keep track of other arrays + that are used by a specific implementation of the algorithm. + For instance the MCP_Connect class uses it to update an id map. + """ + pass + + + cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost, + FLOAT_T offset_length): + return self.travel_cost(old_cost, new_cost, offset_length) + + + cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + self.examine_neighbor(index, new_index, offset_length) + + + cdef void _update_node(self, INDEX_T index, INDEX_T new_index, + FLOAT_T offset_length): + self.update_node(index, new_index, offset_length) diff --git a/skimage/graph/mcp.py b/skimage/graph/mcp.py index dc584226..bf693a45 100644 --- a/skimage/graph/mcp.py +++ b/skimage/graph/mcp.py @@ -1,4 +1,4 @@ -from ._mcp import MCP, MCP_Geometric +from ._mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible def route_through_array(array, start, end, fully_connected=True, diff --git a/skimage/graph/tests/test_anisotropy.py b/skimage/graph/tests/test_anisotropy.py new file mode 100644 index 00000000..199d2e73 --- /dev/null +++ b/skimage/graph/tests/test_anisotropy.py @@ -0,0 +1,53 @@ +import skimage.graph.mcp as mcp +from numpy.testing import (assert_array_equal, + assert_almost_equal, + ) + +import numpy as np + +a = np.ones((8, 8), dtype=np.float32) + + +horizontal_ramp = np.array([[ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,], + [ 0., 1., 2., 3., 4., 5., 6., 7.,]]) + +vertical_ramp = np.array( [[ 0., 0., 0., 0., 0., 0., 0., 0.,], + [ 1., 1., 1., 1., 1., 1., 1., 1.,], + [ 2., 2., 2., 2., 2., 2., 2., 2.,], + [ 3., 3., 3., 3., 3., 3., 3., 3.,], + [ 4., 4., 4., 4., 4., 4., 4., 4.,], + [ 5., 5., 5., 5., 5., 5., 5., 5.,], + [ 6., 6., 6., 6., 6., 6., 6., 6.,], + [ 7., 7., 7., 7., 7., 7., 7., 7.,]]) + + +def test_anisotropy(): + + # Create seeds; vertical seeds create a horizonral ramp + seeds_for_horizontal = [(i, 0) for i in range(8) ] + seeds_for_vertcal = [(0, i) for i in range(8) ] + + + for sy in range(1, 5): + for sx in range(1,5): + sampling = sy, sx + # Trace horizontally + m1 = mcp.MCP_Geometric(a, sampling=sampling, fully_connected=True) + costs1, traceback = m1.find_costs(seeds_for_horizontal) + # Trace vertically + m2 = mcp.MCP_Geometric(a, sampling=sampling, fully_connected=True) + costs2, traceback = m2.find_costs(seeds_for_vertcal) + + # Check + assert_array_equal(costs1, horizontal_ramp * sx) + assert_array_equal(costs2, vertical_ramp * sy) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/graph/tests/test_connect.py b/skimage/graph/tests/test_connect.py new file mode 100644 index 00000000..619ac3b4 --- /dev/null +++ b/skimage/graph/tests/test_connect.py @@ -0,0 +1,83 @@ +import skimage.graph.mcp as mcp +# import stentseg.graph._mcp as mcp +from numpy.testing import (assert_array_equal, + assert_almost_equal, + ) + +import numpy as np + + +a = np.ones((8, 8), dtype=np.float32) + +count = 0 +class MCP(mcp.MCP_Connect): + + def _reset(self): + """ Reset the id map. + """ + mcp.MCP_Connect._reset(self) + self._conn = {} + self._bestconn = {} + + + def create_connection(self, id1, id2, pos1, pos2, cost1, cost2): + # Process data + hash = min(id1, id2), max(id1, id2) + val = min(pos1, pos2), max(pos1, pos2) + cost = min(cost1, cost2) + # Add to total list + self._conn.setdefault(hash, []).append(val) + # Keep track of connection with lowest cost + curcost = self._bestconn.get(hash, (np.inf,))[0] + if cost < curcost: + self._bestconn[hash] = (cost,) + val + + +def test_connections(): + + # Create MCP object with three seed points + mcp = MCP(a) + costs, traceback = mcp.find_costs([ (1,1), (7,7), (1,7) ]) + + # Test that all three seed points are connected + connections = set(mcp._conn.keys()) + assert (0, 1) in connections + assert (1, 2) in connections + assert (0, 2) in connections + + # Test that any two neighbors have only been connected once + for position_tuples in mcp._conn.values(): + n1 = len(position_tuples) + n2 = len(set(position_tuples)) + assert n1 == n2 + + # For seed 0 and 1 + cost, pos1, pos2 = mcp._bestconn[(0,1)] + # Test meeting points + assert (pos1, pos2) == ( (3,3), (4,4) ) + # Test the whole path + path = mcp.traceback(pos1) + list(reversed(mcp.traceback(pos2))) + assert_array_equal(path, + [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7)]) + + # For seed 1 and 2 + cost, pos1, pos2 = mcp._bestconn[(1,2)] + # Test meeting points + assert (pos1, pos2) == ( (3,7), (4,7) ) + # Test the whole path + path = mcp.traceback(pos1) + list(reversed(mcp.traceback(pos2))) + assert_array_equal(path, + [(1, 7), (2, 7), (3, 7), (4, 7), (5, 7), (6, 7), (7, 7)]) + + # For seed 0 and 2 + cost, pos1, pos2 = mcp._bestconn[(0,2)] + # Test meeting points + assert (pos1, pos2) == ( (1,3), (1,4) ) + # Test the whole path + path = mcp.traceback(pos1) + list(reversed(mcp.traceback(pos2))) + assert_array_equal(path, + [(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7)]) + + +if __name__ == "__main__": + np.testing.run_module_suite() \ No newline at end of file diff --git a/skimage/graph/tests/test_flexible.py b/skimage/graph/tests/test_flexible.py new file mode 100644 index 00000000..8fc40d1b --- /dev/null +++ b/skimage/graph/tests/test_flexible.py @@ -0,0 +1,61 @@ +import skimage.graph.mcp as mcp +from numpy.testing import (assert_array_equal, + assert_almost_equal, + ) + +import numpy as np + +a = np.ones((8, 8), dtype=np.float32) +a[1::2] *= 2.0 + + +class FlexibleMCP(mcp.MCP_Flexible): + """ Simple MCP subclass that allows the front to travel + a certain distance from the seed point, and uses a constant + cost factor that is independant of the cost array. + """ + + def _reset(self): + mcp.MCP_Flexible._reset(self) + self._distance = np.zeros((8, 8), dtype=np.float32).ravel() + + def goal_reached(self, index, cumcost): + if self._distance[index] > 4: + return 2 + else: + return 0 + + def travel_cost(self, index, new_index, offset_length): + return 1.0 # fixed cost + + def examine_neighbor(self, index, new_index, offset_length): + pass # We do not test this + + def update_node(self, index, new_index, offset_length): + self._distance[new_index] = self._distance[index] + 1 + + +def test_flexible(): + + # Create MCP and do a traceback + mcp = FlexibleMCP(a) + costs, traceback = mcp.find_costs([(0, 0)]) + + # Check that inner part is correct. This basically + # tests whether travel_cost works. + assert_array_equal(costs[:4,:4], [[1, 2, 3, 4], + [2, 2, 3, 4], + [3, 3, 3, 4], + [4, 4, 4, 4]]) + + # Test that the algorithm stopped at the right distance. + # Note that some of the costs are filled in but not yet frozen, + # so we take a bit of margin + assert np.all( costs[-2:,:] == np.inf ) + assert np.all( costs[:,-2:] == np.inf ) + + #print(costs) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index 560f19d0..5c44abad 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -116,8 +116,8 @@ def test_offsets(): m = mcp.MCP(a, offsets=offsets) costs, traceback = m.find_costs([(1, 6)]) assert_array_equal(traceback, - [[-1, -1, -1, -1, -1, -1, -1, -1], - [-1, -1, -1, -1, -1, -1, -1, -1], + [[-2, -2, -2, -2, -2, -2, -2, -2], + [-2, -2, -2, -2, -2, -2, -1, -2], [15, 14, 13, 12, 11, 10, 0, 1], [10, 0, 1, 2, 3, 4, 5, 6], [10, 0, 1, 2, 3, 4, 5, 6], @@ -151,5 +151,6 @@ def _test_random(shape): return a, costs, offsets + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/io/__init__.py b/skimage/io/__init__.py index 5e701a51..316253ca 100644 --- a/skimage/io/__init__.py +++ b/skimage/io/__init__.py @@ -1,49 +1,34 @@ -__doc__ = """Utilities to read and write images in various formats. +"""Utilities to read and write images in various formats. The following plug-ins are available: """ -from ._plugins import use as use_plugin -from ._plugins import available as plugins -from ._plugins import info as plugin_info -from ._plugins import configuration as plugin_order -from ._plugins import reset_plugins as _reset_plugins - +from .manage_plugins import * from .sift import * from .collection import * from ._io import * +from ._image_stack import * from .video import * -available_plugins = plugins() +reset_plugins() + +WRAP_LEN = 73 -def _load_preferred_plugins(): - # Load preferred plugin for each io function. - io_funcs = ['imsave', 'imshow', 'imread_collection', 'imread'] - preferred_plugins = ['matplotlib', 'pil', 'qt', 'freeimage', 'null'] - for func in io_funcs: - for plugin in preferred_plugins: - if plugin not in available_plugins: - continue - try: - use_plugin(plugin, kind=func) - break - except (ImportError, RuntimeError, OSError): - pass +def _separator(char, lengths): + return [char * separator_length for separator_length in lengths] - # Use PIL as the default imread plugin, since matplotlib (1.2.x) - # is buggy (flips PNGs around, returns bytes as floats, etc.) - try: - use_plugin('pil', 'imread') - except ImportError: - pass -def reset_plugins(): - _reset_plugins() - _load_preferred_plugins() +def _format_plugin_info_table(info_table, column_lengths): + """Add separators and column titles to plugin info table.""" + info_table.insert(0, _separator('=', column_lengths)) + info_table.insert(1, ('Plugin', 'Description')) + info_table.insert(2, _separator('-', column_lengths)) + info_table.append(_separator('-', column_lengths)) + def _update_doc(doc): """Add a list of plugins to the module docstring, formatted as @@ -52,27 +37,24 @@ def _update_doc(doc): """ from textwrap import wrap - info = [(p, plugin_info(p)) for p in plugins() if not p == 'test'] - col_1_len = max([len(n) for (n, _) in info]) - wrap_len = 73 - col_2_len = wrap_len - 1 - col_1_len + info_table = [(p, plugin_info(p).get('description', 'no description')) + for p in available_plugins if not p == 'test'] - # Insert table header - info.insert(0, ('=' * col_1_len, {'description': '=' * col_2_len})) - info.insert(1, ('Plugin', {'description': 'Description'})) - info.insert(2, ('-' * col_1_len, {'description': '-' * col_2_len})) - info.append(('=' * col_1_len, {'description': '=' * col_2_len})) + name_length = max([len(n) for (n, _) in info_table]) + description_length = WRAP_LEN - 1 - name_length + column_lengths = [name_length, description_length] + _format_plugin_info_table(info_table, column_lengths) - for (name, meta_data) in info: - wrapped_descr = wrap(meta_data.get('description', ''), - col_2_len) - doc += "%s %s\n" % (name.ljust(col_1_len), - '\n'.join(wrapped_descr)) + for (name, plugin_description) in info_table: + description_lines = wrap(plugin_description, description_length) + name_column = [name] + name_column.extend(['' for _ in range(len(description_lines) - 1)]) + for name, description in zip(name_column, description_lines): + doc += "%s %s\n" % (name.ljust(name_length), description) doc = doc.strip() return doc -__doc__ = _update_doc(__doc__) -reset_plugins() +__doc__ = _update_doc(__doc__) diff --git a/skimage/io/_image_stack.py b/skimage/io/_image_stack.py new file mode 100644 index 00000000..ca9896d5 --- /dev/null +++ b/skimage/io/_image_stack.py @@ -0,0 +1,35 @@ +import numpy as np + + +__all__ = ['image_stack', 'push', 'pop'] + + +# Shared image queue +image_stack = [] + + +def push(img): + """Push an image onto the shared image stack. + + Parameters + ---------- + img : ndarray + Image to push. + + """ + if not isinstance(img, np.ndarray): + raise ValueError("Can only push ndarrays to the image stack.") + + image_stack.append(img) + + +def pop(): + """Pop an image from the shared image stack. + + Returns + ------- + img : ndarray + Image popped from the stack. + + """ + return image_stack.pop() diff --git a/skimage/io/_io.py b/skimage/io/_io.py index eb704a6f..97f8718e 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,34 +1,14 @@ -__all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show', - 'push', 'pop'] - -try: - from urllib.request import urlopen -except ImportError: - from urllib2 import urlopen - -import os -import re -import tempfile from io import BytesIO import numpy as np import six -from skimage.io._plugins import call as call_plugin +from skimage.io.manage_plugins import call_plugin from skimage.color import rgb2grey +from .util import file_or_url_context - -# Shared image queue -_image_stack = [] - -URL_REGEX = re.compile(r'http://|https://|ftp://|file://|file:\\') - - -def is_url(filename): - """Return True if string is an http or ftp path.""" - return (isinstance(filename, six.string_types) and - URL_REGEX.match(filename) is not None) +__all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show'] class Image(np.ndarray): @@ -77,33 +57,6 @@ class Image(np.ndarray): return return_str -def push(img): - """Push an image onto the shared image stack. - - Parameters - ---------- - img : ndarray - Image to push. - - """ - if not isinstance(img, np.ndarray): - raise ValueError("Can only push ndarrays to the image stack.") - - _image_stack.append(img) - - -def pop(): - """Pop an image from the shared image stack. - - Returns - ------- - img : ndarray - Image popped from the stack. - - """ - return _image_stack.pop() - - def imread(fname, as_grey=False, plugin=None, flatten=None, **plugin_args): """Load an image from file. @@ -140,14 +93,7 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, if flatten is not None: as_grey = flatten - if is_url(fname): - _, ext = os.path.splitext(fname) - with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as f: - u = urlopen(fname) - f.write(u.read()) - img = call_plugin('imread', f.name, plugin=plugin, **plugin_args) - os.remove(f.name) - else: + with file_or_url_context(fname) as fname: img = call_plugin('imread', fname, plugin=plugin, **plugin_args) if as_grey and getattr(img, 'ndim', 0) >= 3: diff --git a/skimage/io/_plugins/__init__.py b/skimage/io/_plugins/__init__.py index 48aad58e..e69de29b 100644 --- a/skimage/io/_plugins/__init__.py +++ b/skimage/io/_plugins/__init__.py @@ -1 +0,0 @@ -from .plugin import * diff --git a/skimage/io/_plugins/null_plugin.ini b/skimage/io/_plugins/null_plugin.ini index ddc45f33..9703aab0 100644 --- a/skimage/io/_plugins/null_plugin.ini +++ b/skimage/io/_plugins/null_plugin.ini @@ -1,3 +1,3 @@ [null] description = Default plugin that does nothing -provides = imshow, imread, _app_show +provides = imshow, imread, imsave, _app_show diff --git a/skimage/io/_plugins/null_plugin.py b/skimage/io/_plugins/null_plugin.py index 70bfd5d8..4eb7adf3 100644 --- a/skimage/io/_plugins/null_plugin.py +++ b/skimage/io/_plugins/null_plugin.py @@ -1,4 +1,4 @@ -__all__ = ['imshow', 'imread', '_app_show'] +__all__ = ['imshow', 'imread', 'imsave', '_app_show'] import warnings @@ -17,4 +17,9 @@ def imshow(*args, **kwargs): def imread(*args, **kwargs): warnings.warn(RuntimeWarning(message)) + +def imsave(*args, **kwargs): + warnings.warn(RuntimeWarning(message)) + + _app_show = imshow diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index c6e968dc..6dd78035 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -109,7 +109,13 @@ def imsave(fname, arr, format_str=None): if not isinstance(fname, string_types) and format_str is None: format_str = "PNG" - img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) + try: + img = Image.frombytes(mode, (arr.shape[1], arr.shape[0]), + arr.tostring()) + except AttributeError: + img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), + arr.tostring()) + img.save(fname, format=format_str) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 5708983a..83811fc0 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -2,18 +2,19 @@ from __future__ import with_statement -__all__ = ['MultiImage', 'ImageCollection', 'imread', 'concatenate_images'] - +import os from glob import glob import re from copy import copy import numpy as np -from ._io import imread - import six +__all__ = ['MultiImage', 'ImageCollection', 'concatenate_images', + 'imread_collection_wrapper'] + + def concatenate_images(ic): """Concatenate all images in the image collection into an array. @@ -229,7 +230,7 @@ class ImageCollection(object): ---------- load_pattern : str or list Pattern glob or filenames to load. The path can be absolute or - relative. Multiple patterns should be separated by a colon, + relative. Multiple patterns should be separated by os.pathsep, e.g. '/tmp/work/*.png:/tmp/other/*.jpg'. Also see implementation notes below. conserve_memory : bool, optional @@ -295,7 +296,7 @@ class ImageCollection(object): def __init__(self, load_pattern, conserve_memory=True, load_func=None): """Load and manage a collection of images.""" if isinstance(load_pattern, six.string_types): - load_pattern = load_pattern.split(':') + load_pattern = load_pattern.split(os.pathsep) self._files = [] for pattern in load_pattern: self._files.extend(glob(pattern)) @@ -312,6 +313,7 @@ class ImageCollection(object): self._cached = None if load_func is None: + from ._io import imread self.load_func = imread else: self.load_func = load_func @@ -430,3 +432,29 @@ class ImageCollection(object): If images in the `ImageCollection` don't have identical shapes. """ return concatenate_images(self) + + +def imread_collection_wrapper(imread): + def imread_collection(load_pattern, conserve_memory=True): + """Return an `ImageCollection` from files matching the given pattern. + + Note that files are always stored in alphabetical order. Also note that + slicing returns a new ImageCollection, *not* a view into the data. + + See `skimage.io.ImageCollection` for details. + + Parameters + ---------- + load_pattern : str or list + Pattern glob or filenames to load. The path can be absolute or + relative. Multiple patterns should be separated by a colon, + e.g. '/tmp/work/*.png:/tmp/other/*.jpg'. Also see + implementation notes below. + conserve_memory : bool, optional + If True, never keep more than one in memory at a specific + time. Otherwise, images will be cached once they are loaded. + + """ + return ImageCollection(load_pattern, conserve_memory=conserve_memory, + load_func=imread) + return imread_collection diff --git a/skimage/io/_plugins/plugin.py b/skimage/io/manage_plugins.py similarity index 50% rename from skimage/io/_plugins/plugin.py rename to skimage/io/manage_plugins.py index 5e04f320..d31e6dbe 100644 --- a/skimage/io/_plugins/plugin.py +++ b/skimage/io/manage_plugins.py @@ -1,26 +1,57 @@ """Handle image reading, writing and plotting plugins. +To improve performance, plugins are only loaded as needed. As a result, there +can be multiple states for a given plugin: + + available: Defined in an *ini file located in `skimage.io._plugins`. + See also `skimage.io.available_plugins`. + partial definition: Specified in an *ini file, but not defined in the + corresponding plugin module. This will raise an error when loaded. + available but not on this system: Defined in `skimage.io._plugins`, but + a dependent library (e.g. Qt, PIL) is not available on your system. + This will raise an error when loaded. + loaded: The real availability is determined when it's explicitly loaded, + either because it's one of the default plugins, or because it's + loaded explicitly by the user. + """ -__all__ = ['use', 'available', 'call', 'info', 'configuration', 'reset_plugins'] - try: - from configparser import ConfigParser + from configparser import ConfigParser # Python 3 except ImportError: - from ConfigParser import ConfigParser + from ConfigParser import ConfigParser # Python 2 import os.path from glob import glob +from .collection import imread_collection_wrapper + +__all__ = ['use_plugin', 'call_plugin', 'plugin_info', 'plugin_order', + 'reset_plugins', 'find_available_plugins', 'available_plugins'] + + +# The plugin store will save a list of *loaded* io functions for each io type +# (e.g. 'imread', 'imsave', etc.). Plugins are loaded as requested. plugin_store = None - +# Dictionary mapping plugin names to a list of functions they provide. plugin_provides = {} +# The module names for the plugins in `skimage.io._plugins`. plugin_module_name = {} +# Meta-data about plugins provided by *.ini files. plugin_meta_data = {} +# For each plugin type, default to the first available plugin as defined by +# the following preferences. +preferred_plugins = { + # Default plugins for all types (overridden by specific types below). + 'all': ['matplotlib', 'pil', 'qt', 'freeimage', 'null'], + # Use PIL as the default imread plugin, since matplotlib (1.2.x) + # is buggy (flips PNGs around, returns bytes as floats, etc.) + 'imread': ['pil'], +} -def reset_plugins(): +def _clear_plugins(): """Clear the plugin state to the default, i.e., where no plugins are loaded """ @@ -30,8 +61,47 @@ def reset_plugins(): 'imshow': [], 'imread_collection': [], '_app_show': []} +_clear_plugins() -reset_plugins() + +def _load_preferred_plugins(): + # Load preferred plugin for each io function. + io_types = ['imsave', 'imshow', 'imread_collection', 'imread'] + for p_type in io_types: + _set_plugin(p_type, preferred_plugins['all']) + + plugin_types = (p for p in preferred_plugins.keys() if p != 'all') + for p_type in plugin_types: + _set_plugin(p_type, preferred_plugins[p_type]) + + +def _set_plugin(plugin_type, plugin_list): + for plugin in plugin_list: + if plugin not in available_plugins: + continue + try: + use_plugin(plugin, kind=plugin_type) + break + except (ImportError, RuntimeError, OSError): + pass + + +def reset_plugins(): + _clear_plugins() + _load_preferred_plugins() + + +def _parse_config_file(filename): + """Return plugin name and meta-data dict from plugin config file.""" + parser = ConfigParser() + parser.read(filename) + name = parser.sections()[0] + + meta_data = {} + for opt in parser.options(name): + meta_data[opt] = parser.get(name, opt) + + return name, meta_data def _scan_plugins(): @@ -40,19 +110,13 @@ def _scan_plugins(): """ pd = os.path.dirname(__file__) - ini = glob(os.path.join(pd, '*.ini')) + config_files = glob(os.path.join(pd, '_plugins', '*.ini')) - for f in ini: - cp = ConfigParser() - cp.read(f) - name = cp.sections()[0] - - meta_data = {} - for opt in cp.options(name): - meta_data[opt] = cp.get(name, opt) + for filename in config_files: + name, meta_data = _parse_config_file(filename) plugin_meta_data[name] = meta_data - provides = [s.strip() for s in cp.get(name, 'provides').split(',')] + provides = [s.strip() for s in meta_data['provides'].split(',')] valid_provides = [p for p in provides if p in plugin_store] for p in provides: @@ -60,13 +124,53 @@ def _scan_plugins(): print("Plugin `%s` wants to provide non-existent `%s`." \ " Ignoring." % (name, p)) + # Add plugins that provide 'imread' as provider of 'imread_collection'. + need_to_add_collection = ('imread_collection' not in valid_provides and + 'imread' in valid_provides) + if need_to_add_collection: + valid_provides.append('imread_collection') + plugin_provides[name] = valid_provides - plugin_module_name[name] = os.path.basename(f)[:-4] + + plugin_module_name[name] = os.path.basename(filename)[:-4] _scan_plugins() -def call(kind, *args, **kwargs): +def find_available_plugins(loaded=False): + """List available plugins. + + Parameters + ---------- + loaded : bool + If True, show only those plugins currently loaded. By default, + all plugins are shown. + + Returns + ------- + p : dict + Dictionary with plugin names as keys and exposed functions as + values. + + """ + active_plugins = set() + for plugin_func in plugin_store.values(): + for plugin, func in plugin_func: + active_plugins.add(plugin) + + d = {} + for plugin in plugin_provides: + if not loaded or plugin in active_plugins: + d[plugin] = [f for f in plugin_provides[plugin] + if not f.startswith('_')] + + return d + + +available_plugins = find_available_plugins() + + +def call_plugin(kind, *args, **kwargs): """Find the appropriate plugin of 'kind' and execute it. Parameters @@ -85,11 +189,11 @@ def call(kind, *args, **kwargs): plugin_funcs = plugin_store[kind] if len(plugin_funcs) == 0: - raise RuntimeError('''No suitable plugin registered for %s. - -You may load I/O plugins with the `skimage.io.use_plugin` -command. A list of all available plugins can be found using -`skimage.io.plugins()`.''' % kind) + msg = ("No suitable plugin registered for %s.\n\n" + "You may load I/O plugins with the `skimage.io.use_plugin` " + "command. A list of all available plugins can be found using " + "`skimage.io.plugins()`.") + raise RuntimeError(msg % kind) plugin = kwargs.pop('plugin', None) if plugin is None: @@ -105,7 +209,7 @@ command. A list of all available plugins can be found using return func(*args, **kwargs) -def use(name, kind=None): +def use_plugin(name, kind=None): """Set the default plugin for a specified operation. The plugin will be loaded if it hasn't been already. @@ -119,15 +223,19 @@ def use(name, kind=None): See Also -------- - plugins : List of available plugins + available_plugins : List of available plugins Examples -------- - Use the Python Imaging Library to read images: + To use Matplotlib as the default image reader, you would write: - >>> from skimage.io import use_plugin - >>> use_plugin('pil', 'imread') + >>> from skimage import io + >>> io.use_plugin('matplotlib', 'imread') + + To see a list of available plugins run ``io.available_plugins``. Note that + this lists plugins that are defined, but the full list may not be usable + if your system does not have the required libraries installed. """ if kind is None: @@ -158,34 +266,12 @@ def use(name, kind=None): plugin_store[k] = funcs -def available(loaded=False): - """List available plugins. - - Parameters - ---------- - loaded : bool - If True, show only those plugins currently loaded. By default, - all plugins are shown. - - Returns - ------- - p : dict - Dictionary with plugin names as keys and exposed functions as - values. - - """ - active_plugins = set() - for plugin_func in plugin_store.values(): - for plugin, func in plugin_func: - active_plugins.add(plugin) - - d = {} - for plugin in plugin_provides: - if not loaded or plugin in active_plugins: - d[plugin] = [f for f in plugin_provides[plugin] \ - if not f.startswith('_')] - - return d +def _inject_imread_collection_if_needed(module): + """Add `imread_collection` to module if not already present.""" + if not hasattr(module, 'imread_collection') and hasattr(module, 'imread'): + imread = getattr(module, 'imread') + func = imread_collection_wrapper(imread) + setattr(module, 'imread_collection', func) def _load(plugin): @@ -201,7 +287,7 @@ def _load(plugin): plugins : List of available plugins """ - if plugin in available(loaded=True): + if plugin in find_available_plugins(loaded=True): return if not plugin in plugin_module_name: raise ValueError("Plugin %s not found." % plugin) @@ -212,17 +298,20 @@ def _load(plugin): provides = plugin_provides[plugin] for p in provides: - if not hasattr(plugin_module, p): + if p == 'imread_collection': + _inject_imread_collection_if_needed(plugin_module) + elif not hasattr(plugin_module, p): print("Plugin %s does not provide %s as advertised. Ignoring." % \ (plugin, p)) - else: - store = plugin_store[p] - func = getattr(plugin_module, p) - if not (plugin, func) in store: - store.append((plugin, func)) + continue + + store = plugin_store[p] + func = getattr(plugin_module, p) + if not (plugin, func) in store: + store.append((plugin, func)) -def info(plugin): +def plugin_info(plugin): """Return plugin meta-data. Parameters @@ -242,7 +331,7 @@ def info(plugin): raise ValueError('No information on plugin "%s"' % plugin) -def configuration(): +def plugin_order(): """Return the currently preferred plugin order. Returns diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index cf5ef820..1e9fea73 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -1,88 +1,72 @@ -import sys import os.path import numpy as np -from numpy.testing import (assert_raises, - assert_equal, - assert_array_almost_equal, - ) -from numpy.testing.decorators import skipif +from numpy.testing import assert_raises, assert_equal, assert_allclose from skimage import data_dir -from skimage.io import ImageCollection, MultiImage -from skimage.io.collection import alphanumeric_key -from skimage.io import Image as ioImage - -import six +from skimage.io.collection import ImageCollection, alphanumeric_key -try: - from PIL import Image -except ImportError: - PIL_available = False -else: - PIL_available = True +def test_string_split(): + test_string = 'z23a' + test_str_result = ['z', 23, 'a'] + assert_equal(alphanumeric_key(test_string), test_str_result) -class TestAlphanumericKey(): - def setUp(self): - self.test_string = 'z23a' - self.test_str_result = ['z', 23, 'a'] - self.filenames = ['f9.10.png', 'f9.9.png', 'f10.10.png', 'f10.9.png', - 'e9.png', 'e10.png', 'em.png'] - self.sorted_filenames = \ - ['e9.png', 'e10.png', 'em.png', 'f9.9.png', 'f9.10.png', - 'f10.9.png', 'f10.10.png'] - - def test_string_split(self): - assert_equal(alphanumeric_key(self.test_string), self.test_str_result) - - def test_string_sort(self): - sorted_filenames = sorted(self.filenames, key=alphanumeric_key) - assert_equal(sorted_filenames, self.sorted_filenames) +def test_string_sort(): + filenames = ['f9.10.png', 'f9.9.png', 'f10.10.png', 'f10.9.png', + 'e9.png', 'e10.png', 'em.png'] + sorted_filenames = ['e9.png', 'e10.png', 'em.png', 'f9.9.png', + 'f9.10.png', 'f10.9.png', 'f10.10.png'] + sorted_filenames = sorted(filenames, key=alphanumeric_key) + assert_equal(sorted_filenames, sorted_filenames) class TestImageCollection(): - pattern = [os.path.join(data_dir, pic) for pic in ['camera.png', - 'color.png']] - pattern_matched = [os.path.join(data_dir, pic) for pic in - ['camera.png', 'moon.png']] + + pattern = [os.path.join(data_dir, pic) + for pic in ['camera.png', 'color.png']] + + pattern_matched = [os.path.join(data_dir, pic) + for pic in ['camera.png', 'moon.png']] def setUp(self): - self.collection = ImageCollection(self.pattern) - self.collection_matched = ImageCollection(self.pattern_matched) + # Generic image collection with images of different shapes. + self.images = ImageCollection(self.pattern) + # Image collection with images having shapes that match. + self.images_matched = ImageCollection(self.pattern_matched) def test_len(self): - assert len(self.collection) == 2 + assert len(self.images) == 2 def test_getitem(self): - num = len(self.collection) + num = len(self.images) for i in range(-num, num): - assert type(self.collection[i]) is np.ndarray - assert_array_almost_equal(self.collection[0], - self.collection[-num]) + assert type(self.images[i]) is np.ndarray + assert_allclose(self.images[0], + self.images[-num]) - #assert_raises expects a callable, hence this do-very-little func + # assert_raises expects a callable, hence this thin wrapper function. def return_img(n): - return self.collection[n] + return self.images[n] assert_raises(IndexError, return_img, num) assert_raises(IndexError, return_img, -num - 1) def test_slicing(self): - assert type(self.collection[:]) is ImageCollection - assert len(self.collection[:]) == 2 - assert len(self.collection[:1]) == 1 - assert len(self.collection[1:]) == 1 - assert_array_almost_equal(self.collection[0], self.collection[:1][0]) - assert_array_almost_equal(self.collection[1], self.collection[1:][0]) - assert_array_almost_equal(self.collection[1], self.collection[::-1][0]) - assert_array_almost_equal(self.collection[0], self.collection[::-1][1]) + assert type(self.images[:]) is ImageCollection + assert len(self.images[:]) == 2 + assert len(self.images[:1]) == 1 + assert len(self.images[1:]) == 1 + assert_allclose(self.images[0], self.images[:1][0]) + assert_allclose(self.images[1], self.images[1:][0]) + assert_allclose(self.images[1], self.images[::-1][0]) + assert_allclose(self.images[0], self.images[::-1][1]) def test_files_property(self): - assert isinstance(self.collection.files, list) + assert isinstance(self.images.files, list) def set_files(f): - self.collection.files = f + self.images.files = f assert_raises(AttributeError, set_files, 'newfiles') def test_custom_load(self): @@ -95,59 +79,12 @@ class TestImageCollection(): assert_equal(ic[1], (2, 'two')) def test_concatenate(self): - ar = self.collection_matched.concatenate() - assert_equal(ar.shape, (len(self.collection_matched),) + - self.collection[0].shape) - assert_raises(ValueError, self.collection.concatenate) + array = self.images_matched.concatenate() + expected_shape = (len(self.images_matched),) + self.images[0].shape + assert_equal(array.shape, expected_shape) - -class TestMultiImage(): - - def setUp(self): - # This multipage TIF file was created with imagemagick: - # convert im1.tif im2.tif -adjoin multipage.tif - if PIL_available: - self.img = MultiImage(os.path.join(data_dir, 'multipage.tif')) - - @skipif(not PIL_available) - def test_len(self): - assert len(self.img) == 2 - - @skipif(not PIL_available) - def test_getitem(self): - num = len(self.img) - for i in range(-num, num): - assert type(self.img[i]) is np.ndarray - assert_array_almost_equal(self.img[0], - self.img[-num]) - - #assert_raises expects a callable, hence this do-very-little func - def return_img(n): - return self.img[n] - assert_raises(IndexError, return_img, num) - assert_raises(IndexError, return_img, -num - 1) - - @skipif(not PIL_available) - def test_files_property(self): - assert isinstance(self.img.filename, six.string_types) - - def set_filename(f): - self.img.filename = f - assert_raises(AttributeError, set_filename, 'newfile') - - @skipif(not PIL_available) - def test_conserve_memory_property(self): - assert isinstance(self.img.conserve_memory, bool) - - def set_mem(val): - self.img.conserve_memory = val - assert_raises(AttributeError, set_mem, True) - - @skipif(not PIL_available) - def test_concatenate(self): - ar = self.img.concatenate() - assert_equal(ar.shape, (len(self.img),) + - self.img[0].shape) + def test_concatentate_mismatched_image_shapes(self): + assert_raises(ValueError, self.images.concatenate) if __name__ == "__main__": diff --git a/skimage/io/tests/test_image.py b/skimage/io/tests/test_image.py index 6c54695b..b74c973e 100644 --- a/skimage/io/tests/test_image.py +++ b/skimage/io/tests/test_image.py @@ -1,7 +1,12 @@ -from skimage.io import Image +from io import BytesIO + +import numpy as np +from skimage import img_as_ubyte +from skimage.io import Image, imread from numpy.testing import assert_equal, assert_array_equal + def test_tags(): f = Image([1, 2, 3], foo='bar', sigma='delta') g = Image([3, 2, 1], sun='moon') @@ -11,7 +16,18 @@ def test_tags(): assert_array_equal((g + 2).tags['sun'], 'moon') assert_equal(h.tags, {}) + +def test_repr_png_roundtrip(): + # Use RGB-like shape since some backends convert grayscale to RGB + original_array = 255 * np.ones((5, 5, 3), dtype=np.uint8) + image = Image(original_array) + array = imread(BytesIO(image._repr_png_())) + # Force output to ubyte range for plugin compatibility. + # For example, Matplotlib will return floats even if the image is uint8. + assert_array_equal(img_as_ubyte(array), original_array) + # Note that PIL breaks with `_repr_jpeg_`. + + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() - diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index 8049860a..14879e64 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -4,6 +4,7 @@ from numpy.testing import assert_array_equal, raises, run_module_suite import numpy as np import skimage.io as io +from skimage.io.manage_plugins import plugin_store from skimage import data_dir @@ -28,5 +29,17 @@ def test_imread_url(): assert image.shape == (512, 512) +@raises(RuntimeError) +def test_imread_no_plugin(): + # tweak data path so that file URI works on both unix and windows. + image_path = os.path.join(data_dir, 'lena.png') + plugins = plugin_store['imread'] + plugin_store['imread'] = [] + try: + io.imread(image_path) + finally: + plugin_store['imread'] = plugins + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/test_multi_image.py b/skimage/io/tests/test_multi_image.py new file mode 100644 index 00000000..ebaa71dc --- /dev/null +++ b/skimage/io/tests/test_multi_image.py @@ -0,0 +1,69 @@ +import os + +import numpy as np +from numpy.testing.decorators import skipif +from numpy.testing import assert_raises, assert_equal, assert_allclose + +from skimage import data_dir +from skimage.io.collection import MultiImage + +try: + from PIL import Image +except ImportError: + PIL_available = False +else: + PIL_available = True + +import six + + +class TestMultiImage(): + + def setUp(self): + # This multipage TIF file was created with imagemagick: + # convert im1.tif im2.tif -adjoin multipage.tif + if PIL_available: + self.img = MultiImage(os.path.join(data_dir, 'multipage.tif')) + + @skipif(not PIL_available) + def test_len(self): + assert len(self.img) == 2 + + @skipif(not PIL_available) + def test_getitem(self): + num = len(self.img) + for i in range(-num, num): + assert type(self.img[i]) is np.ndarray + assert_allclose(self.img[0], self.img[-num]) + + # assert_raises expects a callable, hence this thin wrapper function. + def return_img(n): + return self.img[n] + assert_raises(IndexError, return_img, num) + assert_raises(IndexError, return_img, -num - 1) + + @skipif(not PIL_available) + def test_files_property(self): + assert isinstance(self.img.filename, six.string_types) + + def set_filename(f): + self.img.filename = f + assert_raises(AttributeError, set_filename, 'newfile') + + @skipif(not PIL_available) + def test_conserve_memory_property(self): + assert isinstance(self.img.conserve_memory, bool) + + def set_mem(val): + self.img.conserve_memory = val + assert_raises(AttributeError, set_mem, True) + + @skipif(not PIL_available) + def test_concatenate(self): + array = self.img.concatenate() + assert_equal(array.shape, (len(self.img),) + self.img[0].shape) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/io/tests/test_null.py b/skimage/io/tests/test_null.py new file mode 100644 index 00000000..56f5df89 --- /dev/null +++ b/skimage/io/tests/test_null.py @@ -0,0 +1,50 @@ +import os +import warnings +from contextlib import contextmanager + +import numpy as np +from numpy.testing import raises + +from skimage import io +from skimage import data_dir + + +@contextmanager +def warnings_as_errors(): + # Temporarily set warnings as errors so we can test the warning is raised. + with warnings.catch_warnings(): + warnings.filterwarnings('error') + yield + +@raises(Warning) +def test_null_imread(): + path = os.path.join(data_dir, 'color.png') + with warnings_as_errors(): + io.imread(path, plugin='null') + + +@raises(Warning) +def test_null_imsave(): + with warnings_as_errors(): + io.imsave('dummy.png', np.zeros((3, 3)), plugin='null') + + +@raises(Warning) +def test_null_imshow(): + with warnings_as_errors(): + io.imshow(np.zeros((3, 3)), plugin='null') + + +@raises(Warning) +def test_null_imread_collection(): + # Note that the null plugin doesn't define an `imread_collection` plugin + # but this function is dynamically added by the plugin manager. + path = os.path.join(data_dir, '*.png') + with warnings_as_errors(): + collection = io.imread_collection(path, plugin='null') + collection[0] + + +if __name__ == '__main__': + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/io/tests/test_plugin.py b/skimage/io/tests/test_plugin.py index 5d1febe4..06f3a600 100644 --- a/skimage/io/tests/test_plugin.py +++ b/skimage/io/tests/test_plugin.py @@ -1,7 +1,9 @@ -from numpy.testing import * +from contextlib import contextmanager + +from numpy.testing import assert_equal, raises from skimage import io -from skimage.io._plugins import plugin +from skimage.io import manage_plugins from numpy.testing.decorators import skipif try: @@ -19,70 +21,115 @@ except RuntimeError: FI_available = False -def setup_module(self): - plugin.use('test') # see ../_plugins/test_plugin.py +def setup_module(): + manage_plugins.use_plugin('test') # see ../_plugins/test_plugin.py -def teardown_module(self): +def teardown_module(): io.reset_plugins() -class TestPlugin: - def test_read(self): - io.imread('test.png', as_grey=True, dtype='i4', plugin='test') +@contextmanager +def protect_preferred_plugins(): + """Contexts where `preferred_plugins` can be modified w/o side-effects.""" + preferred_plugins = manage_plugins.preferred_plugins.copy() + try: + yield + finally: + manage_plugins.preferred_plugins = preferred_plugins - def test_save(self): - io.imsave('test.png', [1, 2, 3], plugin='test') - def test_show(self): - io.imshow([1, 2, 3], plugin_arg=(1, 2), plugin='test') +def test_read(): + io.imread('test.png', as_grey=True, dtype='i4', plugin='test') - def test_collection(self): - io.imread_collection('*.png', conserve_memory=False, plugin='test') - def test_use(self): - plugin.use('test') - plugin.use('test', 'imshow') +def test_save(): + io.imsave('test.png', [1, 2, 3], plugin='test') - @raises(ValueError) - def test_failed_use(self): - plugin.use('asd') - @skipif(not PIL_available and not FI_available) - def test_use_priority(self): - plugin.use(priority_plugin) - plug, func = plugin.plugin_store['imread'][0] - assert_equal(plug, priority_plugin) +def test_show(): + io.imshow([1, 2, 3], plugin_arg=(1, 2), plugin='test') - plugin.use('test') - plug, func = plugin.plugin_store['imread'][0] - assert_equal(plug, 'test') - @skipif(not PIL_available) - def test_use_priority_with_func(self): - plugin.use('pil') - plug, func = plugin.plugin_store['imread'][0] - assert_equal(plug, 'pil') +def test_collection(): + io.imread_collection('*.png', conserve_memory=False, plugin='test') - plugin.use('test', 'imread') - plug, func = plugin.plugin_store['imread'][0] - assert_equal(plug, 'test') - plug, func = plugin.plugin_store['imsave'][0] - assert_equal(plug, 'pil') +def test_use(): + manage_plugins.use_plugin('test') + manage_plugins.use_plugin('test', 'imshow') - plugin.use('test') - plug, func = plugin.plugin_store['imsave'][0] - assert_equal(plug, 'test') - def test_plugin_order(self): - p = io.plugin_order() - assert 'imread' in p - assert 'test' in p['imread'] +@raises(ValueError) +def test_failed_use(): + manage_plugins.use_plugin('asd') + + +@skipif(not PIL_available and not FI_available) +def test_use_priority(): + manage_plugins.use_plugin(priority_plugin) + plug, func = manage_plugins.plugin_store['imread'][0] + assert_equal(plug, priority_plugin) + + manage_plugins.use_plugin('test') + plug, func = manage_plugins.plugin_store['imread'][0] + assert_equal(plug, 'test') + + +@skipif(not PIL_available) +def test_use_priority_with_func(): + manage_plugins.use_plugin('pil') + plug, func = manage_plugins.plugin_store['imread'][0] + assert_equal(plug, 'pil') + + manage_plugins.use_plugin('test', 'imread') + plug, func = manage_plugins.plugin_store['imread'][0] + assert_equal(plug, 'test') + + plug, func = manage_plugins.plugin_store['imsave'][0] + assert_equal(plug, 'pil') + + manage_plugins.use_plugin('test') + plug, func = manage_plugins.plugin_store['imsave'][0] + assert_equal(plug, 'test') + + +def test_plugin_order(): + p = io.plugin_order() + assert 'imread' in p + assert 'test' in p['imread'] + + +def test_available(): + assert 'qt' in io.available_plugins + assert 'test' in io.find_available_plugins(loaded=True) + + +def test_load_preferred_plugins_all(): + from skimage.io._plugins import null_plugin + + with protect_preferred_plugins(): + manage_plugins.preferred_plugins = {'all': ['null']} + manage_plugins.reset_plugins() + + for plugin_type in ('imread', 'imsave', 'imshow'): + plug, func = manage_plugins.plugin_store[plugin_type][0] + assert func == getattr(null_plugin, plugin_type) + + +def test_load_preferred_plugins_imread(): + from skimage.io._plugins import null_plugin + + with protect_preferred_plugins(): + manage_plugins.preferred_plugins['imread'] = ['null'] + manage_plugins.reset_plugins() + + plug, func = manage_plugins.plugin_store['imread'][0] + assert func == null_plugin.imread + plug, func = manage_plugins.plugin_store['imshow'][0] + assert func != null_plugin.imshow - def test_available(self): - assert 'qt' in io.plugins() - assert 'test' in io.plugins(loaded=True) if __name__ == "__main__": + from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/io/util.py b/skimage/io/util.py new file mode 100644 index 00000000..74158cac --- /dev/null +++ b/skimage/io/util.py @@ -0,0 +1,35 @@ +try: + from urllib.request import urlopen # Python 3 +except ImportError: + from urllib2 import urlopen # Python 2 + +import os +import re +import tempfile +from contextlib import contextmanager +import six + + +URL_REGEX = re.compile(r'http://|https://|ftp://|file://|file:\\') + + +def is_url(filename): + """Return True if string is an http or ftp path.""" + return (isinstance(filename, six.string_types) and + URL_REGEX.match(filename) is not None) + + +@contextmanager +def file_or_url_context(resource_name): + """Yield name of file from the given resource (i.e. file or url).""" + if is_url(resource_name): + _, ext = os.path.splitext(resource_name) + with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as f: + u = urlopen(resource_name) + f.write(u.read()) + try: + yield f.name + finally: + os.remove(f.name) + else: + yield resource_name diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 7bcb166a..d76e7a67 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -1,9 +1,11 @@ from ._find_contours import find_contours -from ._marching_cubes import marching_cubes, mesh_surface_area +from ._marching_cubes import (marching_cubes, mesh_surface_area, + correct_mesh_orientation) from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon from ._moments import moments, moments_central, moments_normalized, moments_hu +from .profile import profile_line from .fit import LineModel, CircleModel, EllipseModel, ransac from .block import block_reduce @@ -24,4 +26,6 @@ __all__ = ['find_contours', 'moments_normalized', 'moments_hu', 'marching_cubes', - 'mesh_surface_area'] + 'mesh_surface_area', + 'correct_mesh_orientation', + 'profile_line'] diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py index 7ea0667e..8f9dfcbc 100644 --- a/skimage/measure/_marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -68,6 +68,12 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): lexicographical order) coordinate in the contour. This is a side-effect of how the input array is traversed, but can be relied upon. + The generated mesh does not guarantee coherent orientation because of how + symmetry is used in the algorithm. If this is required, e.g. due to a + particular visualization package or for generating 3D printing STL files, + the utility ``skimage.measure.correct_mesh_orientation`` is available to + fix this in post-processing. + To quantify the area of an isosurface generated by this algorithm, pass the outputs directly into `skimage.measure.mesh_surface_area`. @@ -75,11 +81,11 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): is recommended. To contour a volume named `myvolume` about the level 0.0:: >>> from mayavi import mlab # doctest: +SKIP - >>> verts, tris = marching_cubes(myvolume, 0.0, (1., 1., 2.)) # doctest: +SKIP + >>> verts, faces = marching_cubes(myvolume, 0.0, (1., 1., 2.)) # doctest: +SKIP >>> mlab.triangular_mesh([vert[0] for vert in verts], ... [vert[1] for vert in verts], ... [vert[2] for vert in verts], - ... tris) # doctest: +SKIP + ... faces) # doctest: +SKIP >>> mlab.show() # doctest: +SKIP References @@ -90,6 +96,7 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): See Also -------- + skimage.measure.correct_mesh_orientation skimage.measure.mesh_surface_area """ @@ -106,17 +113,17 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): # Note: this algorithm is fast, but returns degenerate "triangles" which # have repeated vertices - and equivalent vertices are redundantly # placed in every triangle they connect with. - raw_tris = _marching_cubes_cy.iterate_and_store_3d(volume, float(level), - spacing) + raw_faces = _marching_cubes_cy.iterate_and_store_3d(volume, float(level), + spacing) # Find and collect unique vertices, storing triangle verts as indices. # Returns a true mesh with no degenerate faces. - verts, faces = _marching_cubes_cy.unpack_unique_verts(raw_tris) + verts, faces = _marching_cubes_cy.unpack_unique_verts(raw_faces) return np.asarray(verts), np.asarray(faces) -def mesh_surface_area(verts, tris): +def mesh_surface_area(verts, faces): """ Compute surface area, given vertices & triangular faces @@ -145,13 +152,123 @@ def mesh_surface_area(verts, tris): See Also -------- skimage.measure.marching_cubes + skimage.measure.correct_mesh_orientation """ # Fancy indexing to define two vector arrays from triangle vertices - actual_verts = verts[tris] + actual_verts = verts[faces] a = actual_verts[:, 0, :] - actual_verts[:, 1, :] b = actual_verts[:, 0, :] - actual_verts[:, 2, :] del actual_verts # Area of triangle in 3D = 1/2 * Euclidean norm of cross product return ((np.cross(a, b) ** 2).sum(axis=1) ** 0.5).sum() / 2. + + +def correct_mesh_orientation(volume, verts, faces, spacing=(1., 1., 1.), + gradient_direction='descent'): + """ + Correct orientations of mesh faces. + + Parameters + ---------- + volume : (M, N, P) array of doubles + Input data volume to find isosurfaces. Will be cast to `np.float64`. + verts : (V, 3) array of floats + Array containing (x, y, z) coordinates for V unique mesh vertices. + faces : (F, 3) array of ints + List of length-3 lists of integers, referencing vertex coordinates as + provided in `verts`. + spacing : length-3 tuple of floats + Voxel spacing in spatial dimensions corresponding to numpy array + indexing dimensions (M, N, P) as in `volume`. + gradient_direction : string + Controls if the mesh was generated from an isosurface with gradient + ascent toward objects of interest (the default), or the opposite. + The two options are: + * descent : Object was greater than exterior + * ascent : Exterior was greater than object + + Returns + ------- + faces_corrected (F, 3) array of ints + Corrected list of faces referencing vertex coordinates in `verts`. + + Notes + ----- + Certain applications and mesh processing algorithms require all faces + to be oriented in a consistent way. Generally, this means a normal vector + points "out" of the meshed shapes. This algorithm corrects the output from + `skimage.measure.marching_cubes` by flipping the orientation of + mis-oriented faces. + + Because marching cubes could be used to find isosurfaces either on + gradient descent (where the desired object has greater values than the + exterior) or ascent (where the desired object has lower values than the + exterior), the ``gradient_direction`` kwarg allows the user to inform this + algorithm which is correct. If the resulting mesh appears to be oriented + completely incorrectly, try changing this option. + + The arguments expected by this function are the exact outputs from + `skimage.measure.marching_cubes`. Only `faces` is corrected and returned, + as the vertices do not change; only the order in which they are + referenced. + + This algorithm assumes ``faces`` provided are all triangles. + + See Also + -------- + skimage.measure.marching_cubes + skimage.measure.mesh_surface_area + + """ + import scipy.ndimage as ndi + + # Calculate gradient of `volume`, then interpolate to vertices in `verts` + grad_x, grad_y, grad_z = np.gradient(volume, *spacing) + + # Fancy indexing to define two vector arrays from triangle vertices + actual_verts = verts[faces] + a = actual_verts[:, 0, :] - actual_verts[:, 1, :] + b = actual_verts[:, 0, :] - actual_verts[:, 2, :] + + # Find triangle centroids + centroids = (actual_verts.sum(axis=1) / 3.).T + + del actual_verts + + # Interpolate face centroids into each gradient axis + grad_centroids_x = ndi.map_coordinates(grad_x, centroids) + grad_centroids_y = ndi.map_coordinates(grad_y, centroids) + grad_centroids_z = ndi.map_coordinates(grad_z, centroids) + + # Combine and normalize interpolated gradients + grad_centroids = np.c_[grad_centroids_x, grad_centroids_y, + grad_centroids_z] + grad_centroids = (grad_centroids / + (np.sum(grad_centroids ** 2, + axis=1) ** 0.5)[:, np.newaxis]) + + # Find normal vectors for each face via cross product + crosses = np.cross(a, b) + crosses = crosses / (np.sum(crosses ** 2, axis=1) ** (0.5))[:, np.newaxis] + + # Take dot product + dotproducts = (grad_centroids * crosses).sum(axis=1) + + # Find mis-oriented faces + if 'descent' in gradient_direction: + # Faces with incorrect orientations have dot product < 0 + indices = (dotproducts < 0).nonzero()[0] + elif 'ascent' in gradient_direction: + # Faces with incorrection orientation have dot product > 0 + indices = (dotproducts > 0).nonzero()[0] + else: + raise ValueError("Incorrect input %s in `gradient_direction`, see " + "docstring." % (gradient_direction)) + + # Correct orientation and return, without modifying original data + faces_corrected = faces.copy() + faces_corrected[indices] = faces_corrected[indices, ::-1] + + return faces_corrected diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index 6b7197d2..e6ccdb75 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -12,7 +12,7 @@ def moments(double[:, :] image, Py_ssize_t order=3): * Area as ``m[0, 0]``. * Centroid as {``m[0, 1] / m[0, 0]``, ``m[1, 0] / m[0, 0]``}. - Note that raw moments are whether translation, scale nor rotation + Note that raw moments are neither translation, scale nor rotation invariant. Parameters diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index ef1086da..5817580d 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -399,7 +399,7 @@ def regionprops(label_image, properties=None, **major_axis_length** : float The length of the major axis of the ellipse that has the same normalized second central moments as the region. - **min_intensity** : float + **max_intensity** : float Value with the greatest intensity in the region. **mean_intensity** : float Value with the mean intensity in the region. diff --git a/skimage/measure/fit.py b/skimage/measure/fit.py index e16cd218..8f9ceb9c 100644 --- a/skimage/measure/fit.py +++ b/skimage/measure/fit.py @@ -1,4 +1,5 @@ import math +import warnings import numpy as np from scipy import optimize @@ -11,7 +12,13 @@ def _check_data_dim(data, dim): class BaseModel(object): def __init__(self): - self._params = None + self.params = None + + @property + def _params(self): + warnings.warn('`_params` attribute is deprecated, ' + 'use `params` instead.') + return self.params class LineModel(BaseModel): @@ -30,12 +37,13 @@ class LineModel(BaseModel): min{ sum((dist - x_i * cos(theta) + y_i * sin(theta))**2) } - The ``_params`` attribute contains the parameters in the following order:: - - dist, theta - A minimum number of 2 points is required to solve for the parameters. + Attributes + ---------- + params : tuple + Line model parameters in the following order `dist`, `theta`. + """ def estimate(self, data): @@ -68,7 +76,7 @@ class LineModel(BaseModel): # line always passes through mean dist = X0[0] * math.cos(theta) + X0[1] * math.sin(theta) - self._params = (dist, theta) + self.params = (dist, theta) def residuals(self, data): """Determine residuals of data to model. @@ -89,7 +97,7 @@ class LineModel(BaseModel): _check_data_dim(data, dim=2) - dist, theta = self._params + dist, theta = self.params x = data[:, 0] y = data[:, 1] @@ -114,7 +122,7 @@ class LineModel(BaseModel): """ if params is None: - params = self._params + params = self.params dist, theta = params return (dist - y * math.sin(theta)) / math.cos(theta) @@ -136,7 +144,7 @@ class LineModel(BaseModel): """ if params is None: - params = self._params + params = self.params dist, theta = params return (dist - x * math.cos(theta)) / math.sin(theta) @@ -154,12 +162,13 @@ class CircleModel(BaseModel): min{ sum((r - sqrt((x_i - xc)**2 + (y_i - yc)**2))**2) } - The ``_params`` attribute contains the parameters in the following order:: - - xc, yc, r - A minimum number of 3 points is required to solve for the parameters. + Attributes + ---------- + params : tuple + Circle model parameters in the following order `xc`, `yc`, `r`. + """ def estimate(self, data): @@ -203,7 +212,7 @@ class CircleModel(BaseModel): params0 = (xc0, yc0, r0) params, _ = optimize.leastsq(fun, params0, Dfun=Dfun, col_deriv=True) - self._params = params + self.params = params def residuals(self, data): """Determine residuals of data to model. @@ -224,7 +233,7 @@ class CircleModel(BaseModel): _check_data_dim(data, dim=2) - xc, yc, r = self._params + xc, yc, r = self.params x = data[:, 0] y = data[:, 1] @@ -249,7 +258,7 @@ class CircleModel(BaseModel): """ if params is None: - params = self._params + params = self.params xc, yc, r = params x = xc + r * np.cos(t) @@ -279,12 +288,18 @@ class EllipseModel(BaseModel): Thus you have ``2 * N`` equations (x_i, y_i) for ``N + 5`` unknowns (t_i, xc, yc, a, b, theta), which gives you an effective redundancy of ``N - 5``. - The ``_params`` attribute contains the parameters in the following order:: + The ``params`` attribute contains the parameters in the following order:: xc, yc, a, b, theta A minimum number of 5 points is required to solve for the parameters. + Attributes + ---------- + params : tuple + Ellipse model parameters in the following order `xc`, `yc`, `a`, + `b`, `theta`. + """ def estimate(self, data): @@ -353,7 +368,7 @@ class EllipseModel(BaseModel): params, _ = optimize.leastsq(fun, params0, Dfun=Dfun, col_deriv=True) - self._params = params[:5] + self.params = params[:5] def residuals(self, data): """Determine residuals of data to model. @@ -374,7 +389,7 @@ class EllipseModel(BaseModel): _check_data_dim(data, dim=2) - xc, yc, a, b, theta = self._params + xc, yc, a, b, theta = self.params ctheta = math.cos(theta) stheta = math.sin(theta) @@ -436,7 +451,7 @@ class EllipseModel(BaseModel): """ if params is None: - params = self._params + params = self.params xc, yc, a, b, theta = params ct = np.cos(t) @@ -550,7 +565,7 @@ def ransac(data, model_class, min_samples, residual_threshold, >>> model = EllipseModel() >>> model.estimate(data) - >>> model._params # doctest: +SKIP + >>> model.params # doctest: +SKIP array([ -3.30354146e+03, -2.87791160e+03, 5.59062118e+03, 7.84365066e+00, 7.19203152e-01]) @@ -558,7 +573,7 @@ def ransac(data, model_class, min_samples, residual_threshold, Estimate ellipse model using RANSAC: >>> ransac_model, inliers = ransac(data, EllipseModel, 5, 3, max_trials=50) - >>> ransac_model._params + >>> ransac_model.params array([ 20.12762373, 29.73563063, 4.81499637, 10.4743584 , 0.05217117]) >>> inliers array([False, False, False, False, True, True, True, True, True, diff --git a/skimage/measure/profile.py b/skimage/measure/profile.py new file mode 100644 index 00000000..fef3fac8 --- /dev/null +++ b/skimage/measure/profile.py @@ -0,0 +1,112 @@ +import numpy as np +import scipy.ndimage as nd + + +def profile_line(img, src, dst, linewidth=1, + order=1, mode='constant', cval=0.0): + """Return the intensity profile of an image measured along a scan line. + + Parameters + ---------- + img : numeric array, shape (M, N[, C]) + The image, either grayscale (2D array) or multichannel + (3D array, where the final axis contains the channel + information). + src : 2-tuple of numeric scalar (float or int) + The start point of the scan line. + dst : 2-tuple of numeric scalar (float or int) + The end point of the scan line. + linewidth : int, optional + Width of the scan, perpendicular to the line + order : int in {0, 1, 2, 3, 4, 5}, optional + The order of the spline interpolation to compute image values at + non-integer coordinates. 0 means nearest-neighbor interpolation. + mode : string, one of {'constant', 'nearest', 'reflect', 'wrap'}, optional + How to compute any values falling outside of the image. + cval : float, optional + If `mode` is 'constant', what constant value to use outside the image. + + Returns + ------- + return_value : array + The intensity profile along the scan line. The length of the profile + is the ceil of the computed length of the scan line. + + Examples + -------- + >>> x = np.array([[1, 1, 1, 2, 2, 2]]) + >>> img = np.vstack([np.zeros_like(x), x, x, x, np.zeros_like(x)]) + >>> img + array([[0, 0, 0, 0, 0, 0], + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [0, 0, 0, 0, 0, 0]]) + >>> profile_line(img, (2, 1), (2, 4)) + array([ 1., 1., 2., 2.]) + + Notes + ----- + The destination point is included in the profile, in contrast to + standard numpy indexing. + """ + perp_lines = _line_profile_coordinates(src, dst, linewidth=linewidth) + if img.ndim == 3: + pixels = [nd.map_coordinates(img[..., i], perp_lines, + order=order, mode=mode, cval=cval) + for i in range(img.shape[2])] + pixels = np.transpose(np.asarray(pixels), (1, 2, 0)) + else: + pixels = nd.map_coordinates(img, perp_lines, + order=order, mode=mode, cval=cval) + intensities = pixels.mean(axis=1) + + return intensities + + +def _line_profile_coordinates(src, dst, linewidth=1): + """Return the coordinates of the profile of an image along a scan line. + + Parameters + ---------- + src : 2-tuple of numeric scalar (float or int) + The start point of the scan line. + dst : 2-tuple of numeric scalar (float or int) + The end point of the scan line. + linewidth : int, optional + Width of the scan, perpendicular to the line + + Returns + ------- + coords : array, shape (2, N, C), float + The coordinates of the profile along the scan line. The length of the + profile is the ceil of the computed length of the scan line. + + Notes + ----- + This is a utility method meant to be used internally by skimage functions. + The destination point is included in the profile, in contrast to + standard numpy indexing. + """ + src_row, src_col = src = np.asarray(src, dtype=float) + dst_row, dst_col = dst = np.asarray(dst, dtype=float) + d_row, d_col = dst - src + theta = np.arctan2(d_row, d_col) + + length = np.ceil(np.hypot(d_row, d_col) + 1) + # we add one above because we include the last point in the profile + # (in contrast to standard numpy indexing) + line_col = np.linspace(src_col, dst_col, length) + line_row = np.linspace(src_row, dst_row, length) + + # we subtract 1 from linewidth to change from pixel-counting + # (make this line 3 pixels wide) to point distances (the + # distance between pixel centers) + col_width = (linewidth - 1) * np.sin(-theta) / 2 + row_width = (linewidth - 1) * np.cos(theta) / 2 + perp_rows = np.array([np.linspace(row_i - row_width, row_i + row_width, + linewidth) for row_i in line_row]) + perp_cols = np.array([np.linspace(col_i - col_width, col_i + col_width, + linewidth) for col_i in line_col]) + return np.array([perp_rows, perp_cols]) + diff --git a/skimage/measure/tests/test_fit.py b/skimage/measure/tests/test_fit.py index bde1fcc7..3ef7a0f0 100644 --- a/skimage/measure/tests/test_fit.py +++ b/skimage/measure/tests/test_fit.py @@ -10,7 +10,7 @@ def test_line_model_invalid_input(): def test_line_model_predict(): model = LineModel() - model._params = (10, 1) + model.params = (10, 1) x = np.arange(-10, 10) y = model.predict_y(x) assert_almost_equal(x, model.predict_x(y)) @@ -19,7 +19,7 @@ def test_line_model_predict(): def test_line_model_estimate(): # generate original data without noise model0 = LineModel() - model0._params = (10, 1) + model0.params = (10, 1) x0 = np.arange(-100, 100) y0 = model0.predict_y(x0) data0 = np.column_stack([x0, y0]) @@ -33,18 +33,18 @@ def test_line_model_estimate(): model_est.estimate(data) # test whether estimated parameters almost equal original parameters - assert_almost_equal(model0._params, model_est._params, 1) + assert_almost_equal(model0.params, model_est.params, 1) def test_line_model_residuals(): model = LineModel() - model._params = (0, 0) + model.params = (0, 0) assert_equal(abs(model.residuals(np.array([[0, 0]]))), 0) assert_equal(abs(model.residuals(np.array([[0, 10]]))), 0) assert_equal(abs(model.residuals(np.array([[10, 0]]))), 10) - model._params = (5, np.pi / 4) + model.params = (5, np.pi / 4) assert_equal(abs(model.residuals(np.array([[0, 0]]))), 5) - assert_equal(abs(model.residuals(np.array([[np.sqrt(50), 0]]))), 5) + assert_almost_equal(abs(model.residuals(np.array([[np.sqrt(50), 0]]))), 0) def test_line_model_under_determined(): @@ -59,7 +59,7 @@ def test_circle_model_invalid_input(): def test_circle_model_predict(): model = CircleModel() r = 5 - model._params = (0, 0, r) + model.params = (0, 0, r) t = np.arange(0, 2 * np.pi, np.pi / 2) xy = np.array(((5, 0), (0, 5), (-5, 0), (0, -5))) @@ -69,7 +69,7 @@ def test_circle_model_predict(): def test_circle_model_estimate(): # generate original data without noise model0 = CircleModel() - model0._params = (10, 12, 3) + model0.params = (10, 12, 3) t = np.linspace(0, 2 * np.pi, 1000) data0 = model0.predict_xy(t) @@ -82,12 +82,12 @@ def test_circle_model_estimate(): model_est.estimate(data) # test whether estimated parameters almost equal original parameters - assert_almost_equal(model0._params, model_est._params, 1) + assert_almost_equal(model0.params, model_est.params, 1) def test_circle_model_residuals(): model = CircleModel() - model._params = (0, 0, 5) + model.params = (0, 0, 5) assert_almost_equal(abs(model.residuals(np.array([[5, 0]]))), 0) assert_almost_equal(abs(model.residuals(np.array([[6, 6]]))), np.sqrt(2 * 6**2) - 5) @@ -101,7 +101,7 @@ def test_ellipse_model_invalid_input(): def test_ellipse_model_predict(): model = EllipseModel() r = 5 - model._params = (0, 0, 5, 10, 0) + model.params = (0, 0, 5, 10, 0) t = np.arange(0, 2 * np.pi, np.pi / 2) xy = np.array(((5, 0), (0, 10), (-5, 0), (0, -10))) @@ -111,7 +111,7 @@ def test_ellipse_model_predict(): def test_ellipse_model_estimate(): # generate original data without noise model0 = EllipseModel() - model0._params = (10, 20, 15, 25, 0) + model0.params = (10, 20, 15, 25, 0) t = np.linspace(0, 2 * np.pi, 100) data0 = model0.predict_xy(t) @@ -124,13 +124,13 @@ def test_ellipse_model_estimate(): model_est.estimate(data) # test whether estimated parameters almost equal original parameters - assert_almost_equal(model0._params, model_est._params, 0) + assert_almost_equal(model0.params, model_est.params, 0) -def test_line_model_residuals(): +def test_ellipse_model_residuals(): model = EllipseModel() # vertical line through origin - model._params = (0, 0, 10, 5, 0) + model.params = (0, 0, 10, 5, 0) assert_almost_equal(abs(model.residuals(np.array([[10, 0]]))), 0) assert_almost_equal(abs(model.residuals(np.array([[0, 5]]))), 0) assert_almost_equal(abs(model.residuals(np.array([[0, 10]]))), 5) @@ -141,7 +141,7 @@ def test_ransac_shape(): # generate original data without noise model0 = CircleModel() - model0._params = (10, 12, 3) + model0.params = (10, 12, 3) t = np.linspace(0, 2 * np.pi, 1000) data0 = model0.predict_xy(t) @@ -155,7 +155,7 @@ def test_ransac_shape(): model_est, inliers = ransac(data0, CircleModel, 3, 5) # test whether estimated parameters equal original parameters - assert_equal(model0._params, model_est._params) + assert_equal(model0.params, model_est.params) for outlier in outliers: assert outlier not in inliers @@ -204,5 +204,13 @@ def test_ransac_is_model_valid(): assert_equal(inliers, None) +def test_deprecated_params_attribute(): + model = LineModel() + model.params = (10, 1) + x = np.arange(-10, 10) + y = model.predict_y(x) + assert_equal(model.params, model._params) + + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_marching_cubes.py b/skimage/measure/tests/test_marching_cubes.py index b3c2ddc1..60adbb0d 100644 --- a/skimage/measure/tests/test_marching_cubes.py +++ b/skimage/measure/tests/test_marching_cubes.py @@ -2,7 +2,8 @@ import numpy as np from numpy.testing import assert_raises from skimage.draw import ellipsoid, ellipsoid_stats -from skimage.measure import marching_cubes, mesh_surface_area +from skimage.measure import (marching_cubes, mesh_surface_area, + correct_mesh_orientation) def test_marching_cubes_isotropic(): @@ -36,5 +37,25 @@ def test_invalid_input(): assert_raises(ValueError, marching_cubes, np.zeros((20, 20)), 0) +def test_correct_mesh_orientation(): + sphere_small = ellipsoid(1, 1, 1, levelset=True) + verts, faces = marching_cubes(sphere_small, 0.) + + # Correct mesh orientation - descent + corrected_faces1 = correct_mesh_orientation(sphere_small, verts, faces, + gradient_direction='descent') + corrected_faces2 = correct_mesh_orientation(sphere_small, verts, faces, + gradient_direction='ascent') + + # Ensure ascent is opposite of descent for all faces + np.testing.assert_array_equal(corrected_faces1, corrected_faces2[:, ::-1]) + + # Ensure correct faces have been reversed: 1, 4, and 5 + idx = [1, 4, 5] + expected = faces.copy() + expected[idx] = expected[idx, ::-1] + np.testing.assert_array_equal(expected, corrected_faces1) + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_profile.py b/skimage/measure/tests/test_profile.py new file mode 100644 index 00000000..e911673b --- /dev/null +++ b/skimage/measure/tests/test_profile.py @@ -0,0 +1,110 @@ +from numpy.testing import assert_equal, assert_almost_equal +import numpy as np + +from skimage.measure import profile_line + +image = np.arange(100).reshape((10, 10)).astype(np.float) + +def test_horizontal_rightward(): + prof = profile_line(image, (0, 2), (0, 8), order=0) + expected_prof = np.arange(2, 9) + assert_equal(prof, expected_prof) + + +def test_horizontal_leftward(): + prof = profile_line(image, (0, 8), (0, 2), order=0) + expected_prof = np.arange(8, 1, -1) + assert_equal(prof, expected_prof) + + +def test_vertical_downward(): + prof = profile_line(image, (2, 5), (8, 5), order=0) + expected_prof = np.arange(25, 95, 10) + assert_equal(prof, expected_prof) + + +def test_vertical_upward(): + prof = profile_line(image, (8, 5), (2, 5), order=0) + expected_prof = np.arange(85, 15, -10) + assert_equal(prof, expected_prof) + + +def test_45deg_right_downward(): + prof = profile_line(image, (2, 2), (8, 8), order=0) + expected_prof = np.array([22, 33, 33, 44, 55, 55, 66, 77, 77, 88]) + # repeats are due to aliasing using nearest neighbor interpolation. + # to see this, imagine a diagonal line with markers every unit of + # length traversing a checkerboard pattern of squares also of unit + # length. Because the line is diagonal, sometimes more than one + # marker will fall on the same checkerboard box. + assert_almost_equal(prof, expected_prof) + + +def test_45deg_right_downward_interpolated(): + prof = profile_line(image, (2, 2), (8, 8), order=1) + expected_prof = np.linspace(22, 88, 10) + assert_almost_equal(prof, expected_prof) + + +def test_45deg_right_upward(): + prof = profile_line(image, (8, 2), (2, 8), order=1) + expected_prof = np.arange(82, 27, -6) + assert_almost_equal(prof, expected_prof) + + +def test_45deg_left_upward(): + prof = profile_line(image, (8, 8), (2, 2), order=1) + expected_prof = np.arange(88, 21, -22. / 3) + assert_almost_equal(prof, expected_prof) + + +def test_45deg_left_downward(): + prof = profile_line(image, (2, 8), (8, 2), order=1) + expected_prof = np.arange(28, 83, 6) + assert_almost_equal(prof, expected_prof) + + +def test_pythagorean_triangle_right_downward(): + prof = profile_line(image, (1, 1), (7, 9), order=0) + expected_prof = np.array([11, 22, 23, 33, 34, 45, 56, 57, 67, 68, 79]) + assert_equal(prof, expected_prof) + + +def test_pythagorean_triangle_right_downward_interpolated(): + prof = profile_line(image, (1, 1), (7, 9), order=1) + expected_prof = np.linspace(11, 79, 11) + assert_almost_equal(prof, expected_prof) + +pyth_image = np.zeros((6, 7), np.float) +line = ((1, 2, 2, 3, 3, 4), (1, 2, 3, 3, 4, 5)) +below = ((2, 2, 3, 4, 4, 5), (0, 1, 2, 3, 4, 4)) +above = ((0, 1, 1, 2, 3, 3), (2, 2, 3, 4, 5, 6)) +pyth_image[line] = 1.8 +pyth_image[below] = 0.6 +pyth_image[above] = 0.6 + + +def test_pythagorean_triangle_right_downward_linewidth(): + prof = profile_line(pyth_image, (1, 1), (4, 5), linewidth=3, order=0) + expected_prof = np.ones(6) + assert_almost_equal(prof, expected_prof) + + +def test_pythagorean_triangle_right_upward_linewidth(): + prof = profile_line(pyth_image[::-1, :], (4, 1), (1, 5), + linewidth=3, order=0) + expected_prof = np.ones(6) + assert_almost_equal(prof, expected_prof) + + +def test_pythagorean_triangle_transpose_left_down_linewidth(): + prof = profile_line(pyth_image.T[:, ::-1], (1, 4), (5, 1), + linewidth=3, order=0) + expected_prof = np.ones(6) + assert_almost_equal(prof, expected_prof) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() + diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index 5a4dd117..42926499 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -345,6 +345,9 @@ def test_old_dict_interface(): np.array([list(props.values()) for props in feats], np.float) assert_equal(len(feats[0]), 8) + def assign(): + feats[0]['Area'] = 0 + assert_raises(RuntimeError, assign) def test_label_sequence(): @@ -361,6 +364,13 @@ def test_pure_background(): assert len(ps) == 0 +def test_invalid(): + ps = regionprops(SAMPLE) + def get_intensity_image(): + ps[0].intensity_image + assert_raises(AttributeError, get_intensity_image) + + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index 1a26b842..4d54498d 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -54,6 +54,10 @@ def convex_hull_image(image): raise ImportError('Could not import scipy.spatial, only available in ' 'scipy >= 0.9.') + # Subtract offset + offset = coords.mean(axis=0) + coords -= offset + # Find the convex hull chull = Delaunay(coords).convex_hull v = coords[np.unique(chull)] @@ -63,6 +67,9 @@ def convex_hull_image(image): angles = np.arctan2(v_centred[:, 0], v_centred[:, 1]) v = v[np.argsort(angles)] + # Add back offset + v += offset + # For each pixel coordinate, check whether that pixel # lies inside the convex hull mask = grid_points_inside_poly(image.shape[:2], v) diff --git a/skimage/morphology/tests/test_convex_hull.py b/skimage/morphology/tests/test_convex_hull.py index ee3b6bfa..67850cf2 100644 --- a/skimage/morphology/tests/test_convex_hull.py +++ b/skimage/morphology/tests/test_convex_hull.py @@ -32,6 +32,25 @@ def test_basic(): assert_array_equal(convex_hull_image(image), expected) +@skipif(not scipy_spatial) +def test_qhull_offset_example(): + nonzeros = (([1367, 1368, 1368, 1368, 1369, 1369, 1369, 1369, 1369, 1370, 1370, + 1370, 1370, 1370, 1370, 1370, 1371, 1371, 1371, 1371, 1371, 1371, + 1371, 1371, 1371, 1372, 1372, 1372, 1372, 1372, 1372, 1372, 1372, + 1372, 1373, 1373, 1373, 1373, 1373, 1373, 1373, 1373, 1373, 1374, + 1374, 1374, 1374, 1374, 1374, 1374, 1375, 1375, 1375, 1375, 1375, + 1376, 1376, 1376, 1377]), + ([151, 150, 151, 152, 149, 150, 151, 152, 153, 148, 149, 150, 151, + 152, 153, 154, 147, 148, 149, 150, 151, 152, 153, 154, 155, 146, + 147, 148, 149, 150, 151, 152, 153, 154, 146, 147, 148, 149, 150, + 151, 152, 153, 154, 147, 148, 149, 150, 151, 152, 153, 148, 149, + 150, 151, 152, 149, 150, 151, 150])) + image = np.zeros((1392, 1040), dtype=bool) + image[nonzeros] = True + expected = image.copy() + assert_array_equal(convex_hull_image(image), expected) + + @skipif(not scipy_spatial) def test_pathological_qhull_example(): image = np.array( diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index e2a3928d..f0099ee5 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -155,5 +155,15 @@ class TestDTypes(): self._test_image(image) +def test_inplace(): + selem = np.ones((3, 3)) + image = np.zeros((5, 5)) + out = image + + for f in (grey.erosion, grey.dilation, + grey.white_tophat, grey.black_tophat): + testing.assert_raises(NotImplementedError, f, image, selem, out=out) + + if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/novice/_novice.py b/skimage/novice/_novice.py index 76aabaee..843afed4 100644 --- a/skimage/novice/_novice.py +++ b/skimage/novice/_novice.py @@ -8,6 +8,7 @@ from skimage import io from skimage import img_as_ubyte from skimage.transform import resize from skimage.color import color_dict +from skimage.io.util import file_or_url_context, is_url import six from six.moves.urllib_parse import urlparse @@ -23,13 +24,13 @@ colors = namedtuple('colors', color_dict.keys())(**color_dict) def open(path): """Return Picture object from the given image path.""" - return Picture(path=path) + return Picture(path) def _verify_picture_index(index): """Raise error if picture index is not a 2D index/slice.""" if not (isinstance(index, tuple) and len(index) == 2): - raise IndexError("Expected 2D index but got {!r}".format(index)) + raise IndexError("Expected 2D index but got {0!r}".format(index)) if all(isinstance(i, int) for i in index): return index @@ -194,7 +195,7 @@ class Picture(object): Attributes ---------- path : str - Path to an image file to load. + Path to an image file to load / URL of an image array : array Raw RGB or RGBA image data [0-255], with origin at top-left. xy_array : array @@ -207,6 +208,9 @@ class Picture(object): >>> from skimage import data >>> picture = novice.open(data.data_dir + '/chelsea.png') + Load an image from a URL. URL must start with http(s):// or ftp(s):// + >>> picture = novice.open('http://scikit-image.org/_static/img/logo.png') + Create a blank 100 pixel wide, 200 pixel tall white image >>> pic = Picture.from_size((100, 200), color=(255, 255, 255)) @@ -242,15 +246,12 @@ class Picture(object): msg = "Must provide a single keyword arg (path, array, xy_array)." ValueError(msg) elif path is not None: + if not is_url(path): + path = os.path.abspath(path) self._path = path - - if urlparse(path).scheme == "": - self.array = img_as_ubyte(io.imread(path)) - self._format = imghdr.what(path) - else: - data = urlopen(path).read() - self.array = img_as_ubyte(io.imread(BytesIO(data))) - self._format = imghdr.what("", h=data) + with file_or_url_context(path) as context: + self.array = io.imread(context) + self._format = imghdr.what(context) elif array is not None: self.array = array elif xy_array is not None: diff --git a/skimage/novice/tests/test_novice.py b/skimage/novice/tests/test_novice.py index da0008fc..78193bf5 100644 --- a/skimage/novice/tests/test_novice.py +++ b/skimage/novice/tests/test_novice.py @@ -139,12 +139,16 @@ def test_update_on_save(): assert pic.modified assert pic.path is None - with tempfile.NamedTemporaryFile(suffix=".jpg") as tmp: - pic.save(tmp.name) + fd, filename = tempfile.mkstemp(suffix=".jpg") + os.close(fd) + try: + pic.save(filename) assert not pic.modified - assert_equal(pic.path, os.path.abspath(tmp.name)) + assert_equal(pic.path, os.path.abspath(filename)) assert_equal(pic.format, "jpeg") + finally: + os.unlink(filename) def test_indexing(): @@ -302,5 +306,4 @@ def test_pixel_alpha_raises(): if __name__ == '__main__': - from numpy import testing - testing.run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/restoration/__init__.py b/skimage/restoration/__init__.py new file mode 100644 index 00000000..2b593ccf --- /dev/null +++ b/skimage/restoration/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""Image restoration module. + +References +---------- +.. [1] François Orieux, Jean-François Giovannelli, and Thomas + Rodet, "Bayesian estimation of regularization and point + spread function parameters for Wiener-Hunt deconvolution", + J. Opt. Soc. Am. A 27, 1593-1607 (2010) + + http://www.opticsinfobase.org/josaa/abstract.cfm?URI=josaa-27-7-1593 + +.. [2] Richardson, William Hadley, "Bayesian-Based Iterative Method of + Image Restoration". JOSA 62 (1): 55–59. doi:10.1364/JOSA.62.000055, 1972 + +.. [3] B. R. Hunt "A matrix theory proof of the discrete + convolution theorem", IEEE Trans. on Audio and + Electroacoustics, vol. au-19, no. 4, pp. 285-288, dec. 1971 +""" + +from .deconvolution import wiener, unsupervised_wiener, richardson_lucy +from .unwrap import unwrap_phase +from ._denoise import denoise_tv_chambolle, denoise_tv_bregman, \ + denoise_bilateral + + +__all__ = ['wiener', + 'unsupervised_wiener', + 'richardson_lucy', + 'unwrap_phase', + 'denoise_tv_bregman', + 'denoise_tv_chambolle', + 'denoise_bilateral'] diff --git a/skimage/filter/_denoise.py b/skimage/restoration/_denoise.py similarity index 64% rename from skimage/filter/_denoise.py rename to skimage/restoration/_denoise.py index c0aed443..260b1078 100644 --- a/skimage/filter/_denoise.py +++ b/skimage/restoration/_denoise.py @@ -1,5 +1,109 @@ +# coding: utf-8 import numpy as np from skimage import img_as_float +from skimage.restoration._denoise_cy import _denoise_bilateral, \ + _denoise_tv_bregman + + +def denoise_bilateral(image, win_size=5, sigma_range=None, sigma_spatial=1, + bins=10000, mode='constant', cval=0): + """Denoise image using bilateral filter. + + This is an edge-preserving and noise reducing denoising filter. It averages + pixels based on their spatial closeness and radiometric similarity. + + Spatial closeness is measured by the gaussian function of the euclidian + distance between two pixels and a certain standard deviation + (`sigma_spatial`). + + Radiometric similarity is measured by the gaussian function of the euclidian + distance between two color values and a certain standard deviation + (`sigma_range`). + + Parameters + ---------- + image : ndarray + Input image. + win_size : int + Window size for filtering. + sigma_range : float + Standard deviation for grayvalue/color distance (radiometric + similarity). A larger value results in averaging of pixels with larger + radiometric differences. Note, that the image will be converted using + the `img_as_float` function and thus the standard deviation is in + respect to the range `[0, 1]`. + sigma_spatial : float + Standard deviation for range distance. A larger value results in + averaging of pixels with larger spatial differences. + bins : int + Number of discrete values for gaussian weights of color filtering. + A larger value results in improved accuracy. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + denoised : ndarray + Denoised image. + + References + ---------- + .. [1] http://users.soe.ucsc.edu/~manduchi/Papers/ICCV98.pdf + + """ + return _denoise_bilateral(image, win_size, sigma_range, sigma_spatial, + bins, mode, cval) + + +def denoise_tv_bregman(image, weight, max_iter=100, eps=1e-3, isotropic=True): + """Perform total-variation denoising using split-Bregman optimization. + + Total-variation denoising (also know as total-variation regularization) + tries to find an image with less total-variation under the constraint + of being similar to the input image, which is controlled by the + regularization parameter. + + Parameters + ---------- + image : ndarray + Input data to be denoised (converted using img_as_float`). + weight : float, optional + Denoising weight. The smaller the `weight`, the more denoising (at + the expense of less similarity to the `input`). The regularization + parameter `lambda` is chosen as `2 * weight`. + eps : float, optional + Relative difference of the value of the cost function that determines + the stop criterion. The algorithm stops when:: + + SUM((u(n) - u(n-1))**2) < eps + + max_iter : int, optional + Maximal number of iterations used for the optimization. + isotropic : boolean, optional + Switch between isotropic and anisotropic TV denoising. + + Returns + ------- + u : ndarray + Denoised image. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Total_variation_denoising + .. [2] Tom Goldstein and Stanley Osher, "The Split Bregman Method For L1 + Regularized Problems", + ftp://ftp.math.ucla.edu/pub/camreport/cam08-29.pdf + .. [3] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising + using Split Bregman" in Image Processing On Line on 2012–05–19, + http://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf + .. [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf + + """ + return _denoise_tv_bregman(image, weight, max_iter, eps, isotropic) def _denoise_tv_chambolle_3d(im, weight=100, eps=2.e-4, n_iter_max=200): diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/restoration/_denoise_cy.pyx similarity index 68% rename from skimage/filter/_denoise_cy.pyx rename to skimage/restoration/_denoise_cy.pyx index 0c4f2539..34404dce 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/restoration/_denoise_cy.pyx @@ -10,7 +10,6 @@ from libc.stdlib cimport malloc, free from libc.float cimport DBL_MAX from skimage._shared.interpolation cimport get_pixel3d from skimage.util import img_as_float -from skimage._shared.utils import deprecated cdef inline double _gaussian_weight(double sigma, double value): @@ -45,58 +44,9 @@ cdef double* _compute_range_lut(Py_ssize_t win_size, double sigma): return range_lut -def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, - double sigma_spatial=1, Py_ssize_t bins=10000, - mode='constant', double cval=0): - """Denoise image using bilateral filter. - - This is an edge-preserving and noise reducing denoising filter. It averages - pixels based on their spatial closeness and radiometric similarity. - - Spatial closeness is measured by the gaussian function of the euclidian - distance between two pixels and a certain standard deviation - (`sigma_spatial`). - - Radiometric similarity is measured by the gaussian function of the euclidian - distance between two color values and a certain standard deviation - (`sigma_range`). - - Parameters - ---------- - image : ndarray - Input image. - win_size : int - Window size for filtering. - sigma_range : float - Standard deviation for grayvalue/color distance (radiometric - similarity). A larger value results in averaging of pixels with larger - radiometric differences. Note, that the image will be converted using - the `img_as_float` function and thus the standard deviation is in - respect to the range `[0, 1]`. - sigma_spatial : float - Standard deviation for range distance. A larger value results in - averaging of pixels with larger spatial differences. - bins : int - Number of discrete values for gaussian weights of color filtering. - A larger value results in improved accuracy. - mode : string - How to handle values outside the image borders. See - `scipy.ndimage.map_coordinates` for detail. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Returns - ------- - denoised : ndarray - Denoised image. - - References - ---------- - .. [1] http://users.soe.ucsc.edu/~manduchi/Papers/ICCV98.pdf - - """ - +def _denoise_bilateral(image, Py_ssize_t win_size, sigma_range, + double sigma_spatial, Py_ssize_t bins, + mode, double cval): image = np.atleast_3d(img_as_float(image)) # if image.max() is 0, then dist_scale can have an unverified value @@ -194,52 +144,8 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, return np.squeeze(np.asarray(out)) -def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, - char isotropic=True): - """Perform total-variation denoising using split-Bregman optimization. - - Total-variation denoising (also know as total-variation regularization) - tries to find an image with less total-variation under the constraint - of being similar to the input image, which is controlled by the - regularization parameter. - - Parameters - ---------- - image : ndarray - Input data to be denoised (converted using img_as_float`). - weight : float, optional - Denoising weight. The smaller the `weight`, the more denoising (at - the expense of less similarity to the `input`). The regularization - parameter `lambda` is chosen as `2 * weight`. - eps : float, optional - Relative difference of the value of the cost function that determines - the stop criterion. The algorithm stops when:: - - SUM((u(n) - u(n-1))**2) < eps - - max_iter : int, optional - Maximal number of iterations used for the optimization. - isotropic : boolean, optional - Switch between isotropic and anisotropic TV denoising. - - Returns - ------- - u : ndarray - Denoised image. - - References - ---------- - .. [1] http://en.wikipedia.org/wiki/Total_variation_denoising - .. [2] Tom Goldstein and Stanley Osher, "The Split Bregman Method For L1 - Regularized Problems", - ftp://ftp.math.ucla.edu/pub/camreport/cam08-29.pdf - .. [3] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising - using Split Bregman" in Image Processing On Line on 2012–05–19, - http://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf - .. [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf - - """ - +def _denoise_tv_bregman(image, double weight, int max_iter, double eps, + char isotropic): image = np.atleast_3d(img_as_float(image)) cdef: diff --git a/skimage/restoration/_unwrap_1d.pyx b/skimage/restoration/_unwrap_1d.pyx new file mode 100644 index 00000000..f23e5f40 --- /dev/null +++ b/skimage/restoration/_unwrap_1d.pyx @@ -0,0 +1,22 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +from libc.math cimport M_PI + + +def unwrap_1d(double[::1] image, double[::1] unwrapped_image): + '''Phase unwrapping using the naive approach.''' + cdef: + Py_ssize_t i + double difference + long periods = 0 + unwrapped_image[0] = image[0] + for i in range(1, image.shape[0]): + difference = image[i] - image[i - 1] + if difference > M_PI: + periods -= 1 + elif difference < -M_PI: + periods += 1 + unwrapped_image[i] = image[i] + 2 * M_PI * periods diff --git a/skimage/restoration/_unwrap_2d.pyx b/skimage/restoration/_unwrap_2d.pyx new file mode 100644 index 00000000..6e889729 --- /dev/null +++ b/skimage/restoration/_unwrap_2d.pyx @@ -0,0 +1,16 @@ +cdef extern void unwrap2D(double* wrapped_image, + double* unwrapped_image, + unsigned char* input_mask, + int image_width, int image_height, + int wrap_around_x, int wrap_around_y) + +def unwrap_2d(double[:, ::1] image, + unsigned char[:, ::1] mask, + double[:, ::1] unwrapped_image, + wrap_around): + unwrap2D(&image[0, 0], + &unwrapped_image[0, 0], + &mask[0, 0], + image.shape[1], image.shape[0], + wrap_around[1], wrap_around[0], + ) diff --git a/skimage/restoration/_unwrap_3d.pyx b/skimage/restoration/_unwrap_3d.pyx new file mode 100644 index 00000000..370d58be --- /dev/null +++ b/skimage/restoration/_unwrap_3d.pyx @@ -0,0 +1,16 @@ +cdef extern void unwrap3D(double* wrapped_volume, + double* unwrapped_volume, + unsigned char* input_mask, + int image_width, int image_height, int volume_depth, + int wrap_around_x, int wrap_around_y, int wrap_around_z) + +def unwrap_3d(double[:, :, ::1] image, + unsigned char[:, :, ::1] mask, + double[:, :, ::1] unwrapped_image, + wrap_around): + unwrap3D(&image[0, 0, 0], + &unwrapped_image[0, 0, 0], + &mask[0, 0, 0], + image.shape[2], image.shape[1], image.shape[0], #TODO: check!!! + wrap_around[2], wrap_around[1], wrap_around[0], + ) diff --git a/skimage/restoration/deconvolution.py b/skimage/restoration/deconvolution.py new file mode 100644 index 00000000..51c2c705 --- /dev/null +++ b/skimage/restoration/deconvolution.py @@ -0,0 +1,382 @@ +# -*- coding: utf-8 -*- +# deconvolution.py --- Image deconvolution + +"""Implementations restoration functions""" + +from __future__ import division + +import numpy as np +import numpy.random as npr +from scipy.signal import convolve2d + +from . import uft + +__keywords__ = "restoration, image, deconvolution" + + +def wiener(image, psf, balance, reg=None, is_real=True, clip=True): + """Wiener-Hunt deconvolution + + Return the deconvolution with a Wiener-Hunt approach (i.e. with + Fourier diagonalisation). + + Parameters + ---------- + image : (M, N) ndarray + Input degraded image + psf : ndarray + Point Spread Function. This is assumed to be the impulse + response (input image space) if the data-type is real, or the + transfer function (Fourier space) if the data-type is + complex. There is no constraints on the shape of the impulse + response. The transfer function must be of shape `(M, N)` if + `is_real is True`, `(M, N // 2 + 1)` otherwise (see + `np.fft.rfftn`). + balance : float + The regularisation parameter value that tunes the balance + between the data adequacy that improve frequency restoration + and the prior adequacy that reduce frequency restoration (to + avoid noise artifact). + reg : ndarray, optional + The regularisation operator. The Laplacian by default. It can + be an impulse response or a transfer function, as for the + psf. Shape constraint is the same than for the `psf` parameter. + is_real : boolean, optional + True by default. Specify if ``psf`` and ``reg`` are provided + with hermitian hypothesis, that is only half of the frequency + plane is provided (due to the redundancy of Fourier transform + of real signal). It's apply only if ``psf`` and/or ``reg`` are + provided as transfer function. For the hermitian property see + ``uft`` module or ``np.fft.rfftn``. + clip : boolean, optional + True by default. If true, pixel value of the result above 1 or + under -1 are thresholded for skimage pipeline + compatibility. + + Returns + ------- + im_deconv : (M, N) ndarray + The deconvolved image + + Examples + -------- + >>> from skimage import color, data, restoration + >>> lena = color.rgb2gray(data.lena()) + >>> from scipy.signal import convolve2d + >>> psf = np.ones((5, 5)) / 25 + >>> lena = convolve2d(lena, psf, 'same') + >>> lena += 0.1 * lena.std() * np.random.standard_normal(lena.shape) + >>> deconvolved_lena = restoration.wiener(lena, psf, 1100) + + Notes + ----- + This function applies the Wiener filter to a noisy and degraded + image by an impulse response (or PSF). If the data model is + + .. math:: y = Hx + n + + where :math:`n` is noise, :math:`H` the PSF and :math:`x` the + unknown original image, the Wiener filter is + + .. math:: + \hat x = F^\dag (|\Lambda_H|^2 + \lambda |\Lambda_D|^2) + \Lambda_H^\dag F y + + where :math:`F` and :math:`F^\dag` are the Fourier and inverse + Fourier transfroms respectively, :math:`\Lambda_H` the transfer + function (or the Fourier transfrom of the PSF, see [Hunt] below) + and :math:`\Lambda_D` the filter to penalize the restored image + frequencies (Laplacian by default, that is penalization of high + frequency). The parameter :math:`\lambda` tunes the balance + between the data (that tends to increase high frequency, even + those coming from noise), and the regularization. + + These methods are then specific to a prior model. Consequently, + the application or the true image nature must corresponds to the + prior model. By default, the prior model (Laplacian) introduce + image smoothness or pixel correlation. It can also be interpreted + as high-frequency penalization to compensate the instability of + the solution wrt. data (sometimes called noise amplification or + "explosive" solution). + + Finally, the use of Fourier space implies a circulant property of + :math:`H`, see [Hunt]. + + References + ---------- + .. [1] François Orieux, Jean-François Giovannelli, and Thomas + Rodet, "Bayesian estimation of regularization and point + spread function parameters for Wiener-Hunt deconvolution", + J. Opt. Soc. Am. A 27, 1593-1607 (2010) + + http://www.opticsinfobase.org/josaa/abstract.cfm?URI=josaa-27-7-1593 + + http://research.orieux.fr/files/papers/OGR-JOSA10.pdf + + .. [2] B. R. Hunt "A matrix theory proof of the discrete + convolution theorem", IEEE Trans. on Audio and + Electroacoustics, vol. au-19, no. 4, pp. 285-288, dec. 1971 + """ + if reg is None: + reg, _ = uft.laplacian(image.ndim, image.shape, is_real=is_real) + if not np.iscomplexobj(reg): + reg = uft.ir2tf(reg, image.shape, is_real=is_real) + + if psf.shape != reg.shape: + trans_func = uft.ir2tf(psf, image.shape, is_real=is_real) + else: + trans_func = psf + + wiener_filter = np.conj(trans_func) / (np.abs(trans_func)**2 + + balance * np.abs(reg)**2) + if is_real: + deconv = uft.uirfft2(wiener_filter * uft.urfft2(image)) + else: + deconv = uft.uifft2(wiener_filter * uft.ufft2(image)) + + if clip: + deconv[deconv > 1] = 1 + deconv[deconv < -1] = -1 + + return deconv + + +def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, + clip=True): + """Unsupervised Wiener-Hunt deconvolution + + Return the deconvolution with a Wiener-Hunt approach, where the + hyperparameters are automatically estimated. The algorithm is a + stochastic iterative process (Gibbs sampler) described in the + reference below. See also ``wiener`` function. + + Parameters + ---------- + image : (M, N) ndarray + The input degraded image + psf : ndarray + The impulse response (input image's space) or the transfer + function (Fourier space). Both are accepted. The transfer + function is recognize as being complex + (``np.iscomplexobj(psf)``). + reg : ndarray, optional + The regularisation operator. The Laplacian by default. It can + be an impulse response or a transfer function, as for the psf. + user_params : dict + dictionary of gibbs parameters. See below. + clip : boolean, optional + True by default. If true, pixel value of the result above 1 or + under -1 are thresholded for skimage pipeline + compatibility. + + Returns + ------- + x_postmean : (M, N) ndarray + The deconvolved image (the posterior mean). + chains : dict + The keys ``noise`` and ``prior`` contain the chain list of + noise and prior precision respectively. + + Other parameters + ---------------- + The keys of ``user_params`` are: + + threshold : float + The stopping criterion: the norm of the difference between to + successive approximated solution (empirical mean of object + samples, see Notes section). 1e-4 by default. + burnin : int + The number of sample to ignore to start computation of the + mean. 100 by default. + min_iter : int + The minimum number of iterations. 30 by default. + max_iter : int + The maximum number of iterations if ``threshold`` is not + satisfied. 150 by default. + callback : callable (None by default) + A user provided callable to which is passed, if the function + exists, the current image sample for whatever purpose. The user + can store the sample, or compute other moments than the + mean. It has no influence on the algorithm execution and is + only for inspection. + + Examples + -------- + >>> from skimage import color, data, restoration + >>> lena = color.rgb2gray(data.lena()) + >>> from scipy.signal import convolve2d + >>> psf = np.ones((5, 5)) / 25 + >>> lena = convolve2d(lena, psf, 'same') + >>> lena += 0.1 * lena.std() * np.random.standard_normal(lena.shape) + >>> deconvolved_lena = restoration.unsupervised_wiener(lena, psf) + + Notes + ----- + The estimated image is design as the posterior mean of a + probability law (from a Bayesian analysis). The mean is defined as + a sum over all the possible images weighted by their respective + probability. Given the size of the problem, the exact sum is not + tractable. This algorithm use of MCMC to draw image under the + posterior law. The practical idea is to only draw high probable + image since they have the biggest contribution to the mean. At the + opposite, the lowest probable image are draw less often since + their contribution are low. Finally the empirical mean of these + samples give us an estimation of the mean, and an exact + computation with an infinite sample set. + + References + ---------- + .. [1] François Orieux, Jean-François Giovannelli, and Thomas + Rodet, "Bayesian estimation of regularization and point + spread function parameters for Wiener-Hunt deconvolution", + J. Opt. Soc. Am. A 27, 1593-1607 (2010) + + http://www.opticsinfobase.org/josaa/abstract.cfm?URI=josaa-27-7-1593 + + http://research.orieux.fr/files/papers/OGR-JOSA10.pdf + """ + params = {'threshold': 1e-4, 'max_iter': 200, + 'min_iter': 30, 'burnin': 15, 'callback': None} + params.update(user_params or {}) + + if reg is None: + reg, _ = uft.laplacian(image.ndim, image.shape, is_real=is_real) + if not np.iscomplexobj(reg): + reg = uft.ir2tf(reg, image.shape, is_real=is_real) + + if psf.shape != reg.shape: + trans_fct = uft.ir2tf(psf, image.shape, is_real=is_real) + else: + trans_fct = psf + + # The mean of the object + x_postmean = np.zeros(trans_fct.shape) + # The previous computed mean in the iterative loop + prev_x_postmean = np.zeros(trans_fct.shape) + + # Difference between two successive mean + delta = np.NAN + + # Initial state of the chain + gn_chain, gx_chain = [1], [1] + + # The correlation of the object in Fourier space (if size is big, + # this can reduce computation time in the loop) + areg2 = np.abs(reg)**2 + atf2 = np.abs(trans_fct)**2 + + # The Fourier transfrom may change the image.size attribut, so we + # store it. + if is_real: + data_spectrum = uft.urfft2(image.astype(np.float)) + else: + data_spectrum = uft.ufft2(image.astype(np.float)) + + # Gibbs sampling + for iteration in range(params['max_iter']): + # Sample of Eq. 27 p(circX^k | gn^k-1, gx^k-1, y). + + # weighting (correlation in direct space) + precision = gn_chain[-1] * atf2 + gx_chain[-1] * areg2 # Eq. 29 + excursion = np.sqrt(0.5) / np.sqrt(precision) * ( + np.random.standard_normal(data_spectrum.shape) + + 1j * np.random.standard_normal(data_spectrum.shape)) + + # mean Eq. 30 (RLS for fixed gn, gamma0 and gamma1 ...) + wiener_filter = gn_chain[-1] * np.conj(trans_fct) / precision + + # sample of X in Fourier space + x_sample = wiener_filter * data_spectrum + excursion + if params['callback']: + params['callback'](x_sample) + + # sample of Eq. 31 p(gn | x^k, gx^k, y) + gn_chain.append(npr.gamma(image.size / 2, + 2 / uft.image_quad_norm(data_spectrum - + x_sample * + trans_fct))) + + # sample of Eq. 31 p(gx | x^k, gn^k-1, y) + gx_chain.append(npr.gamma((image.size - 1) / 2, + 2 / uft.image_quad_norm(x_sample * reg))) + + # current empirical average + if iteration > params['burnin']: + x_postmean = prev_x_postmean + x_sample + + if iteration > (params['burnin'] + 1): + current = x_postmean / (iteration - params['burnin']) + previous = prev_x_postmean / (iteration - params['burnin'] - 1) + + delta = np.sum(np.abs(current - previous)) / \ + np.sum(np.abs(x_postmean)) / (iteration - params['burnin']) + + prev_x_postmean = x_postmean + + # stop of the algorithm + if (iteration > params['min_iter']) and (delta < params['threshold']): + break + + # Empirical average \approx POSTMEAN Eq. 44 + x_postmean = x_postmean / (iteration - params['burnin']) + if is_real: + x_postmean = uft.uirfft2(x_postmean) + else: + x_postmean = uft.uifft2(x_postmean) + + if clip: + x_postmean[x_postmean > 1] = 1 + x_postmean[x_postmean < -1] = -1 + + return (x_postmean, {'noise': gn_chain, 'prior': gx_chain}) + + +def richardson_lucy(image, psf, iterations=50, clip=True): + """Richardson-Lucy deconvolution. + + Parameters + ---------- + image : ndarray + Input degraded image + psf : ndarray + The point spread function + iterations : int + Number of iterations. This parameter play to role of + regularisation. + clip : boolean, optional + True by default. If true, pixel value of the result above 1 or + under -1 are thresholded for skimage pipeline + compatibility. + + Returns + ------- + im_deconv : ndarray + The deconvolved image + + Examples + -------- + >>> from skimage import color, data, restoration + >>> camera = color.rgb2gray(data.camera()) + >>> from scipy.signal import convolve2d + >>> psf = np.ones((5, 5)) / 25 + >>> camera = convolve2d(camera, psf, 'same') + >>> camera += 0.1 * camera.std() * np.random.standard_normal(camera.shape) + >>> deconvolved = restoration.richardson_lucy(camera, psf, 5) + + References + ---------- + .. [2] http://en.wikipedia.org/wiki/Richardson%E2%80%93Lucy_deconvolution + """ + image = image.astype(np.float) + psf = psf.astype(np.float) + im_deconv = 0.5 * np.ones(image.shape) + psf_mirror = psf[::-1, ::-1] + for _ in range(iterations): + relative_blur = image / convolve2d(im_deconv, psf, 'same') + im_deconv *= convolve2d(relative_blur, psf_mirror, 'same') + + if clip: + im_deconv[im_deconv > 1] = 1 + im_deconv[im_deconv < -1] = -1 + + return im_deconv diff --git a/skimage/restoration/setup.py b/skimage/restoration/setup.py new file mode 100644 index 00000000..e20073e0 --- /dev/null +++ b/skimage/restoration/setup.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +import os + +from skimage._build import cython + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs + + config = Configuration('restoration', parent_package, top_path) + config.add_data_dir('tests') + + cython(['_unwrap_1d.pyx'], working_path=base_path) + cython(['_unwrap_2d.pyx'], working_path=base_path) + cython(['_unwrap_3d.pyx'], working_path=base_path) + cython(['_denoise_cy.pyx'], working_path=base_path) + + config.add_extension('_unwrap_1d', sources=['_unwrap_1d.c'], + include_dirs=[get_numpy_include_dirs()]) + unwrap_sources_2d = ['_unwrap_2d.c', 'unwrap_2d_ljmu.c'] + config.add_extension('_unwrap_2d', sources=unwrap_sources_2d, + include_dirs=[get_numpy_include_dirs()]) + unwrap_sources_3d = ['_unwrap_3d.c', 'unwrap_3d_ljmu.c'] + config.add_extension('_unwrap_3d', sources=unwrap_sources_3d, + include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_denoise_cy', sources=['_denoise_cy.c'], + include_dirs=[get_numpy_include_dirs(), '../_shared']) + + return config + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='scikit-image Developers', + author='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='Restoration', + url='https://github.com/scikit-image/scikit-image', + license='SciPy License (BSD Style)', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/restoration/tests/camera_rl.npy b/skimage/restoration/tests/camera_rl.npy new file mode 100644 index 00000000..10995f85 Binary files /dev/null and b/skimage/restoration/tests/camera_rl.npy differ diff --git a/skimage/restoration/tests/camera_unsup.npy b/skimage/restoration/tests/camera_unsup.npy new file mode 100644 index 00000000..12d2e8b8 Binary files /dev/null and b/skimage/restoration/tests/camera_unsup.npy differ diff --git a/skimage/restoration/tests/camera_unsup2.npy b/skimage/restoration/tests/camera_unsup2.npy new file mode 100644 index 00000000..5bb974db Binary files /dev/null and b/skimage/restoration/tests/camera_unsup2.npy differ diff --git a/skimage/restoration/tests/camera_wiener.npy b/skimage/restoration/tests/camera_wiener.npy new file mode 100644 index 00000000..831b2be8 Binary files /dev/null and b/skimage/restoration/tests/camera_wiener.npy differ diff --git a/skimage/filter/tests/test_denoise.py b/skimage/restoration/tests/test_denoise.py similarity index 71% rename from skimage/filter/tests/test_denoise.py rename to skimage/restoration/tests/test_denoise.py index f81ba07b..d451aa74 100644 --- a/skimage/filter/tests/test_denoise.py +++ b/skimage/restoration/tests/test_denoise.py @@ -1,7 +1,7 @@ import numpy as np from numpy.testing import run_module_suite, assert_raises, assert_equal -from skimage import filter, data, color, img_as_float +from skimage import restoration, data, color, img_as_float np.random.seed(1234) @@ -19,7 +19,7 @@ def test_denoise_tv_chambolle_2d(): # clip noise so that it does not exceed allowed range for float images. img = np.clip(img, 0, 1) # denoise - denoised_lena = filter.denoise_tv_chambolle(img, weight=60.0) + denoised_lena = restoration.denoise_tv_chambolle(img, weight=60.0) # which dtype? assert denoised_lena.dtype in [np.float, np.float32, np.float64] from scipy import ndimage @@ -33,8 +33,9 @@ def test_denoise_tv_chambolle_2d(): def test_denoise_tv_chambolle_multichannel(): - denoised0 = filter.denoise_tv_chambolle(lena[..., 0], weight=60.0) - denoised = filter.denoise_tv_chambolle(lena, weight=60.0, multichannel=True) + denoised0 = restoration.denoise_tv_chambolle(lena[..., 0], weight=60.0) + denoised = restoration.denoise_tv_chambolle(lena, weight=60.0, + multichannel=True) assert_equal(denoised[..., 0], denoised0) @@ -43,7 +44,7 @@ def test_denoise_tv_chambolle_float_result_range(): img = lena_gray int_lena = np.multiply(img, 255).astype(np.uint8) assert np.max(int_lena) > 1 - denoised_int_lena = filter.denoise_tv_chambolle(int_lena, weight=60.0) + denoised_int_lena = restoration.denoise_tv_chambolle(int_lena, weight=60.0) # test if the value range of output float data is within [0.0:1.0] assert denoised_int_lena.dtype == np.float assert np.max(denoised_int_lena) <= 1.0 @@ -59,12 +60,12 @@ def test_denoise_tv_chambolle_3d(): mask += 20 * np.random.random(mask.shape) mask[mask < 0] = 0 mask[mask > 255] = 255 - res = filter.denoise_tv_chambolle(mask.astype(np.uint8), weight=100) + res = restoration.denoise_tv_chambolle(mask.astype(np.uint8), weight=100) assert res.dtype == np.float assert res.std() * 255 < mask.std() # test wrong number of dimensions - assert_raises(ValueError, filter.denoise_tv_chambolle, + assert_raises(ValueError, restoration.denoise_tv_chambolle, np.random.random((8, 8, 8, 8))) @@ -74,8 +75,8 @@ def test_denoise_tv_bregman_2d(): img += 0.5 * img.std() * np.random.random(img.shape) img = np.clip(img, 0, 1) - out1 = filter.denoise_tv_bregman(img, weight=10) - out2 = filter.denoise_tv_bregman(img, weight=5) + out1 = restoration.denoise_tv_bregman(img, weight=10) + out2 = restoration.denoise_tv_bregman(img, weight=5) # make sure noise is reduced assert img.std() > out1.std() @@ -87,7 +88,7 @@ def test_denoise_tv_bregman_float_result_range(): img = lena_gray int_lena = np.multiply(img, 255).astype(np.uint8) assert np.max(int_lena) > 1 - denoised_int_lena = filter.denoise_tv_bregman(int_lena, weight=60.0) + denoised_int_lena = restoration.denoise_tv_bregman(int_lena, weight=60.0) # test if the value range of output float data is within [0.0:1.0] assert denoised_int_lena.dtype == np.float assert np.max(denoised_int_lena) <= 1.0 @@ -100,8 +101,8 @@ def test_denoise_tv_bregman_3d(): img += 0.5 * img.std() * np.random.random(img.shape) img = np.clip(img, 0, 1) - out1 = filter.denoise_tv_bregman(img, weight=10) - out2 = filter.denoise_tv_bregman(img, weight=5) + out1 = restoration.denoise_tv_bregman(img, weight=10) + out2 = restoration.denoise_tv_bregman(img, weight=5) # make sure noise is reduced assert img.std() > out1.std() @@ -114,8 +115,10 @@ def test_denoise_bilateral_2d(): img += 0.5 * img.std() * np.random.random(img.shape) img = np.clip(img, 0, 1) - out1 = filter.denoise_bilateral(img, sigma_range=0.1, sigma_spatial=20) - out2 = filter.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) + out1 = restoration.denoise_bilateral(img, sigma_range=0.1, + sigma_spatial=20) + out2 = restoration.denoise_bilateral(img, sigma_range=0.2, + sigma_spatial=30) # make sure noise is reduced assert img.std() > out1.std() @@ -128,8 +131,10 @@ def test_denoise_bilateral_3d(): img += 0.5 * img.std() * np.random.random(img.shape) img = np.clip(img, 0, 1) - out1 = filter.denoise_bilateral(img, sigma_range=0.1, sigma_spatial=20) - out2 = filter.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) + out1 = restoration.denoise_bilateral(img, sigma_range=0.1, + sigma_spatial=20) + out2 = restoration.denoise_bilateral(img, sigma_range=0.2, + sigma_spatial=30) # make sure noise is reduced assert img.std() > out1.std() diff --git a/skimage/restoration/tests/test_restoration.py b/skimage/restoration/tests/test_restoration.py new file mode 100644 index 00000000..915640af --- /dev/null +++ b/skimage/restoration/tests/test_restoration.py @@ -0,0 +1,64 @@ +from os.path import abspath, dirname, join as pjoin + +import numpy as np +from scipy.signal import convolve2d + +import skimage +from skimage.data import camera +from skimage import restoration +from skimage.restoration import uft + +test_img = skimage.img_as_float(camera()) + + +def test_wiener(): + psf = np.ones((5, 5)) / 25 + data = convolve2d(test_img, psf, 'same') + np.random.seed(0) + data += 0.1 * data.std() * np.random.standard_normal(data.shape) + deconvolved = restoration.wiener(data, psf, 0.05) + + path = pjoin(dirname(abspath(__file__)), 'camera_wiener.npy') + np.testing.assert_allclose(deconvolved, np.load(path), rtol=1e-3) + + _, laplacian = uft.laplacian(2, data.shape) + otf = uft.ir2tf(psf, data.shape, is_real=False) + deconvolved = restoration.wiener(data, otf, 0.05, + reg=laplacian, + is_real=False) + np.testing.assert_allclose(np.real(deconvolved), + np.load(path), + rtol=1e-3) + + +def test_unsupervised_wiener(): + psf = np.ones((5, 5)) / 25 + data = convolve2d(test_img, psf, 'same') + np.random.seed(0) + data += 0.1 * data.std() * np.random.standard_normal(data.shape) + deconvolved, _ = restoration.unsupervised_wiener(data, psf) + + path = pjoin(dirname(abspath(__file__)), 'camera_unsup.npy') + np.testing.assert_allclose(deconvolved, np.load(path), rtol=1e-3) + + _, laplacian = uft.laplacian(2, data.shape) + otf = uft.ir2tf(psf, data.shape, is_real=False) + np.random.seed(0) + deconvolved = restoration.unsupervised_wiener( + data, otf, reg=laplacian, is_real=False, + user_params={"callback": lambda x: None})[0] + path = pjoin(dirname(abspath(__file__)), 'camera_unsup2.npy') + np.testing.assert_allclose(np.real(deconvolved), + np.load(path), + rtol=1e-3) + + +def test_richardson_lucy(): + psf = np.ones((5, 5)) / 25 + data = convolve2d(test_img, psf, 'same') + np.random.seed(0) + data += 0.1 * data.std() * np.random.standard_normal(data.shape) + deconvolved = restoration.richardson_lucy(data, psf, 5) + + path = pjoin(dirname(abspath(__file__)), 'camera_rl.npy') + np.testing.assert_allclose(deconvolved, np.load(path), rtol=1e-3) diff --git a/skimage/restoration/tests/test_unwrap.py b/skimage/restoration/tests/test_unwrap.py new file mode 100644 index 00000000..3d548fc3 --- /dev/null +++ b/skimage/restoration/tests/test_unwrap.py @@ -0,0 +1,141 @@ +from __future__ import print_function, division + +import numpy as np +from numpy.testing import (run_module_suite, assert_array_almost_equal, + assert_almost_equal, assert_array_equal, + assert_raises) +import warnings + +from skimage.restoration import unwrap_phase + + +def assert_phase_almost_equal(a, b, *args, **kwargs): + '''An assert_almost_equal insensitive to phase shifts of n*2*pi.''' + shift = 2 * np.pi * np.round((b.mean() - a.mean()) / (2 * np.pi)) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + print('assert_phase_allclose, abs', np.max(np.abs(a - (b - shift)))) + print('assert_phase_allclose, rel', + np.max(np.abs((a - (b - shift)) / a))) + if np.ma.isMaskedArray(a): + assert np.ma.isMaskedArray(b) + assert_array_equal(a.mask, b.mask) + au = np.asarray(a) + bu = np.asarray(b) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + print('assert_phase_allclose, no mask, abs', + np.max(np.abs(au - (bu - shift)))) + print('assert_phase_allclose, no mask, rel', + np.max(np.abs((au - (bu - shift)) / au))) + assert_array_almost_equal(a + shift, b, *args, **kwargs) + + +def check_unwrap(image, mask=None): + image_wrapped = np.angle(np.exp(1j * image)) + if not mask is None: + print('Testing a masked image') + image = np.ma.array(image, mask=mask) + image_wrapped = np.ma.array(image_wrapped, mask=mask) + image_unwrapped = unwrap_phase(image_wrapped) + assert_phase_almost_equal(image_unwrapped, image) + + +def test_unwrap_1d(): + image = np.linspace(0, 10 * np.pi, 100) + check_unwrap(image) + # Masked arrays are not allowed in 1D + assert_raises(ValueError, check_unwrap, image, True) + # wrap_around is not allowed in 1D + assert_raises(ValueError, unwrap_phase, image, True) + + +def test_unwrap_2d(): + x, y = np.ogrid[:8, :16] + image = 2 * np.pi * (x * 0.2 + y * 0.1) + yield check_unwrap, image + mask = np.zeros(image.shape, dtype=np.bool) + mask[4:6, 4:8] = True + yield check_unwrap, image, mask + + +def test_unwrap_3d(): + x, y, z = np.ogrid[:8, :12, :16] + image = 2 * np.pi * (x * 0.2 + y * 0.1 + z * 0.05) + yield check_unwrap, image + mask = np.zeros(image.shape, dtype=np.bool) + mask[4:6, 4:6, 1:3] = True + yield check_unwrap, image, mask + + +def check_wrap_around(ndim, axis): + # create a ramp, but with the last pixel along axis equalling the first + elements = 100 + ramp = np.linspace(0, 12 * np.pi, elements) + ramp[-1] = ramp[0] + image = ramp.reshape(tuple([elements if n == axis else 1 + for n in range(ndim)])) + image_wrapped = np.angle(np.exp(1j * image)) + + index_first = tuple([0] * ndim) + index_last = tuple([-1 if n == axis else 0 for n in range(ndim)]) + # unwrap the image without wrap around + with warnings.catch_warnings(): + # We do not want warnings about length 1 dimensions + warnings.simplefilter("ignore") + image_unwrap_no_wrap_around = unwrap_phase(image_wrapped) + print('endpoints without wrap_around:', + image_unwrap_no_wrap_around[index_first], + image_unwrap_no_wrap_around[index_last]) + # without wrap around, the endpoints of the image should differ + assert abs(image_unwrap_no_wrap_around[index_first] + - image_unwrap_no_wrap_around[index_last]) > np.pi + # unwrap the image with wrap around + wrap_around = [n == axis for n in range(ndim)] + with warnings.catch_warnings(): + # We do not want warnings about length 1 dimensions + warnings.simplefilter("ignore") + image_unwrap_wrap_around = unwrap_phase(image_wrapped, wrap_around) + print('endpoints with wrap_around:', + image_unwrap_wrap_around[index_first], + image_unwrap_wrap_around[index_last]) + # with wrap around, the endpoints of the image should be equal + assert_almost_equal(image_unwrap_wrap_around[index_first], + image_unwrap_wrap_around[index_last]) + + +def test_wrap_around(): + for ndim in (2, 3): + for axis in range(ndim): + yield check_wrap_around, ndim, axis + + +def test_mask(): + length = 100 + ramps = [np.linspace(0, 4 * np.pi, length), + np.linspace(0, 8 * np.pi, length), + np.linspace(0, 6 * np.pi, length)] + image = np.vstack(ramps) + mask_1d = np.ones((length,), dtype=np.bool) + mask_1d[0] = mask_1d[-1] = False + for i in range(len(ramps)): + # mask all ramps but the i'th one + mask = np.zeros(image.shape, dtype=np.bool) + mask |= mask_1d.reshape(1, -1) + mask[i, :] = False # unmask i'th ramp + image_wrapped = np.ma.array(np.angle(np.exp(1j * image)), mask=mask) + image_unwrapped = unwrap_phase(image_wrapped) + image_unwrapped -= image_unwrapped[0, 0] # remove phase shift + # The end of the unwrapped array should have value equal to the + # endpoint of the unmasked ramp + assert_array_almost_equal(image_unwrapped[:, -1], image[i, -1]) + + # Same tests, but forcing use of the 3D unwrapper by reshaping + image_wrapped_3d = image_wrapped.reshape((1,) + image_wrapped.shape) + image_unwrapped_3d = unwrap_phase(image_wrapped_3d) + image_unwrapped_3d -= image_unwrapped_3d[0, 0, 0] # remove phase shift + assert_array_almost_equal(image_unwrapped_3d[:, :, -1], image[i, -1]) + + +if __name__ == "__main__": + run_module_suite() diff --git a/skimage/restoration/uft.py b/skimage/restoration/uft.py new file mode 100644 index 00000000..fedcc6f6 --- /dev/null +++ b/skimage/restoration/uft.py @@ -0,0 +1,445 @@ +# -*- coding: utf-8 -*- +# uft.py --- Unitary fourier transform + +"""Function of unitary fourier transform and utilities + +This module implement unitary fourier transform, that is ortho-normal +transform. They are especially and useful for convolution [1]: they +respect the Parseval equality, the value of the null frequency is +equal to + +.. math:: \frac{1}{\sqrt{n}} \sum_i x_i + +or the Fourier tranform have the same energy than the original image +(see ``image_quad_norm`` function). The transform is applied from the +last axes for performance reason (c order array). You may use directly +the numpy.fft module for more sophisticated purpose. + +References +---------- +.. [1] B. R. Hunt "A matrix theory proof of the discrete convolution + theorem", IEEE Trans. on Audio and Electroacoustics, + vol. au-19, no. 4, pp. 285-288, dec. 1971 + +""" + +from __future__ import division, print_function + +import numpy as np + +__keywords__ = "fft, Fourier Transform, orthonormal, unitary" + + +def ufftn(inarray, dim=None): + """N-dim unitary Fourier transform + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The ``dim`` last axis along wich to compute the transform. All + axes by default. + + Returns + ------- + outarray : ndarray (same shape than inarray) + The unitary N-D Fourier transform of ``inarray``. + + Examples + -------- + >>> input = np.ones((3, 3, 3)) + >>> output = ufftn(input) + >>> np.allclose(np.sum(input) / np.sqrt(input.size), output[0, 0, 0]) + True + >>> output.shape + (3, 3, 3) + """ + if dim is None: + dim = inarray.ndim + outarray = np.fft.fftn(inarray, axes=range(-dim, 0)) + return outarray / np.sqrt(np.prod(inarray.shape[-dim:])) + + +def uifftn(inarray, dim=None): + """N-dim unitary inverse Fourier transform + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The ``dim`` last axis along wich to compute the transform. All + axes by default. + + Returns + ------- + outarray : ndarray (same shape than inarray) + The unitary inverse N-D Fourier transform of ``inarray``. + + Examples + -------- + >>> input = np.ones((3, 3, 3)) + >>> output = uifftn(input) + >>> np.allclose(np.sum(input) / np.sqrt(input.size), output[0, 0, 0]) + True + >>> output.shape + (3, 3, 3) + """ + if dim is None: + dim = inarray.ndim + outarray = np.fft.ifftn(inarray, axes=range(-dim, 0)) + return outarray * np.sqrt(np.prod(inarray.shape[-dim:])) + + +def urfftn(inarray, dim=None): + """N-dim real unitary Fourier transform + + This transform consider the Hermitian property of the transform on + real input + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The ``dim`` last axis along wich to compute the transform. All + axes by default. + + Returns + ------- + outarray : ndarray (the last dim as N / 2 + 1 lenght) + The unitary N-D real Fourier transform of ``inarray``. + + Notes + ----- + The ``urfft`` functions assume an input array of real + values. Consequently, the output have an Hermitian property and + redondant values are not computed and returned. + + Examples + -------- + >>> input = np.ones((5, 5, 5)) + >>> output = urfftn(input) + >>> np.allclose(np.sum(input) / np.sqrt(input.size), output[0, 0, 0]) + True + >>> output.shape + (5, 5, 3) + """ + if dim is None: + dim = inarray.ndim + outarray = np.fft.rfftn(inarray, axes=range(-dim, 0)) + return outarray / np.sqrt(np.prod(inarray.shape[-dim:])) + + +def uirfftn(inarray, dim=None, shape=None): + """N-dim real unitary Fourier transform + + This transform consider the Hermitian property of the transform + from complex to real real input. + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The ``dim`` last axis along wich to compute the transform. All + axes by default. + shape : tuple of int + The shape of the output. The shape of ``rfft`` is ambiguous in + case of odd shape. In this case, the parameter must be + used. see ``np.fft.irfftn``. + + Returns + ------- + outarray : ndarray + The unitary N-D inverse real Fourier transform of ``inarray``. + + Notes + ----- + The ``uirfft`` function assume that output array is of real + values. Consequently, the input is assumed of having an Hermitian + property and redondant values are implicit. + + Examples + -------- + >>> input = np.ones((5, 5, 5)) + >>> output = uirfftn(urfftn(input), shape=input.shape) + >>> np.allclose(input, output) + True + >>> output.shape + (5, 5, 5) + """ + if dim is None: + dim = inarray.ndim + outarray = np.fft.irfftn(inarray, shape, axes=range(-dim, 0)) + return outarray * np.sqrt(np.prod(outarray.shape[-dim:])) + + +def ufft2(inarray): + """2-dim unitary Fourier transform + + Compute the Fourier transform on the last 2 axes. + + Parameters + ---------- + inarray : ndarray + The array to transform. + + Returns + ------- + outarray : ndarray (same shape than inarray) + The unitary 2-D Fourier transform of ``inarray``. + + See Also + -------- + uifft2, ufftn, urfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = ufft2(input) + >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), output[1, 0, 0]) + True + >>> output.shape + (10, 128, 128) + """ + return ufftn(inarray, 2) + + +def uifft2(inarray): + """2-dim inverse unitary Fourier transform + + Compute the inverse Fourier transform on the last 2 axes. + + Parameters + ---------- + inarray : ndarray + The array to transform. + + Returns + ------- + outarray : ndarray (same shape than inarray) + The unitary 2-D inverse Fourier transform of ``inarray``. + + See Also + -------- + uifft2, uifftn, uirfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = uifft2(input) + >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), output[0, 0, 0]) + True + >>> output.shape + (10, 128, 128) + """ + return uifftn(inarray, 2) + + +def urfft2(inarray): + """2-dim real unitary Fourier transform + + Compute the real Fourier transform on the last 2 axes. This + transform consider the Hermitian property of the transform from + complex to real real input. + + Parameters + ---------- + inarray : ndarray + The array to transform. + + Returns + ------- + outarray : ndarray (the last dim as (N - 1) *2 lenght) + The unitary 2-D real Fourier transform of ``inarray``. + + See Also + -------- + ufft2, ufftn, urfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = urfft2(input) + >>> np.allclose(np.sum(input[1,...]) / np.sqrt(input[1,...].size), output[1, 0, 0]) + True + >>> output.shape + (10, 128, 65) + """ + return urfftn(inarray, 2) + + +def uirfft2(inarray, shape=None): + """2-dim real unitary Fourier transform + + Compute the real inverse Fourier transform on the last 2 axes. + This transform consider the Hermitian property of the transform + from complex to real real input. + + Parameters + ---------- + inarray : ndarray + The array to transform. + + Returns + ------- + outarray : ndarray (the last dim as (N - 1) *2 lenght) + The unitary 2-D inverse real Fourier transform of ``inarray``. + + See Also + -------- + urfft2, uifftn, uirfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = uirfftn(urfftn(input), shape=input.shape) + >>> np.allclose(input, output) + True + >>> output.shape + (10, 128, 128) + """ + return uirfftn(inarray, 2, shape=shape) + + +def image_quad_norm(inarray): + """Return quadratic norm of images in Fourier space + + This function detect if the image suppose the hermitian property. + + Parameters + ---------- + inarray : ndarray + The images are supposed to be in the last two axes + + Returns + ------- + norm : float + The quadratic norm of ``inarray``. + + Examples + -------- + >>> input = np.ones((5, 5)) + >>> image_quad_norm(ufft2(input)) == np.sum(np.abs(input)**2) + True + >>> image_quad_norm(ufft2(input)) == image_quad_norm(urfft2(input)) + True + """ + # If there is an hermitian symmetry + if inarray.shape[-1] != inarray.shape[-2]: + return 2 * np.sum(np.sum(np.abs(inarray)**2, axis=-1), axis=-1) - \ + np.sum(np.abs(inarray[..., 0])**2, axis=-1) + else: + return np.sum(np.sum(np.abs(inarray)**2, axis=-1), axis=-1) + + +def ir2tf(imp_resp, shape, dim=None, is_real=True): + """Compute the transfer function of IR + + This function make the necessary correct zero-padding, zero + convention, correct fft2 etc... to compute the transfer function + of IR. To use with unitary Fourier transform for the signal (ufftn + or equivalent). + + Parameters + ---------- + imp_resp : ndarray + The impulsionnal responses. + shape : tuple of int + A tuple of integer corresponding to the target shape of the + tranfert function. + dim : int, optional + The ``dim`` last axis along wich to compute the transform. All + axes by default. + is_real : boolean (optionnal, default True) + If True, imp_resp is supposed real and the hermissian property + is used with rfftn Fourier transform. + + Returns + ------- + y : complex ndarray + The tranfert function of shape ``shape``. + + See Also + -------- + ufftn, uifftn, urfftn, uirfftn + + Examples + -------- + >>> np.all(np.array([[4, 0], [0, 0]]) == ir2tf(np.ones((2, 2)), (2, 2))) + True + >>> ir2tf(np.ones((2, 2)), (512, 512)).shape == (512, 257) + True + >>> ir2tf(np.ones((2, 2)), (512, 512), is_real=False).shape == (512, 512) + True + + Notes + ----- + The input array can be composed of multiple dimentionnal IR with + an arbitraru number of IR. The individual IR must be accesed + through first axes. The last ``dim`` axes of space definition. The + ``dim`` parameter must be specified to compute the transform only + along these last axes. + """ + if not dim: + dim = imp_resp.ndim + # Zero padding and fill + irpadded = np.zeros(shape) + irpadded[tuple([slice(0, s) for s in imp_resp.shape])] = imp_resp + # Roll for zero convention of the fft to avoid the phase + # problem. Work with odd and even size. + for axis, axis_size in enumerate(imp_resp.shape): + if axis >= imp_resp.ndim - dim: + irpadded = np.roll(irpadded, + shift=-int(np.floor(axis_size / 2)), + axis=axis) + if is_real: + return np.fft.rfftn(irpadded, axes=range(-dim, 0)) + else: + return np.fft.fftn(irpadded, axes=range(-dim, 0)) + + +def laplacian(ndim, shape, is_real=True): + """Return the transfer function of the Laplacian + + Laplacian is the second order difference, on line and column. + + Parameters + ---------- + ndim : int + The dimension of the Laplacian + shape : tuple, shape + The support on which to compute the transfer function + is_real : boolean (optionnal, default True) + If True, imp_resp is supposed real and the hermissian property + is used with rfftn Fourier transform to return the transfer + function. + + Returns + ------- + tf : array_like, complex + The transfer function + impr : array_like, real + The Laplacian + + Examples + -------- + >>> tf, ir = laplacian(2, (32, 32)) + >>> np.all(ir == np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]])) + True + >>> np.all(tf == ir2tf(ir, (32, 32))) + True + """ + impr = np.zeros([3] * ndim) + for dim in range(ndim): + idx = tuple([slice(1, 2)] * dim + + [slice(None)] + + [slice(1, 2)] * (ndim - dim - 1)) + impr[idx] = np.array([-1.0, + 0.0, + -1.0]).reshape([-1 if i == dim else 1 + for i in range(ndim)]) + impr[([slice(1, 2)] * ndim)] = 2.0 * ndim + return ir2tf(impr, shape, is_real=is_real), impr diff --git a/skimage/restoration/unwrap.py b/skimage/restoration/unwrap.py new file mode 100644 index 00000000..085bef0b --- /dev/null +++ b/skimage/restoration/unwrap.py @@ -0,0 +1,104 @@ +import numpy as np +import warnings +from six import string_types + +from ._unwrap_1d import unwrap_1d +from ._unwrap_2d import unwrap_2d +from ._unwrap_3d import unwrap_3d + + +def unwrap_phase(image, wrap_around=False): + '''From ``image``, wrapped to lie in the interval [-pi, pi), recover the + original, unwrapped image. + + Parameters + ---------- + image : 1D, 2D or 3D ndarray of floats, optionally a masked array + The values should be in the range ``[-pi, pi)``. If a masked array is + provided, the masked entries will not be changed, and their values + will not be used to guide the unwrapping of neighboring, unmasked + values. Masked 1D arrays are not allowed, and will raise a + ``ValueError``. + wrap_around : bool or sequence of bool + When an element of the sequence is ``True``, the unwrapping process + will regard the edges along the corresponding axis of the image to be + connected and use this connectivity to guide the phase unwrapping + process. If only a single boolean is given, it will apply to all axes. + Wrap around is not supported for 1D arrays. + + Returns + ------- + image_unwrapped : array_like, float32 + Unwrapped image of the same shape as the input. If the input ``image`` + was a masked array, the mask will be preserved. + + Raises + ------ + ValueError + If called with a masked 1D array or called with a 1D array and + ``wrap_around=True``. + + Examples + -------- + >>> c0, c1 = np.ogrid[-1:1:128j, -1:1:128j] + >>> image = 12 * np.pi * np.exp(-(c0**2 + c1**2)) + >>> image_wrapped = np.angle(np.exp(1j * image)) + >>> image_unwrapped = unwrap_phase(image_wrapped) + >>> np.std(image_unwrapped - image) < 1e-6 # A constant offset is normal + True + + References + ---------- + .. [1] Miguel Arevallilo Herraez, David R. Burton, Michael J. Lalor, + and Munther A. Gdeisat, "Fast two-dimensional phase-unwrapping + algorithm based on sorting by reliability following a noncontinuous + path", Journal Applied Optics, Vol. 41, No. 35 (2002) 7437, + .. [2] Abdul-Rahman, H., Gdeisat, M., Burton, D., & Lalor, M., "Fast + three-dimensional phase-unwrapping algorithm based on sorting by + reliability following a non-continuous path. In W. Osten, + C. Gorecki, & E. L. Novak (Eds.), Optical Metrology (2005) 32--40, + International Society for Optics and Photonics. + ''' + if image.ndim not in (1, 2, 3): + raise ValueError('image must be 1, 2 or 3 dimensional') + if isinstance(wrap_around, bool): + wrap_around = [wrap_around] * image.ndim + elif (hasattr(wrap_around, '__getitem__') + and not isinstance(wrap_around, string_types)): + if len(wrap_around) != image.ndim: + raise ValueError('Length of wrap_around must equal the ' + 'dimensionality of image') + wrap_around = [bool(wa) for wa in wrap_around] + else: + raise ValueError('wrap_around must be a bool or a sequence with ' + 'length equal to the dimensionality of image') + if image.ndim == 1: + if np.ma.isMaskedArray(image): + raise ValueError('1D masked images cannot be unwrapped') + if wrap_around[0]: + raise ValueError('wrap_around is not supported for 1D images') + if image.ndim in (2, 3) and 1 in image.shape: + warnings.warn('image has a length 1 dimension; consider using an ' + 'array of lower dimensionality to use a more efficient ' + 'algorithm') + + if np.ma.isMaskedArray(image): + mask = np.require(image.mask, np.uint8, ['C']) + else: + mask = np.zeros_like(image, dtype=np.uint8, order='C') + image_not_masked = np.asarray(image, dtype=np.float64, order='C') + image_unwrapped = np.empty_like(image, dtype=np.float64, order='C') + + if image.ndim == 1: + unwrap_1d(image_not_masked, image_unwrapped) + elif image.ndim == 2: + unwrap_2d(image_not_masked, mask, image_unwrapped, + wrap_around) + elif image.ndim == 3: + unwrap_3d(image_not_masked, mask, image_unwrapped, + wrap_around) + + if np.ma.isMaskedArray(image): + return np.ma.array(image_unwrapped, mask=mask) + else: + return image_unwrapped diff --git a/skimage/restoration/unwrap_2d_ljmu.c b/skimage/restoration/unwrap_2d_ljmu.c new file mode 100644 index 00000000..9b5c53b8 --- /dev/null +++ b/skimage/restoration/unwrap_2d_ljmu.c @@ -0,0 +1,729 @@ +// 2D phase unwrapping, modified for inclusion in scipy by Gregor Thalhammer +// Original file name: Miguel_2D_unwrapper_with_mask_and_wrap_around_option.c + +//This program was written by Munther Gdeisat and Miguel Arevallilo Herraez to program the two-dimensional unwrapper +//entitled "Fast two-dimensional phase-unwrapping algorithm based on sorting by +//reliability following a noncontinuous path" +//by Miguel Arevallilo Herraez, David R. Burton, Michael J. Lalor, and Munther A. Gdeisat +//published in the Journal Applied Optics, Vol. 41, No. 35, pp. 7437, 2002. +//This program was written by Munther Gdeisat, Liverpool John Moores University, United Kingdom. +//Date 26th August 2007 +//The wrapped phase map is assumed to be of floating point data type. The resultant unwrapped phase map is also of floating point type. +//The mask is of byte data type. +//When the mask is 255 this means that the pixel is valid +//When the mask is 0 this means that the pixel is invalid (noisy or corrupted pixel) +//This program takes into consideration the image wrap around problem encountered in MRI imaging. + +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif + +#define PI M_PI +#define TWOPI (2 * M_PI) + +//TODO: remove global variables +//TODO: make thresholds independent + +#define NOMASK 0 +#define MASK 1 + +typedef struct +{ + double mod; + int x_connectivity; + int y_connectivity; + int no_of_edges; +} params_t; + +//PIXELM information +struct PIXELM +{ + int increment; //No. of 2*pi to add to the pixel to unwrap it + int number_of_pixels_in_group;//No. of pixel in the pixel group + double value; //value of the pixel + double reliability; + unsigned char input_mask; //0 pixel is masked. NOMASK pixel is not masked + unsigned char extended_mask; //0 pixel is masked. NOMASK pixel is not masked + int group; //group No. + int new_group; + struct PIXELM *head; //pointer to the first pixel in the group in the linked list + struct PIXELM *last; //pointer to the last pixel in the group + struct PIXELM *next; //pointer to the next pixel in the group +}; + +typedef struct PIXELM PIXELM; + +//the EDGE is the line that connects two pixels. +//if we have S pixels, then we have S horizontal edges and S vertical edges +struct EDGE +{ + double reliab; //reliabilty of the edge and it depends on the two pixels + PIXELM *pointer_1; //pointer to the first pixel + PIXELM *pointer_2; //pointer to the second pixel + int increment; //No. of 2*pi to add to one of the pixels to + //unwrap it with respect to the second +}; + +typedef struct EDGE EDGE; + +//---------------start quicker_sort algorithm -------------------------------- +#define swap(x,y) {EDGE t; t=x; x=y; y=t;} +#define order(x,y) if (x.reliab > y.reliab) swap(x,y) +#define o2(x,y) order(x,y) +#define o3(x,y,z) o2(x,y); o2(x,z); o2(y,z) + +typedef enum {yes, no} yes_no; + +yes_no find_pivot(EDGE *left, EDGE *right, double *pivot_ptr) +{ + EDGE a, b, c, *p; + + a = *left; + b = *(left + (right - left) / 2); + c = *right; + o3(a, b, c); + + if (a.reliab < b.reliab) + { + *pivot_ptr = b.reliab; + return yes; + } + + if (b.reliab < c.reliab) + { + *pivot_ptr = c.reliab; + return yes; + } + + for (p = left + 1; p <= right; ++p) + { + if (p->reliab != left->reliab) + { + *pivot_ptr = (p->reliab < left->reliab) ? left->reliab : p->reliab; + return yes; + } + } + return no; +} + +EDGE *partition(EDGE *left, EDGE *right, double pivot) +{ + while (left <= right) + { + while (left->reliab < pivot) + ++left; + while (right->reliab >= pivot) + --right; + if (left < right) + { + swap (*left, *right); + ++left; + --right; + } + } + return left; +} + +void quicker_sort(EDGE *left, EDGE *right) +{ + EDGE *p; + double pivot; + + if (find_pivot(left, right, &pivot) == yes) + { + p = partition(left, right, pivot); + quicker_sort(left, p - 1); + quicker_sort(p, right); + } +} +//--------------end quicker_sort algorithm ----------------------------------- + +//--------------------start initialize pixels ---------------------------------- +//initialize pixels. See the explination of the pixel class above. +//initially every pixel is assumed to belong to a group consisting of only itself +void initialisePIXELs(double *wrapped_image, unsigned char *input_mask, unsigned char *extended_mask, PIXELM *pixel, int image_width, int image_height) +{ + PIXELM *pixel_pointer = pixel; + double *wrapped_image_pointer = wrapped_image; + unsigned char *input_mask_pointer = input_mask; + unsigned char *extended_mask_pointer = extended_mask; + int i, j; + + for (i=0; i < image_height; i++) + { + for (j=0; j < image_width; j++) + { + pixel_pointer->increment = 0; + pixel_pointer->number_of_pixels_in_group = 1; + pixel_pointer->value = *wrapped_image_pointer; + pixel_pointer->reliability = 9999999. + rand(); + pixel_pointer->input_mask = *input_mask_pointer; + pixel_pointer->extended_mask = *extended_mask_pointer; + pixel_pointer->head = pixel_pointer; + pixel_pointer->last = pixel_pointer; + pixel_pointer->next = NULL; + pixel_pointer->new_group = 0; + pixel_pointer->group = -1; + pixel_pointer++; + wrapped_image_pointer++; + input_mask_pointer++; + extended_mask_pointer++; + } + } +} +//-------------------end initialize pixels ----------- + +//gamma function in the paper +double wrap(double pixel_value) +{ + double wrapped_pixel_value; + if (pixel_value > PI) wrapped_pixel_value = pixel_value - TWOPI; + else if (pixel_value < -PI) wrapped_pixel_value = pixel_value + TWOPI; + else wrapped_pixel_value = pixel_value; + return wrapped_pixel_value; +} + +// pixelL_value is the left pixel, pixelR_value is the right pixel +int find_wrap(double pixelL_value, double pixelR_value) +{ + double difference; + int wrap_value; + difference = pixelL_value - pixelR_value; + + if (difference > PI) wrap_value = -1; + else if (difference < -PI) wrap_value = 1; + else wrap_value = 0; + + return wrap_value; +} + +void extend_mask(unsigned char *input_mask, unsigned char *extended_mask, + int image_width, int image_height, + params_t *params) +{ + int i,j; + int image_width_plus_one = image_width + 1; + int image_width_minus_one = image_width - 1; + unsigned char *IMP = input_mask + image_width + 1; //input mask pointer + unsigned char *EMP = extended_mask + image_width + 1; //extended mask pointer + + //extend the mask for the image except borders + for (i=1; i < image_height - 1; ++i) + { + for (j=1; j < image_width - 1; ++j) + { + if ( (*IMP) == NOMASK && (*(IMP + 1) == NOMASK) && (*(IMP - 1) == NOMASK) && + (*(IMP + image_width) == NOMASK) && (*(IMP - image_width) == NOMASK) && + (*(IMP - image_width_minus_one) == NOMASK) && (*(IMP - image_width_plus_one) == NOMASK) && + (*(IMP + image_width_minus_one) == NOMASK) && (*(IMP + image_width_plus_one) == NOMASK) ) + { + *EMP = NOMASK; + } + ++EMP; + ++IMP; + } + EMP += 2; + IMP += 2; + } + + if (params->x_connectivity == 1) + { + //extend the mask for the right border of the image + IMP = input_mask + 2 * image_width - 1; + EMP = extended_mask + 2 * image_width -1; + for (i=1; i < image_height - 1; ++ i) + { + if ( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + image_width) == NOMASK) && (*(IMP - image_width) == NOMASK) && + (*(IMP - image_width - 1) == NOMASK) && (*(IMP - image_width + 1) == NOMASK) && + (*(IMP + image_width - 1) == NOMASK) && (*(IMP - 2 * image_width + 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP += image_width; + IMP += image_width; + } + + //extend the mask for the left border of the image + IMP = input_mask + image_width; + EMP = extended_mask + image_width; + for (i=1; i < image_height - 1; ++i) + { + if ( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + image_width) == NOMASK) && (*(IMP - image_width) == NOMASK) && + (*(IMP - image_width + 1) == NOMASK) && (*(IMP + image_width + 1) == NOMASK) && + (*(IMP + image_width - 1) == NOMASK) && (*(IMP + 2 * image_width - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP += image_width; + IMP += image_width; + } + } + + if (params->y_connectivity == 1) + { + //extend the mask for the top border of the image + IMP = input_mask + 1; + EMP = extended_mask + 1; + for (i=1; i < image_width - 1; ++i) + { + if ( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + image_width) == NOMASK) && (*(IMP + image_width * (image_height - 1)) == NOMASK) && + (*(IMP + image_width + 1) == NOMASK) && (*(IMP + image_width - 1) == NOMASK) && + (*(IMP + image_width * (image_height - 1) - 1) == NOMASK) && (*(IMP + image_width * (image_height - 1) + 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + + //extend the mask for the bottom border of the image + IMP = input_mask + image_width * (image_height - 1) + 1; + EMP = extended_mask + image_width * (image_height - 1) + 1; + for (i=1; i < image_width - 1; ++i) + { + if ( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP - image_width) == NOMASK) && (*(IMP - image_width - 1) == NOMASK) && (*(IMP - image_width + 1) == NOMASK) && + (*(IMP - image_width * (image_height - 1) ) == NOMASK) && + (*(IMP - image_width * (image_height - 1) - 1) == NOMASK) && + (*(IMP - image_width * (image_height - 1) + 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + } +} + +void calculate_reliability(double *wrappedImage, PIXELM *pixel, + int image_width, int image_height, + params_t *params) +{ + int image_width_plus_one = image_width + 1; + int image_width_minus_one = image_width - 1; + PIXELM *pixel_pointer = pixel + image_width_plus_one; + double *WIP = wrappedImage + image_width_plus_one; //WIP is the wrapped image pointer + double H, V, D1, D2; + int i, j; + + for (i = 1; i < image_height -1; ++i) + { + for (j = 1; j < image_width - 1; ++j) + { + if (pixel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WIP - 1) - *WIP) - wrap(*WIP - *(WIP + 1)); + V = wrap(*(WIP - image_width) - *WIP) - wrap(*WIP - *(WIP + image_width)); + D1 = wrap(*(WIP - image_width_plus_one) - *WIP) - wrap(*WIP - *(WIP + image_width_plus_one)); + D2 = wrap(*(WIP - image_width_minus_one) - *WIP) - wrap(*WIP - *(WIP + image_width_minus_one)); + pixel_pointer->reliability = H*H + V*V + D1*D1 + D2*D2; + } + pixel_pointer++; + WIP++; + } + pixel_pointer += 2; + WIP += 2; + } + + if (params->x_connectivity == 1) + { + //calculating the reliability for the left border of the image + PIXELM *pixel_pointer = pixel + image_width; + double *WIP = wrappedImage + image_width; + + for (i = 1; i < image_height - 1; ++i) + { + if (pixel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WIP + image_width - 1) - *WIP) - wrap(*WIP - *(WIP + 1)); + V = wrap(*(WIP - image_width) - *WIP) - wrap(*WIP - *(WIP + image_width)); + D1 = wrap(*(WIP - 1) - *WIP) - wrap(*WIP - *(WIP + image_width_plus_one)); + D2 = wrap(*(WIP - image_width_minus_one) - *WIP) - wrap(*WIP - *(WIP + 2* image_width - 1)); + pixel_pointer->reliability = H*H + V*V + D1*D1 + D2*D2; + } + pixel_pointer += image_width; + WIP += image_width; + } + + //calculating the reliability for the right border of the image + pixel_pointer = pixel + 2 * image_width - 1; + WIP = wrappedImage + 2 * image_width - 1; + + for (i = 1; i < image_height - 1; ++i) + { + if (pixel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WIP - 1) - *WIP) - wrap(*WIP - *(WIP - image_width_minus_one)); + V = wrap(*(WIP - image_width) - *WIP) - wrap(*WIP - *(WIP + image_width)); + D1 = wrap(*(WIP - image_width_plus_one) - *WIP) - wrap(*WIP - *(WIP + 1)); + D2 = wrap(*(WIP - 2 * image_width - 1) - *WIP) - wrap(*WIP - *(WIP + image_width_minus_one)); + pixel_pointer->reliability = H*H + V*V + D1*D1 + D2*D2; + } + pixel_pointer += image_width; + WIP += image_width; + } + } + + if (params->y_connectivity == 1) + { + //calculating the reliability for the top border of the image + PIXELM *pixel_pointer = pixel + 1; + double *WIP = wrappedImage + 1; + + for (i = 1; i < image_width - 1; ++i) + { + if (pixel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WIP - 1) - *WIP) - wrap(*WIP - *(WIP + 1)); + V = wrap(*(WIP + image_width*(image_height - 1)) - *WIP) - wrap(*WIP - *(WIP + image_width)); + D1 = wrap(*(WIP + image_width*(image_height - 1) - 1) - *WIP) - wrap(*WIP - *(WIP + image_width_plus_one)); + D2 = wrap(*(WIP + image_width*(image_height - 1) + 1) - *WIP) - wrap(*WIP - *(WIP + image_width_minus_one)); + pixel_pointer->reliability = H*H + V*V + D1*D1 + D2*D2; + } + pixel_pointer++; + WIP++; + } + + //calculating the reliability for the bottom border of the image + pixel_pointer = pixel + (image_height - 1) * image_width + 1; + WIP = wrappedImage + (image_height - 1) * image_width + 1; + + for (i = 1; i < image_width - 1; ++i) + { + if (pixel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WIP - 1) - *WIP) - wrap(*WIP - *(WIP + 1)); + V = wrap(*(WIP - image_width) - *WIP) - wrap(*WIP - *(WIP -(image_height - 1) * (image_width))); + D1 = wrap(*(WIP - image_width_plus_one) - *WIP) - wrap(*WIP - *(WIP - (image_height - 1) * (image_width) + 1)); + D2 = wrap(*(WIP - image_width_minus_one) - *WIP) - wrap(*WIP - *(WIP - (image_height - 1) * (image_width) - 1)); + pixel_pointer->reliability = H*H + V*V + D1*D1 + D2*D2; + } + pixel_pointer++; + WIP++; + } + } +} + +//calculate the reliability of the horizontal edges of the image +//it is calculated by adding the reliability of pixel and the relibility of +//its right-hand neighbour +//edge is calculated between a pixel and its next neighbour +void horizontalEDGEs(PIXELM *pixel, EDGE *edge, + int image_width, int image_height, + params_t *params) +{ + int i, j; + EDGE *edge_pointer = edge; + PIXELM *pixel_pointer = pixel; + int no_of_edges = params->no_of_edges; + + for (i = 0; i < image_height; i++) + { + for (j = 0; j < image_width - 1; j++) + { + if (pixel_pointer->input_mask == NOMASK && (pixel_pointer + 1)->input_mask == NOMASK) + { + edge_pointer->pointer_1 = pixel_pointer; + edge_pointer->pointer_2 = (pixel_pointer+1); + edge_pointer->reliab = pixel_pointer->reliability + (pixel_pointer + 1)->reliability; + edge_pointer->increment = find_wrap(pixel_pointer->value, (pixel_pointer + 1)->value); + edge_pointer++; + no_of_edges++; + } + pixel_pointer++; + } + pixel_pointer++; + } + //construct edges at the right border of the image + if (params->x_connectivity == 1) + { + pixel_pointer = pixel + image_width - 1; + for (i = 0; i < image_height; i++) + { + if (pixel_pointer->input_mask == NOMASK && (pixel_pointer - image_width + 1)->input_mask == NOMASK) + { + edge_pointer->pointer_1 = pixel_pointer; + edge_pointer->pointer_2 = (pixel_pointer - image_width + 1); + edge_pointer->reliab = pixel_pointer->reliability + (pixel_pointer - image_width + 1)->reliability; + edge_pointer->increment = find_wrap(pixel_pointer->value, (pixel_pointer - image_width + 1)->value); + edge_pointer++; + no_of_edges++; + } + pixel_pointer+=image_width; + } + } + params->no_of_edges = no_of_edges; +} + +//calculate the reliability of the vertical edges of the image +//it is calculated by adding the reliability of pixel and the relibility of +//its lower neighbour in the image. +void verticalEDGEs(PIXELM *pixel, EDGE *edge, + int image_width, int image_height, + params_t *params) +{ + int i, j; + int no_of_edges = params->no_of_edges; + PIXELM *pixel_pointer = pixel; + EDGE *edge_pointer = edge + no_of_edges; + + for (i=0; i < image_height - 1; i++) + { + for (j=0; j < image_width; j++) + { + if (pixel_pointer->input_mask == NOMASK && (pixel_pointer + image_width)->input_mask == NOMASK) + { + edge_pointer->pointer_1 = pixel_pointer; + edge_pointer->pointer_2 = (pixel_pointer + image_width); + edge_pointer->reliab = pixel_pointer->reliability + (pixel_pointer + image_width)->reliability; + edge_pointer->increment = find_wrap(pixel_pointer->value, (pixel_pointer + image_width)->value); + edge_pointer++; + no_of_edges++; + } + pixel_pointer++; + } //j loop + } // i loop + + //construct edges that connect at the bottom border of the image + if (params->y_connectivity == 1) + { + pixel_pointer = pixel + image_width *(image_height - 1); + for (i = 0; i < image_width; i++) + { + if (pixel_pointer->input_mask == NOMASK && (pixel_pointer - image_width *(image_height - 1))->input_mask == NOMASK) + { + edge_pointer->pointer_1 = pixel_pointer; + edge_pointer->pointer_2 = (pixel_pointer - image_width *(image_height - 1)); + edge_pointer->reliab = pixel_pointer->reliability + (pixel_pointer - image_width *(image_height - 1))->reliability; + edge_pointer->increment = find_wrap(pixel_pointer->value, (pixel_pointer - image_width *(image_height - 1))->value); + edge_pointer++; + no_of_edges++; + } + pixel_pointer++; + } + } + params->no_of_edges = no_of_edges; +} + +//gather the pixels of the image into groups +void gatherPIXELs(EDGE *edge, params_t *params) +{ + int k; + PIXELM *PIXEL1; + PIXELM *PIXEL2; + PIXELM *group1; + PIXELM *group2; + EDGE *pointer_edge = edge; + int incremento; + + for (k = 0; k < params->no_of_edges; k++) + { + PIXEL1 = pointer_edge->pointer_1; + PIXEL2 = pointer_edge->pointer_2; + + //PIXELM 1 and PIXELM 2 belong to different groups + //initially each pixel is a group by it self and one pixel can construct a group + //no else or else if to this if + if (PIXEL2->head != PIXEL1->head) + { + //PIXELM 2 is alone in its group + //merge this pixel with PIXELM 1 group and find the number of 2 pi to add + //to or subtract to unwrap it + if ((PIXEL2->next == NULL) && (PIXEL2->head == PIXEL2)) + { + PIXEL1->head->last->next = PIXEL2; + PIXEL1->head->last = PIXEL2; + (PIXEL1->head->number_of_pixels_in_group)++; + PIXEL2->head=PIXEL1->head; + PIXEL2->increment = PIXEL1->increment-pointer_edge->increment; + } + + //PIXELM 1 is alone in its group + //merge this pixel with PIXELM 2 group and find the number of 2 pi to add + //to or subtract to unwrap it + else if ((PIXEL1->next == NULL) && (PIXEL1->head == PIXEL1)) + { + PIXEL2->head->last->next = PIXEL1; + PIXEL2->head->last = PIXEL1; + (PIXEL2->head->number_of_pixels_in_group)++; + PIXEL1->head = PIXEL2->head; + PIXEL1->increment = PIXEL2->increment+pointer_edge->increment; + } + + //PIXELM 1 and PIXELM 2 both have groups + else + { + group1 = PIXEL1->head; + group2 = PIXEL2->head; + //if the no. of pixels in PIXELM 1 group is larger than the + //no. of pixels in PIXELM 2 group. Merge PIXELM 2 group to + //PIXELM 1 group and find the number of wraps between PIXELM 2 + //group and PIXELM 1 group to unwrap PIXELM 2 group with respect + //to PIXELM 1 group. the no. of wraps will be added to PIXELM 2 + //group in the future + if (group1->number_of_pixels_in_group > group2->number_of_pixels_in_group) + { + //merge PIXELM 2 with PIXELM 1 group + group1->last->next = group2; + group1->last = group2->last; + group1->number_of_pixels_in_group = group1->number_of_pixels_in_group + group2->number_of_pixels_in_group; + incremento = PIXEL1->increment-pointer_edge->increment - PIXEL2->increment; + //merge the other pixels in PIXELM 2 group to PIXELM 1 group + while (group2 != NULL) + { + group2->head = group1; + group2->increment += incremento; + group2 = group2->next; + } + } + + //if the no. of pixels in PIXELM 2 group is larger than the + //no. of pixels in PIXELM 1 group. Merge PIXELM 1 group to + //PIXELM 2 group and find the number of wraps between PIXELM 2 + //group and PIXELM 1 group to unwrap PIXELM 1 group with respect + //to PIXELM 2 group. the no. of wraps will be added to PIXELM 1 + //group in the future + else + { + //merge PIXELM 1 with PIXELM 2 group + group2->last->next = group1; + group2->last = group1->last; + group2->number_of_pixels_in_group = group2->number_of_pixels_in_group + group1->number_of_pixels_in_group; + incremento = PIXEL2->increment + pointer_edge->increment - PIXEL1->increment; + //merge the other pixels in PIXELM 2 group to PIXELM 1 group + while (group1 != NULL) + { + group1->head = group2; + group1->increment += incremento; + group1 = group1->next; + } // while + + } // else + } //else + } //if + pointer_edge++; + } +} + +//unwrap the image +void unwrapImage(PIXELM *pixel, int image_width, int image_height) +{ + int i; + int image_size = image_width * image_height; + PIXELM *pixel_pointer=pixel; + + for (i = 0; i < image_size; i++) + { + pixel_pointer->value += TWOPI * (double)(pixel_pointer->increment); + pixel_pointer++; + } +} + +//set the masked pixels (mask = 0) to the minimum of the unwrapper phase +void maskImage(PIXELM *pixel, unsigned char *input_mask, int image_width, int image_height) +{ + int image_width_plus_one = image_width + 1; + int image_height_plus_one = image_height + 1; + int image_width_minus_one = image_width - 1; + int image_height_minus_one = image_height - 1; + + PIXELM *pointer_pixel = pixel; + unsigned char *IMP = input_mask; //input mask pointer + double min=99999999; + int i; + int image_size = image_width * image_height; + + //find the minimum of the unwrapped phase + for (i = 0; i < image_size; i++) + { + if ((pointer_pixel->value < min) && (*IMP == NOMASK)) + min = pointer_pixel->value; + + pointer_pixel++; + IMP++; + } + + pointer_pixel = pixel; + IMP = input_mask; + + //set the masked pixels to minimum + for (i = 0; i < image_size; i++) + { + if ((*IMP) == MASK) + { + pointer_pixel->value = min; + } + pointer_pixel++; + IMP++; + } +} + +//the input to this unwrapper is an array that contains the wrapped +//phase map. copy the image on the buffer passed to this unwrapper to +//over-write the unwrapped phase map on the buffer of the wrapped +//phase map. +void returnImage(PIXELM *pixel, double *unwrapped_image, int image_width, int image_height) +{ + int i; + int image_size = image_width * image_height; + double *unwrapped_image_pointer = unwrapped_image; + PIXELM *pixel_pointer = pixel; + + for (i=0; i < image_size; i++) + { + *unwrapped_image_pointer = pixel_pointer->value; + pixel_pointer++; + unwrapped_image_pointer++; + } +} + +//the main function of the unwrapper +void +unwrap2D(double* wrapped_image, double* UnwrappedImage, unsigned char* input_mask, + int image_width, int image_height, + int wrap_around_x, int wrap_around_y) +{ + params_t params = {TWOPI, wrap_around_x, wrap_around_y, 0}; + unsigned char *extended_mask; + PIXELM *pixel; + EDGE *edge; + int image_size = image_height * image_width; + int No_of_Edges_initially = 2 * image_width * image_height; + + extended_mask = (unsigned char *) calloc(image_size, sizeof(unsigned char)); + pixel = (PIXELM *) calloc(image_size, sizeof(PIXELM)); + edge = (EDGE *) calloc(No_of_Edges_initially, sizeof(EDGE)); + + extend_mask(input_mask, extended_mask, image_width, image_height, ¶ms); + initialisePIXELs(wrapped_image, input_mask, extended_mask, pixel, image_width, image_height); + calculate_reliability(wrapped_image, pixel, image_width, image_height, ¶ms); + horizontalEDGEs(pixel, edge, image_width, image_height, ¶ms); + verticalEDGEs(pixel, edge, image_width, image_height, ¶ms); + + //sort the EDGEs depending on their reiability. The PIXELs with higher + //relibility (small value) first + quicker_sort(edge, edge + params.no_of_edges - 1); + + //gather PIXELs into groups + gatherPIXELs(edge, ¶ms); + + unwrapImage(pixel, image_width, image_height); + maskImage(pixel, input_mask, image_width, image_height); + + //copy the image from PIXELM structure to the unwrapped phase array + //passed to this function + //TODO: replace by (cython?) function to directly write into numpy array ? + returnImage(pixel, UnwrappedImage, image_width, image_height); + + free(edge); + free(pixel); + free(extended_mask); +} diff --git a/skimage/restoration/unwrap_3d_ljmu.c b/skimage/restoration/unwrap_3d_ljmu.c new file mode 100644 index 00000000..88012f18 --- /dev/null +++ b/skimage/restoration/unwrap_3d_ljmu.c @@ -0,0 +1,1058 @@ +// 3D phase unwrapping, modified for inclusion in scipy by Gregor Thalhammer +// Original file name: Hussein_3D_unwrapper_with_mask_and_wrap_around_option.c + +//This program was written by Hussein Abdul-Rahman and Munther Gdeisat to program the three-dimensional phase unwrapper +//entitled "Fast three-dimensional phase-unwrapping algorithm based on sorting by +//reliability following a noncontinuous path" +//by Hussein Abdul-Rahman, Munther A. Gdeisat, David R. Burton, and Michael J. Lalor, +//published in the Proceedings of SPIE - +//The International Society for Optical Engineering, Vol. 5856, No. 1, 2005, pp. 32-40 +//This program was written by Munther Gdeisat, Liverpool John Moores University, United Kingdom. +//Date 31st August 2007 +//The wrapped phase volume is assumed to be of floating point data type. The resultant unwrapped phase volume is also of floating point type. +//Read the data from the file frame by frame +//The mask is of byte data type. +//When the mask is 255 this means that the voxel is valid +//When the mask is 0 this means that the voxel is invalid (noisy or corrupted voxel) +//This program takes into consideration the image wrap around problem encountered in MRI imaging. + +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif + +#define PI M_PI +#define TWOPI (2 * M_PI) + +#define NOMASK 0 +#define MASK 1 + +typedef struct +{ + double mod; + int x_connectivity; + int y_connectivity; + int z_connectivity; + int no_of_edges; +} params_t; + +//VOXELM information +struct VOXELM +{ + int increment; //No. of 2*pi to add to the voxel to unwrap it + int number_of_voxels_in_group;//No. of voxel in the voxel group + double value; //value of the voxel + double reliability; + unsigned char input_mask; //MASK voxel is masked. NOMASK voxel is not masked + unsigned char extended_mask; //MASK voxel is masked. NOMASK voxel is not masked + int group; //group No. + int new_group; + struct VOXELM *head; //pointer to the first voxel in the group in the linked list + struct VOXELM *last; //pointer to the last voxel in the group + struct VOXELM *next; //pointer to the next voxel in the group +}; + +typedef struct VOXELM VOXELM; + +//the EDGE is the line that connects two voxels. +//if we have S voxels, then we have S horizontal edges and S vertical edges +struct EDGE +{ + double reliab; //reliabilty of the edge and it depends on the two voxels + VOXELM *pointer_1; //pointer to the first voxel + VOXELM *pointer_2; //pointer to the second voxel + int increment; //No. of 2*pi to add to one of the + //voxels to unwrap it with respect to + //the second +}; + +typedef struct EDGE EDGE; + +//---------------start quicker_sort algorithm -------------------------------- +#define swap(x,y) {EDGE t; t=x; x=y; y=t;} +#define order(x,y) if (x.reliab > y.reliab) swap(x,y) +#define o2(x,y) order(x,y) +#define o3(x,y,z) o2(x,y); o2(x,z); o2(y,z) + +typedef enum {yes, no} yes_no; + +yes_no find_pivot(EDGE *left, EDGE *right, double *pivot_ptr) +{ + EDGE a, b, c, *p; + + a = *left; + b = *(left + (right - left) /2 ); + c = *right; + o3(a,b,c); + + if (a.reliab < b.reliab) + { + *pivot_ptr = b.reliab; + return yes; + } + + if (b.reliab < c.reliab) + { + *pivot_ptr = c.reliab; + return yes; + } + + for (p = left + 1; p <= right; ++p) + { + if (p->reliab != left->reliab) + { + *pivot_ptr = (p->reliab < left->reliab) ? left->reliab : p->reliab; + return yes; + } + } + return no; +} + +EDGE *partition(EDGE *left, EDGE *right, double pivot) +{ + while (left <= right) + { + while (left->reliab < pivot) + ++left; + while (right->reliab >= pivot) + --right; + if (left < right) + { + swap (*left, *right); + ++left; + --right; + } + } + return left; +} + +void quicker_sort(EDGE *left, EDGE *right) +{ + EDGE *p; + double pivot; + + if (find_pivot(left, right, &pivot) == yes) + { + p = partition(left, right, pivot); + quicker_sort(left, p - 1); + quicker_sort(p, right); + } +} + +//--------------end quicker_sort algorithm ----------------------------------- + +//--------------------start initialize voxels ---------------------------------- +//initiale voxels. See the explanation of the voxel class above. +//initially every voxel is assumed to belong to a group consisting of only itself +void initialiseVOXELs(double *WrappedVolume, unsigned char *input_mask, unsigned char *extended_mask, VOXELM *voxel, int volume_width, int volume_height, int volume_depth) +{ + VOXELM *voxel_pointer = voxel; + double *wrapped_volume_pointer = WrappedVolume; + unsigned char *input_mask_pointer = input_mask; + unsigned char *extended_mask_pointer = extended_mask; + int n, i, j; + + for (n=0; n < volume_depth; n++) + { + for (i=0; i < volume_height; i++) + { + for (j=0; j < volume_width; j++) + { + voxel_pointer->increment = 0; + voxel_pointer->number_of_voxels_in_group = 1; + voxel_pointer->value = *wrapped_volume_pointer; + voxel_pointer->reliability = 9999999 + rand(); + voxel_pointer->input_mask = *input_mask_pointer; + voxel_pointer->extended_mask = *extended_mask_pointer; + voxel_pointer->head = voxel_pointer; + voxel_pointer->last = voxel_pointer; + voxel_pointer->next = NULL; + voxel_pointer->new_group = 0; + voxel_pointer->group = -1; + voxel_pointer++; + wrapped_volume_pointer++; + input_mask_pointer++; + extended_mask_pointer++; + } + } + } +} +//-------------------end initialize voxels ----------- + +//gamma function in the paper +double wrap(double voxel_value) +{ + double wrapped_voxel_value; + if (voxel_value > PI) wrapped_voxel_value = voxel_value - TWOPI; + else if (voxel_value < -PI) wrapped_voxel_value = voxel_value + TWOPI; + else wrapped_voxel_value = voxel_value; + return wrapped_voxel_value; +} + +// voxelL_value is the left voxel, voxelR_value is the right voxel +int find_wrap(double voxelL_value, double voxelR_value) +{ + double difference; + int wrap_value; + difference = voxelL_value - voxelR_value; + + if (difference > PI) wrap_value = -1; + else if (difference < -PI) wrap_value = 1; + else wrap_value = 0; + + return wrap_value; +} + +void extend_mask(unsigned char *input_mask, unsigned char *extended_mask, int volume_width, int volume_height, int volume_depth, params_t *params) +{ + int n, i, j; + int vw = volume_width, vh = volume_height, vd = volume_depth; + int fs = volume_width * volume_height; //frame size + int frame_size = volume_width * volume_height; + int volume_size = volume_width * volume_height * volume_depth; //volume size + int vs = volume_size; + unsigned char *IMP = input_mask + frame_size + volume_width + 1; //input mask pointer + unsigned char *EMP = extended_mask + frame_size + volume_width + 1; //extended mask pointer + + //extend the mask for the volume except borders + for (n=1; n < volume_depth - 1; n++) + { + for (i=1; i < volume_height - 1; i++) + { + for (j=1; j < volume_width - 1; j++) + { + if( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + vw) == NOMASK) && (*(IMP + vw - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP - vw - 1) == NOMASK) && (*(IMP - vw + 1) == NOMASK) && + (*(IMP + fs) == NOMASK) && (*(IMP + fs - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP + fs - vw) == NOMASK) && (*(IMP + fs - vw - 1) == NOMASK) && (*(IMP + fs - vw + 1) == NOMASK) && + (*(IMP + fs + vw) == NOMASK) && (*(IMP + fs + vw - 1) == NOMASK) && (*(IMP + fs + vw + 1) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP - fs - 1) == NOMASK) && (*(IMP - fs + 1) == NOMASK) && + (*(IMP - fs - vw) == NOMASK) && (*(IMP - fs - vw - 1) == NOMASK) && (*(IMP - fs - vw + 1) == NOMASK) && + (*(IMP - fs + vw) == NOMASK) && (*(IMP - fs + vw - 1) == NOMASK) && (*(IMP - fs + vw + 1) == NOMASK)) + { + *EMP = NOMASK; + } + ++EMP; + ++IMP; + } + EMP += 2; + IMP += 2; + } + EMP += 2 * volume_width; + IMP += 2 * volume_width; + } + + if (params->x_connectivity == 1) + { + //extend the mask to the front side of the phase volume + IMP = input_mask + frame_size + volume_width; //input mask pointer + EMP = extended_mask + frame_size + volume_width; //extended mask pointer + for (n=1; n < volume_depth - 1; n++) + { + for (i=1; i < volume_height - 1; i++) + { + if( (*IMP) == NOMASK && (*(IMP + vw - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP + vw) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP + fs) == NOMASK) && + (*(IMP - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP - vw + 1) == NOMASK) && (*(IMP + 2 * vw - 1) == NOMASK) && + (*(IMP - fs - 1) == NOMASK) && (*(IMP + fs + vw + 1) == NOMASK) && + (*(IMP - fs - vw) == NOMASK) && (*(IMP + fs + vw) == NOMASK) && + (*(IMP - fs - vw + 1) == NOMASK) && (*(IMP + fs + 2 * vw - 1) == NOMASK) && + (*(IMP - fs + vw - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP - fs + 1) == NOMASK) && (*(IMP + fs + vw - 1) == NOMASK) && + (*(IMP - fs + 2 * vw - 1) == NOMASK) && (*(IMP + fs - vw + 1) == NOMASK) && + (*(IMP - fs + vw) == NOMASK) && (*(IMP + fs - vw) == NOMASK) && + (*(IMP - fs + vw + 1) == NOMASK) && (*(IMP + fs - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP += vw; + IMP += vw; + } + EMP += 2 * vw; + IMP += 2 *vw; + } + + //extend the mask to the rear side of the phase volume + IMP = input_mask + frame_size + 2 * volume_width - 1; //input mask pointer + EMP = extended_mask + frame_size + 2 * volume_width - 1; //extended mask pointer + for (n=1; n < volume_depth - 1; n++) + { + for (i=1; i < volume_height - 1; i++) + { + if( (*IMP) == NOMASK && (*(IMP - vw + 1) == NOMASK) && (*(IMP - 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP + vw) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP + fs) == NOMASK) && + (*(IMP - vw - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + vw - 1) == NOMASK) && (*(IMP - 2 * vw + 1) == NOMASK) && + (*(IMP - fs - vw - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP - fs - 2 * vw + 1) == NOMASK) && (*(IMP + fs + vw - 1) == NOMASK) && + (*(IMP - fs - 1) == NOMASK) && (*(IMP + fs - vw + 1) == NOMASK) && + (*(IMP - fs - vw + 1) == NOMASK) && (*(IMP + fs - 1) == NOMASK) && + (*(IMP - fs - vw) == NOMASK) && (*(IMP + fs + vw) == NOMASK) && + (*(IMP - fs + vw - 1) == NOMASK) && (*(IMP + fs - 2 * vw + 1) == NOMASK) && + (*(IMP - fs + vw) == NOMASK) && (*(IMP + fs - vw) == NOMASK) && + (*(IMP - fs + 1) == NOMASK) && (*(IMP + fs - vw - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP += vw; + IMP += vw; + } + EMP += 2 * vw; + IMP += 2 *vw; + } + } + + if (params->y_connectivity == 1) + { + //extend the mask to the left side of the phase volume + IMP = input_mask + frame_size + 1; + EMP = extended_mask + frame_size + 1; + for (n=1; n < volume_depth - 1; n++) + { + for (j=1; j < volume_width - 1; j++) + { + if( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP + fs - vw) == NOMASK) && (*(IMP + vw) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP + fs) == NOMASK) && + (*(IMP + fs - vw - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP + fs - vw + 1) == NOMASK) && (*(IMP + vw - 1) == NOMASK) && + (*(IMP - vw - 1) == NOMASK) && (*(IMP + fs + vw + 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP + fs + vw) == NOMASK) && + (*(IMP - vw + 1) == NOMASK) && (*(IMP + fs + vw - 1) == NOMASK) && + (*(IMP - fs - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP - fs + 1) == NOMASK) && (*(IMP + fs - 1) == NOMASK) && + (*(IMP - fs + vw - 1) == NOMASK) && (*(IMP + 2 * fs - vw + 1) == NOMASK) && + (*(IMP - fs + vw) == NOMASK) && (*(IMP + 2 * fs - vw) == NOMASK) && + (*(IMP - fs + vw + 1) == NOMASK) && (*(IMP + 2 * fs - vw - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + EMP += fs - vw + 2; + IMP += fs - vw + 2; + } + + //extend the mask to the right side of the phase volume + IMP = input_mask + 2 * frame_size - volume_width + 1; + EMP = extended_mask + 2 * frame_size - volume_width + 1; + for (n=1; n < volume_depth - 1; n++) + { + for (j=1; j < volume_width - 1; j++) + { + if( (*IMP) == NOMASK && (*(IMP + 1) == NOMASK) && (*(IMP - 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP - fs + vw) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP + fs) == NOMASK) && + (*(IMP - vw - 1) == NOMASK) && (*(IMP - fs + vw + 1) == NOMASK) && + (*(IMP - vw + 1) == NOMASK) && (*(IMP - fs + vw - 1) == NOMASK) && + (*(IMP - fs - vw - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP - fs - vw + 1) == NOMASK) && (*(IMP + vw - 1) == NOMASK) && + (*(IMP - fs - vw) == NOMASK) && (*(IMP + vw) == NOMASK) && + (*(IMP - fs - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP - fs + 1) == NOMASK) && (*(IMP + fs - 1) == NOMASK) && + (*(IMP - 2 * fs + vw - 1) == NOMASK) && (*(IMP + fs - vw + 1) == NOMASK) && + (*(IMP - 2 * fs + vw) == NOMASK) && (*(IMP + fs - vw) == NOMASK) && + (*(IMP - 2 * fs + vw + 1) == NOMASK) && (*(IMP + fs - vw - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + EMP += fs - vw + 2; + IMP += fs - vw + 2; + } + } + + if (params->z_connectivity == 1) + { + //extend the mask to the bottom side of the phase volume + IMP = input_mask + volume_width + 1; + EMP = extended_mask + volume_width + 1; + for (i=1; i < volume_height - 1; ++i) + { + for (j=1; j < volume_width - 1; ++j) + { + if( (*IMP) == NOMASK && (*(IMP - 1) == NOMASK) && (*(IMP + 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP + vw) == NOMASK) && + (*(IMP + fs) == NOMASK) && (*(IMP + vs - fs) == NOMASK) && + (*(IMP - vw - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP - vw + 1) == NOMASK) && (*(IMP + vw - 1) == NOMASK) && + (*(IMP + vs - fs - vw - 1) == NOMASK) && (*(IMP + fs + vw + 1) == NOMASK) && + (*(IMP + vs - fs - vw) == NOMASK) && (*(IMP + fs + vw) == NOMASK) && + (*(IMP + vs - fs - vw + 1) == NOMASK) && (*(IMP + fs + vw - 1) == NOMASK) && + (*(IMP + vs - fs - 1) == NOMASK) && (*(IMP + fs + 1) == NOMASK) && + (*(IMP + vs - fs + 1) == NOMASK) && (*(IMP + fs - 1) == NOMASK) && + (*(IMP + vs - fs + vw - 1) == NOMASK) && (*(IMP + fs - vw + 1) == NOMASK) && + (*(IMP + vs - fs + vw) == NOMASK) && (*(IMP + fs - vw) == NOMASK) && + (*(IMP + vs - fs + vw + 1) == NOMASK) && (*(IMP + fs - vw - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + EMP += 2; + IMP += 2; + } + + //extend the mask to the top side of the phase volume + IMP = input_mask + volume_size - frame_size + volume_width + 1; + EMP = extended_mask + volume_size - frame_size + volume_width + 1; + for (i=1; i < volume_height - 1; ++i) + { + for (j=1; j < volume_width - 1; ++j) + { + if( (*IMP) == NOMASK && (*(IMP + 1) == NOMASK) && (*(IMP - 1) == NOMASK) && + (*(IMP - vw) == NOMASK) && (*(IMP - fs + vw) == NOMASK) && + (*(IMP - fs) == NOMASK) && (*(IMP - vs + fs) == NOMASK) && + (*(IMP - vw - 1) == NOMASK) && (*(IMP + vw + 1) == NOMASK) && + (*(IMP - vw + 1) == NOMASK) && (*(IMP + vw - 1) == NOMASK) && + (*(IMP - fs - vw - 1) == NOMASK) && (*(IMP - vs + fs + vw + 1) == NOMASK) && + (*(IMP - fs - vw + 1) == NOMASK) && (*(IMP - vs + fs + vw - 1) == NOMASK) && + (*(IMP - fs - vw) == NOMASK) && (*(IMP - vs + fs + vw) == NOMASK) && + (*(IMP - fs - 1) == NOMASK) && (*(IMP - vs + fs + 1) == NOMASK) && + (*(IMP - fs + 1) == NOMASK) && (*(IMP - vs + fs - 1) == NOMASK) && + (*(IMP - fs + vw - 1) == NOMASK) && (*(IMP - vs + fs - vw + 1) == NOMASK) && + (*(IMP - fs + vw) == NOMASK) && (*(IMP - vs + fs - vw) == NOMASK) && + (*(IMP - fs + vw + 1) == NOMASK) && (*(IMP - vs + fs - vw - 1) == NOMASK) ) + { + *EMP = NOMASK; + } + EMP++; + IMP++; + } + EMP += 2; + IMP += 2; + } + } +} + +void calculate_reliability(double *wrappedVolume, VOXELM *voxel, int volume_width, int volume_height, int volume_depth, params_t *params) +{ + int frame_size = volume_width * volume_height; + int volume_size = volume_width * volume_height * volume_depth; + VOXELM *voxel_pointer; + double H, V, N, D1, D2, D3, D4, D5, D6, D7, D8, D9, D10; + double *WVP; + int n, i, j; + + WVP = wrappedVolume + frame_size + volume_width + 1; + voxel_pointer = voxel + frame_size + volume_width + 1; + for (n=1; n < volume_depth - 1; n++) + { + for (i=1; i < volume_height - 1; i++) + { + for (j=1; j < volume_width - 1; j++) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP - 1) - *WVP) - wrap(*WVP - *(WVP + 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP + frame_size)); + D1 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1)); + D2 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + volume_width - 1)); + D3 = wrap(*(WVP - frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width + 1)); + D4 = wrap(*(WVP - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width)); + D5 = wrap(*(WVP - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width - 1)); + D6 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D7 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + D8 = wrap(*(WVP - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width + 1)); + D9 = wrap(*(WVP - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width)); + D10 = wrap(*(WVP - frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer++; + WVP++; + } + voxel_pointer += 2; + WVP += 2; + } + voxel_pointer += 2 * volume_width; + WVP += 2 * volume_width; + } + + if (params->x_connectivity == 1) + { + //calculating reliability for the front side of the phase volume...add volume_width + WVP = wrappedVolume + frame_size + volume_width; + voxel_pointer = voxel + frame_size + volume_width; + for (n=1; n < volume_depth - 1; ++n) + { + for (i=1; i < volume_height - 1; ++i) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP + frame_size)); + D1 = wrap(*(WVP - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1)); + D2 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + 2 * volume_width - 1)); + D3 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width + 1)); + D4 = wrap(*(WVP - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width)); + D5 = wrap(*(WVP - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 2 * volume_width - 1)); + D6 = wrap(*(WVP - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D7 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width - 1)); + D8 = wrap(*(WVP - frame_size + 2 * volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width + 1)); + D9 = wrap(*(WVP - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width)); + D10 = wrap(*(WVP - frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer += volume_width; + WVP += volume_width; + } + voxel_pointer += 2 * volume_width; + WVP += 2 * volume_width; + } + + //calculating reliability for the rear side of the phase volume..... subtract volume_width + WVP = wrappedVolume + frame_size + 2 * volume_width - 1; + voxel_pointer = voxel + frame_size + 2 * volume_width - 1; + for (n=1; n < volume_depth - 1; ++n) + { + for (i=1; i < volume_height - 1; ++i) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP - 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP + frame_size)); + D1 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + 1)); + D2 = wrap(*(WVP + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP - 2 * volume_width + 1)); + D3 = wrap(*(WVP - frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D4 = wrap(*(WVP - frame_size - 2 * volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width - 1)); + D5 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width + 1)); + D6 = wrap(*(WVP - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + D7 = wrap(*(WVP - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width)); + D8 = wrap(*(WVP - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 2 * volume_width + 1)); + D9 = wrap(*(WVP - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width)); + D10 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer += volume_width; + WVP += volume_width; + } + voxel_pointer += 2 * volume_width; + WVP += 2 * volume_width; + } + } + + if (params->y_connectivity == 1) + { + //calculating reliability for the left side of the phase volume...add frame_size + WVP = wrappedVolume + frame_size + 1; + voxel_pointer = voxel + frame_size + 1; + for (n=1; n < volume_depth - 1; ++n) + { + for (j=1; j < volume_width - 1; ++j) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP - 1) - *WVP) - wrap(*WVP - *(WVP + 1)); + V = wrap(*(WVP + frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP + frame_size)); + D1 = wrap(*(WVP + frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1)); + D2 = wrap(*(WVP + frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + volume_width - 1)); + D3 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width + 1)); + D4 = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width)); + D5 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width - 1)); + D6 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D7 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + D8 = wrap(*(WVP - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + 2 * frame_size - volume_width + 1)); + D9 = wrap(*(WVP - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + 2 * frame_size - volume_width)); + D10 = wrap(*(WVP - frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + 2 * frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer++; + WVP++; + } + voxel_pointer += frame_size - volume_width + 2; + WVP += frame_size - volume_width + 2; + } + + //calculating reliability for the right side of the phase volume...subtract frame_size + WVP = wrappedVolume + 2 * frame_size - volume_width + 1; + voxel_pointer = voxel + 2 * frame_size - volume_width + 1; + for (n=1; n < volume_depth - 1; ++n) + { + for (j=1; j < volume_width - 1; ++j) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP + 1) - *WVP) - wrap(*WVP - *(WVP - 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP - frame_size + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP + frame_size)); + D1 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP - frame_size + volume_width + 1)); + D2 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP - frame_size + volume_width - 1)); + D3 = wrap(*(WVP - frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1) ); + D4 = wrap(*(WVP - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + volume_width - 1)); + D5 = wrap(*(WVP - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + D6 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D7 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + D8 = wrap(*(WVP - 2 * frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width + 1)); + D9 = wrap(*(WVP - 2 * frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width)); + D10 = wrap(*(WVP - 2 * frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer++; + WVP++; + } + voxel_pointer += frame_size - volume_width + 2; + WVP += frame_size - volume_width + 2; + } + } + + if (params->z_connectivity == 1) + { + //calculating reliability for the bottom side of the phase volume...add volume_size + WVP = wrappedVolume + volume_width + 1; + voxel_pointer = voxel + volume_width + 1; + for (i=1; i < volume_height - 1; ++i) + { + for (j=1; j < volume_width - 1; ++j) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP - 1) - *WVP) - wrap(*WVP - *(WVP + 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP + frame_size) - *WVP) - wrap(*WVP - *(WVP + volume_size - frame_size)); + D1 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1)); + D2 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + volume_width - 1)); + D3 = wrap(*(WVP + volume_size - frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width + 1)); + D4 = wrap(*(WVP + volume_size - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width)); + D5 = wrap(*(WVP + volume_size - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + volume_width - 1)); + D6 = wrap(*(WVP + volume_size - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size + 1)); + D7 = wrap(*(WVP + volume_size - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - 1)); + D8 = wrap(*(WVP + volume_size - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width + 1)); + D9 = wrap(*(WVP + volume_size - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width)); + D10 = wrap(*(WVP + volume_size - frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer++; + WVP++; + } + voxel_pointer += 2; + WVP += 2; + } + + //calculating reliability for the top side of the phase volume...subtract volume_size + WVP = wrappedVolume + volume_size - frame_size + volume_width + 1; + voxel_pointer = voxel + volume_size - frame_size + volume_width + 1; + for (i=1; i < volume_height - 1; ++i) + { + for (j=1; j < volume_width - 1; ++j) + { + if (voxel_pointer->extended_mask == NOMASK) + { + H = wrap(*(WVP + 1) - *WVP) - wrap(*WVP - *(WVP - 1)); + V = wrap(*(WVP - volume_width) - *WVP) - wrap(*WVP - *(WVP + volume_width)); + N = wrap(*(WVP - frame_size) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size)); + D1 = wrap(*(WVP - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP + volume_width + 1)); + D2 = wrap(*(WVP - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP + volume_width - 1)); + D3 = wrap(*(WVP - frame_size - volume_width - 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size + volume_width + 1)); + D4 = wrap(*(WVP - frame_size - volume_width + 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size + volume_width - 1)); + D5 = wrap(*(WVP - frame_size - volume_width) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size + volume_width)); + D6 = wrap(*(WVP - frame_size - 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size + 1)); + D7 = wrap(*(WVP - frame_size + 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size - 1)); + D8 = wrap(*(WVP - frame_size + volume_width - 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size - volume_width + 1)); + D9 = wrap(*(WVP - frame_size + volume_width) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size - volume_width)); + D10 = wrap(*(WVP - frame_size + volume_width + 1) - *WVP) - wrap(*WVP - *(WVP - volume_size + frame_size - volume_width - 1)); + voxel_pointer->reliability = H*H + V*V + N*N + D1*D1 + D2*D2 + D3*D3 + D4*D4 + D5*D5 + D6*D6 + + D7*D7 + D8*D8 + D9*D9 + D10*D10; + } + voxel_pointer++; + WVP++; + } + voxel_pointer += 2; + WVP += 2; + } + } +} + +//calculate the reliability of the horizontal edges of the volume. it +//is calculated by adding the reliability of voxel and the relibility +//of its right neighbour. edge is calculated between a voxel and its +//next neighbour +void horizontalEDGEs(VOXELM *voxel, EDGE *edge, int volume_width, int volume_height, int volume_depth, params_t *params) +{ + int n, i, j; + EDGE *edge_pointer = edge; + VOXELM *voxel_pointer = voxel; + int no_of_edges = params->no_of_edges; + + for (n=0; n < volume_depth; n++) + { + for (i = 0; i < volume_height; i++) + { + for (j = 0; j < volume_width - 1; j++) + { + if (voxel_pointer->input_mask == NOMASK && (voxel_pointer + 1)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer+1); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer + 1)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer + 1)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer++; + } + voxel_pointer++; + } + } + if (params->x_connectivity == 1) + { + voxel_pointer = voxel + volume_width - 1; + for (n=0; n < volume_depth; n++) + { + for (i = 0; i < volume_height; i++) + { + if (voxel_pointer->input_mask == NOMASK && (voxel_pointer - volume_width + 1)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer - volume_width + 1); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer - volume_width + 1)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer - volume_width + 1)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer += volume_width; + } + } + } + params->no_of_edges = no_of_edges; +} + +void verticalEDGEs(VOXELM *voxel, EDGE *edge, int volume_width, int volume_height, int volume_depth, params_t *params) +{ + int n, i, j; + int no_of_edges = params->no_of_edges; + VOXELM *voxel_pointer = voxel; + EDGE *edge_pointer = edge + no_of_edges; + int frame_size = volume_width * volume_height; + int next_voxel = frame_size - volume_width; + + for (n=0; n < volume_depth; n++) + { + for (i=0; iinput_mask == NOMASK && (voxel_pointer + volume_width)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer + volume_width); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer + volume_width)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer + volume_width)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer++; + } + } + voxel_pointer += volume_width; + } + + if (params->y_connectivity == 1) + { + voxel_pointer = voxel + frame_size - volume_width; + for (n=0; n < volume_depth; n++) + { + for (i = 0; i < volume_width; i++) + { + if (voxel_pointer->input_mask == NOMASK && (voxel_pointer - next_voxel)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer - next_voxel); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer - next_voxel)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer - next_voxel)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer++; + } + voxel_pointer += next_voxel + 1; + } + } + params->no_of_edges = no_of_edges; +} + +void normalEDGEs(VOXELM *voxel, EDGE *edge, int volume_width, int volume_height, int volume_depth, params_t *params) +{ + int n, i, j; + int no_of_edges = params->no_of_edges; + int frame_size = volume_width * volume_height; + int volume_size = volume_width * volume_height * volume_depth; + VOXELM *voxel_pointer = voxel; + EDGE *edge_pointer = edge + no_of_edges; + int next_voxel = volume_size - frame_size; + + for (n=0; n < volume_depth - 1; n++) + { + for (i=0; iinput_mask == NOMASK && (voxel_pointer + frame_size)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer + frame_size); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer + frame_size)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer + frame_size)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer++; + } + } + } + + + if (params->z_connectivity == 1) + { + voxel_pointer = voxel + next_voxel; + for (i=0; i < volume_height; i++) + { + for (j = 0; j < volume_width; j++) + { + if (voxel_pointer->input_mask == NOMASK && (voxel_pointer - next_voxel)->input_mask == NOMASK ) + { + edge_pointer->pointer_1 = voxel_pointer; + edge_pointer->pointer_2 = (voxel_pointer - next_voxel); + edge_pointer->reliab = voxel_pointer->reliability + (voxel_pointer - next_voxel)->reliability; + edge_pointer->increment = find_wrap(voxel_pointer->value, (voxel_pointer - next_voxel)->value); + edge_pointer++; + no_of_edges++; + } + voxel_pointer++; + } + } + } + params->no_of_edges = no_of_edges; +} + +//gather the voxels of the volume into groups +void gatherVOXELs(EDGE *edge, params_t *params) +{ + int k; + VOXELM *VOXEL1; + VOXELM *VOXEL2; + VOXELM *group1; + VOXELM *group2; + EDGE *pointer_edge = edge; + int incremento; + + for (k = 0; k < params->no_of_edges; k++) + { + VOXEL1 = pointer_edge->pointer_1; + VOXEL2 = pointer_edge->pointer_2; + + //VOXELM 1 and VOXELM 2 belong to different groups + //initially each voxel is in a group by itself and one voxel can construct a group + //no else or else if to this if + if (VOXEL2->head != VOXEL1->head) + { + //VOXELM 2 is alone in its group + //merge this voxel with VOXELM 1 group and find the number of 2 pi to add + //to or subtract to unwrap it + if ((VOXEL2->next == NULL) && (VOXEL2->head == VOXEL2)) + { + VOXEL1->head->last->next = VOXEL2; + VOXEL1->head->last = VOXEL2; + (VOXEL1->head->number_of_voxels_in_group)++; + VOXEL2->head=VOXEL1->head; + VOXEL2->increment = VOXEL1->increment-pointer_edge->increment; + } + + //VOXELM 1 is alone in its group + //merge this voxel with VOXELM 2 group and find the number of 2 pi to add + //to or subtract to unwrap it + else if ((VOXEL1->next == NULL) && (VOXEL1->head == VOXEL1)) + { + VOXEL2->head->last->next = VOXEL1; + VOXEL2->head->last = VOXEL1; + (VOXEL2->head->number_of_voxels_in_group)++; + VOXEL1->head = VOXEL2->head; + VOXEL1->increment = VOXEL2->increment+pointer_edge->increment; + } + + //VOXELM 1 and VOXELM 2 both have groups + else + { + group1 = VOXEL1->head; + group2 = VOXEL2->head; + //if the no. of voxels in VOXELM 1 group is larger than the no. of voxels + //in VOXELM 2 group. Merge VOXELM 2 group to VOXELM 1 group + //and find the number of wraps between VOXELM 2 group and VOXELM 1 group + //to unwrap VOXELM 2 group with respect to VOXELM 1 group. + //the no. of wraps will be added to VOXELM 2 grop in the future + if (group1->number_of_voxels_in_group > group2->number_of_voxels_in_group) + { + //merge VOXELM 2 with VOXELM 1 group + group1->last->next = group2; + group1->last = group2->last; + group1->number_of_voxels_in_group = group1->number_of_voxels_in_group + group2->number_of_voxels_in_group; + incremento = VOXEL1->increment-pointer_edge->increment - VOXEL2->increment; + //merge the other voxels in VOXELM 2 group to VOXELM 1 group + while (group2 != NULL) + { + group2->head = group1; + group2->increment += incremento; + group2 = group2->next; + } + } + + //if the no. of voxels in VOXELM 2 group is larger than the no. of voxels + //in VOXELM 1 group. Merge VOXELM 1 group to VOXELM 2 group + //and find the number of wraps between VOXELM 2 group and VOXELM 1 group + //to unwrap VOXELM 1 group with respect to VOXELM 2 group. + //the no. of wraps will be added to VOXELM 1 grop in the future + else + { + //merge VOXELM 1 with VOXELM 2 group + group2->last->next = group1; + group2->last = group1->last; + group2->number_of_voxels_in_group = group2->number_of_voxels_in_group + group1->number_of_voxels_in_group; + incremento = VOXEL2->increment + pointer_edge->increment - VOXEL1->increment; + //merge the other voxels in VOXELM 2 group to VOXELM 1 group + while (group1 != NULL) + { + group1->head = group2; + group1->increment += incremento; + group1 = group1->next; + } // while + + } // else + } //else + } //if + pointer_edge++; + } +} + +//unwrap the volume +void unwrapVolume(VOXELM *voxel, int volume_width, int volume_height, int volume_depth) +{ + int i; + int volume_size = volume_width * volume_height * volume_depth; + VOXELM *voxel_pointer=voxel; + + for (i = 0; i < volume_size; i++) + { + voxel_pointer->value += TWOPI * (double)(voxel_pointer->increment); + voxel_pointer++; + } +} + +//set the masked voxels (mask = 0) to the minimum of the unwrapper phase +void maskVolume(VOXELM *voxel, unsigned char *input_mask, int volume_width, int volume_height, int volume_depth) +{ + int volume_width_plus_one = volume_width + 1; + int volume_height_plus_one = volume_height + 1; + int volume_width_minus_one = volume_width - 1; + int volume_height_minus_one = volume_height - 1; + + VOXELM *pointer_voxel = voxel; + unsigned char *IMP = input_mask; //input mask pointer + double min=99999999.; + int i, j; + int volume_size = volume_width * volume_height * volume_depth; + + //find the minimum of the unwrapped phase + for (i = 0; i < volume_size; i++) + { + if ((pointer_voxel->value < min) && (*IMP == NOMASK)) + min = pointer_voxel->value; + + pointer_voxel++; + IMP++; + } + + pointer_voxel = voxel; + IMP = input_mask; + + //set the masked voxels to minimum + for (i = 0; i < volume_size; i++) + { + if ((*IMP) == MASK) + { + pointer_voxel->value = min; + } + pointer_voxel++; + IMP++; + } +} + +//the input to this unwrapper is an array that contains the wrapped +//phase map. copy the volume on the buffer passed to this unwrapper +//to over-write the unwrapped phase map on the buffer of the wrapped +//phase map. +void returnVolume(VOXELM *voxel, double *unwrappedVolume, int volume_width, int volume_height, int volume_depth) +{ + int i; + int volume_size = volume_width * volume_height * volume_depth; + double *unwrappedVolume_pointer = unwrappedVolume; + VOXELM *voxel_pointer = voxel; + + for (i=0; i < volume_size; i++) + { + *unwrappedVolume_pointer = voxel_pointer->value; + voxel_pointer++; + unwrappedVolume_pointer++; + } +} + +//the main function of the unwrapper +void +unwrap3D(double* wrapped_volume, double* unwrapped_volume, unsigned char* input_mask, + int volume_width, int volume_height, int volume_depth, + int wrap_around_x, int wrap_around_y, int wrap_around_z) +{ + params_t params = {TWOPI, wrap_around_x, wrap_around_y, wrap_around_z, 0}; + unsigned char *extended_mask; + VOXELM *voxel; + EDGE *edge; + int volume_size = volume_height * volume_width * volume_depth; + int No_of_Edges_initially = 3 * volume_width * volume_height * volume_depth; + + extended_mask = (unsigned char *) calloc(volume_size, sizeof(unsigned char)); + voxel = (VOXELM *) calloc(volume_size, sizeof(VOXELM)); + edge = (EDGE *) calloc(No_of_Edges_initially, sizeof(EDGE));; + + extend_mask(input_mask, extended_mask, volume_width, volume_height, volume_depth, ¶ms); + initialiseVOXELs(wrapped_volume, input_mask, extended_mask, voxel, volume_width, volume_height, volume_depth); + calculate_reliability(wrapped_volume, voxel, volume_width, volume_height, volume_depth, ¶ms); + horizontalEDGEs(voxel, edge, volume_width, volume_height, volume_depth, ¶ms); + verticalEDGEs(voxel, edge, volume_width, volume_height, volume_depth, ¶ms); + normalEDGEs(voxel, edge, volume_width, volume_height, volume_depth, ¶ms); + + //sort the EDGEs depending on their reiability. The VOXELs with higher relibility (small value) first + quicker_sort(edge, edge + params.no_of_edges - 1); + + //gather VOXELs into groups + gatherVOXELs(edge, ¶ms); + + unwrapVolume(voxel, volume_width, volume_height, volume_depth); + maskVolume(voxel, input_mask, volume_width, volume_height, volume_depth); + + //copy the volume from VOXELM structure to the unwrapped phase array passed to this function + returnVolume(voxel, unwrapped_volume, volume_width, volume_height, volume_depth); + + free(edge); + free(voxel); + free(extended_mask); +} diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index c3d95ee0..766cd317 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -3,6 +3,7 @@ #cython: nonecheck=False #cython: wraparound=False from libc.float cimport DBL_MAX +from cpython cimport bool import numpy as np cimport numpy as cnp @@ -12,8 +13,10 @@ from skimage.util import regular_grid def _slic_cython(double[:, :, :, ::1] image_zyx, double[:, ::1] segments, + float step, Py_ssize_t max_iter, - double[::1] spacing): + double[::1] spacing, + bint slic_zero): """Helper function for SLIC segmentation. Parameters @@ -22,12 +25,16 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, The input image. segments : 2D array of double, shape (N, 3 + C) The initial centroids obtained by SLIC as [Z, Y, X, C...]. + step : double + The size of the step between two seeds in voxels. max_iter : int The maximum number of k-means iterations. spacing : 1D array of double, shape (3,) The voxel spacing along each image dimension. This parameter controls the weights of the distances along z, y, and x during - k-means clustering. + k-means clustering. + slic_zero : bool + True to run SLIC-zero, False to run original SLIC. Returns ------- @@ -86,6 +93,14 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, sy = spacing[1] sx = spacing[2] + # The colors are scaled before being passed to _slic_cython so + # max_color_sq can be initialised as all ones + cdef double[::1] max_dist_color = np.ones(n_segments, dtype=np.double) + cdef double dist_color + + # The reference implementation (Achanta et al.) calls this invxywt + cdef double spatial_weight = float(1) / (step ** 2) + for i in range(max_iter): change = 0 distance[:, :, :] = DBL_MAX @@ -111,10 +126,16 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, for y in range(y_min, y_max): dy = (sy * (cy - y)) ** 2 for x in range(x_min, x_max): - dist_center = dz + dy + (sx * (cx - x)) ** 2 + dist_center = (dz + dy + (sx * (cx - x)) ** 2) * spatial_weight + dist_color = 0 for c in range(3, n_features): - dist_center += (image_zyx[z, y, x, c - 3] + dist_color += (image_zyx[z, y, x, c - 3] - segments[k, c]) ** 2 + if slic_zero: + dist_center += dist_color / max_dist_color[k] + else: + dist_center += dist_color + if distance[z, y, x] > dist_center: nearest_segments[z, y, x] = k distance[z, y, x] = dist_center @@ -145,4 +166,123 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, for c in range(n_features): segments[k, c] /= n_segment_elems[k] + # If in SLICO mode, update the color distance maxima + if slic_zero: + for z in range(depth): + for y in range(height): + for x in range(width): + + k = nearest_segments[z, y, x] + dist_color = 0 + + for c in range(3, n_features): + dist_color += (image_zyx[z, y, x, c - 3] - + segments[k, c]) ** 2 + + # The reference implementation seems to only change + # the color if it increases from previous iteration + if max_dist_color[k] < dist_color: + max_dist_color[k] = dist_color + return np.asarray(nearest_segments) + + +def _enforce_label_connectivity_cython(Py_ssize_t[:, :, ::1] segments, + Py_ssize_t n_segments, + Py_ssize_t min_size, + Py_ssize_t max_size): + """ Helper function to remove small disconnected regions from the labels + + Parameters + ---------- + segments : 3D array of int, shape (Z, Y, X) + The label field/superpixels found by SLIC. + n_segments: int + Number of specified segments + min_size: int + Minimum size of the segment + max_size: int + Maximum size of the segment. This is done for performance reasons, + to pre-allocate a sufficiently large array for the breadth first search + Returns + ------- + connected_segments : 3D array of int, shape (Z, Y, X) + A label field with connected labels starting at label=1 + """ + + # get image dimensions + cdef Py_ssize_t depth, height, width + depth = segments.shape[0] + height = segments.shape[1] + width = segments.shape[2] + + # neighborhood arrays + cdef Py_ssize_t[::1] ddx = np.array((1, -1, 0, 0, 0, 0), dtype=np.intp) + cdef Py_ssize_t[::1] ddy = np.array((0, 0, 1, -1, 0, 0), dtype=np.intp) + cdef Py_ssize_t[::1] ddz = np.array((0, 0, 0, 0, 1, -1), dtype=np.intp) + + # new object with connected segments initialized to -1 + cdef Py_ssize_t[:, :, ::1] connected_segments \ + = -1 * np.ones_like(segments, dtype=np.intp) + + cdef Py_ssize_t current_new_label = 0 + cdef Py_ssize_t label = 0 + + # variables for the breadth first search + cdef Py_ssize_t current_segment_size = 1 + cdef Py_ssize_t bfs_visited = 0 + cdef Py_ssize_t adjacent + + cdef Py_ssize_t zz, yy, xx + + cdef Py_ssize_t[:, ::1] coord_list = np.zeros((max_size, 3), dtype=np.intp) + + # loop through all image + for z in range(depth): + for y in range(height): + for x in range(width): + if connected_segments[z, y, x] >= 0: + continue + # find the component size + adjacent = 0 + label = segments[z, y, x] + connected_segments[z, y, x] = current_new_label + current_segment_size = 1 + bfs_visited = 0 + coord_list[bfs_visited, 0] = z + coord_list[bfs_visited, 1] = y + coord_list[bfs_visited, 2] = x + + #perform a breadth first search to find + # the size of the connected component + while bfs_visited != current_segment_size: + for i in range(6): + zz = coord_list[bfs_visited, 0] + ddz[i] + yy = coord_list[bfs_visited, 1] + ddy[i] + xx = coord_list[bfs_visited, 2] + ddx[i] + if (0 <= xx < width and + 0 <= yy < height and + 0 <= zz < depth): + if (segments[zz, yy, xx] == label and + connected_segments[zz, yy, xx] == -1): + connected_segments[zz, yy, xx] = \ + current_new_label + coord_list[current_segment_size, 0] = zz + coord_list[current_segment_size, 1] = yy + coord_list[current_segment_size, 2] = xx + current_segment_size += 1 + elif (connected_segments[zz, yy, xx] >= 0 and + connected_segments[zz, yy, xx] != current_new_label): + adjacent = connected_segments[zz, yy, xx] + bfs_visited += 1 + + # change to an adjacent one, like in the original paper + if current_segment_size < min_size: + for i in range(current_segment_size): + connected_segments[coord_list[i, 0], + coord_list[i, 1], + coord_list[i, 2]] = adjacent + else: + current_new_label += 1 + + return np.asarray(connected_segments) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 21df45fd..11373381 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -6,12 +6,14 @@ from scipy import ndimage import warnings from skimage.util import img_as_float, regular_grid -from skimage.segmentation._slic import _slic_cython +from skimage.segmentation._slic import _slic_cython, _enforce_label_connectivity_cython from skimage.color import rgb2lab def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, - spacing=None, multichannel=True, convert2lab=True, ratio=None): + spacing=None, multichannel=True, convert2lab=True, ratio=None, + enforce_connectivity=False, min_size_factor=0.5, max_size_factor=3, + slic_zero=False): """Segments image using k-means clustering in Color-(x,y,z) space. Parameters @@ -24,7 +26,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, compactness : float, optional Balances color-space proximity and image-space proximity. Higher values give more weight to image-space. As `compactness` tends to - infinity, superpixel shapes become square/cubic. + infinity, superpixel shapes become square/cubic. In SLICO mode, this + is the initial compactness. max_iter : int, optional Maximum number of iterations of k-means. sigma : float or (3,) array-like of floats, optional @@ -47,6 +50,16 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, recommended. ratio : float, optional Synonym for `compactness`. This keyword is deprecated. + enforce_connectivity: bool, optional (default False) + Whether the generated segments are connected or not + min_size_factor: float, optional + Proportion of the minimum segment size to be removed with respect + to the supposed segment size ```depth*width*height/n_segments``` + max_size_factor: float, optional + Proportion of the maximum connected segment size. A value of 3 works + in most of the cases. + slic_zero: bool, optional + Run SLIC-zero, the zero-parameter mode of SLIC Returns ------- @@ -104,6 +117,11 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, 'instead.') compactness = ratio + if enforce_connectivity is None: + warnings.warn('Deprecation: enforce_connectivity will default to' + ' True in future versions.') + enforce_connectivity = False + image = img_as_float(image) is_2d = False if image.ndim == 2: @@ -152,16 +170,27 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, segments = np.concatenate([segments_z[..., np.newaxis], segments_y[..., np.newaxis], segments_x[..., np.newaxis], - segments_color - ], axis=-1).reshape(-1, 3 + image.shape[3]) + segments_color], + axis=-1).reshape(-1, 3 + image.shape[3]) segments = np.ascontiguousarray(segments) # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning - ratio = float(max((step_z, step_y, step_x))) / compactness + step = float(max((step_z, step_y, step_x))) + ratio = 1.0 / compactness + image = np.ascontiguousarray(image * ratio) - labels = _slic_cython(image, segments, max_iter, spacing) + labels = _slic_cython(image, segments, step, max_iter, spacing, slic_zero) + + if enforce_connectivity: + segment_size = depth * height * width / n_segments + min_size = int(min_size_factor * segment_size) + max_size = int(max_size_factor * segment_size) + labels = _enforce_label_connectivity_cython(labels, + n_segments, + min_size, + max_size) if is_2d: labels = labels[0] diff --git a/skimage/segmentation/tests/test_boundaries.py b/skimage/segmentation/tests/test_boundaries.py new file mode 100644 index 00000000..2fff52f8 --- /dev/null +++ b/skimage/segmentation/tests/test_boundaries.py @@ -0,0 +1,59 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.segmentation import find_boundaries, mark_boundaries + + +def test_find_boundaries(): + image = np.zeros((10, 10)) + image[2:7, 2:7] = 1 + + ref = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + + result = find_boundaries(image) + assert_array_equal(result, ref) + + +def test_mark_boundaries(): + image = np.zeros((10, 10)) + label_image = np.zeros((10, 10)) + label_image[2:7, 2:7] = 1 + + ref = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + result = mark_boundaries(image, label_image, color=(1, 1, 1)).mean(axis=2) + assert_array_equal(result, ref) + + ref = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 2, 0], + [0, 0, 1, 2, 2, 2, 2, 1, 2, 0], + [0, 0, 1, 2, 0, 0, 0, 1, 2, 0], + [0, 0, 1, 2, 0, 0, 0, 1, 2, 0], + [0, 0, 1, 2, 0, 0, 0, 1, 2, 0], + [0, 0, 1, 1, 1, 1, 1, 2, 2, 0], + [0, 0, 2, 2, 2, 2, 2, 2, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + result = mark_boundaries(image, label_image, color=(1, 1, 1), + outline_color=(2, 2, 2)).mean(axis=2) + assert_array_equal(result, ref) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index a4657785..7dda66d2 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -1,7 +1,7 @@ import itertools as it import warnings import numpy as np -from numpy.testing import assert_equal, assert_array_equal +from numpy.testing import assert_equal, assert_raises from skimage.segmentation import slic @@ -21,10 +21,10 @@ def test_color_2d(): # we expect 4 segments assert_equal(len(np.unique(seg)), 4) assert_equal(seg.shape, img.shape[:-1]) - assert_array_equal(seg[:10, :10], 0) - assert_array_equal(seg[10:, :10], 2) - assert_array_equal(seg[:10, 10:], 1) - assert_array_equal(seg[10:, 10:], 3) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) def test_gray_2d(): @@ -41,10 +41,10 @@ def test_gray_2d(): assert_equal(len(np.unique(seg)), 4) assert_equal(seg.shape, img.shape) - assert_array_equal(seg[:10, :10], 0) - assert_array_equal(seg[10:, :10], 2) - assert_array_equal(seg[:10, 10:], 1) - assert_array_equal(seg[10:, 10:], 3) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) def test_color_3d(): @@ -65,7 +65,7 @@ def test_color_3d(): assert_equal(len(np.unique(seg)), 8) for s, c in zip(slices, range(8)): - assert_array_equal(seg[s], c) + assert_equal(seg[s], c) def test_gray_3d(): @@ -76,7 +76,7 @@ def test_gray_3d(): midpoint = dim_size // 2 slices.append((slice(None, midpoint), slice(midpoint, None))) slices = list(it.product(*slices)) - shades = np.arange(0, 1.000001, 1.0/7) + shades = np.arange(0, 1.000001, 1.0 / 7) for s, sh in zip(slices, shades): img[s] = sh img += 0.001 * rnd.normal(size=img.shape) @@ -87,7 +87,7 @@ def test_gray_3d(): assert_equal(len(np.unique(seg)), 8) for s, c in zip(slices, range(8)): - assert_array_equal(seg[s], c) + assert_equal(seg[s], c) def test_list_sigma(): @@ -118,7 +118,60 @@ def test_spacing(): assert_equal(seg_spaced, result_spaced) +def test_invalid_lab_conversion(): + img = np.array([[1, 1, 1, 0, 0], + [1, 1, 0, 0, 0]], np.float) + 1 + assert_raises(ValueError, slic, img, multichannel=True, convert2lab=True) + + +def test_enforce_connectivity(): + img = np.array([[0, 0, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 0], + [0, 0, 0, 1, 1, 0]], np.float) + + segments_connected = slic(img, 2, compactness=0.0001, + enforce_connectivity=True, + convert2lab=False) + segments_disconnected = slic(img, 2, compactness=0.0001, + enforce_connectivity=False, + convert2lab=False) + + result_connected = np.array([[0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1]], np.float) + + result_disconnected = np.array([[0, 0, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 0], + [0, 0, 0, 1, 1, 0]], np.float) + + assert_equal(segments_connected, result_connected) + assert_equal(segments_disconnected, result_disconnected) + + +def test_slic_zero(): + # Same as test_color_2d but with slic_zero=True + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.01 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + seg = slic(img, n_segments=4, sigma=0, slic_zero=True) + + # we expect 4 segments + assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape[:-1]) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) + if __name__ == '__main__': from numpy import testing + testing.run_module_suite() diff --git a/skimage/setup.py b/skimage/setup.py index 1082ba07..962adb97 100644 --- a/skimage/setup.py +++ b/skimage/setup.py @@ -10,7 +10,9 @@ def configuration(parent_package='', top_path=None): config.add_subpackage('color') config.add_subpackage('data') config.add_subpackage('draw') + config.add_subpackage('exposure') config.add_subpackage('feature') + config.add_subpackage('restoration') config.add_subpackage('filter') config.add_subpackage('graph') config.add_subpackage('io') diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 4f00a076..877f1614 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -1,10 +1,11 @@ -from ._hough_transform import (hough_circle, hough_ellipse, hough_line, +from ._hough_transform import (hough_ellipse, hough_line, probabilistic_hough_line) -from .hough_transform import hough_line_peaks +from .hough_transform import hough_circle, hough_line_peaks from .radon_transform import radon, iradon, iradon_sart from .finite_radon_transform import frt2, ifrt2 from .integral import integral_image, integrate from ._geometric import (warp, warp_coords, estimate_transform, + matrix_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) @@ -30,6 +31,7 @@ __all__ = ['hough_circle', 'warp', 'warp_coords', 'estimate_transform', + 'matrix_transform', 'SimilarityTransform', 'AffineTransform', 'ProjectiveTransform', diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index eb89460e..d5b25b6d 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1,5 +1,6 @@ import six import math +import warnings import numpy as np from scipy import ndimage, spatial @@ -103,6 +104,11 @@ class ProjectiveTransform(GeometricTransform): matrix : (3, 3) array, optional Homogeneous transformation matrix. + Attributes + ---------- + params : (3, 3) array + Homogeneous transformation matrix. + """ _coeffs = range(8) @@ -113,11 +119,17 @@ class ProjectiveTransform(GeometricTransform): matrix = np.eye(3) if matrix.shape != (3, 3): raise ValueError("invalid shape of transformation matrix") - self._matrix = matrix + self.params = matrix + + @property + def _matrix(self): + warnings.warn('`_matrix` attribute is deprecated, ' + 'use `params` instead.') + return self.params @property def _inv_matrix(self): - return np.linalg.inv(self._matrix) + return np.linalg.inv(self.params) def _apply_mat(self, coords, matrix): coords = np.array(coords, copy=False, ndmin=2) @@ -133,7 +145,7 @@ class ProjectiveTransform(GeometricTransform): return dst[:, :2] def __call__(self, coords): - return self._apply_mat(coords, self._matrix) + return self._apply_mat(coords, self.params) def inverse(self, coords): """Apply inverse transformation. @@ -235,7 +247,7 @@ class ProjectiveTransform(GeometricTransform): H.flat[list(self._coeffs) + [8]] = - V[-1, :-1] / V[-1, -1] H[2, 2] = 1 - self._matrix = H + self.params = H def __add__(self, other): """Combine this transformation with another. @@ -248,7 +260,7 @@ class ProjectiveTransform(GeometricTransform): tform = self.__class__ else: tform = ProjectiveTransform - return tform(other._matrix.dot(self._matrix)) + return tform(other.params.dot(self.params)) else: raise TypeError("Cannot combine transformations of differing " "types.") @@ -284,6 +296,11 @@ class AffineTransform(ProjectiveTransform): translation : (tx, ty) as array, list or tuple, optional Translation parameters. + Attributes + ---------- + params : (3, 3) array + Homogeneous transformation matrix. + """ _coeffs = range(6) @@ -299,7 +316,7 @@ class AffineTransform(ProjectiveTransform): elif matrix is not None: if matrix.shape != (3, 3): raise ValueError("Invalid shape of transformation matrix.") - self._matrix = matrix + self.params = matrix elif params: if scale is None: scale = (1, 1) @@ -311,34 +328,34 @@ class AffineTransform(ProjectiveTransform): translation = (0, 0) sx, sy = scale - self._matrix = np.array([ + self.params = np.array([ [sx * math.cos(rotation), -sy * math.sin(rotation + shear), 0], [sx * math.sin(rotation), sy * math.cos(rotation + shear), 0], [ 0, 0, 1] ]) - self._matrix[0:2, 2] = translation + self.params[0:2, 2] = translation else: # default to an identity transform - self._matrix = np.eye(3) + self.params = np.eye(3) @property def scale(self): - sx = math.sqrt(self._matrix[0, 0] ** 2 + self._matrix[1, 0] ** 2) - sy = math.sqrt(self._matrix[0, 1] ** 2 + self._matrix[1, 1] ** 2) + sx = math.sqrt(self.params[0, 0] ** 2 + self.params[1, 0] ** 2) + sy = math.sqrt(self.params[0, 1] ** 2 + self.params[1, 1] ** 2) return sx, sy @property def rotation(self): - return math.atan2(self._matrix[1, 0], self._matrix[0, 0]) + return math.atan2(self.params[1, 0], self.params[0, 0]) @property def shear(self): - beta = math.atan2(- self._matrix[0, 1], self._matrix[1, 1]) + beta = math.atan2(- self.params[0, 1], self.params[1, 1]) return beta - self.rotation @property def translation(self): - return self._matrix[0:2, 2] + return self.params[0:2, 2] class PiecewiseAffineTransform(GeometricTransform): @@ -349,13 +366,20 @@ class PiecewiseAffineTransform(GeometricTransform): a Delaunay triangulation of the points to form a mesh. Each triangle is used to find a local affine transform. + Attributes + ---------- + affines : list of AffineTransform objects + Affine transformations for each triangle in the mesh. + inverse_affines : list of AffineTransform objects + Inverse affine transformations for each triangle in the mesh. + """ def __init__(self): self._tesselation = None self._inverse_tesselation = None - self.affines = [] - self.inverse_affines = [] + self.affines = None + self.inverse_affines = None def estimate(self, src, dst): """Set the control points with which to perform the piecewise mapping. @@ -488,6 +512,11 @@ class SimilarityTransform(ProjectiveTransform): translation : (tx, ty) as array, list or tuple, optional x, y translation parameters. + Attributes + ---------- + params : (3, 3) array + Homogeneous transformation matrix. + """ def __init__(self, matrix=None, scale=None, rotation=None, @@ -501,7 +530,7 @@ class SimilarityTransform(ProjectiveTransform): elif matrix is not None: if matrix.shape != (3, 3): raise ValueError("Invalid shape of transformation matrix.") - self._matrix = matrix + self.params = matrix elif params: if scale is None: scale = 1 @@ -510,16 +539,16 @@ class SimilarityTransform(ProjectiveTransform): if translation is None: translation = (0, 0) - self._matrix = np.array([ + self.params = np.array([ [math.cos(rotation), - math.sin(rotation), 0], [math.sin(rotation), math.cos(rotation), 0], [ 0, 0, 1] ]) - self._matrix[0:2, 0:2] *= scale - self._matrix[0:2, 2] = translation + self.params[0:2, 0:2] *= scale + self.params[0:2, 2] = translation else: # default to an identity transform - self._matrix = np.eye(3) + self.params = np.eye(3) def estimate(self, src, dst): """Set the transformation matrix with the explicit parameters. @@ -585,7 +614,7 @@ class SimilarityTransform(ProjectiveTransform): # singular value a0, a1, b0, b1 = - V[-1, :-1] / V[-1, -1] - self._matrix = np.array([[a0, -b0, a1], + self.params = np.array([[a0, -b0, a1], [b0, a0, b1], [ 0, 0, 1]]) @@ -593,18 +622,18 @@ class SimilarityTransform(ProjectiveTransform): def scale(self): if math.cos(self.rotation) == 0: # sin(self.rotation) == 1 - scale = self._matrix[0, 1] + scale = self.params[0, 1] else: - scale = self._matrix[0, 0] / math.cos(self.rotation) + scale = self.params[0, 0] / math.cos(self.rotation) return scale @property def rotation(self): - return math.atan2(self._matrix[1, 0], self._matrix[1, 1]) + return math.atan2(self.params[1, 0], self.params[1, 1]) @property def translation(self): - return self._matrix[0:2, 2] + return self.params[0:2, 2] class PolynomialTransform(GeometricTransform): @@ -619,6 +648,12 @@ class PolynomialTransform(GeometricTransform): Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So, a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`. + Attributes + ---------- + params : (2, N) array + Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So, + a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`. + """ def __init__(self, params=None): @@ -627,7 +662,13 @@ class PolynomialTransform(GeometricTransform): params = np.array([[0, 1, 0], [0, 0, 1]]) if params.shape[0] != 2: raise ValueError("invalid shape of transformation parameters") - self._params = params + self.params = params + + @property + def _params(self): + warnings.warn('`_params` attribute is deprecated, ' + 'use `params` instead.') + return self.params def estimate(self, src, dst, order=2): """Set the transformation matrix with the explicit transformation @@ -700,7 +741,7 @@ class PolynomialTransform(GeometricTransform): # singular value params = - V[-1, :-1] / V[-1, -1] - self._params = params.reshape((2, u / 2)) + self.params = params.reshape((2, u / 2)) def __call__(self, coords): """Apply forward transformation. @@ -718,7 +759,7 @@ class PolynomialTransform(GeometricTransform): """ x = coords[:, 0] y = coords[:, 1] - u = len(self._params.ravel()) + u = len(self.params.ravel()) # number of coefficients -> u = (order + 1) * (order + 2) order = int((- 3 + math.sqrt(9 - 4 * (2 - u))) / 2) dst = np.zeros(coords.shape) @@ -726,8 +767,8 @@ class PolynomialTransform(GeometricTransform): pidx = 0 for j in range(order + 1): for i in range(j + 1): - dst[:, 0] += self._params[0, pidx] * x ** (j - i) * y ** i - dst[:, 1] += self._params[1, pidx] * x ** (j - i) * y ** i + dst[:, 0] += self.params[0, pidx] * x ** (j - i) * y ** i + dst[:, 1] += self.params[1, pidx] * x ** (j - i) * y ** i pidx += 1 return dst @@ -992,39 +1033,41 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, >>> image = data.camera() The following image warps are all equal but differ substantially in - execution time. + execution time. The image is shifted to the bottom. Use a geometric transform to warp an image (fast): >>> from skimage.transform import SimilarityTransform >>> tform = SimilarityTransform(translation=(0, -10)) - >>> warp(image, tform) # doctest: +SKIP + >>> warped = warp(image, tform) - Shift an image to the right with a callable (slow): + Use a callable (slow): - >>> def shift(xy): + >>> def shift_down(xy): ... xy[:, 1] -= 10 ... return xy - >>> warp(image, shift_right) # doctest: +SKIP + >>> warped = warp(image, shift_down) Use a transformation matrix to warp an image (fast): >>> matrix = np.array([[1, 0, 0], [0, 1, -10], [0, 0, 1]]) - >>> warp(image, matrix) # doctest: +SKIP + >>> warped = warp(image, matrix) >>> from skimage.transform import ProjectiveTransform - >>> warp(image, ProjectiveTransform(matrix=matrix)) # doctest: +SKIP + >>> warped = warp(image, ProjectiveTransform(matrix=matrix)) You can also use the inverse of a geometric transformation (fast): - >>> warp(image, tform.inverse) # doctest: +SKIP + >>> warped = warp(image, tform.inverse) """ # Backward API compatibility if reverse_map is not None: + warnings.warn('`reverse_map` parameter is deprecated and replaced by ' + 'the `inverse_map` parameter.') inverse_map = reverse_map - if image.ndim < 2: - raise ValueError("Input must have more than 1 dimension.") + if image.ndim < 2 or image.ndim > 3: + raise ValueError("Input must have 2 or 3 dimensions.") orig_ndim = image.ndim image = np.atleast_3d(img_as_float(image)) @@ -1044,14 +1087,14 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # inverse_map is a homography elif isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): - matrix = inverse_map._matrix + matrix = inverse_map.params # inverse_map is the inverse of a homography elif (hasattr(inverse_map, '__name__') and inverse_map.__name__ == 'inverse' - and isinstance(get_bound_method_class(inverse_map), - HOMOGRAPHY_TRANSFORMS)): - matrix = np.linalg.inv(six.get_method_self(inverse_map)._matrix) + and get_bound_method_class(inverse_map) \ + in HOMOGRAPHY_TRANSFORMS): + matrix = np.linalg.inv(six.get_method_self(inverse_map).params) if matrix is not None: matrix = matrix.astype(np.double) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 29344fa8..104fed62 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -15,14 +15,12 @@ from skimage.draw import circle_perimeter cdef double PI_2 = 1.5707963267948966 cdef double NEG_PI_2 = -PI_2 - -cdef inline Py_ssize_t round(double r): - return ((r + 0.5) if (r > 0.0) else (r - 0.5)) +from skimage._shared.interpolation cimport round -def hough_circle(cnp.ndarray img, - cnp.ndarray[ndim=1, dtype=cnp.intp_t] radius, - char normalize=True, char full_output=False): +def _hough_circle(cnp.ndarray img, + cnp.ndarray[ndim=1, dtype=cnp.intp_t] radius, + char normalize=True, char full_output=False): """Perform a circular Hough transform. Parameters diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 48cad399..12401aae 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -1,6 +1,7 @@ import numpy as np from scipy import ndimage from skimage import measure, morphology +from ._hough_transform import _hough_circle def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, @@ -122,3 +123,31 @@ def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, angle_peaks = angle_peaks[idx_maxsort] return hspace_peaks, angle_peaks, dist_peaks + + +def hough_circle(image, radius, normalize=True, full_output=False): + """Perform a circular Hough transform. + + Parameters + ---------- + image : (M, N) ndarray + Input image with nonzero values representing edges. + radius : ndarray + Radii at which to compute the Hough transform. + normalize : boolean, optional (default True) + Normalize the accumulator with the number + of pixels used to draw the radius. + full_output : boolean, optional (default False) + Extend the output size by twice the largest + radius in order to detect centers outside the + input picture. + + Returns + ------- + H : 3D ndarray (radius index, (M + 2R, N + 2R) ndarray) + Hough transform accumulator for each radius. + R designates the larger radius if full_output is True. + Otherwise, R = 0. + """ + return _hough_circle(image, radius.astype(np.intp), + normalize=normalize, full_output=full_output) diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index c7f9f832..2a6a5c7e 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -1,7 +1,9 @@ import numpy as np -from numpy.testing import assert_equal, assert_array_almost_equal +from numpy.testing import (assert_equal, assert_array_almost_equal, + assert_raises) from skimage.transform._geometric import _stackcopy -from skimage.transform import (estimate_transform, +from skimage.transform._geometric import GeometricTransform +from skimage.transform import (estimate_transform, matrix_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) @@ -38,23 +40,35 @@ def test_stackcopy(): assert_array_almost_equal(x[..., i], y) +def test_estimate_transform(): + for tform in ('similarity', 'affine', 'projective', 'polynomial'): + estimate_transform(tform, SRC[:2, :], DST[:2, :]) + assert_raises(ValueError, estimate_transform, 'foobar', + SRC[:2, :], DST[:2, :]) + + +def test_matrix_transform(): + tform = AffineTransform(scale=(0.1, 0.5), rotation=2) + assert_equal(tform(SRC), matrix_transform(SRC, tform._matrix)) + + def test_similarity_estimation(): # exact solution tform = estimate_transform('similarity', SRC[:2, :], DST[:2, :]) assert_array_almost_equal(tform(SRC[:2, :]), DST[:2, :]) - assert_equal(tform._matrix[0, 0], tform._matrix[1, 1]) - assert_equal(tform._matrix[0, 1], - tform._matrix[1, 0]) + assert_equal(tform.params[0, 0], tform.params[1, 1]) + assert_equal(tform.params[0, 1], - tform.params[1, 0]) # over-determined tform2 = estimate_transform('similarity', SRC, DST) assert_array_almost_equal(tform2.inverse(tform2(SRC)), SRC) - assert_equal(tform2._matrix[0, 0], tform2._matrix[1, 1]) - assert_equal(tform2._matrix[0, 1], - tform2._matrix[1, 0]) + assert_equal(tform2.params[0, 0], tform2.params[1, 1]) + assert_equal(tform2.params[0, 1], - tform2.params[1, 0]) # via estimate method tform3 = SimilarityTransform() tform3.estimate(SRC, DST) - assert_array_almost_equal(tform3._matrix, tform2._matrix) + assert_array_almost_equal(tform3.params, tform2.params) def test_similarity_init(): @@ -69,11 +83,21 @@ def test_similarity_init(): assert_array_almost_equal(tform.translation, translation) # init with transformation matrix - tform2 = SimilarityTransform(tform._matrix) + tform2 = SimilarityTransform(tform.params) assert_array_almost_equal(tform2.scale, scale) assert_array_almost_equal(tform2.rotation, rotation) assert_array_almost_equal(tform2.translation, translation) + # test special case for scale if rotation=0 + scale = 0.1 + rotation = 0 + translation = (1, 1) + tform = SimilarityTransform(scale=scale, rotation=rotation, + translation=translation) + assert_array_almost_equal(tform.scale, scale) + assert_array_almost_equal(tform.rotation, rotation) + assert_array_almost_equal(tform.translation, translation) + def test_affine_estimation(): # exact solution @@ -87,7 +111,7 @@ def test_affine_estimation(): # via estimate method tform3 = AffineTransform() tform3.estimate(SRC, DST) - assert_array_almost_equal(tform3._matrix, tform2._matrix) + assert_array_almost_equal(tform3.params, tform2.params) def test_affine_init(): @@ -104,7 +128,7 @@ def test_affine_init(): assert_array_almost_equal(tform.translation, translation) # init with transformation matrix - tform2 = AffineTransform(tform._matrix) + tform2 = AffineTransform(tform.params) assert_array_almost_equal(tform2.scale, scale) assert_array_almost_equal(tform2.rotation, rotation) assert_array_almost_equal(tform2.shear, shear) @@ -131,14 +155,14 @@ def test_projective_estimation(): # via estimate method tform3 = ProjectiveTransform() tform3.estimate(SRC, DST) - assert_array_almost_equal(tform3._matrix, tform2._matrix) + assert_array_almost_equal(tform3.params, tform2.params) def test_projective_init(): tform = estimate_transform('projective', SRC, DST) # init with transformation matrix - tform2 = ProjectiveTransform(tform._matrix) - assert_array_almost_equal(tform2._matrix, tform._matrix) + tform2 = ProjectiveTransform(tform.params) + assert_array_almost_equal(tform2.params, tform.params) def test_polynomial_estimation(): @@ -149,31 +173,69 @@ def test_polynomial_estimation(): # via estimate method tform2 = PolynomialTransform() tform2.estimate(SRC, DST, order=10) - assert_array_almost_equal(tform2._params, tform._params) + assert_array_almost_equal(tform2.params, tform.params) def test_polynomial_init(): tform = estimate_transform('polynomial', SRC, DST, order=10) # init with transformation parameters - tform2 = PolynomialTransform(tform._params) - assert_array_almost_equal(tform2._params, tform._params) + tform2 = PolynomialTransform(tform.params) + assert_array_almost_equal(tform2.params, tform.params) def test_polynomial_default_order(): tform = estimate_transform('polynomial', SRC, DST) tform2 = estimate_transform('polynomial', SRC, DST, order=2) - assert_array_almost_equal(tform2._params, tform._params) + assert_array_almost_equal(tform2.params, tform.params) + + +def test_polynomial_inverse(): + assert_raises(Exception, PolynomialTransform().inverse, 0) def test_union(): tform1 = SimilarityTransform(scale=0.1, rotation=0.3) tform2 = SimilarityTransform(scale=0.1, rotation=0.9) tform3 = SimilarityTransform(scale=0.1 ** 2, rotation=0.3 + 0.9) - tform = tform1 + tform2 - assert_array_almost_equal(tform._matrix, tform3._matrix) + tform1 = AffineTransform(scale=(0.1, 0.1), rotation=0.3) + tform2 = SimilarityTransform(scale=0.1, rotation=0.9) + tform3 = SimilarityTransform(scale=0.1 ** 2, rotation=0.3 + 0.9) + tform = tform1 + tform2 + assert_array_almost_equal(tform._matrix, tform3._matrix) + assert tform.__class__ == ProjectiveTransform + + +def test_geometric_tform(): + tform = GeometricTransform() + assert_raises(NotImplementedError, tform, 0) + assert_raises(NotImplementedError, tform.inverse, 0) + assert_raises(NotImplementedError, tform.__add__, 0) + + +def test_invalid_input(): + assert_raises(ValueError, ProjectiveTransform, np.zeros((2, 3))) + assert_raises(ValueError, AffineTransform, np.zeros((2, 3))) + assert_raises(ValueError, SimilarityTransform, np.zeros((2, 3))) + + assert_raises(ValueError, AffineTransform, + matrix=np.zeros((2, 3)), scale=1) + assert_raises(ValueError, SimilarityTransform, + matrix=np.zeros((2, 3)), scale=1) + + assert_raises(ValueError, PolynomialTransform, np.zeros((3, 3))) + + +def test_deprecated_params_attributes(): + for t in ('projective', 'affine', 'similarity'): + tform = estimate_transform(t, SRC, DST) + assert_equal(tform._matrix, tform.params) + + tform = estimate_transform('polynomial', SRC, DST, order=3) + assert_equal(tform._params, tform.params) + if __name__ == "__main__": from numpy.testing import run_module_suite diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 7f7ef47d..e49ab098 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -1,4 +1,5 @@ -from numpy.testing import assert_array_almost_equal, run_module_suite, assert_array_equal +from numpy.testing import (assert_array_almost_equal, run_module_suite, + assert_array_equal, assert_raises) import numpy as np from scipy.ndimage import map_coordinates @@ -234,5 +235,18 @@ def test_downscale_local_mean(): assert_array_equal(expected2, out2) +def test_invalid(): + assert_raises(ValueError, warp, np.ones((4, )), SimilarityTransform()) + assert_raises(ValueError, warp, np.ones((4, 3, 3, 3)), + SimilarityTransform()) + + +def test_inverse(): + tform = SimilarityTransform(scale=0.5, rotation=0.1) + inverse_tform = SimilarityTransform(matrix=np.linalg.inv(tform.params)) + image = np.arange(10 * 10).reshape(10, 10).astype(np.double) + assert_array_equal(warp(image, inverse_tform), warp(image, tform.inverse)) + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index 9cd2bc50..5577e46b 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -3,14 +3,7 @@ from .dtype import (img_as_float, img_as_int, img_as_uint, img_as_ubyte, from .shape import view_as_blocks, view_as_windows from .noise import random_noise -import numpy -ver = numpy.__version__.split('.') -chk = int(ver[0] + ver[1]) -if chk < 18: # Use internal version for numpy versions < 1.8.x - from .arraypad import pad -else: - from numpy import pad -del numpy, ver, chk +from .arraypad import pad from ._regular_grid import regular_grid from .unique import unique_rows diff --git a/skimage/util/_regular_grid.py b/skimage/util/_regular_grid.py index 898a4aed..92b32082 100644 --- a/skimage/util/_regular_grid.py +++ b/skimage/util/_regular_grid.py @@ -29,21 +29,21 @@ def regular_grid(ar_shape, n_points): >>> ar = np.zeros((20, 40)) >>> g = regular_grid(ar.shape, 8) >>> g - [slice(5.0, None, 10.0), slice(5.0, None, 10.0)] + [slice(5, None, 10), slice(5, None, 10)] >>> ar[g] = 1 >>> ar.sum() 8.0 >>> ar = np.zeros((20, 40)) >>> g = regular_grid(ar.shape, 32) >>> g - [slice(2.0, None, 5.0), slice(2.0, None, 5.0)] + [slice(2, None, 5), slice(2, None, 5)] >>> ar[g] = 1 >>> ar.sum() 32.0 >>> ar = np.zeros((3, 20, 40)) >>> g = regular_grid(ar.shape, 8) >>> g - [slice(1.0, None, 3.0), slice(5.0, None, 10.0), slice(5.0, None, 10.0)] + [slice(1, None, 3), slice(5, None, 10), slice(5, None, 10)] >>> ar[g] = 1 >>> ar.sum() 8.0 @@ -64,8 +64,8 @@ def regular_grid(ar_shape, n_points): (1.0 / (ndim - dim - 1))) if (sorted_dims >= stepsizes).all(): break - starts = stepsizes // 2 - stepsizes = np.round(stepsizes) + starts = (stepsizes // 2).astype(int) + stepsizes = np.round(stepsizes).astype(int) slices = [slice(start, None, step) for start, step in zip(starts, stepsizes)] slices = [slices[i] for i in unsort_dim_idxs] diff --git a/skimage/util/arraypad.py b/skimage/util/arraypad.py index 66ad55ea..0bc92d7b 100644 --- a/skimage/util/arraypad.py +++ b/skimage/util/arraypad.py @@ -1027,7 +1027,11 @@ def _normalize_shape(narray, shape): """ normshp = None shapelen = len(np.shape(narray)) - if (isinstance(shape, int)) or shape is None: + + if isinstance(shape, np.ndarray): + shape = shape.tolist() + + if isinstance(shape, (int, float)) or shape is None: normshp = ((shape, shape), ) * shapelen elif (isinstance(shape, (tuple, list)) and isinstance(shape[0], (tuple, list)) diff --git a/skimage/util/shape.py b/skimage/util/shape.py index f91286c3..95e0a42e 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -111,9 +111,10 @@ def view_as_windows(arr_in, window_shape, step=1): window_shape: tuple Defines the shape of the elementary n-dimensional orthotope (better know as hyperrectangle [1]_) of the rolling window view. - step : int + step : int, optional Number of elements to skip when moving the window forward (by - default, move forward by one). + default, move forward by one). The value must be equal or larger + than one. Returns ------- diff --git a/skimage/util/tests/test_arraypad.py b/skimage/util/tests/test_arraypad.py index 008c3516..3fbe50a1 100644 --- a/skimage/util/tests/test_arraypad.py +++ b/skimage/util/tests/test_arraypad.py @@ -13,209 +13,219 @@ class TestStatistic(TestCase): def test_check_mean_stat_length(self): a = np.arange(100).astype('f') a = pad(a, ((25, 20), ), 'mean', stat_length=((2, 3), )) - b = np.array([ - 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, - 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, - 0.5, 0.5, 0.5, 0.5, 0.5, + b = np.array( + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, - 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., - 10., 11., 12., 13., 14., 15., 16., 17., 18., 19., - 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., - 30., 31., 32., 33., 34., 35., 36., 37., 38., 39., - 40., 41., 42., 43., 44., 45., 46., 47., 48., 49., - 50., 51., 52., 53., 54., 55., 56., 57., 58., 59., - 60., 61., 62., 63., 64., 65., 66., 67., 68., 69., - 70., 71., 72., 73., 74., 75., 76., 77., 78., 79., - 80., 81., 82., 83., 84., 85., 86., 87., 88., 89., - 90., 91., 92., 93., 94., 95., 96., 97., 98., 99., + 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., + 10., 11., 12., 13., 14., 15., 16., 17., 18., 19., + 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., + 30., 31., 32., 33., 34., 35., 36., 37., 38., 39., + 40., 41., 42., 43., 44., 45., 46., 47., 48., 49., + 50., 51., 52., 53., 54., 55., 56., 57., 58., 59., + 60., 61., 62., 63., 64., 65., 66., 67., 68., 69., + 70., 71., 72., 73., 74., 75., 76., 77., 78., 79., + 80., 81., 82., 83., 84., 85., 86., 87., 88., 89., + 90., 91., 92., 93., 94., 95., 96., 97., 98., 99., - 98., 98., 98., 98., 98., 98., 98., 98., 98., 98., - 98., 98., 98., 98., 98., 98., 98., 98., 98., 98.]) + 98., 98., 98., 98., 98., 98., 98., 98., 98., 98., + 98., 98., 98., 98., 98., 98., 98., 98., 98., 98. + ]) assert_array_equal(a, b) def test_check_maximum_1(self): a = np.arange(100) a = pad(a, (25, 20), 'maximum') - b = np.array([ - 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, + b = np.array( + [99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, - 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, - 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, - 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, - 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, - 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, 99, 99]) + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99] + ) assert_array_equal(a, b) def test_check_maximum_2(self): a = np.arange(100) + 1 a = pad(a, (25, 20), 'maximum') - b = np.array([ - 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, - 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, - 100, 100, 100, 100, 100, + b = np.array( + [100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, - 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, - 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, - 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, - 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, - 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, - 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, - 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, + 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, + 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, + 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, - 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, - 100, 100, 100, 100, 100, 100, 100, 100, 100, 100]) + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100] + ) assert_array_equal(a, b) def test_check_minimum_1(self): a = np.arange(100) a = pad(a, (25, 20), 'minimum') - b = np.array([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, + b = np.array( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, - 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, - 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, - 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, - 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ) assert_array_equal(a, b) def test_check_minimum_2(self): a = np.arange(100) + 2 a = pad(a, (25, 20), 'minimum') - b = np.array([ - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, + b = np.array( + [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, - 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, - 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, - 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, - 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, - 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, - 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, - 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, + 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, + 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, + 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2] + ) assert_array_equal(a, b) def test_check_median(self): a = np.arange(100).astype('f') a = pad(a, (25, 20), 'median') - b = np.array([ - 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, - 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, - 49.5, 49.5, 49.5, 49.5, 49.5, + b = np.array( + [49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, - 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., - 10., 11., 12., 13., 14., 15., 16., 17., 18., 19., - 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., - 30., 31., 32., 33., 34., 35., 36., 37., 38., 39., - 40., 41., 42., 43., 44., 45., 46., 47., 48., 49., - 50., 51., 52., 53., 54., 55., 56., 57., 58., 59., - 60., 61., 62., 63., 64., 65., 66., 67., 68., 69., - 70., 71., 72., 73., 74., 75., 76., 77., 78., 79., - 80., 81., 82., 83., 84., 85., 86., 87., 88., 89., - 90., 91., 92., 93., 94., 95., 96., 97., 98., 99., + 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., + 10., 11., 12., 13., 14., 15., 16., 17., 18., 19., + 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., + 30., 31., 32., 33., 34., 35., 36., 37., 38., 39., + 40., 41., 42., 43., 44., 45., 46., 47., 48., 49., + 50., 51., 52., 53., 54., 55., 56., 57., 58., 59., + 60., 61., 62., 63., 64., 65., 66., 67., 68., 69., + 70., 71., 72., 73., 74., 75., 76., 77., 78., 79., + 80., 81., 82., 83., 84., 85., 86., 87., 88., 89., + 90., 91., 92., 93., 94., 95., 96., 97., 98., 99., - 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, - 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5]) + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5] + ) assert_array_equal(a, b) def test_check_median_01(self): a = np.array([[3, 1, 4], [4, 5, 9], [9, 8, 2]]) a = pad(a, 1, 'median') - b = np.array([ - [4, 4, 5, 4, 4], + b = np.array( + [[4, 4, 5, 4, 4], - [3, 3, 1, 4, 3], - [5, 4, 5, 9, 5], - [8, 9, 8, 2, 8], + [3, 3, 1, 4, 3], + [5, 4, 5, 9, 5], + [8, 9, 8, 2, 8], - [4, 4, 5, 4, 4]]) + [4, 4, 5, 4, 4]] + ) assert_array_equal(a, b) def test_check_median_02(self): a = np.array([[3, 1, 4], [4, 5, 9], [9, 8, 2]]) a = pad(a.T, 1, 'median').T - b = np.array([ - [5, 4, 5, 4, 5], + b = np.array( + [[5, 4, 5, 4, 5], - [3, 3, 1, 4, 3], - [5, 4, 5, 9, 5], - [8, 9, 8, 2, 8], + [3, 3, 1, 4, 3], + [5, 4, 5, 9, 5], + [8, 9, 8, 2, 8], - [5, 4, 5, 4, 5]]) + [5, 4, 5, 4, 5]] + ) assert_array_equal(a, b) def test_check_mean_shape_one(self): a = [[4, 5, 6]] a = pad(a, (5, 7), 'mean', stat_length=2) - b = np.array([ - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + b = np.array( + [[4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], - [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6]]) + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6], + [4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6]] + ) assert_array_equal(a, b) def test_check_mean_2(self): a = np.arange(100).astype('f') a = pad(a, (25, 20), 'mean') - b = np.array([ - 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, - 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, - 49.5, 49.5, 49.5, 49.5, 49.5, + b = np.array( + [49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, - 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., - 10., 11., 12., 13., 14., 15., 16., 17., 18., 19., - 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., - 30., 31., 32., 33., 34., 35., 36., 37., 38., 39., - 40., 41., 42., 43., 44., 45., 46., 47., 48., 49., - 50., 51., 52., 53., 54., 55., 56., 57., 58., 59., - 60., 61., 62., 63., 64., 65., 66., 67., 68., 69., - 70., 71., 72., 73., 74., 75., 76., 77., 78., 79., - 80., 81., 82., 83., 84., 85., 86., 87., 88., 89., - 90., 91., 92., 93., 94., 95., 96., 97., 98., 99., + 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., + 10., 11., 12., 13., 14., 15., 16., 17., 18., 19., + 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., + 30., 31., 32., 33., 34., 35., 36., 37., 38., 39., + 40., 41., 42., 43., 44., 45., 46., 47., 48., 49., + 50., 51., 52., 53., 54., 55., 56., 57., 58., 59., + 60., 61., 62., 63., 64., 65., 66., 67., 68., 69., + 70., 71., 72., 73., 74., 75., 76., 77., 78., 79., + 80., 81., 82., 83., 84., 85., 86., 87., 88., 89., + 90., 91., 92., 93., 94., 95., 96., 97., 98., 99., - 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, - 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5]) + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, + 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5, 49.5] + ) assert_array_equal(a, b) @@ -223,23 +233,73 @@ class TestConstant(TestCase): def test_check_constant(self): a = np.arange(100) a = pad(a, (25, 20), 'constant', constant_values=(10, 20)) - b = np.array([10, 10, 10, 10, 10, 10, 10, 10, 10, 10, - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, - 10, 10, 10, 10, 10, + b = np.array( + [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, - 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, - 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, - 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, - 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, - 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, - 20, 20, 20, 20, 20, 20, 20, 20, 20, 20]) + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20] + ) + assert_array_equal(a, b) + + def test_check_constant_float(self): + a = np.arange(100) + a = pad(a, (25, 20), 'constant', constant_values=-1.1) + b = np.array( + [-1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, + -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, + -1.1, -1.1, -1.1, -1.1, -1.1, + + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + + -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, + -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1] + ) + assert_array_equal(a, b) + + def test_check_constant_float(self): + a = np.arange(100, dtype=float) + a = pad(a, (25, 20), 'constant', constant_values=(-1.1, -1.2)) + b = np.array( + [-1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, + -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, -1.1, + -1.1, -1.1, -1.1, -1.1, -1.1, + + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + + -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, + -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2, -1.2] + ) assert_array_equal(a, b) @@ -247,24 +307,25 @@ class TestLinearRamp(TestCase): def test_check_simple(self): a = np.arange(100).astype('f') a = pad(a, (25, 20), 'linear_ramp', end_values=(4, 5)) - b = np.array([ - 4.00, 3.84, 3.68, 3.52, 3.36, 3.20, 3.04, 2.88, 2.72, 2.56, - 2.40, 2.24, 2.08, 1.92, 1.76, 1.60, 1.44, 1.28, 1.12, 0.96, - 0.80, 0.64, 0.48, 0.32, 0.16, + b = np.array( + [4.00, 3.84, 3.68, 3.52, 3.36, 3.20, 3.04, 2.88, 2.72, 2.56, + 2.40, 2.24, 2.08, 1.92, 1.76, 1.60, 1.44, 1.28, 1.12, 0.96, + 0.80, 0.64, 0.48, 0.32, 0.16, - 0.00, 1.00, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00, 9.00, - 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, - 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, - 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, - 40.0, 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, - 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, - 60.0, 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, - 70.0, 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, - 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, - 90.0, 91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, + 0.00, 1.00, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00, 9.00, + 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, + 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, + 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, + 40.0, 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, + 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, + 60.0, 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, + 70.0, 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, + 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, + 90.0, 91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, - 94.3, 89.6, 84.9, 80.2, 75.5, 70.8, 66.1, 61.4, 56.7, 52.0, - 47.3, 42.6, 37.9, 33.2, 28.5, 23.8, 19.1, 14.4, 9.7, 5.]) + 94.3, 89.6, 84.9, 80.2, 75.5, 70.8, 66.1, 61.4, 56.7, 52.0, + 47.3, 42.6, 37.9, 33.2, 28.5, 23.8, 19.1, 14.4, 9.7, 5.] + ) assert_array_almost_equal(a, b, decimal=5) @@ -272,67 +333,70 @@ class TestReflect(TestCase): def test_check_simple(self): a = np.arange(100) a = pad(a, (25, 20), 'reflect') - b = np.array([ - 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, - 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, - 5, 4, 3, 2, 1, + b = np.array( + [25, 24, 23, 22, 21, 20, 19, 18, 17, 16, + 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, + 5, 4, 3, 2, 1, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, - 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, - 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, - 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, - 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, - 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, - 88, 87, 86, 85, 84, 83, 82, 81, 80, 79]) + 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, + 88, 87, 86, 85, 84, 83, 82, 81, 80, 79] + ) assert_array_equal(a, b) def test_check_large_pad(self): a = [[4, 5, 6], [6, 7, 8]] a = pad(a, (5, 7), 'reflect') - b = np.array([ - [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + b = np.array( + [[7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5]]) + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7, 8, 7, 6, 7], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5]] + ) assert_array_equal(a, b) def test_check_shape(self): a = [[4, 5, 6]] a = pad(a, (5, 7), 'reflect') - b = np.array([ - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + b = np.array( + [[5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], - [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5]]) + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5], + [5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5, 6, 5, 4, 5]] + ) assert_array_equal(a, b) def test_check_01(self): @@ -355,83 +419,85 @@ class TestWrap(TestCase): def test_check_simple(self): a = np.arange(100) a = pad(a, (25, 20), 'wrap') - b = np.array([ - 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, - 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, - 95, 96, 97, 98, 99, + b = np.array( + [75, 76, 77, 78, 79, 80, 81, 82, 83, 84, + 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, + 95, 96, 97, 98, 99, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, - 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, - 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, - 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, - 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + ) assert_array_equal(a, b) def test_check_large_pad(self): a = np.arange(12) a = np.reshape(a, (3, 4)) a = pad(a, (10, 12), 'wrap') - b = np.array([ - [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, - 11, 8, 9, 10, 11, 8, 9, 10, 11], - [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, - 3, 0, 1, 2, 3, 0, 1, 2, 3], - [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, - 7, 4, 5, 6, 7, 4, 5, 6, 7], - [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, - 11, 8, 9, 10, 11, 8, 9, 10, 11], - [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, - 3, 0, 1, 2, 3, 0, 1, 2, 3], - [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, - 7, 4, 5, 6, 7, 4, 5, 6, 7], - [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, - 11, 8, 9, 10, 11, 8, 9, 10, 11], - [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, - 3, 0, 1, 2, 3, 0, 1, 2, 3], - [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, - 7, 4, 5, 6, 7, 4, 5, 6, 7], - [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, - 11, 8, 9, 10, 11, 8, 9, 10, 11], + b = np.array( + [[10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], - [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, - 3, 0, 1, 2, 3, 0, 1, 2, 3], - [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, - 7, 4, 5, 6, 7, 4, 5, 6, 7], - [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, - 11, 8, 9, 10, 11, 8, 9, 10, 11], + [2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], - [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, - 3, 0, 1, 2, 3, 0, 1, 2, 3], - [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, - 7, 4, 5, 6, 7, 4, 5, 6, 7], - [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, - 11, 8, 9, 10, 11, 8, 9, 10, 11], - [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, - 3, 0, 1, 2, 3, 0, 1, 2, 3], - [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, - 7, 4, 5, 6, 7, 4, 5, 6, 7], - [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, - 11, 8, 9, 10, 11, 8, 9, 10, 11], - [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, - 3, 0, 1, 2, 3, 0, 1, 2, 3], - [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, - 7, 4, 5, 6, 7, 4, 5, 6, 7], - [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, - 11, 8, 9, 10, 11, 8, 9, 10, 11], - [ 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, - 3, 0, 1, 2, 3, 0, 1, 2, 3], - [ 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, - 7, 4, 5, 6, 7, 4, 5, 6, 7], - [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, - 11, 8, 9, 10, 11, 8, 9, 10, 11]]) + [2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11], + [2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, + 3, 0, 1, 2, 3, 0, 1, 2, 3], + [6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, 7, 4, 5, 6, + 7, 4, 5, 6, 7, 4, 5, 6, 7], + [10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, 11, 8, 9, 10, + 11, 8, 9, 10, 11, 8, 9, 10, 11]] + ) assert_array_equal(a, b) def test_check_01(self): @@ -450,19 +516,21 @@ class TestStatLen(TestCase): a = np.arange(30) a = np.reshape(a, (6, 5)) a = pad(a, ((2, 3), (3, 2)), mode='mean', stat_length=(3,)) - b = np.array([[ 6, 6, 6, 5, 6, 7, 8, 9, 8, 8], - [ 6, 6, 6, 5, 6, 7, 8, 9, 8, 8], + b = np.array( + [[6, 6, 6, 5, 6, 7, 8, 9, 8, 8], + [6, 6, 6, 5, 6, 7, 8, 9, 8, 8], - [ 1, 1, 1, 0, 1, 2, 3, 4, 3, 3], - [ 6, 6, 6, 5, 6, 7, 8, 9, 8, 8], - [11, 11, 11, 10, 11, 12, 13, 14, 13, 13], - [16, 16, 16, 15, 16, 17, 18, 19, 18, 18], - [21, 21, 21, 20, 21, 22, 23, 24, 23, 23], - [26, 26, 26, 25, 26, 27, 28, 29, 28, 28], + [1, 1, 1, 0, 1, 2, 3, 4, 3, 3], + [6, 6, 6, 5, 6, 7, 8, 9, 8, 8], + [11, 11, 11, 10, 11, 12, 13, 14, 13, 13], + [16, 16, 16, 15, 16, 17, 18, 19, 18, 18], + [21, 21, 21, 20, 21, 22, 23, 24, 23, 23], + [26, 26, 26, 25, 26, 27, 28, 29, 28, 28], - [21, 21, 21, 20, 21, 22, 23, 24, 23, 23], - [21, 21, 21, 20, 21, 22, 23, 24, 23, 23], - [21, 21, 21, 20, 21, 22, 23, 24, 23, 23]]) + [21, 21, 21, 20, 21, 22, 23, 24, 23, 23], + [21, 21, 21, 20, 21, 22, 23, 24, 23, 23], + [21, 21, 21, 20, 21, 22, 23, 24, 23, 23]] + ) assert_array_equal(a, b) @@ -471,58 +539,90 @@ class TestEdge(TestCase): a = np.arange(12) a = np.reshape(a, (4, 3)) a = pad(a, ((2, 3), (3, 2)), 'edge') - b = np.array([ - [0, 0, 0, 0, 1, 2, 2, 2], - [0, 0, 0, 0, 1, 2, 2, 2], + b = np.array( + [[0, 0, 0, 0, 1, 2, 2, 2], + [0, 0, 0, 0, 1, 2, 2, 2], - [0, 0, 0, 0, 1, 2, 2, 2], - [3, 3, 3, 3, 4, 5, 5, 5], - [6, 6, 6, 6, 7, 8, 8, 8], - [9, 9, 9, 9, 10, 11, 11, 11], + [0, 0, 0, 0, 1, 2, 2, 2], + [3, 3, 3, 3, 4, 5, 5, 5], + [6, 6, 6, 6, 7, 8, 8, 8], + [9, 9, 9, 9, 10, 11, 11, 11], - [9, 9, 9, 9, 10, 11, 11, 11], - [9, 9, 9, 9, 10, 11, 11, 11], - [9, 9, 9, 9, 10, 11, 11, 11]]) + [9, 9, 9, 9, 10, 11, 11, 11], + [9, 9, 9, 9, 10, 11, 11, 11], + [9, 9, 9, 9, 10, 11, 11, 11]] + ) assert_array_equal(a, b) -def test_check_too_many_pad_axes(): - arr = np.arange(30) - arr = np.reshape(arr, (6, 5)) - kwargs = dict(mode='mean', stat_length=(3, )) - assert_raises(ValueError, pad, arr, ((2, 3), (3, 2), (4, 5)), - **kwargs) +class TestZeroPadWidth(TestCase): + def test_zero_pad_width(self): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + for pad_width in (0, (0, 0), ((0, 0), (0, 0))): + assert_array_equal(arr, pad(arr, pad_width, mode='constant')) -def test_check_negative_stat_length(): - arr = np.arange(30) - arr = np.reshape(arr, (6, 5)) - kwargs = dict(mode='mean', stat_length=(-3, )) - assert_raises(ValueError, pad, arr, ((2, 3), (3, 2)), - **kwargs) +class TestNdarrayPadWidth(TestCase): + def test_check_simple(self): + a = np.arange(12) + a = np.reshape(a, (4, 3)) + a = pad(a, np.array(((2, 3), (3, 2))), 'edge') + b = np.array( + [[0, 0, 0, 0, 1, 2, 2, 2], + [0, 0, 0, 0, 1, 2, 2, 2], + + [0, 0, 0, 0, 1, 2, 2, 2], + [3, 3, 3, 3, 4, 5, 5, 5], + [6, 6, 6, 6, 7, 8, 8, 8], + [9, 9, 9, 9, 10, 11, 11, 11], + + [9, 9, 9, 9, 10, 11, 11, 11], + [9, 9, 9, 9, 10, 11, 11, 11], + [9, 9, 9, 9, 10, 11, 11, 11]] + ) + assert_array_equal(a, b) -def test_check_negative_pad_width(): - arr = np.arange(30) - arr = np.reshape(arr, (6, 5)) - kwargs = dict(mode='mean', stat_length=(3, )) - assert_raises(ValueError, pad, arr, ((-2, 3), (3, 2)), - **kwargs) +class ValueError1(TestCase): + def test_check_simple(self): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + kwargs = dict(mode='mean', stat_length=(3, )) + assert_raises(ValueError, pad, arr, ((2, 3), (3, 2), (4, 5)), + **kwargs) + + def test_check_negative_stat_length(self): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + kwargs = dict(mode='mean', stat_length=(-3, )) + assert_raises(ValueError, pad, arr, ((2, 3), (3, 2)), + **kwargs) + + def test_check_negative_pad_width(self): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + kwargs = dict(mode='mean', stat_length=(3, )) + assert_raises(ValueError, pad, arr, ((-2, 3), (3, 2)), + **kwargs) -def test_pad_one_axis_three_ways(): - arr = np.arange(30) - arr = np.reshape(arr, (6, 5)) - kwargs = dict(mode='mean', stat_length=(3, )) - assert_raises(ValueError, pad, arr, ((2, 3, 4), (3, 2)), - **kwargs) +class ValueError2(TestCase): + def test_check_simple(self): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + kwargs = dict(mode='mean', stat_length=(3, )) + assert_raises(ValueError, pad, arr, ((2, 3, 4), (3, 2)), + **kwargs) -def test_zero_pad_width(): - arr = np.arange(30) - arr = np.reshape(arr, (6, 5)) - for pad_width in (0, (0, 0), ((0, 0), (0, 0))): - assert np.all(arr == pad(arr, pad_width, mode='constant')) +class ValueError3(TestCase): + def test_check_simple(self): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + kwargs = dict(mode='mean', stat_length=(3, )) + assert_raises(ValueError, pad, arr, ((-2, 3), (3, 2)), + **kwargs) if __name__ == "__main__": diff --git a/skimage/util/tests/test_shape.py b/skimage/util/tests/test_shape.py index b6975d0f..b66ac2e2 100644 --- a/skimage/util/tests/test_shape.py +++ b/skimage/util/tests/test_shape.py @@ -6,41 +6,35 @@ from skimage.util.shape import view_as_blocks, view_as_windows @raises(TypeError) def test_view_as_blocks_block_not_a_tuple(): - A = np.arange(10) view_as_blocks(A, [5]) @raises(ValueError) def test_view_as_blocks_negative_shape(): - A = np.arange(10) view_as_blocks(A, (-2,)) @raises(ValueError) def test_view_as_blocks_block_too_large(): - A = np.arange(10) view_as_blocks(A, (11,)) @raises(ValueError) def test_view_as_blocks_wrong_block_dimension(): - A = np.arange(10) view_as_blocks(A, (2, 2)) @raises(ValueError) def test_view_as_blocks_1D_array_wrong_block_shape(): - A = np.arange(10) view_as_blocks(A, (3,)) def test_view_as_blocks_1D_array(): - A = np.arange(10) B = view_as_blocks(A, (5,)) assert_equal(B, np.array([[0, 1, 2, 3, 4], @@ -48,7 +42,6 @@ def test_view_as_blocks_1D_array(): def test_view_as_blocks_2D_array(): - A = np.arange(4 * 4).reshape(4, 4) B = view_as_blocks(A, (2, 2)) assert_equal(B[0, 1], np.array([[2, 3], @@ -57,7 +50,6 @@ def test_view_as_blocks_2D_array(): def test_view_as_blocks_3D_array(): - A = np.arange(4 * 4 * 6).reshape(4, 4, 6) B = view_as_blocks(A, (1, 2, 2)) assert_equal(B.shape, (4, 2, 3, 1, 2, 2)) @@ -69,41 +61,40 @@ def test_view_as_blocks_3D_array(): @raises(TypeError) def test_view_as_windows_input_not_array(): - A = [1, 2, 3, 4, 5] view_as_windows(A, (2,)) @raises(TypeError) def test_view_as_windows_window_not_tuple(): - A = np.arange(10) view_as_windows(A, [2]) @raises(ValueError) def test_view_as_windows_wrong_window_dimension(): - A = np.arange(10) view_as_windows(A, (2, 2)) @raises(ValueError) def test_view_as_windows_negative_window_length(): - A = np.arange(10) view_as_windows(A, (-1,)) @raises(ValueError) def test_view_as_windows_window_too_large(): - A = np.arange(10) view_as_windows(A, (11,)) -def test_view_as_windows_1D(): +@raises(ValueError) +def test_view_as_windows_step_below_one(): + A = np.arange(10) + view_as_windows(A, (11,), step=0.9) +def test_view_as_windows_1D(): A = np.arange(10) window_shape = (3,) B = view_as_windows(A, window_shape) @@ -118,7 +109,6 @@ def test_view_as_windows_1D(): def test_view_as_windows_2D(): - A = np.arange(5 * 4).reshape(5, 4) window_shape = (4, 3) B = view_as_windows(A, window_shape) diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index 7e7dd33a..cc5f9705 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -50,8 +50,7 @@ class PaintTool(CanvasToolBase): self.alpha = alpha self.cmap = LABELS_CMAP self._overlay_plot = None - self._shape = overlay_shape - self.overlay = np.zeros(overlay_shape, dtype='uint8') + self.shape = overlay_shape self._cursor = plt.Rectangle((0, 0), 0, 0, **props) self._cursor.set_visible(False) @@ -109,6 +108,19 @@ class PaintTool(CanvasToolBase): self._overlay_plot.set_data(image) self.redraw() + @property + def shape(self): + return self._shape + + @shape.setter + def shape(self, shape): + self._shape = shape + if not self._overlay_plot is None: + self._overlay_plot.set_extent((-0.5, shape[1] + 0.5, + shape[0] + 0.5, -0.5)) + self.radius = self._radius + self.overlay = np.zeros(shape, dtype='uint8') + def _on_key_press(self, event): if event.key == 'enter': self.callback_on_enter(self.geometry) diff --git a/skimage/viewer/plugins/__init__.py b/skimage/viewer/plugins/__init__.py index e69de29b..7bb07333 100644 --- a/skimage/viewer/plugins/__init__.py +++ b/skimage/viewer/plugins/__init__.py @@ -0,0 +1,9 @@ +from .base import Plugin +from .canny import CannyPlugin +from .color_histogram import ColorHistogram +from .crop import Crop +from .labelplugin import LabelPainter +from .lineprofile import LineProfile +from .measure import Measure +from .overlayplugin import OverlayPlugin +from .plotplugin import PlotPlugin diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index c8cf6875..dd3aebb0 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -5,9 +5,15 @@ from warnings import warn import numpy as np -from ..qt import QtGui +from ..qt import QtGui, qt_api from ..qt.QtCore import Qt, Signal from ..utils import RequiredAttr, init_qtapp +from skimage._shared.testing import doctest_skip_parser + +if qt_api is not None: + has_qt = True +else: + has_qt = False class Plugin(QtGui.QDialog): @@ -52,13 +58,14 @@ class Plugin(QtGui.QDialog): >>> from skimage.viewer.widgets import Slider >>> from skimage import data >>> - >>> plugin = Plugin(image_filter=lambda img, threshold: img > threshold) - >>> plugin += Slider('threshold', 0, 255) + >>> plugin = Plugin(image_filter=lambda img, + ... threshold: img > threshold) # doctest: +SKIP + >>> plugin += Slider('threshold', 0, 255) # doctest: +SKIP >>> >>> image = data.coins() - >>> viewer = ImageViewer(image) - >>> viewer += plugin - >>> # viewer.show() + >>> viewer = ImageViewer(image) # doctest: +SKIP + >>> viewer += plugin # doctest: +SKIP + >>> viewer.show() # doctest: +SKIP The plugin will automatically delegate parameters to `image_filter` based on its parameter type, i.e., `ptype` (widgets for required arguments must @@ -99,7 +106,7 @@ class Plugin(QtGui.QDialog): self.row = 0 self.arguments = [] - self.keyword_arguments= {} + self.keyword_arguments = {} self.useblit = useblit self.cids = [] @@ -259,4 +266,3 @@ class Plugin(QtGui.QDialog): you don't want to return a value. """ return (self.image_viewer.image, None) - diff --git a/skimage/viewer/plugins/labelplugin.py b/skimage/viewer/plugins/labelplugin.py index b3c289f1..06ba2dd9 100644 --- a/skimage/viewer/plugins/labelplugin.py +++ b/skimage/viewer/plugins/labelplugin.py @@ -43,6 +43,10 @@ class LabelPainter(Plugin): self.paint_tool.label = self._label_widget.index = 1 self.artists.append(self.paint_tool) + def _on_new_image(self, image): + """Update plugin for new images.""" + self.paint_tool.shape = image.shape + def on_enter(self, overlay): pass diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 0d555eb5..e7c04d87 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -1,8 +1,10 @@ +from __future__ import division import warnings import numpy as np -import scipy.ndimage as ndi from skimage.util.dtype import dtype_range +from skimage import draw +from skimage import measure from .plotplugin import PlotPlugin from ..canvastools import ThickLineTool @@ -70,7 +72,11 @@ class LineProfile(PlotPlugin): on_change=self.line_changed) self.line_tool.end_points = np.transpose([x, y]) - scan_data = profile_line(image, self.line_tool.end_points) + scan_data = measure.profile_line(image, + *self.line_tool.end_points[:, ::-1]) + self.scan_data = scan_data + if scan_data.ndim == 1: + scan_data = scan_data[:, np.newaxis] self.reset_axes(scan_data) @@ -104,8 +110,12 @@ class LineProfile(PlotPlugin): def line_changed(self, end_points): x, y = np.transpose(end_points) self.line_tool.end_points = end_points - scan = profile_line(self.image_viewer.original_image, end_points, - linewidth=self.line_tool.linewidth) + scan = measure.profile_line(self.image_viewer.original_image, + *end_points[:, ::-1], + linewidth=self.line_tool.linewidth) + self.scan_data = scan + if scan.ndim == 1: + scan = scan[:, np.newaxis] if scan.shape[1] != len(self.profile): self.reset_axes(scan) @@ -131,79 +141,32 @@ class LineProfile(PlotPlugin): scan_data[:, 1], 'g-', scan_data[:, 2], 'b-') + def output(self): + """Return the drawn line and the resulting scan. -def _calc_vert(img, x1, x2, y1, y2, linewidth): - # Quick calculation if perfectly horizontal - pixels = img[min(y1, y2): max(y1, y2) + 1, - x1 - linewidth / 2: x1 + linewidth / 2 + 1] + Returns + ------- + line_image : (M, N) uint8 array, same shape as image + An array of 0s with the scanned line set to 255. + If the linewidth of the line tool is greater than 1, + sets the values within the profiled polygon to 128. + scan : (P,) or (P, 3) array of int or float + The line scan values across the image. + """ + end_points = self.line_tool.end_points + line_image = np.zeros(self.image_viewer.original_image.shape[:2], + np.uint8) + width = self.line_tool.linewidth + if width > 1: + rp, cp = measure.profile._line_profile_coordinates( + *end_points[:, ::-1], linewidth=width) + # the points are aliased, so create a polygon using the corners + yp = np.rint(rp[[0, 0, -1, -1],[0, -1, -1, 0]]).astype(int) + xp = np.rint(cp[[0, 0, -1, -1],[0, -1, -1, 0]]).astype(int) + rp, cp = draw.polygon(yp, xp, line_image.shape) + line_image[rp, cp] = 128 + (x1, y1), (x2, y2) = end_points.astype(int) + rr, cc = draw.line(y1, x1, y2, x2) + line_image[rr, cc] = 255 + return line_image, self.scan_data - # Reverse index if necessary - if y2 > y1: - pixels = pixels[::-1, :] - - return pixels.mean(axis=1)[:, np.newaxis] - - -def profile_line(img, end_points, linewidth=1): - """Return the intensity profile of an image measured along a scan line. - - Parameters - ---------- - img : 2d or 3d array - The image, in grayscale (2d) or RGB (3d) format. - end_points: (2, 2) list - End points ((x1, y1), (x2, y2)) of scan line. - linewidth: int - Width of the scan, perpendicular to the line - - Returns - ------- - return_value : array - The intensity profile along the scan line. The length of the profile - is the ceil of the computed length of the scan line. - """ - point1, point2 = end_points - x1, y1 = point1 = np.asarray(point1, dtype=float) - x2, y2 = point2 = np.asarray(point2, dtype=float) - dx, dy = point2 - point1 - channels = 1 - if img.ndim == 3: - channels = 3 - - # Quick calculation if perfectly vertical; shortcuts div0 error - if x1 == x2: - if channels == 1: - img = img[:, :, np.newaxis] - - img = np.rollaxis(img, -1) - intensities = np.hstack([_calc_vert(im, x1, x2, y1, y2, linewidth) - for im in img]) - return intensities - - theta = np.arctan2(dy, dx) - a = dy / dx - b = y1 - a * x1 - length = np.hypot(dx, dy) - - line_x = np.linspace(x2, x1, np.ceil(length)) - line_y = line_x * a + b - y_width = abs(linewidth * np.cos(theta) / 2) - perp_ys = np.array([np.linspace(yi - y_width, - yi + y_width, linewidth) for yi in line_y]) - perp_xs = - a * perp_ys + (line_x + a * line_y)[:, np.newaxis] - - perp_lines = np.array([perp_ys, perp_xs]) - if img.ndim == 3: - pixels = [ndi.map_coordinates(img[..., i], perp_lines) - for i in range(3)] - pixels = np.transpose(np.asarray(pixels), (1, 2, 0)) - else: - pixels = ndi.map_coordinates(img, perp_lines) - pixels = pixels[..., np.newaxis] - - intensities = pixels.mean(axis=1) - - if intensities.ndim == 1: - return intensities[..., np.newaxis] - else: - return intensities diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 334838d7..373d6bf4 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -45,7 +45,7 @@ class OverlayPlugin(Plugin): self._overlay_plot = None self._overlay = None self.cmap = None - self.color_names = self.colors.keys() + self.color_names = list(self.colors.keys()) def attach(self, image_viewer): super(OverlayPlugin, self).attach(image_viewer) diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py new file mode 100644 index 00000000..7fa4e374 --- /dev/null +++ b/skimage/viewer/tests/test_viewer.py @@ -0,0 +1,48 @@ +import skimage +import skimage.data as data +from skimage.viewer import ImageViewer +from skimage.viewer.qt import qt_api +from numpy.testing import assert_equal, assert_allclose +from numpy.testing.decorators import skipif + + +def setup_line_profile(image): + from skimage.viewer.plugins.lineprofile import LineProfile + viewer = ImageViewer(skimage.img_as_float(image)) + plugin = LineProfile() + viewer += plugin + return plugin + + +@skipif(qt_api is None) +def test_line_profile(): + """ Test a line profile using an ndim=2 image""" + plugin = setup_line_profile(data.camera()) + line_image, scan_data = plugin.output() + for inp in [line_image.nonzero()[0].size, + line_image.sum() / line_image.max(), + scan_data.size]: + assert_equal(inp, 172) + assert_equal(line_image.shape, (512, 512)) + assert_allclose(scan_data.max(), 0.9139, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.2828, rtol=1e-3) + + +@skipif(qt_api is None) +def test_line_profile_rgb(): + """ Test a line profile using an ndim=3 image""" + plugin = setup_line_profile(data.chelsea()) + for i in range(6): + plugin.line_tool._thicken_scan_line() + line_image, scan_data = plugin.output() + assert_equal(line_image[line_image == 128].size, 755) + assert_equal(line_image[line_image == 255].size, 151) + assert_equal(line_image.shape, (300, 451)) + assert_equal(scan_data.shape, (152, 3)) + assert_allclose(scan_data.max(), 0.772, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.4355, rtol=1e-3) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 781b6877..32c10cd4 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -1,12 +1,18 @@ """ ImageViewer class for viewing and interacting with images. """ -from ..qt import QtGui +from ..qt import QtGui, qt_api from ..qt.QtCore import Qt, Signal +if qt_api is not None: + has_qt = True +else: + has_qt = False + from skimage import io, img_as_float from skimage.util.dtype import dtype_range from skimage.exposure import rescale_intensity +from skimage._shared.testing import doctest_skip_parser import numpy as np from .. import utils from ..widgets import Slider @@ -72,8 +78,8 @@ class ImageViewer(QtGui.QMainWindow): -------- >>> from skimage import data >>> image = data.coins() - >>> viewer = ImageViewer(image) - >>> viewer.show() # doctest: +SKIP + >>> viewer = ImageViewer(image) # doctest: +SKIP + >>> viewer.show() # doctest: +SKIP """ diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index b9714d38..2bbf53d2 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -232,7 +232,7 @@ class ComboBox(BaseWidget): self.name_label.setAlignment(QtCore.Qt.AlignLeft) self._combo_box = QtGui.QComboBox() - self._combo_box.addItems(items) + self._combo_box.addItems(list(items)) self.layout = QtGui.QHBoxLayout(self) self.layout.addWidget(self.name_label)